cascade-ai 0.76.0 → 0.78.0

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
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var dns = require('dns');
3
+ var dnsCallback = require('dns');
4
4
  var http = require('http');
5
5
  var https = require('https');
6
6
  var zlib = require('zlib');
@@ -9,8 +9,8 @@ var Anthropic = require('@anthropic-ai/sdk');
9
9
  var OpenAI = require('openai');
10
10
  var genai = require('@google/genai');
11
11
  var Database2 = require('better-sqlite3');
12
- var path35 = require('path');
13
- var fs32 = require('fs');
12
+ var path36 = require('path');
13
+ var fs33 = require('fs');
14
14
  var os10 = require('os');
15
15
  var crypto = require('crypto');
16
16
  var ink = require('ink');
@@ -31,6 +31,7 @@ var simpleGit = require('simple-git');
31
31
  var PDFDocument = require('pdfkit');
32
32
  var dns2 = require('dns/promises');
33
33
  var net = require('net');
34
+ var undici = require('undici');
34
35
  var index_js = require('@modelcontextprotocol/sdk/client/index.js');
35
36
  var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
36
37
  var streamableHttp_js = require('@modelcontextprotocol/sdk/client/streamableHttp.js');
@@ -70,15 +71,15 @@ function _interopNamespace(e) {
70
71
  return Object.freeze(n);
71
72
  }
72
73
 
73
- var dns__default = /*#__PURE__*/_interopDefault(dns);
74
+ var dnsCallback__default = /*#__PURE__*/_interopDefault(dnsCallback);
74
75
  var http__default = /*#__PURE__*/_interopDefault(http);
75
76
  var https__default = /*#__PURE__*/_interopDefault(https);
76
77
  var zlib__default = /*#__PURE__*/_interopDefault(zlib);
77
78
  var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
78
79
  var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
79
80
  var Database2__default = /*#__PURE__*/_interopDefault(Database2);
80
- var path35__default = /*#__PURE__*/_interopDefault(path35);
81
- var fs32__default = /*#__PURE__*/_interopDefault(fs32);
81
+ var path36__default = /*#__PURE__*/_interopDefault(path36);
82
+ var fs33__default = /*#__PURE__*/_interopDefault(fs33);
82
83
  var os10__default = /*#__PURE__*/_interopDefault(os10);
83
84
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
84
85
  var React2__default = /*#__PURE__*/_interopDefault(React2);
@@ -1848,7 +1849,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1848
1849
  var init_constants = __esm({
1849
1850
  "src/constants.ts"() {
1850
1851
  init_pricing();
1851
- CASCADE_VERSION = "0.76.0";
1852
+ CASCADE_VERSION = "0.78.0";
1852
1853
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1853
1854
  CASCADE_DB_FILE = ".cascade/memory.db";
1854
1855
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -1861,64 +1862,88 @@ var init_constants = __esm({
1861
1862
  DEFAULT_CONTEXT_LIMIT = 2e5;
1862
1863
  DEFAULT_AUTO_SUMMARIZE_AT = 15e4;
1863
1864
  MODELS = {
1864
- // Anthropic
1865
- "claude-opus-4": {
1866
- id: "claude-opus-4-5",
1867
- name: "Claude Opus 4",
1865
+ // Anthropic — active Claude API models (2026-08-27).
1866
+ "claude-fable-5": {
1867
+ id: "claude-fable-5",
1868
+ name: "Claude Fable 5",
1868
1869
  provider: "anthropic",
1869
- contextWindow: 2e5,
1870
+ contextWindow: 1e6,
1870
1871
  isVisionCapable: true,
1871
- inputCostPer1kTokens: 0.015,
1872
- outputCostPer1kTokens: 0.075,
1873
- maxOutputTokens: 32e3,
1872
+ inputCostPer1kTokens: 0.01,
1873
+ outputCostPer1kTokens: 0.05,
1874
+ maxOutputTokens: 128e3,
1874
1875
  supportsStreaming: true,
1875
1876
  isLocal: false
1876
1877
  },
1877
- "claude-sonnet-4": {
1878
- id: "claude-sonnet-4-5",
1879
- name: "Claude Sonnet 4",
1878
+ "claude-opus-5": {
1879
+ id: "claude-opus-5",
1880
+ name: "Claude Opus 5",
1880
1881
  provider: "anthropic",
1881
- contextWindow: 2e5,
1882
+ contextWindow: 1e6,
1882
1883
  isVisionCapable: true,
1883
- inputCostPer1kTokens: 3e-3,
1884
- outputCostPer1kTokens: 0.015,
1885
- maxOutputTokens: 16e3,
1884
+ inputCostPer1kTokens: 5e-3,
1885
+ outputCostPer1kTokens: 0.025,
1886
+ maxOutputTokens: 128e3,
1886
1887
  supportsStreaming: true,
1887
1888
  isLocal: false
1888
1889
  },
1889
- "claude-haiku-3-5": {
1890
- id: "claude-haiku-3-5-20251001",
1891
- name: "Claude Haiku 3.5",
1890
+ "claude-opus-4-8": {
1891
+ id: "claude-opus-4-8",
1892
+ name: "Claude Opus 4.8",
1892
1893
  provider: "anthropic",
1893
- contextWindow: 2e5,
1894
+ contextWindow: 1e6,
1894
1895
  isVisionCapable: true,
1895
- inputCostPer1kTokens: 8e-4,
1896
- outputCostPer1kTokens: 4e-3,
1897
- maxOutputTokens: 8e3,
1896
+ inputCostPer1kTokens: 5e-3,
1897
+ outputCostPer1kTokens: 0.025,
1898
+ maxOutputTokens: 128e3,
1898
1899
  supportsStreaming: true,
1899
1900
  isLocal: false
1900
1901
  },
1901
- "claude-haiku-4-5": {
1902
- id: "claude-haiku-4-5-20251001",
1903
- name: "Claude Haiku 4.5",
1902
+ "claude-opus-4-7": {
1903
+ id: "claude-opus-4-7",
1904
+ name: "Claude Opus 4.7",
1904
1905
  provider: "anthropic",
1905
- contextWindow: 2e5,
1906
+ contextWindow: 1e6,
1906
1907
  isVisionCapable: true,
1907
- inputCostPer1kTokens: 8e-4,
1908
- outputCostPer1kTokens: 4e-3,
1909
- maxOutputTokens: 8e3,
1908
+ inputCostPer1kTokens: 5e-3,
1909
+ outputCostPer1kTokens: 0.025,
1910
+ maxOutputTokens: 128e3,
1910
1911
  supportsStreaming: true,
1911
1912
  isLocal: false
1912
1913
  },
1913
- "claude-opus-4-8": {
1914
- id: "claude-opus-4-8",
1915
- name: "Claude Opus 4.8",
1914
+ "claude-opus-4-6": {
1915
+ id: "claude-opus-4-6",
1916
+ name: "Claude Opus 4.6",
1917
+ provider: "anthropic",
1918
+ contextWindow: 1e6,
1919
+ isVisionCapable: true,
1920
+ inputCostPer1kTokens: 5e-3,
1921
+ outputCostPer1kTokens: 0.025,
1922
+ maxOutputTokens: 128e3,
1923
+ supportsStreaming: true,
1924
+ isLocal: false
1925
+ },
1926
+ "claude-opus-4-5": {
1927
+ id: "claude-opus-4-5-20251101",
1928
+ name: "Claude Opus 4.5",
1916
1929
  provider: "anthropic",
1917
1930
  contextWindow: 2e5,
1918
1931
  isVisionCapable: true,
1919
- inputCostPer1kTokens: 0.015,
1920
- outputCostPer1kTokens: 0.075,
1921
- maxOutputTokens: 32e3,
1932
+ inputCostPer1kTokens: 5e-3,
1933
+ outputCostPer1kTokens: 0.025,
1934
+ maxOutputTokens: 64e3,
1935
+ supportsStreaming: true,
1936
+ isLocal: false
1937
+ },
1938
+ "claude-sonnet-5": {
1939
+ id: "claude-sonnet-5",
1940
+ name: "Claude Sonnet 5",
1941
+ provider: "anthropic",
1942
+ contextWindow: 1e6,
1943
+ isVisionCapable: true,
1944
+ inputCostPer1kTokens: 2e-3,
1945
+ outputCostPer1kTokens: 0.01,
1946
+ maxOutputTokens: 128e3,
1922
1947
  supportsStreaming: true,
1923
1948
  isLocal: false
1924
1949
  },
@@ -1926,76 +1951,135 @@ var init_constants = __esm({
1926
1951
  id: "claude-sonnet-4-6",
1927
1952
  name: "Claude Sonnet 4.6",
1928
1953
  provider: "anthropic",
1954
+ contextWindow: 1e6,
1955
+ isVisionCapable: true,
1956
+ inputCostPer1kTokens: 3e-3,
1957
+ outputCostPer1kTokens: 0.015,
1958
+ maxOutputTokens: 64e3,
1959
+ supportsStreaming: true,
1960
+ isLocal: false
1961
+ },
1962
+ "claude-sonnet-4-5": {
1963
+ id: "claude-sonnet-4-5-20250929",
1964
+ name: "Claude Sonnet 4.5",
1965
+ provider: "anthropic",
1929
1966
  contextWindow: 2e5,
1930
1967
  isVisionCapable: true,
1931
1968
  inputCostPer1kTokens: 3e-3,
1932
1969
  outputCostPer1kTokens: 0.015,
1933
- maxOutputTokens: 16e3,
1970
+ maxOutputTokens: 64e3,
1934
1971
  supportsStreaming: true,
1935
1972
  isLocal: false
1936
1973
  },
1937
- // OpenAI
1938
- "gpt-4o": {
1939
- id: "gpt-4o",
1940
- name: "GPT-4o",
1974
+ "claude-haiku-4-5": {
1975
+ id: "claude-haiku-4-5-20251001",
1976
+ name: "Claude Haiku 4.5",
1977
+ provider: "anthropic",
1978
+ contextWindow: 2e5,
1979
+ isVisionCapable: true,
1980
+ inputCostPer1kTokens: 1e-3,
1981
+ outputCostPer1kTokens: 5e-3,
1982
+ maxOutputTokens: 64e3,
1983
+ supportsStreaming: true,
1984
+ isLocal: false
1985
+ },
1986
+ // OpenAI — active chat-capable models used by Cascade's Chat Completions path.
1987
+ "gpt-5.6-sol": {
1988
+ id: "gpt-5.6-sol",
1989
+ name: "GPT-5.6 Sol",
1941
1990
  provider: "openai",
1942
- contextWindow: 128e3,
1991
+ contextWindow: 105e4,
1992
+ isVisionCapable: true,
1993
+ inputCostPer1kTokens: 4e-3,
1994
+ outputCostPer1kTokens: 0.02,
1995
+ maxOutputTokens: 128e3,
1996
+ supportsStreaming: true,
1997
+ isLocal: false
1998
+ },
1999
+ "gpt-5.6-terra": {
2000
+ id: "gpt-5.6-terra",
2001
+ name: "GPT-5.6 Terra",
2002
+ provider: "openai",
2003
+ contextWindow: 105e4,
2004
+ isVisionCapable: true,
2005
+ inputCostPer1kTokens: 2e-3,
2006
+ outputCostPer1kTokens: 0.012,
2007
+ maxOutputTokens: 128e3,
2008
+ supportsStreaming: true,
2009
+ isLocal: false
2010
+ },
2011
+ "gpt-5.6-luna": {
2012
+ id: "gpt-5.6-luna",
2013
+ name: "GPT-5.6 Luna",
2014
+ provider: "openai",
2015
+ contextWindow: 105e4,
2016
+ isVisionCapable: true,
2017
+ inputCostPer1kTokens: 2e-4,
2018
+ outputCostPer1kTokens: 12e-4,
2019
+ maxOutputTokens: 128e3,
2020
+ supportsStreaming: true,
2021
+ isLocal: false
2022
+ },
2023
+ "gpt-5.5": {
2024
+ id: "gpt-5.5",
2025
+ name: "GPT-5.5",
2026
+ provider: "openai",
2027
+ contextWindow: 105e4,
1943
2028
  isVisionCapable: true,
1944
2029
  inputCostPer1kTokens: 5e-3,
1945
- outputCostPer1kTokens: 0.015,
1946
- maxOutputTokens: 16e3,
2030
+ outputCostPer1kTokens: 0.03,
2031
+ maxOutputTokens: 128e3,
1947
2032
  supportsStreaming: true,
1948
2033
  isLocal: false
1949
2034
  },
1950
- "gpt-4o-mini": {
1951
- id: "gpt-4o-mini",
1952
- name: "GPT-4o Mini",
2035
+ "gpt-5.4": {
2036
+ id: "gpt-5.4",
2037
+ name: "GPT-5.4",
1953
2038
  provider: "openai",
1954
- contextWindow: 128e3,
2039
+ contextWindow: 105e4,
1955
2040
  isVisionCapable: true,
1956
- inputCostPer1kTokens: 15e-5,
1957
- outputCostPer1kTokens: 6e-4,
1958
- maxOutputTokens: 16e3,
2041
+ inputCostPer1kTokens: 25e-4,
2042
+ outputCostPer1kTokens: 0.015,
2043
+ maxOutputTokens: 128e3,
1959
2044
  supportsStreaming: true,
1960
2045
  isLocal: false
1961
2046
  },
1962
- "gpt-4.1": {
1963
- id: "gpt-4.1",
1964
- name: "GPT-4.1",
2047
+ "gpt-5.4-mini": {
2048
+ id: "gpt-5.4-mini",
2049
+ name: "GPT-5.4 Mini",
1965
2050
  provider: "openai",
1966
- contextWindow: 1047576,
2051
+ contextWindow: 4e5,
1967
2052
  isVisionCapable: true,
1968
- inputCostPer1kTokens: 2e-3,
1969
- outputCostPer1kTokens: 8e-3,
1970
- maxOutputTokens: 32768,
2053
+ inputCostPer1kTokens: 75e-5,
2054
+ outputCostPer1kTokens: 45e-4,
2055
+ maxOutputTokens: 128e3,
1971
2056
  supportsStreaming: true,
1972
2057
  isLocal: false
1973
2058
  },
1974
- "gpt-4.1-mini": {
1975
- id: "gpt-4.1-mini",
1976
- name: "GPT-4.1 Mini",
2059
+ "gpt-5.4-nano": {
2060
+ id: "gpt-5.4-nano",
2061
+ name: "GPT-5.4 Nano",
1977
2062
  provider: "openai",
1978
- contextWindow: 1047576,
2063
+ contextWindow: 4e5,
1979
2064
  isVisionCapable: true,
1980
- inputCostPer1kTokens: 4e-4,
1981
- outputCostPer1kTokens: 16e-4,
1982
- maxOutputTokens: 32768,
2065
+ inputCostPer1kTokens: 2e-4,
2066
+ outputCostPer1kTokens: 125e-5,
2067
+ maxOutputTokens: 128e3,
1983
2068
  supportsStreaming: true,
1984
2069
  isLocal: false
1985
2070
  },
1986
- "gpt-4.1-nano": {
1987
- id: "gpt-4.1-nano",
1988
- name: "GPT-4.1 Nano",
2071
+ "gpt-5.2": {
2072
+ id: "gpt-5.2",
2073
+ name: "GPT-5.2",
1989
2074
  provider: "openai",
1990
- contextWindow: 1047576,
2075
+ contextWindow: 4e5,
1991
2076
  isVisionCapable: true,
1992
- inputCostPer1kTokens: 1e-4,
1993
- outputCostPer1kTokens: 4e-4,
1994
- maxOutputTokens: 32768,
2077
+ inputCostPer1kTokens: 175e-5,
2078
+ outputCostPer1kTokens: 0.014,
2079
+ maxOutputTokens: 128e3,
1995
2080
  supportsStreaming: true,
1996
2081
  isLocal: false
1997
2082
  },
1998
- // GPT-5 family (offline baseline pricing; the live fetch keeps it current).
1999
2083
  "gpt-5": {
2000
2084
  id: "gpt-5",
2001
2085
  name: "GPT-5",
@@ -2032,79 +2116,149 @@ var init_constants = __esm({
2032
2116
  supportsStreaming: true,
2033
2117
  isLocal: false
2034
2118
  },
2035
- // GPT-5 point releases — genuinely distinct capability, so they route as their
2036
- // own families (not folded into gpt-5). Offline baseline pricing; live fetch
2037
- // keeps it current. gpt-5.5 "Spud" (Apr 2026) is the current SWE-bench leader.
2038
- "gpt-5.5": {
2039
- id: "gpt-5.5",
2040
- name: "GPT-5.5",
2119
+ "gpt-4.1": {
2120
+ id: "gpt-4.1",
2121
+ name: "GPT-4.1",
2041
2122
  provider: "openai",
2042
- contextWindow: 4e5,
2123
+ contextWindow: 1047576,
2043
2124
  isVisionCapable: true,
2044
- inputCostPer1kTokens: 25e-4,
2045
- outputCostPer1kTokens: 0.02,
2046
- maxOutputTokens: 128e3,
2125
+ inputCostPer1kTokens: 2e-3,
2126
+ outputCostPer1kTokens: 8e-3,
2127
+ maxOutputTokens: 32768,
2047
2128
  supportsStreaming: true,
2048
2129
  isLocal: false
2049
2130
  },
2050
- "gpt-5.4": {
2051
- id: "gpt-5.4",
2052
- name: "GPT-5.4",
2131
+ "gpt-4.1-mini": {
2132
+ id: "gpt-4.1-mini",
2133
+ name: "GPT-4.1 Mini",
2053
2134
  provider: "openai",
2054
- contextWindow: 4e5,
2135
+ contextWindow: 1047576,
2055
2136
  isVisionCapable: true,
2056
- inputCostPer1kTokens: 125e-5,
2137
+ inputCostPer1kTokens: 4e-4,
2138
+ outputCostPer1kTokens: 16e-4,
2139
+ maxOutputTokens: 32768,
2140
+ supportsStreaming: true,
2141
+ isLocal: false
2142
+ },
2143
+ "gpt-4.1-nano": {
2144
+ id: "gpt-4.1-nano",
2145
+ name: "GPT-4.1 Nano",
2146
+ provider: "openai",
2147
+ contextWindow: 1047576,
2148
+ isVisionCapable: true,
2149
+ inputCostPer1kTokens: 1e-4,
2150
+ outputCostPer1kTokens: 4e-4,
2151
+ maxOutputTokens: 32768,
2152
+ supportsStreaming: true,
2153
+ isLocal: false
2154
+ },
2155
+ "gpt-4o": {
2156
+ id: "gpt-4o",
2157
+ name: "GPT-4o",
2158
+ provider: "openai",
2159
+ contextWindow: 128e3,
2160
+ isVisionCapable: true,
2161
+ inputCostPer1kTokens: 25e-4,
2057
2162
  outputCostPer1kTokens: 0.01,
2058
- maxOutputTokens: 128e3,
2163
+ maxOutputTokens: 16e3,
2059
2164
  supportsStreaming: true,
2060
2165
  isLocal: false
2061
2166
  },
2062
- "gpt-5.4-mini": {
2063
- id: "gpt-5.4-mini",
2064
- name: "GPT-5.4 Mini",
2167
+ "gpt-4o-mini": {
2168
+ id: "gpt-4o-mini",
2169
+ name: "GPT-4o Mini",
2065
2170
  provider: "openai",
2066
- contextWindow: 4e5,
2171
+ contextWindow: 128e3,
2067
2172
  isVisionCapable: true,
2068
- inputCostPer1kTokens: 25e-5,
2069
- outputCostPer1kTokens: 2e-3,
2070
- maxOutputTokens: 128e3,
2173
+ inputCostPer1kTokens: 15e-5,
2174
+ outputCostPer1kTokens: 6e-4,
2175
+ maxOutputTokens: 16e3,
2071
2176
  supportsStreaming: true,
2072
2177
  isLocal: false
2073
2178
  },
2074
- // Google
2075
- "gemini-2.0-flash": {
2076
- id: "gemini-2.0-flash",
2077
- name: "Gemini 2.0 Flash",
2179
+ // Google Gemini — active GA + preview chat models. Gemini 2.0/1.5 are no
2180
+ // longer advertised because the Gemini API shut those generations down.
2181
+ "gemini-3.7-flash": {
2182
+ id: "gemini-3.7-flash",
2183
+ name: "Gemini 3.7 Flash",
2078
2184
  provider: "gemini",
2079
- contextWindow: 1e6,
2185
+ contextWindow: 1048576,
2080
2186
  isVisionCapable: true,
2081
- inputCostPer1kTokens: 1e-4,
2082
- outputCostPer1kTokens: 4e-4,
2083
- maxOutputTokens: 8e3,
2187
+ inputCostPer1kTokens: 75e-5,
2188
+ outputCostPer1kTokens: 375e-5,
2189
+ maxOutputTokens: 65536,
2084
2190
  supportsStreaming: true,
2085
2191
  isLocal: false
2086
2192
  },
2087
- "gemini-1.5-pro": {
2088
- id: "gemini-1.5-pro",
2089
- name: "Gemini 1.5 Pro",
2193
+ "gemini-3.6-flash": {
2194
+ id: "gemini-3.6-flash",
2195
+ name: "Gemini 3.6 Flash",
2090
2196
  provider: "gemini",
2091
- contextWindow: 1e6,
2197
+ contextWindow: 1048576,
2092
2198
  isVisionCapable: true,
2093
- inputCostPer1kTokens: 125e-5,
2094
- outputCostPer1kTokens: 5e-3,
2095
- maxOutputTokens: 8e3,
2199
+ inputCostPer1kTokens: 75e-5,
2200
+ outputCostPer1kTokens: 375e-5,
2201
+ maxOutputTokens: 65536,
2096
2202
  supportsStreaming: true,
2097
2203
  isLocal: false
2098
2204
  },
2099
- "gemini-2.0-flash-lite": {
2100
- id: "gemini-2.0-flash-lite",
2101
- name: "Gemini 2.0 Flash-Lite",
2205
+ "gemini-3.5-flash": {
2206
+ id: "gemini-3.5-flash",
2207
+ name: "Gemini 3.5 Flash",
2102
2208
  provider: "gemini",
2103
- contextWindow: 1e6,
2209
+ contextWindow: 1048576,
2104
2210
  isVisionCapable: true,
2105
- inputCostPer1kTokens: 5e-5,
2106
- outputCostPer1kTokens: 2e-4,
2107
- maxOutputTokens: 8e3,
2211
+ inputCostPer1kTokens: 15e-4,
2212
+ outputCostPer1kTokens: 9e-3,
2213
+ maxOutputTokens: 65536,
2214
+ supportsStreaming: true,
2215
+ isLocal: false
2216
+ },
2217
+ "gemini-3.5-flash-lite": {
2218
+ id: "gemini-3.5-flash-lite",
2219
+ name: "Gemini 3.5 Flash-Lite",
2220
+ provider: "gemini",
2221
+ contextWindow: 1048576,
2222
+ isVisionCapable: true,
2223
+ inputCostPer1kTokens: 3e-4,
2224
+ outputCostPer1kTokens: 25e-4,
2225
+ maxOutputTokens: 65536,
2226
+ supportsStreaming: true,
2227
+ isLocal: false
2228
+ },
2229
+ "gemini-3.1-flash-lite": {
2230
+ id: "gemini-3.1-flash-lite",
2231
+ name: "Gemini 3.1 Flash-Lite",
2232
+ provider: "gemini",
2233
+ contextWindow: 1048576,
2234
+ isVisionCapable: true,
2235
+ inputCostPer1kTokens: 25e-5,
2236
+ outputCostPer1kTokens: 15e-4,
2237
+ maxOutputTokens: 65536,
2238
+ supportsStreaming: true,
2239
+ isLocal: false
2240
+ },
2241
+ "gemini-3.1-pro-preview": {
2242
+ id: "gemini-3.1-pro-preview",
2243
+ name: "Gemini 3.1 Pro Preview",
2244
+ provider: "gemini",
2245
+ contextWindow: 1048576,
2246
+ isVisionCapable: true,
2247
+ inputCostPer1kTokens: 2e-3,
2248
+ outputCostPer1kTokens: 0.012,
2249
+ maxOutputTokens: 65536,
2250
+ supportsStreaming: true,
2251
+ isLocal: false
2252
+ },
2253
+ "gemini-3-flash-preview": {
2254
+ id: "gemini-3-flash-preview",
2255
+ name: "Gemini 3 Flash Preview",
2256
+ provider: "gemini",
2257
+ contextWindow: 1048576,
2258
+ isVisionCapable: true,
2259
+ inputCostPer1kTokens: 5e-4,
2260
+ outputCostPer1kTokens: 3e-3,
2261
+ maxOutputTokens: 65536,
2108
2262
  supportsStreaming: true,
2109
2263
  isLocal: false
2110
2264
  },
@@ -2112,7 +2266,7 @@ var init_constants = __esm({
2112
2266
  id: "gemini-2.5-pro",
2113
2267
  name: "Gemini 2.5 Pro",
2114
2268
  provider: "gemini",
2115
- contextWindow: 1e6,
2269
+ contextWindow: 1048576,
2116
2270
  isVisionCapable: true,
2117
2271
  inputCostPer1kTokens: 125e-5,
2118
2272
  outputCostPer1kTokens: 0.01,
@@ -2124,10 +2278,22 @@ var init_constants = __esm({
2124
2278
  id: "gemini-2.5-flash",
2125
2279
  name: "Gemini 2.5 Flash",
2126
2280
  provider: "gemini",
2127
- contextWindow: 1e6,
2281
+ contextWindow: 1048576,
2128
2282
  isVisionCapable: true,
2129
- inputCostPer1kTokens: 15e-5,
2130
- outputCostPer1kTokens: 6e-4,
2283
+ inputCostPer1kTokens: 3e-4,
2284
+ outputCostPer1kTokens: 25e-4,
2285
+ maxOutputTokens: 65536,
2286
+ supportsStreaming: true,
2287
+ isLocal: false
2288
+ },
2289
+ "gemini-2.5-flash-lite": {
2290
+ id: "gemini-2.5-flash-lite",
2291
+ name: "Gemini 2.5 Flash-Lite",
2292
+ provider: "gemini",
2293
+ contextWindow: 1048576,
2294
+ isVisionCapable: true,
2295
+ inputCostPer1kTokens: 1e-4,
2296
+ outputCostPer1kTokens: 4e-4,
2131
2297
  maxOutputTokens: 65536,
2132
2298
  supportsStreaming: true,
2133
2299
  isLocal: false
@@ -2197,43 +2363,57 @@ var init_constants = __esm({
2197
2363
  _m.pricingUnknown = false;
2198
2364
  }
2199
2365
  T1_MODEL_PRIORITY = [
2366
+ "gpt-5.6-sol",
2367
+ "claude-fable-5",
2368
+ "claude-opus-5",
2200
2369
  "gpt-5.5",
2201
2370
  "gpt-5.4",
2371
+ "gemini-3.1-pro-preview",
2372
+ "claude-opus-4-8",
2373
+ "claude-sonnet-5",
2374
+ "gemini-3.7-flash",
2375
+ "gpt-5.2",
2202
2376
  "gpt-5",
2203
- "claude-opus-4",
2204
- "claude-sonnet-4",
2205
2377
  "gemini-2.5-pro",
2206
2378
  "gpt-4.1",
2207
- "gpt-4o",
2208
- "gemini-1.5-pro"
2379
+ "gpt-4o"
2209
2380
  ];
2210
2381
  T2_MODEL_PRIORITY = [
2382
+ "gpt-5.6-terra",
2383
+ "claude-sonnet-5",
2384
+ "gemini-3.7-flash",
2385
+ "gemini-3.6-flash",
2211
2386
  "gpt-5.4-mini",
2212
2387
  "gpt-5-mini",
2213
- "claude-sonnet-4",
2388
+ "claude-sonnet-4-6",
2214
2389
  "claude-haiku-4-5",
2215
- "claude-haiku-3-5",
2390
+ "gemini-3.5-flash",
2216
2391
  "gemini-2.5-flash",
2217
2392
  "gpt-4.1-mini",
2218
2393
  "gpt-4o-mini",
2219
- "gemini-2.0-flash",
2220
2394
  "llama3:70b"
2221
2395
  ];
2222
2396
  T3_MODEL_PRIORITY = [
2397
+ "gpt-5.6-luna",
2398
+ "gpt-5.4-nano",
2399
+ "gemini-3.5-flash-lite",
2400
+ "gemini-3.1-flash-lite",
2401
+ "claude-haiku-4-5",
2223
2402
  "gpt-5-nano",
2403
+ "gemini-2.5-flash-lite",
2224
2404
  "llama3.2:3b",
2225
2405
  "mistral:7b",
2226
- "claude-haiku-4-5",
2227
- "claude-haiku-3-5",
2228
2406
  "gpt-4.1-nano",
2229
2407
  "gpt-4o-mini",
2230
- "gemini-2.5-flash",
2231
- "gemini-2.0-flash"
2408
+ "gemini-2.5-flash"
2232
2409
  ];
2233
2410
  VISION_MODEL_PRIORITY = [
2234
- "claude-sonnet-4",
2411
+ "gpt-5.6-sol",
2412
+ "claude-fable-5",
2413
+ "claude-sonnet-5",
2414
+ "gemini-3.7-flash",
2235
2415
  "gpt-4o",
2236
- "gemini-2.0-flash",
2416
+ "gemini-2.5-flash",
2237
2417
  "llava"
2238
2418
  ];
2239
2419
  COMPLEXITY_T2_COUNT = {
@@ -2290,8 +2470,8 @@ function normalizeEndpoint(url) {
2290
2470
  if (!raw) return "";
2291
2471
  try {
2292
2472
  const u = new URL(raw);
2293
- const path40 = stripTrailingSlashes(`${u.pathname}${u.search}`);
2294
- return `${u.protocol}//${u.host.toLowerCase()}${path40}`;
2473
+ const path41 = stripTrailingSlashes(`${u.pathname}${u.search}`);
2474
+ return `${u.protocol}//${u.host.toLowerCase()}${path41}`;
2295
2475
  } catch {
2296
2476
  return raw.toLowerCase();
2297
2477
  }
@@ -2423,7 +2603,7 @@ var CLIENT_VERSION_SEGMENT, REDIRECT_STATUSES, NULL_BODY_STATUSES, MAX_REDIRECTS
2423
2603
  var init_net = __esm({
2424
2604
  "src/utils/net.ts"() {
2425
2605
  try {
2426
- dns__default.default.setDefaultResultOrder("ipv4first");
2606
+ dnsCallback__default.default.setDefaultResultOrder("ipv4first");
2427
2607
  } catch {
2428
2608
  }
2429
2609
  CLIENT_VERSION_SEGMENT = {
@@ -3155,6 +3335,56 @@ var init_openai = __esm({
3155
3335
  }
3156
3336
  });
3157
3337
 
3338
+ // src/providers/cloud-model-catalog.json
3339
+ var cloud_model_catalog_default;
3340
+ var init_cloud_model_catalog = __esm({
3341
+ "src/providers/cloud-model-catalog.json"() {
3342
+ cloud_model_catalog_default = {
3343
+ schemaVersion: 1,
3344
+ verifiedAt: "2026-08-27",
3345
+ scope: "GA and Preview text/chat models that Cascade can route through its maintained cloud providers. Retired, shutdown, realtime/audio-only, embedding, image-only, video-only, and access-restricted models are excluded.",
3346
+ note: "Capabilities belong to the canonical model. Pricing deliberately does not live here: Cascade resolves price by model x provider x region in core/router/pricing-data.json. A model exposed by multiple providers may therefore have different prices without duplicating its capability profile.",
3347
+ models: [
3348
+ { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
3349
+ { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
3350
+ { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
3351
+ { id: "gpt-5.5", name: "GPT-5.5", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
3352
+ { id: "gpt-5.4", name: "GPT-5.4", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
3353
+ { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
3354
+ { id: "gpt-5.4-nano", name: "GPT-5.4 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
3355
+ { id: "gpt-5", name: "GPT-5", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
3356
+ { id: "gpt-5-mini", name: "GPT-5 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
3357
+ { id: "gpt-5-nano", name: "GPT-5 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
3358
+ { id: "gpt-4.1", name: "GPT-4.1", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
3359
+ { id: "gpt-4.1-mini", name: "GPT-4.1 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
3360
+ { id: "gpt-4.1-nano", name: "GPT-4.1 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
3361
+ { id: "gpt-4o", name: "GPT-4o", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 128e3, maxOutputTokens: 16384, vision: true, toolUse: true },
3362
+ { id: "gpt-4o-mini", name: "GPT-4o Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 128e3, maxOutputTokens: 16384, vision: true, toolUse: true },
3363
+ { id: "claude-fable-5", name: "Claude Fable 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3364
+ { id: "claude-opus-5", name: "Claude Opus 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3365
+ { id: "claude-opus-4-8", name: "Claude Opus 4.8", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3366
+ { id: "claude-opus-4-7", name: "Claude Opus 4.7", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3367
+ { id: "claude-opus-4-6", name: "Claude Opus 4.6", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, vision: true, toolUse: true },
3368
+ { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, vision: true, toolUse: true },
3369
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3370
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3371
+ { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, vision: true, toolUse: true },
3372
+ { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, maxOutputTokens: 64e3, vision: true, toolUse: true },
3373
+ { id: "gemini-3.7-flash", name: "Gemini 3.7 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3374
+ { id: "gemini-3.6-flash", name: "Gemini 3.6 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3375
+ { id: "gemini-3.5-flash", name: "Gemini 3.5 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3376
+ { id: "gemini-3.5-flash-lite", name: "Gemini 3.5 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3377
+ { id: "gemini-3.1-flash-lite", name: "Gemini 3.1 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3378
+ { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro", providers: ["gemini"], lifecycle: "preview", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3379
+ { id: "gemini-3-flash-preview", name: "Gemini 3 Flash", providers: ["gemini"], lifecycle: "preview", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3380
+ { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3381
+ { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3382
+ { id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true }
3383
+ ]
3384
+ };
3385
+ }
3386
+ });
3387
+
3158
3388
  // src/providers/azure.ts
3159
3389
  var azure_exports = {};
3160
3390
  __export(azure_exports, {
@@ -3163,36 +3393,81 @@ __export(azure_exports, {
3163
3393
  azureModelForDeployment: () => azureModelForDeployment,
3164
3394
  inferAzureBaseModel: () => inferAzureBaseModel
3165
3395
  });
3396
+ function compactModelId(id) {
3397
+ return id.toLowerCase().replace(/[^a-z0-9.]/g, "");
3398
+ }
3399
+ function catalogEntry(id) {
3400
+ const n = id.toLowerCase();
3401
+ return CLOUD_MODELS.find((m) => m.id.toLowerCase() === n);
3402
+ }
3166
3403
  function inferAzureBaseModel(deploymentName) {
3167
3404
  const n = deploymentName.toLowerCase();
3168
- for (const [re, base] of AZURE_BASE_MODEL_RULES) if (re.test(n)) return base;
3405
+ const compact = compactModelId(n);
3406
+ const hasBareGpt56Alias = compact.includes("gpt5.6");
3407
+ for (const model of AZURE_CATALOG) {
3408
+ if (hasBareGpt56Alias && model.id === "gpt-5") continue;
3409
+ if (n.includes(model.id.toLowerCase()) || compact.includes(compactModelId(model.id))) {
3410
+ return model.id;
3411
+ }
3412
+ }
3413
+ if (hasBareGpt56Alias) return "gpt-5.6-sol";
3414
+ const gpt5At = compact.indexOf("gpt5");
3415
+ if (gpt5At !== -1) {
3416
+ const suffix = compact.slice(gpt5At + "gpt5".length);
3417
+ if (suffix.includes("nano")) return "gpt-5-nano";
3418
+ if (suffix.includes("mini")) return "gpt-5-mini";
3419
+ return "gpt-5";
3420
+ }
3421
+ if (compact.includes("gpt4.1nano")) return "gpt-4.1-nano";
3422
+ if (compact.includes("gpt4.1mini")) return "gpt-4.1-mini";
3423
+ if (compact.includes("gpt4.1")) return "gpt-4.1";
3424
+ if (compact.includes("gpt4omini")) return "gpt-4o-mini";
3425
+ if (compact.includes("gpt4o")) return "gpt-4o";
3169
3426
  return null;
3170
3427
  }
3428
+ function catalogModelInfo(baseModelId) {
3429
+ const entry = catalogEntry(baseModelId);
3430
+ if (!entry?.contextWindow) return void 0;
3431
+ return {
3432
+ id: entry.id,
3433
+ name: entry.name,
3434
+ provider: "openai",
3435
+ contextWindow: entry.contextWindow,
3436
+ isVisionCapable: entry.vision ?? false,
3437
+ inputCostPer1kTokens: 0,
3438
+ outputCostPer1kTokens: 0,
3439
+ maxOutputTokens: entry.maxOutputTokens ?? 16e3,
3440
+ supportsStreaming: true,
3441
+ isLocal: false,
3442
+ supportsToolUse: entry.toolUse ?? true,
3443
+ pricingUnknown: true
3444
+ };
3445
+ }
3171
3446
  function azureModelForDeployment(cfg) {
3172
3447
  if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
3173
3448
  const id = cfg.deploymentName.trim();
3174
3449
  const name = cfg.label?.trim() || id;
3175
3450
  const baseModelId = cfg.model?.trim() || inferAzureBaseModel(id) || void 0;
3176
- const base = baseModelId ? MODELS[baseModelId] : void 0;
3177
- if (base) {
3451
+ const base = baseModelId ? MODELS[baseModelId] ?? catalogModelInfo(baseModelId) : void 0;
3452
+ if (base && baseModelId) {
3178
3453
  const azurePrice = resolvePricing(
3179
3454
  { id, provider: "azure", isLocal: false, baseModelId },
3180
3455
  { region: cfg.region }
3181
3456
  );
3457
+ const exactAzurePrice = !azurePrice.unknown && !azurePrice.estimatedFromProvider;
3458
+ const nativeToolsOnThisTransport = /^gpt-5\.6-(?:sol|terra|luna)$/i.test(baseModelId) ? false : base.supportsToolUse ?? true;
3182
3459
  return {
3183
3460
  ...base,
3184
3461
  id,
3185
- // callable deployment name
3186
3462
  name,
3187
3463
  provider: "azure",
3188
3464
  baseModelId,
3189
- // real identity for benchmark + live pricing
3190
- supportsToolUse: base.supportsToolUse ?? true,
3191
- ...azurePrice.unknown ? {} : {
3465
+ supportsToolUse: nativeToolsOnThisTransport,
3466
+ ...!azurePrice.unknown ? {
3192
3467
  inputCostPer1kTokens: azurePrice.input,
3193
3468
  outputCostPer1kTokens: azurePrice.output,
3194
- pricingUnknown: false
3195
- }
3469
+ pricingUnknown: !exactAzurePrice
3470
+ } : { pricingUnknown: true }
3196
3471
  };
3197
3472
  }
3198
3473
  return {
@@ -3207,39 +3482,34 @@ function azureModelForDeployment(cfg) {
3207
3482
  maxOutputTokens: 16e3,
3208
3483
  supportsStreaming: true,
3209
3484
  isLocal: false,
3210
- supportsToolUse: true
3485
+ supportsToolUse: true,
3486
+ pricingUnknown: true
3211
3487
  };
3212
3488
  }
3213
- var DEFAULT_AZURE_API_VERSION, AZURE_BASE_MODEL_RULES, AZURE_BASE_MODELS, AzureOpenAIProvider;
3489
+ function stripTrailingSlashes3(value) {
3490
+ let end = value.length;
3491
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
3492
+ return value.slice(0, end);
3493
+ }
3494
+ var DEFAULT_AZURE_API_VERSION, CLOUD_MODELS, AZURE_CATALOG, AZURE_BASE_MODELS, AzureOpenAIProvider;
3214
3495
  var init_azure = __esm({
3215
3496
  "src/providers/azure.ts"() {
3216
3497
  init_constants();
3217
3498
  init_openai();
3218
3499
  init_pricing();
3500
+ init_cloud_model_catalog();
3219
3501
  DEFAULT_AZURE_API_VERSION = "2024-12-01-preview";
3220
- AZURE_BASE_MODEL_RULES = [
3221
- [/gpt-?5\.5/, "gpt-5.5"],
3222
- [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
3223
- [/gpt-?5\.4/, "gpt-5.4"],
3224
- [/gpt-?5.*nano/, "gpt-5-nano"],
3225
- [/gpt-?5.*mini/, "gpt-5-mini"],
3226
- [/gpt-?5/, "gpt-5"],
3227
- [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
3228
- [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
3229
- [/gpt-?4\.1/, "gpt-4.1"],
3230
- [/gpt-?4o-mini/, "gpt-4o-mini"],
3231
- [/gpt-?4o/, "gpt-4o"]
3232
- ];
3233
- AZURE_BASE_MODELS = AZURE_BASE_MODEL_RULES.map(([, id]) => id);
3502
+ CLOUD_MODELS = cloud_model_catalog_default.models;
3503
+ AZURE_CATALOG = CLOUD_MODELS.filter((m) => m.providers.includes("azure")).slice().sort((a, b) => b.id.length - a.id.length || a.id.localeCompare(b.id));
3504
+ AZURE_BASE_MODELS = AZURE_CATALOG.map((m) => m.id);
3234
3505
  AzureOpenAIProvider = class extends OpenAIProvider {
3235
3506
  constructor(config, model) {
3236
3507
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
3237
- const endpoint = rawUrl.replace(/\/+$/, "");
3508
+ const endpoint = stripTrailingSlashes3(rawUrl);
3238
3509
  super(
3239
3510
  {
3240
3511
  ...config,
3241
3512
  baseUrl: endpoint
3242
- // Kept for superclass compatibility if it reads it
3243
3513
  },
3244
3514
  model
3245
3515
  );
@@ -4223,16 +4493,16 @@ var require_isarray = __commonJS({
4223
4493
  }
4224
4494
  });
4225
4495
 
4226
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/stream.js
4496
+ // node_modules/readable-stream/lib/internal/streams/stream.js
4227
4497
  var require_stream = __commonJS({
4228
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/stream.js"(exports, module) {
4498
+ "node_modules/readable-stream/lib/internal/streams/stream.js"(exports, module) {
4229
4499
  module.exports = __require("stream");
4230
4500
  }
4231
4501
  });
4232
4502
 
4233
- // node_modules/jszip/node_modules/safe-buffer/index.js
4503
+ // node_modules/readable-stream/node_modules/safe-buffer/index.js
4234
4504
  var require_safe_buffer = __commonJS({
4235
- "node_modules/jszip/node_modules/safe-buffer/index.js"(exports, module) {
4505
+ "node_modules/readable-stream/node_modules/safe-buffer/index.js"(exports, module) {
4236
4506
  var buffer = __require("buffer");
4237
4507
  var Buffer2 = buffer.Buffer;
4238
4508
  function copyProps(src, dst) {
@@ -4350,7 +4620,7 @@ var require_util = __commonJS({
4350
4620
  typeof arg === "undefined";
4351
4621
  }
4352
4622
  exports.isPrimitive = isPrimitive;
4353
- exports.isBuffer = Buffer.isBuffer;
4623
+ exports.isBuffer = __require("buffer").Buffer.isBuffer;
4354
4624
  function objectToString(o) {
4355
4625
  return Object.prototype.toString.call(o);
4356
4626
  }
@@ -4403,9 +4673,9 @@ var require_inherits = __commonJS({
4403
4673
  }
4404
4674
  });
4405
4675
 
4406
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/BufferList.js
4676
+ // node_modules/readable-stream/lib/internal/streams/BufferList.js
4407
4677
  var require_BufferList = __commonJS({
4408
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/BufferList.js"(exports, module) {
4678
+ "node_modules/readable-stream/lib/internal/streams/BufferList.js"(exports, module) {
4409
4679
  function _classCallCheck(instance, Constructor) {
4410
4680
  if (!(instance instanceof Constructor)) {
4411
4681
  throw new TypeError("Cannot call a class as a function");
@@ -4480,9 +4750,9 @@ var require_BufferList = __commonJS({
4480
4750
  }
4481
4751
  });
4482
4752
 
4483
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/destroy.js
4753
+ // node_modules/readable-stream/lib/internal/streams/destroy.js
4484
4754
  var require_destroy = __commonJS({
4485
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/destroy.js"(exports, module) {
4755
+ "node_modules/readable-stream/lib/internal/streams/destroy.js"(exports, module) {
4486
4756
  var pna = require_process_nextick_args();
4487
4757
  function destroy(err, cb) {
4488
4758
  var _this = this;
@@ -4555,9 +4825,9 @@ var require_node = __commonJS({
4555
4825
  }
4556
4826
  });
4557
4827
 
4558
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_writable.js
4828
+ // node_modules/readable-stream/lib/_stream_writable.js
4559
4829
  var require_stream_writable = __commonJS({
4560
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_writable.js"(exports, module) {
4830
+ "node_modules/readable-stream/lib/_stream_writable.js"(exports, module) {
4561
4831
  var pna = require_process_nextick_args();
4562
4832
  module.exports = Writable;
4563
4833
  function CorkedRequest(state) {
@@ -4994,9 +5264,9 @@ var require_stream_writable = __commonJS({
4994
5264
  }
4995
5265
  });
4996
5266
 
4997
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_duplex.js
5267
+ // node_modules/readable-stream/lib/_stream_duplex.js
4998
5268
  var require_stream_duplex = __commonJS({
4999
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_duplex.js"(exports, module) {
5269
+ "node_modules/readable-stream/lib/_stream_duplex.js"(exports, module) {
5000
5270
  var pna = require_process_nextick_args();
5001
5271
  var objectKeys = Object.keys || function(obj) {
5002
5272
  var keys2 = [];
@@ -5070,10 +5340,67 @@ var require_stream_duplex = __commonJS({
5070
5340
  }
5071
5341
  });
5072
5342
 
5073
- // node_modules/jszip/node_modules/string_decoder/lib/string_decoder.js
5343
+ // node_modules/string_decoder/node_modules/safe-buffer/index.js
5344
+ var require_safe_buffer2 = __commonJS({
5345
+ "node_modules/string_decoder/node_modules/safe-buffer/index.js"(exports, module) {
5346
+ var buffer = __require("buffer");
5347
+ var Buffer2 = buffer.Buffer;
5348
+ function copyProps(src, dst) {
5349
+ for (var key in src) {
5350
+ dst[key] = src[key];
5351
+ }
5352
+ }
5353
+ if (Buffer2.from && Buffer2.alloc && Buffer2.allocUnsafe && Buffer2.allocUnsafeSlow) {
5354
+ module.exports = buffer;
5355
+ } else {
5356
+ copyProps(buffer, exports);
5357
+ exports.Buffer = SafeBuffer;
5358
+ }
5359
+ function SafeBuffer(arg, encodingOrOffset, length) {
5360
+ return Buffer2(arg, encodingOrOffset, length);
5361
+ }
5362
+ copyProps(Buffer2, SafeBuffer);
5363
+ SafeBuffer.from = function(arg, encodingOrOffset, length) {
5364
+ if (typeof arg === "number") {
5365
+ throw new TypeError("Argument must not be a number");
5366
+ }
5367
+ return Buffer2(arg, encodingOrOffset, length);
5368
+ };
5369
+ SafeBuffer.alloc = function(size, fill, encoding) {
5370
+ if (typeof size !== "number") {
5371
+ throw new TypeError("Argument must be a number");
5372
+ }
5373
+ var buf = Buffer2(size);
5374
+ if (fill !== void 0) {
5375
+ if (typeof encoding === "string") {
5376
+ buf.fill(fill, encoding);
5377
+ } else {
5378
+ buf.fill(fill);
5379
+ }
5380
+ } else {
5381
+ buf.fill(0);
5382
+ }
5383
+ return buf;
5384
+ };
5385
+ SafeBuffer.allocUnsafe = function(size) {
5386
+ if (typeof size !== "number") {
5387
+ throw new TypeError("Argument must be a number");
5388
+ }
5389
+ return Buffer2(size);
5390
+ };
5391
+ SafeBuffer.allocUnsafeSlow = function(size) {
5392
+ if (typeof size !== "number") {
5393
+ throw new TypeError("Argument must be a number");
5394
+ }
5395
+ return buffer.SlowBuffer(size);
5396
+ };
5397
+ }
5398
+ });
5399
+
5400
+ // node_modules/string_decoder/lib/string_decoder.js
5074
5401
  var require_string_decoder = __commonJS({
5075
- "node_modules/jszip/node_modules/string_decoder/lib/string_decoder.js"(exports) {
5076
- var Buffer2 = require_safe_buffer().Buffer;
5402
+ "node_modules/string_decoder/lib/string_decoder.js"(exports) {
5403
+ var Buffer2 = require_safe_buffer2().Buffer;
5077
5404
  var isEncoding = Buffer2.isEncoding || function(encoding) {
5078
5405
  encoding = "" + encoding;
5079
5406
  switch (encoding && encoding.toLowerCase()) {
@@ -5307,9 +5634,9 @@ var require_string_decoder = __commonJS({
5307
5634
  }
5308
5635
  });
5309
5636
 
5310
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_readable.js
5637
+ // node_modules/readable-stream/lib/_stream_readable.js
5311
5638
  var require_stream_readable = __commonJS({
5312
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_readable.js"(exports, module) {
5639
+ "node_modules/readable-stream/lib/_stream_readable.js"(exports, module) {
5313
5640
  var pna = require_process_nextick_args();
5314
5641
  module.exports = Readable2;
5315
5642
  var isArray = require_isarray();
@@ -5992,9 +6319,9 @@ var require_stream_readable = __commonJS({
5992
6319
  }
5993
6320
  });
5994
6321
 
5995
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_transform.js
6322
+ // node_modules/readable-stream/lib/_stream_transform.js
5996
6323
  var require_stream_transform = __commonJS({
5997
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_transform.js"(exports, module) {
6324
+ "node_modules/readable-stream/lib/_stream_transform.js"(exports, module) {
5998
6325
  module.exports = Transform;
5999
6326
  var Duplex = require_stream_duplex();
6000
6327
  var util = Object.create(require_util());
@@ -6091,9 +6418,9 @@ var require_stream_transform = __commonJS({
6091
6418
  }
6092
6419
  });
6093
6420
 
6094
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_passthrough.js
6421
+ // node_modules/readable-stream/lib/_stream_passthrough.js
6095
6422
  var require_stream_passthrough = __commonJS({
6096
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_passthrough.js"(exports, module) {
6423
+ "node_modules/readable-stream/lib/_stream_passthrough.js"(exports, module) {
6097
6424
  module.exports = PassThrough;
6098
6425
  var Transform = require_stream_transform();
6099
6426
  var util = Object.create(require_util());
@@ -6109,9 +6436,9 @@ var require_stream_passthrough = __commonJS({
6109
6436
  }
6110
6437
  });
6111
6438
 
6112
- // node_modules/jszip/node_modules/readable-stream/readable.js
6439
+ // node_modules/readable-stream/readable.js
6113
6440
  var require_readable = __commonJS({
6114
- "node_modules/jszip/node_modules/readable-stream/readable.js"(exports, module) {
6441
+ "node_modules/readable-stream/readable.js"(exports, module) {
6115
6442
  var Stream = __require("stream");
6116
6443
  if (process.env.READABLE_STREAM === "disable" && Stream) {
6117
6444
  module.exports = Stream;
@@ -7020,8 +7347,8 @@ var require_utils = __commonJS({
7020
7347
  var result = transform[inputType][outputType](input);
7021
7348
  return result;
7022
7349
  };
7023
- exports.resolve = function(path40) {
7024
- var parts = path40.split("/");
7350
+ exports.resolve = function(path41) {
7351
+ var parts = path41.split("/");
7025
7352
  var result = [];
7026
7353
  for (var index = 0; index < parts.length; index++) {
7027
7354
  var part = parts[index];
@@ -12836,18 +13163,18 @@ var require_object = __commonJS({
12836
13163
  var object = new ZipObject(name, zipObjectContent, o);
12837
13164
  this.files[name] = object;
12838
13165
  };
12839
- var parentFolder = function(path40) {
12840
- if (path40.slice(-1) === "/") {
12841
- path40 = path40.substring(0, path40.length - 1);
13166
+ var parentFolder = function(path41) {
13167
+ if (path41.slice(-1) === "/") {
13168
+ path41 = path41.substring(0, path41.length - 1);
12842
13169
  }
12843
- var lastSlash = path40.lastIndexOf("/");
12844
- return lastSlash > 0 ? path40.substring(0, lastSlash) : "";
13170
+ var lastSlash = path41.lastIndexOf("/");
13171
+ return lastSlash > 0 ? path41.substring(0, lastSlash) : "";
12845
13172
  };
12846
- var forceTrailingSlash = function(path40) {
12847
- if (path40.slice(-1) !== "/") {
12848
- path40 += "/";
13173
+ var forceTrailingSlash = function(path41) {
13174
+ if (path41.slice(-1) !== "/") {
13175
+ path41 += "/";
12849
13176
  }
12850
- return path40;
13177
+ return path41;
12851
13178
  };
12852
13179
  var folderAdd = function(name, createFolders) {
12853
13180
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -13846,12 +14173,12 @@ var init_audit_logger = __esm({
13846
14173
  constructor(workspacePath, debugMode = false) {
13847
14174
  this.workspacePath = workspacePath;
13848
14175
  this.debugMode = debugMode;
13849
- const cascadeDir = path35__default.default.join(workspacePath, ".cascade");
13850
- if (!fs32__default.default.existsSync(cascadeDir)) {
13851
- fs32__default.default.mkdirSync(cascadeDir, { recursive: true });
14176
+ const cascadeDir = path36__default.default.join(workspacePath, ".cascade");
14177
+ if (!fs33__default.default.existsSync(cascadeDir)) {
14178
+ fs33__default.default.mkdirSync(cascadeDir, { recursive: true });
13852
14179
  }
13853
- this.keyPath = path35__default.default.join(cascadeDir, "audit_log.key");
13854
- this.dbPath = path35__default.default.join(cascadeDir, "audit_log.db");
14180
+ this.keyPath = path36__default.default.join(cascadeDir, "audit_log.key");
14181
+ this.dbPath = path36__default.default.join(cascadeDir, "audit_log.db");
13855
14182
  this.initEncryptionKey();
13856
14183
  this.db = new Database2__default.default(this.dbPath);
13857
14184
  this.db.pragma("journal_mode = WAL");
@@ -13881,11 +14208,11 @@ var init_audit_logger = __esm({
13881
14208
  return crypto__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
13882
14209
  }
13883
14210
  initEncryptionKey() {
13884
- if (fs32__default.default.existsSync(this.keyPath)) {
13885
- this.encryptionKey = fs32__default.default.readFileSync(this.keyPath);
14211
+ if (fs33__default.default.existsSync(this.keyPath)) {
14212
+ this.encryptionKey = fs33__default.default.readFileSync(this.keyPath);
13886
14213
  } else {
13887
14214
  this.encryptionKey = crypto__default.default.randomBytes(32);
13888
- fs32__default.default.writeFileSync(this.keyPath, this.encryptionKey);
14215
+ fs33__default.default.writeFileSync(this.keyPath, this.encryptionKey);
13889
14216
  }
13890
14217
  }
13891
14218
  encrypt(text) {
@@ -13963,9 +14290,9 @@ var init_audit_logger = __esm({
13963
14290
  dumpDebugIfNeeded() {
13964
14291
  if (!this.debugMode) return;
13965
14292
  try {
13966
- const dumpPath = path35__default.default.join(os10__default.default.tmpdir(), "cascade_audit_logs_debug.json");
14293
+ const dumpPath = path36__default.default.join(os10__default.default.tmpdir(), "cascade_audit_logs_debug.json");
13967
14294
  const entries = this.getAllLogs();
13968
- fs32__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
14295
+ fs33__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
13969
14296
  } catch (err) {
13970
14297
  console.error("Failed to dump debug audit logs", err);
13971
14298
  }
@@ -14020,7 +14347,7 @@ var Keystore = class {
14020
14347
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
14021
14348
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
14022
14349
  this.backend = "keytar";
14023
- if (password && fs32__default.default.existsSync(this.storePath)) {
14350
+ if (password && fs33__default.default.existsSync(this.storePath)) {
14024
14351
  try {
14025
14352
  const fileEntries = this.decryptFile(password);
14026
14353
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -14039,7 +14366,7 @@ var Keystore = class {
14039
14366
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
14040
14367
  );
14041
14368
  }
14042
- if (!fs32__default.default.existsSync(this.storePath)) {
14369
+ if (!fs33__default.default.existsSync(this.storePath)) {
14043
14370
  const salt = crypto__default.default.randomBytes(SALT_LEN);
14044
14371
  this.masterKey = this.deriveKey(password, salt);
14045
14372
  this.writeWithSalt({}, salt);
@@ -14053,7 +14380,7 @@ var Keystore = class {
14053
14380
  }
14054
14381
  /** Synchronous legacy unlock kept for AES-only environments. */
14055
14382
  unlockSync(password) {
14056
- if (!fs32__default.default.existsSync(this.storePath)) {
14383
+ if (!fs33__default.default.existsSync(this.storePath)) {
14057
14384
  const salt = crypto__default.default.randomBytes(SALT_LEN);
14058
14385
  this.masterKey = this.deriveKey(password, salt);
14059
14386
  this.writeWithSalt({}, salt);
@@ -14111,7 +14438,7 @@ var Keystore = class {
14111
14438
  }
14112
14439
  }
14113
14440
  decryptFile(password, knownSalt) {
14114
- if (!fs32__default.default.existsSync(this.storePath)) return {};
14441
+ if (!fs33__default.default.existsSync(this.storePath)) return {};
14115
14442
  try {
14116
14443
  const { salt, ciphertext, iv, tag } = this.readRaw();
14117
14444
  const useSalt = knownSalt ?? salt;
@@ -14133,8 +14460,8 @@ var Keystore = class {
14133
14460
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
14134
14461
  const tag = cipher.getAuthTag();
14135
14462
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
14136
- fs32__default.default.mkdirSync(path35__default.default.dirname(this.storePath), { recursive: true });
14137
- fs32__default.default.writeFileSync(this.storePath, out, { mode: 384 });
14463
+ fs33__default.default.mkdirSync(path36__default.default.dirname(this.storePath), { recursive: true });
14464
+ fs33__default.default.writeFileSync(this.storePath, out, { mode: 384 });
14138
14465
  }
14139
14466
  writeWithSalt(data, salt) {
14140
14467
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -14144,11 +14471,11 @@ var Keystore = class {
14144
14471
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
14145
14472
  const tag = cipher.getAuthTag();
14146
14473
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
14147
- fs32__default.default.mkdirSync(path35__default.default.dirname(this.storePath), { recursive: true });
14148
- fs32__default.default.writeFileSync(this.storePath, out, { mode: 384 });
14474
+ fs33__default.default.mkdirSync(path36__default.default.dirname(this.storePath), { recursive: true });
14475
+ fs33__default.default.writeFileSync(this.storePath, out, { mode: 384 });
14149
14476
  }
14150
14477
  readRaw() {
14151
- const buf = fs32__default.default.readFileSync(this.storePath);
14478
+ const buf = fs33__default.default.readFileSync(this.storePath);
14152
14479
  let offset = 0;
14153
14480
  const salt = buf.subarray(offset, offset + SALT_LEN);
14154
14481
  offset += SALT_LEN;
@@ -14181,7 +14508,7 @@ var CascadeIgnore = class {
14181
14508
  ]);
14182
14509
  }
14183
14510
  async load(workspacePath) {
14184
- const filePath = path35__default.default.join(workspacePath, ".cascadeignore");
14511
+ const filePath = path36__default.default.join(workspacePath, ".cascadeignore");
14185
14512
  try {
14186
14513
  const content = await fs11__default.default.readFile(filePath, "utf-8");
14187
14514
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -14192,7 +14519,7 @@ var CascadeIgnore = class {
14192
14519
  }
14193
14520
  isIgnored(filePath, workspacePath) {
14194
14521
  try {
14195
- const relative = workspacePath ? path35__default.default.relative(workspacePath, filePath) : filePath;
14522
+ const relative = workspacePath ? path36__default.default.relative(workspacePath, filePath) : filePath;
14196
14523
  return this.ig.ignores(relative);
14197
14524
  } catch {
14198
14525
  return false;
@@ -14203,7 +14530,7 @@ var CascadeIgnore = class {
14203
14530
  }
14204
14531
  };
14205
14532
  async function createDefaultIgnoreFile(workspacePath) {
14206
- const filePath = path35__default.default.join(workspacePath, ".cascadeignore");
14533
+ const filePath = path36__default.default.join(workspacePath, ".cascadeignore");
14207
14534
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
14208
14535
  # Syntax identical to .gitignore
14209
14536
 
@@ -14233,7 +14560,7 @@ Thumbs.db
14233
14560
  await fs11__default.default.writeFile(filePath, content, "utf-8");
14234
14561
  }
14235
14562
  async function loadCascadeMd(workspacePath) {
14236
- const filePath = path35__default.default.join(workspacePath, "CASCADE.md");
14563
+ const filePath = path36__default.default.join(workspacePath, "CASCADE.md");
14237
14564
  try {
14238
14565
  const raw = await fs11__default.default.readFile(filePath, "utf-8");
14239
14566
  return parseCascadeMd(raw);
@@ -14262,7 +14589,7 @@ ${raw.trim()}`;
14262
14589
  return { raw, sections, systemPrompt };
14263
14590
  }
14264
14591
  async function createDefaultCascadeMd(workspacePath) {
14265
- const filePath = path35__default.default.join(workspacePath, "CASCADE.md");
14592
+ const filePath = path36__default.default.join(workspacePath, "CASCADE.md");
14266
14593
  const content = `# Cascade Project Instructions
14267
14594
 
14268
14595
  This file contains project-specific instructions for the Cascade AI agent.
@@ -14297,7 +14624,7 @@ List any areas the agent should not touch.
14297
14624
  var MemoryStore = class _MemoryStore {
14298
14625
  db;
14299
14626
  constructor(dbPath) {
14300
- fs32__default.default.mkdirSync(path35__default.default.dirname(dbPath), { recursive: true });
14627
+ fs33__default.default.mkdirSync(path36__default.default.dirname(dbPath), { recursive: true });
14301
14628
  try {
14302
14629
  this.db = new Database2__default.default(dbPath, { timeout: 5e3 });
14303
14630
  this.db.pragma("journal_mode = WAL");
@@ -15195,13 +15522,38 @@ var WebSearchConfigSchema = zod.z.object({
15195
15522
  /** Tavily API key — get one at https://tavily.com */
15196
15523
  tavilyApiKey: zod.z.string().optional(),
15197
15524
  /** Max results per search (default 5) */
15198
- maxResults: zod.z.number().default(5)
15525
+ maxResults: zod.z.number().default(5),
15526
+ /**
15527
+ * Send `searxngUrl` through the SSRF guard. Off for a local run, where the
15528
+ * URL is the machine owner's own config and a self-hosted SearXNG is
15529
+ * routinely on a private address; on for a host that accepts it from a
15530
+ * request. See `src/tools/web-search.ts`.
15531
+ *
15532
+ * It has to be named HERE, not just on the TypeScript interfaces: the config
15533
+ * passes through `CascadeConfigSchema.parse()` on its way into a run, and
15534
+ * zod drops every key this object does not declare.
15535
+ */
15536
+ guardSearxngUrl: zod.z.boolean().optional()
15199
15537
  });
15200
15538
  var ToolsConfigSchema = zod.z.object({
15201
15539
  shellAllowlist: zod.z.array(zod.z.string()).default([]),
15202
15540
  shellBlocklist: zod.z.array(zod.z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
15203
15541
  requireApprovalFor: zod.z.array(zod.z.string()).default([]),
15204
15542
  browserEnabled: zod.z.boolean().default(false),
15543
+ /**
15544
+ * Let a run ACT on the page open in the desktop's built-in browser — click,
15545
+ * type, submit — not just read it.
15546
+ *
15547
+ * Distinct from `browserEnabled`, which turns on the headless Playwright
15548
+ * tool. That one drives a throwaway browser with an empty cookie jar; this
15549
+ * one drives the session the user is signed into, which is a different order
15550
+ * of consequence and so gets its own switch rather than riding on that one.
15551
+ *
15552
+ * Off by default. Turning it on does not hand anything over silently: the
15553
+ * tool still goes through the approval chain, and the browser panel carries a
15554
+ * stop control for the run that is acting.
15555
+ */
15556
+ agentBrowserControl: zod.z.boolean().default(false),
15205
15557
  mcpServers: zod.z.array(McpServerConfigSchema).optional(),
15206
15558
  mcpTrusted: zod.z.array(zod.z.string()).optional(),
15207
15559
  /** Web search backends — at least one should be configured for best results */
@@ -15604,7 +15956,7 @@ function credentialEndpointsConflict(type, rowEndpoint, credentialEndpoint) {
15604
15956
 
15605
15957
  // src/config/global-credentials.ts
15606
15958
  function credentialsPath(globalDir2) {
15607
- return path35__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
15959
+ return path36__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
15608
15960
  }
15609
15961
  function providerKey(p) {
15610
15962
  return p.type;
@@ -15634,7 +15986,7 @@ function isPersistable(p) {
15634
15986
  }
15635
15987
  function loadGlobalCredentials(globalDir2) {
15636
15988
  try {
15637
- const raw = fs32__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
15989
+ const raw = fs33__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
15638
15990
  const parsed = JSON.parse(raw);
15639
15991
  if (!Array.isArray(parsed.providers)) return [];
15640
15992
  return parsed.providers.filter(
@@ -15646,13 +15998,13 @@ function loadGlobalCredentials(globalDir2) {
15646
15998
  }
15647
15999
  function saveGlobalCredentials(globalDir2, providers) {
15648
16000
  const filePath = credentialsPath(globalDir2);
15649
- fs32__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
16001
+ fs33__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
15650
16002
  const body = { version: 1, providers: providers.filter(isPersistable) };
15651
16003
  const tmp = `${filePath}.tmp`;
15652
- fs32__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
15653
- fs32__default.default.renameSync(tmp, filePath);
16004
+ fs33__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
16005
+ fs33__default.default.renameSync(tmp, filePath);
15654
16006
  try {
15655
- fs32__default.default.chmodSync(filePath, 384);
16007
+ fs33__default.default.chmodSync(filePath, 384);
15656
16008
  } catch {
15657
16009
  }
15658
16010
  }
@@ -16057,7 +16409,7 @@ var ConfigManager = class {
16057
16409
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
16058
16410
  constructor(workspacePath = process.cwd(), globalDirOverride) {
16059
16411
  this.workspacePath = workspacePath;
16060
- this.globalDir = globalDirOverride ?? path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR);
16412
+ this.globalDir = globalDirOverride ?? path36__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR);
16061
16413
  }
16062
16414
  async load() {
16063
16415
  this.config = await this.loadConfig();
@@ -16071,8 +16423,8 @@ var ConfigManager = class {
16071
16423
  this.ignore = new CascadeIgnore();
16072
16424
  await this.ignore.load(this.workspacePath);
16073
16425
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
16074
- this.keystore = new Keystore(path35__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
16075
- this.store = new MemoryStore(path35__default.default.join(this.workspacePath, CASCADE_DB_FILE));
16426
+ this.keystore = new Keystore(path36__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
16427
+ this.store = new MemoryStore(path36__default.default.join(this.workspacePath, CASCADE_DB_FILE));
16076
16428
  const globalRevoked = stripRevokedCredentials(loadGlobalCredentials(this.globalDir));
16077
16429
  const globalCreds = filterRetiredCredentials(globalRevoked.kept);
16078
16430
  if (globalRevoked.removed > 0) this.revokedCredentials += globalRevoked.removed;
@@ -16145,8 +16497,8 @@ var ConfigManager = class {
16145
16497
  * never reached the disk (see `commitSettings`).
16146
16498
  */
16147
16499
  async save(config = this.config) {
16148
- const configPath = path35__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16149
- await fs11__default.default.mkdir(path35__default.default.dirname(configPath), { recursive: true });
16500
+ const configPath = path36__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16501
+ await fs11__default.default.mkdir(path36__default.default.dirname(configPath), { recursive: true });
16150
16502
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
16151
16503
  try {
16152
16504
  saveGlobalCredentials(this.globalDir, config.providers);
@@ -16196,7 +16548,7 @@ var ConfigManager = class {
16196
16548
  this.revokedCredentials = 0;
16197
16549
  this.revokedPins = [];
16198
16550
  this.revokedNotice = void 0;
16199
- const configPath = path35__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16551
+ const configPath = path36__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16200
16552
  try {
16201
16553
  const raw = await fs11__default.default.readFile(configPath, "utf-8");
16202
16554
  const parsed = JSON.parse(raw);
@@ -16290,7 +16642,7 @@ var ConfigManager = class {
16290
16642
  return onResource;
16291
16643
  }
16292
16644
  async persistClearedPins(tiers) {
16293
- const configPath = path35__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16645
+ const configPath = path36__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16294
16646
  try {
16295
16647
  const raw = JSON.parse(await fs11__default.default.readFile(configPath, "utf-8"));
16296
16648
  const models = raw["models"];
@@ -17584,13 +17936,13 @@ var DeadModelStore = class {
17584
17936
  function fileDeadModelPersistence(filePath) {
17585
17937
  return {
17586
17938
  load() {
17587
- if (!fs32.existsSync(filePath)) return [];
17588
- const raw = JSON.parse(fs32.readFileSync(filePath, "utf-8"));
17939
+ if (!fs33.existsSync(filePath)) return [];
17940
+ const raw = JSON.parse(fs33.readFileSync(filePath, "utf-8"));
17589
17941
  return Array.isArray(raw) ? raw : [];
17590
17942
  },
17591
17943
  save(records) {
17592
- fs32.mkdirSync(path35.dirname(filePath), { recursive: true });
17593
- fs32.writeFileSync(filePath, JSON.stringify(records, null, 2));
17944
+ fs33.mkdirSync(path36.dirname(filePath), { recursive: true });
17945
+ fs33.writeFileSync(filePath, JSON.stringify(records, null, 2));
17594
17946
  }
17595
17947
  };
17596
17948
  }
@@ -18014,7 +18366,7 @@ init_pricing();
18014
18366
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
18015
18367
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
18016
18368
  var FETCH_TIMEOUT_MS = 8e3;
18017
- var DEFAULT_CACHE_FILE = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
18369
+ var DEFAULT_CACHE_FILE = path36__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
18018
18370
  function normalizeModelId2(id) {
18019
18371
  let s = id.toLowerCase();
18020
18372
  const slash = s.lastIndexOf("/");
@@ -18161,7 +18513,7 @@ var LiveDataProvider = class {
18161
18513
  }
18162
18514
  async saveCache() {
18163
18515
  try {
18164
- await fs11__default.default.mkdir(path35__default.default.dirname(this.opts.cacheFile), { recursive: true });
18516
+ await fs11__default.default.mkdir(path36__default.default.dirname(this.opts.cacheFile), { recursive: true });
18165
18517
  const cache = {
18166
18518
  fetchedAt: this.fetchedAt,
18167
18519
  snapshot: this.snapshot ?? void 0,
@@ -18268,9 +18620,15 @@ var FAMILY_BENCHMARKS = {
18268
18620
  "claude-opus": { code: 95, analysis: 92, creative: 90, data: 88 },
18269
18621
  "claude-sonnet": { code: 93, analysis: 88, creative: 87, data: 85 },
18270
18622
  "claude-haiku": { code: 80, analysis: 75, creative: 76, data: 72 },
18271
- // OpenAI GPT-5 family — current flagship; strongest all-round. Offline
18272
- // baseline (the live fetch refreshes these). Distinct point releases carry
18273
- // their own scores: gpt-5.5 "Spud" leads SWE-bench (88.7%); gpt-5.4 ~80%.
18623
+ // OpenAI GPT-5 family. GPT-5.6 is split by durable capability tier instead
18624
+ // of being folded into generic gpt-5: Sol, Terra and Luna have materially
18625
+ // different benchmark/cost envelopes and should compete independently.
18626
+ // Scores are the committed normalized snapshot; provenance is in
18627
+ // scripts/benchmarks/sources/openai-gpt-5.6-2026-08.json.
18628
+ "gpt-5.6-sol": { code: 100, analysis: 98, creative: 88, data: 88 },
18629
+ "gpt-5.6-terra": { code: 97, analysis: 92, creative: 82, data: 85 },
18630
+ "gpt-5.6-luna": { code: 93, analysis: 85, creative: 74, data: 60 },
18631
+ // Older GPT-5 point releases remain distinct where public data supports it.
18274
18632
  "gpt-5.5": { code: 97, analysis: 96, creative: 94, data: 95 },
18275
18633
  "gpt-5.4": { code: 94, analysis: 94, creative: 92, data: 92 },
18276
18634
  "gpt-5.4-mini": { code: 86, analysis: 85, creative: 85, data: 83 },
@@ -18306,13 +18664,19 @@ var FAMILY_BENCHMARKS = {
18306
18664
  "llama-small": { code: 55, analysis: 56, creative: 60, data: 54 }
18307
18665
  };
18308
18666
  var FAMILY_MATCHERS = [
18667
+ [/fable/i, "claude-opus"],
18309
18668
  [/opus/i, "claude-opus"],
18310
18669
  [/sonnet/i, "claude-sonnet"],
18311
18670
  [/haiku/i, "claude-haiku"],
18312
- // GPT-5 family — ordered most-specific first. Distinct point releases
18313
- // (gpt-5.5, gpt-5.4, gpt-5.4-mini) match their own family; nano/mini before
18314
- // the base; unrecognised gpt-5.x still fold into the gpt-5 base.
18671
+ // GPT-5 family — ordered most-specific first. GPT-5.6 durable tiers carry
18672
+ // their own profiles. 5.4 Nano intentionally maps to the nano class rather
18673
+ // than being swallowed by the generic 5.4 rule.
18674
+ [/gpt-?5\.6.*sol/i, "gpt-5.6-sol"],
18675
+ [/gpt-?5\.6.*terra/i, "gpt-5.6-terra"],
18676
+ [/gpt-?5\.6.*luna/i, "gpt-5.6-luna"],
18677
+ [/gpt-?5\.6(?:\s|$)/i, "gpt-5.6-sol"],
18315
18678
  [/gpt-?5\.5/i, "gpt-5.5"],
18679
+ [/gpt-?5\.4.*nano/i, "gpt-5-nano"],
18316
18680
  [/gpt-?5\.4.*mini/i, "gpt-5.4-mini"],
18317
18681
  [/gpt-?5\.4/i, "gpt-5.4"],
18318
18682
  [/gpt-?5.*nano/i, "gpt-5-nano"],
@@ -21114,6 +21478,23 @@ Now execute your subtask using this context where relevant.`
21114
21478
  this.setStatus("FAILED", finalOutput);
21115
21479
  this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput, "FAILED");
21116
21480
  return this.buildResult("ESCALATED", finalOutput, { checksRun, passed, failed }, issues, correctionAttempts);
21481
+ } finally {
21482
+ this.releaseBrowserControl();
21483
+ }
21484
+ }
21485
+ /**
21486
+ * End this worker's hold on the shared browser.
21487
+ *
21488
+ * Duck-typed rather than imported: `browser_control` is registered only by a
21489
+ * host that HAS a browser (the desktop), so in the CLI and hosted runs there
21490
+ * is no such tool and this is a no-op. Importing the class to instanceof-check
21491
+ * it would couple the worker to a tool that usually is not there.
21492
+ */
21493
+ releaseBrowserControl() {
21494
+ try {
21495
+ const tool = this.toolRegistry.getTool?.("browser_control");
21496
+ tool?.releaseActor?.(this.id);
21497
+ } catch {
21117
21498
  }
21118
21499
  }
21119
21500
  sendToPeer(toId, content) {
@@ -21372,8 +21753,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
21372
21753
  // Media generation can run for a minute; without this a cancelled run
21373
21754
  // still pays for an image nobody will see.
21374
21755
  ...this.signal ? { signal: this.signal } : {},
21375
- saveSnapshot: async (path40, content) => {
21376
- this.store?.addFileSnapshot(this.taskId, path40, content);
21756
+ saveSnapshot: async (path41, content) => {
21757
+ this.store?.addFileSnapshot(this.taskId, path41, content);
21377
21758
  },
21378
21759
  sendPeerSync: (to, syncType, content) => {
21379
21760
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -21576,7 +21957,7 @@ ${assignment.expectedOutput}`;
21576
21957
  const { promisify: promisify4 } = await import('util');
21577
21958
  const execFileAsync2 = promisify4(execFile3);
21578
21959
  for (const artifactPath of artifactPaths) {
21579
- const absolutePath = path35__default.default.resolve(this.artifactRoot(), artifactPath);
21960
+ const absolutePath = path36__default.default.resolve(this.artifactRoot(), artifactPath);
21580
21961
  try {
21581
21962
  const stat = await fs11__default.default.stat(absolutePath);
21582
21963
  if (!stat.isFile()) {
@@ -21611,7 +21992,7 @@ ${assignment.expectedOutput}`;
21611
21992
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
21612
21993
  continue;
21613
21994
  }
21614
- const ext = path35__default.default.extname(absolutePath).toLowerCase();
21995
+ const ext = path36__default.default.extname(absolutePath).toLowerCase();
21615
21996
  const check = ext === ".ts" || ext === ".tsx" ? { cmd: "npx", args: ["tsc", "--noEmit", absolutePath] } : ext === ".js" || ext === ".jsx" ? { cmd: process.execPath, args: ["--check", absolutePath] } : ext === ".py" ? { cmd: "python", args: ["-m", "py_compile", absolutePath] } : null;
21616
21997
  if (check) {
21617
21998
  try {
@@ -21703,7 +22084,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
21703
22084
  async checkAcceptance(assignment) {
21704
22085
  const criteria = assignment.acceptance ?? [];
21705
22086
  if (!criteria.length) return [];
21706
- const resolve2 = (target) => path35__default.default.resolve(this.artifactRoot(), target);
22087
+ const resolve2 = (target) => path36__default.default.resolve(this.artifactRoot(), target);
21707
22088
  const workerCanWriteFiles = canProduceFiles(this.tools.map((t) => t.name));
21708
22089
  return evaluateAcceptance(criteria, assignment.files ?? [], {
21709
22090
  stat: async (target) => {
@@ -25014,16 +25395,16 @@ function resolveInWorkspace(workspaceRoot, input) {
25014
25395
  if (typeof input !== "string" || input.length === 0) {
25015
25396
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
25016
25397
  }
25017
- const root = path35__default.default.resolve(workspaceRoot);
25018
- const abs = path35__default.default.isAbsolute(input) ? path35__default.default.resolve(input) : path35__default.default.resolve(root, input);
25019
- const rel = path35__default.default.relative(root, abs);
25020
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path35__default.default.isAbsolute(rel)) {
25398
+ const root = path36__default.default.resolve(workspaceRoot);
25399
+ const abs = path36__default.default.isAbsolute(input) ? path36__default.default.resolve(input) : path36__default.default.resolve(root, input);
25400
+ const rel = path36__default.default.relative(root, abs);
25401
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path36__default.default.isAbsolute(rel)) {
25021
25402
  throw new WorkspaceSandboxError(input, root);
25022
25403
  }
25023
25404
  try {
25024
- const real = fs32__default.default.realpathSync(abs);
25025
- const realRel = path35__default.default.relative(root, real);
25026
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path35__default.default.isAbsolute(realRel))) {
25405
+ const real = fs33__default.default.realpathSync(abs);
25406
+ const realRel = path36__default.default.relative(root, real);
25407
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path36__default.default.isAbsolute(realRel))) {
25027
25408
  throw new WorkspaceSandboxError(input, root);
25028
25409
  }
25029
25410
  } catch (e) {
@@ -25572,9 +25953,9 @@ async function animatePptx(bytes, scheme) {
25572
25953
  const zip = await import_jszip.default.loadAsync(bytes);
25573
25954
  const slidePaths = Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n));
25574
25955
  if (slidePaths.length === 0) return bytes;
25575
- for (const path40 of slidePaths) {
25576
- const xml = await zip.file(path40).async("string");
25577
- zip.file(path40, animateSlideXml(xml, scheme));
25956
+ for (const path41 of slidePaths) {
25957
+ const xml = await zip.file(path41).async("string");
25958
+ zip.file(path41, animateSlideXml(xml, scheme));
25578
25959
  }
25579
25960
  return await zip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
25580
25961
  } catch {
@@ -25955,7 +26336,7 @@ var GenerateDocumentTool = class extends BaseTool {
25955
26336
  }
25956
26337
  }
25957
26338
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
25958
- await fs11__default.default.mkdir(path35__default.default.dirname(absPath), { recursive: true });
26339
+ await fs11__default.default.mkdir(path36__default.default.dirname(absPath), { recursive: true });
25959
26340
  await fs11__default.default.writeFile(absPath, bytes);
25960
26341
  const charts = countCharts(content);
25961
26342
  return [
@@ -26176,7 +26557,7 @@ var FileWriteTool = class extends BaseTool {
26176
26557
  } catch {
26177
26558
  }
26178
26559
  }
26179
- await fs11__default.default.mkdir(path35__default.default.dirname(absPath), { recursive: true });
26560
+ await fs11__default.default.mkdir(path36__default.default.dirname(absPath), { recursive: true });
26180
26561
  await fs11__default.default.writeFile(absPath, content, "utf-8");
26181
26562
  return `Written ${content.length} characters to ${filePath}`;
26182
26563
  }
@@ -26534,12 +26915,10 @@ ${data.description}`;
26534
26915
  }
26535
26916
  }
26536
26917
  };
26537
-
26538
- // src/tools/browser.ts
26539
26918
  var BROWSER_LAUNCH_TIMEOUT_MS = 15e3;
26540
26919
  var BrowserTool = class extends BaseTool {
26541
26920
  name = "browser";
26542
- description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Only available with multimodal models.";
26921
+ description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Screenshots are saved to a file for image_analyze to read.";
26543
26922
  inputSchema = {
26544
26923
  type: "object",
26545
26924
  properties: {
@@ -26606,7 +26985,14 @@ var BrowserTool = class extends BaseTool {
26606
26985
  }
26607
26986
  case "screenshot": {
26608
26987
  const buf = await page2.screenshot({ type: "png" });
26609
- return `data:image/png;base64,${buf.toString("base64")}`;
26988
+ const rel = path36__default.default.join(".cascade", "screenshots", `screenshot-${Date.now()}-${crypto.randomUUID().slice(0, 8)}.png`);
26989
+ const abs = resolveInWorkspace(this.workspaceRoot, rel);
26990
+ await fs11__default.default.mkdir(path36__default.default.dirname(abs), { recursive: true });
26991
+ await fs11__default.default.writeFile(abs, buf);
26992
+ return [
26993
+ `Screenshot saved to ${abs} (${Math.round(buf.byteLength / 1024)} KB).`,
26994
+ `Call image_analyze with that path to see what is on it \u2014 that needs a vision-capable model.`
26995
+ ].join(" ");
26610
26996
  }
26611
26997
  case "evaluate": {
26612
26998
  const result = await page2.evaluate(input["script"]);
@@ -26674,7 +27060,7 @@ var ImageAnalyzeTool = class extends BaseTool {
26674
27060
  };
26675
27061
  async function fileToImageAttachment(filePath) {
26676
27062
  const data = await fs11__default.default.readFile(filePath);
26677
- const ext = path35__default.default.extname(filePath).toLowerCase();
27063
+ const ext = path36__default.default.extname(filePath).toLowerCase();
26678
27064
  const mimeMap = {
26679
27065
  ".jpg": "image/jpeg",
26680
27066
  ".jpeg": "image/jpeg",
@@ -26708,14 +27094,14 @@ var PDFCreateTool = class extends BaseTool {
26708
27094
  const filePath = input["path"];
26709
27095
  const content = input["content"];
26710
27096
  const title = input["title"];
26711
- const dir = path35__default.default.dirname(filePath);
26712
- if (!fs32__default.default.existsSync(dir)) {
26713
- fs32__default.default.mkdirSync(dir, { recursive: true });
27097
+ const dir = path36__default.default.dirname(filePath);
27098
+ if (!fs33__default.default.existsSync(dir)) {
27099
+ fs33__default.default.mkdirSync(dir, { recursive: true });
26714
27100
  }
26715
27101
  return new Promise((resolve2, reject) => {
26716
27102
  try {
26717
27103
  const doc = new PDFDocument__default.default({ margin: 50 });
26718
- const stream = fs32__default.default.createWriteStream(filePath);
27104
+ const stream = fs33__default.default.createWriteStream(filePath);
26719
27105
  doc.pipe(stream);
26720
27106
  if (title) {
26721
27107
  doc.info["Title"] = title;
@@ -26793,22 +27179,22 @@ var CodeInterpreterTool = class extends BaseTool {
26793
27179
  }
26794
27180
  cmdPrefix = NODE_CMD;
26795
27181
  }
26796
- const tmpDir = path35__default.default.join(this.workspaceRoot, ".cascade", "tmp");
26797
- if (!fs32__default.default.existsSync(tmpDir)) {
26798
- fs32__default.default.mkdirSync(tmpDir, { recursive: true });
27182
+ const tmpDir = path36__default.default.join(this.workspaceRoot, ".cascade", "tmp");
27183
+ if (!fs33__default.default.existsSync(tmpDir)) {
27184
+ fs33__default.default.mkdirSync(tmpDir, { recursive: true });
26799
27185
  }
26800
27186
  const extension = language === "python" ? "py" : "js";
26801
27187
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
26802
- const filePath = path35__default.default.join(tmpDir, fileName);
26803
- fs32__default.default.writeFileSync(filePath, code, "utf-8");
27188
+ const filePath = path36__default.default.join(tmpDir, fileName);
27189
+ fs33__default.default.writeFileSync(filePath, code, "utf-8");
26804
27190
  const execArgs = [filePath, ...args];
26805
27191
  return new Promise((resolve2) => {
26806
27192
  const startMs = Date.now();
26807
27193
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
26808
27194
  const duration = Date.now() - startMs;
26809
27195
  try {
26810
- if (fs32__default.default.existsSync(filePath)) {
26811
- fs32__default.default.unlinkSync(filePath);
27196
+ if (fs33__default.default.existsSync(filePath)) {
27197
+ fs33__default.default.unlinkSync(filePath);
26812
27198
  }
26813
27199
  } catch (cleanupErr) {
26814
27200
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -26888,18 +27274,178 @@ ${formatted}`;
26888
27274
  return `Unknown action: ${action}`;
26889
27275
  }
26890
27276
  };
27277
+ var SsrfBlockedError = class extends Error {
27278
+ constructor(message) {
27279
+ super(message);
27280
+ this.name = "SsrfBlockedError";
27281
+ }
27282
+ };
27283
+ var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
27284
+ var MAX_REDIRECTS2 = 5;
27285
+ function allowLocal() {
27286
+ return process.env["CASCADE_ALLOW_LOCAL_FETCH"] === "1";
27287
+ }
27288
+ function isPrivateAddress(ip) {
27289
+ const type = net__default.default.isIP(ip);
27290
+ if (type === 4) return isPrivateIPv4(ip);
27291
+ if (type === 6) return isPrivateIPv6(ip);
27292
+ return true;
27293
+ }
27294
+ function isPrivateIPv4(ip) {
27295
+ const parts = ip.split(".").map((p) => Number(p));
27296
+ if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return true;
27297
+ const [a, b] = parts;
27298
+ if (a === 0) return true;
27299
+ if (a === 10) return true;
27300
+ if (a === 127) return true;
27301
+ if (a === 169 && b === 254) return true;
27302
+ if (a === 172 && b >= 16 && b <= 31) return true;
27303
+ if (a === 192 && b === 168) return true;
27304
+ if (a === 100 && b >= 64 && b <= 127) return true;
27305
+ if (a >= 224) return true;
27306
+ return false;
27307
+ }
27308
+ function ipv6Groups(ip) {
27309
+ let s = ip.toLowerCase().replace(/^\[|\]$/g, "");
27310
+ const zone = s.indexOf("%");
27311
+ if (zone !== -1) s = s.slice(0, zone);
27312
+ const dotted = /^(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
27313
+ if (dotted) {
27314
+ const o = [dotted[2], dotted[3], dotted[4], dotted[5]].map((p) => Number(p));
27315
+ if (o.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
27316
+ s = `${dotted[1]}${(o[0] << 8 | o[1]).toString(16)}:${(o[2] << 8 | o[3]).toString(16)}`;
27317
+ }
27318
+ const halves = s.split("::");
27319
+ if (halves.length > 2) return null;
27320
+ const split = (part) => part ? part.split(":") : [];
27321
+ const head = split(halves[0] ?? "");
27322
+ const tail = halves.length === 2 ? split(halves[1] ?? "") : [];
27323
+ const zeros = halves.length === 2 ? 8 - head.length - tail.length : 0;
27324
+ if (zeros < 0) return null;
27325
+ const groups = [...head, ...Array(zeros).fill("0"), ...tail];
27326
+ if (groups.length !== 8) return null;
27327
+ const out = [];
27328
+ for (const g of groups) {
27329
+ if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
27330
+ out.push(parseInt(g, 16));
27331
+ }
27332
+ return out;
27333
+ }
27334
+ function embeddedIPv4(g) {
27335
+ const quad = (hi, lo) => `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
27336
+ const leadingZero = (n) => g.slice(0, n).every((x) => x === 0);
27337
+ if (leadingZero(5) && g[5] === 65535) return quad(g[6], g[7]);
27338
+ if (leadingZero(6)) return quad(g[6], g[7]);
27339
+ if (g[0] === 100 && g[1] === 65435) {
27340
+ if (g[2] === 1) {
27341
+ return quad(g[3], (g[4] & 255) << 8 | g[5] >> 8);
27342
+ }
27343
+ if (g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0) return quad(g[6], g[7]);
27344
+ return null;
27345
+ }
27346
+ if (g[0] === 8194) return quad(g[1], g[2]);
27347
+ return null;
27348
+ }
27349
+ function isPrivateIPv6(ip) {
27350
+ const g = ipv6Groups(ip);
27351
+ if (!g) return true;
27352
+ const v4 = embeddedIPv4(g);
27353
+ if (v4) return isPrivateIPv4(v4);
27354
+ if (g[0] === 100 && g[1] === 65435) return true;
27355
+ if (g.every((x) => x === 0)) return true;
27356
+ if (leading7Zero(g) && g[7] === 1) return true;
27357
+ if ((g[0] & 65472) === 65152) return true;
27358
+ if ((g[0] & 65024) === 64512) return true;
27359
+ if ((g[0] & 65280) === 65280) return true;
27360
+ return false;
27361
+ }
27362
+ function leading7Zero(g) {
27363
+ return g.slice(0, 7).every((x) => x === 0);
27364
+ }
27365
+ async function assertPublicUrl(rawUrl) {
27366
+ let url;
27367
+ try {
27368
+ url = new URL(rawUrl);
27369
+ } catch {
27370
+ throw new SsrfBlockedError(`Invalid URL: ${rawUrl}`);
27371
+ }
27372
+ if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
27373
+ throw new SsrfBlockedError(`Blocked URL scheme "${url.protocol}" \u2014 only http and https are allowed.`);
27374
+ }
27375
+ if (allowLocal()) return url;
27376
+ const host = url.hostname.replace(/^\[|\]$/g, "");
27377
+ if (net__default.default.isIP(host)) {
27378
+ if (isPrivateAddress(host)) {
27379
+ throw new SsrfBlockedError(`Blocked request to non-public address ${host}.`);
27380
+ }
27381
+ return url;
27382
+ }
27383
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
27384
+ throw new SsrfBlockedError(`Blocked request to local hostname "${host}".`);
27385
+ }
27386
+ let addresses;
27387
+ try {
27388
+ const records = await dns2__default.default.lookup(host, { all: true });
27389
+ addresses = records.map((r) => r.address);
27390
+ } catch {
27391
+ throw new SsrfBlockedError(`Could not resolve host "${host}".`);
27392
+ }
27393
+ if (addresses.length === 0 || addresses.some((addr) => isPrivateAddress(addr))) {
27394
+ throw new SsrfBlockedError(`Blocked request to "${host}" \u2014 resolves to a non-public address.`);
27395
+ }
27396
+ return url;
27397
+ }
27398
+ var ssrfAgent = new undici.Agent({
27399
+ connect: {
27400
+ lookup(hostname, options, callback) {
27401
+ if (allowLocal()) {
27402
+ dnsCallback__default.default.lookup(hostname, options, callback);
27403
+ return;
27404
+ }
27405
+ dnsCallback__default.default.lookup(hostname, { ...options, all: true }, (err, addresses) => {
27406
+ if (err) return callback(err, "", 0);
27407
+ const list = Array.isArray(addresses) ? addresses : [addresses];
27408
+ const bad = list.find((a) => isPrivateAddress(a.address));
27409
+ if (bad) {
27410
+ return callback(
27411
+ new SsrfBlockedError(`Blocked connection to "${hostname}" \u2014 resolved to non-public address ${bad.address}.`),
27412
+ "",
27413
+ 0
27414
+ );
27415
+ }
27416
+ if (options.all) return callback(null, list);
27417
+ const first = list[0];
27418
+ return callback(null, first.address, first.family);
27419
+ });
27420
+ }
27421
+ }
27422
+ });
27423
+ async function safeFetch(rawUrl, init = {}) {
27424
+ let currentUrl = (await assertPublicUrl(rawUrl)).toString();
27425
+ for (let i = 0; i <= MAX_REDIRECTS2; i++) {
27426
+ const resp = await fetch(currentUrl, { ...init, redirect: "manual", dispatcher: ssrfAgent });
27427
+ if (resp.status < 300 || resp.status >= 400) return resp;
27428
+ const location = resp.headers.get("location");
27429
+ if (!location) return resp;
27430
+ const next = new URL(location, currentUrl);
27431
+ await assertPublicUrl(next.toString());
27432
+ currentUrl = next.toString();
27433
+ }
27434
+ throw new SsrfBlockedError(`Too many redirects (>${MAX_REDIRECTS2}).`);
27435
+ }
26891
27436
 
26892
27437
  // src/tools/web-search.ts
26893
- async function searchSearXNG(query, baseUrl, maxResults) {
27438
+ async function searchSearXNG(query, baseUrl, maxResults, guardUrl = false) {
26894
27439
  const url = new URL("/search", baseUrl);
26895
27440
  url.searchParams.set("q", query);
26896
27441
  url.searchParams.set("format", "json");
26897
27442
  url.searchParams.set("categories", "general");
26898
27443
  url.searchParams.set("engines", "google,bing,duckduckgo");
26899
- const resp = await fetch(url.toString(), {
27444
+ const init = {
26900
27445
  headers: { "User-Agent": "Cascade-AI/1.0 WebSearchTool" },
26901
27446
  signal: AbortSignal.timeout(1e4)
26902
- });
27447
+ };
27448
+ const resp = guardUrl ? await safeFetch(url.toString(), init) : await fetch(url.toString(), init);
26903
27449
  if (!resp.ok) {
26904
27450
  throw new Error(`SearXNG returned HTTP ${resp.status}`);
26905
27451
  }
@@ -27028,7 +27574,11 @@ var WebSearchTool = class extends BaseTool {
27028
27574
  searxngUrl: config.searxngUrl ?? process.env["SEARXNG_URL"],
27029
27575
  braveApiKey: config.braveApiKey ?? process.env["BRAVE_SEARCH_API_KEY"],
27030
27576
  tavilyApiKey: config.tavilyApiKey ?? process.env["TAVILY_API_KEY"],
27031
- maxResults: config.maxResults ?? 5
27577
+ maxResults: config.maxResults ?? 5,
27578
+ // Note this reads only from `config`, never from the environment: the
27579
+ // guard tracks the trustworthiness of the URL the host passed in, and an
27580
+ // env var says nothing about that.
27581
+ guardSearxngUrl: config.guardSearxngUrl ?? false
27032
27582
  };
27033
27583
  }
27034
27584
  async execute(input, _options) {
@@ -27043,7 +27593,7 @@ var WebSearchTool = class extends BaseTool {
27043
27593
  let anyBackendReachable = false;
27044
27594
  if (this.config.searxngUrl) {
27045
27595
  try {
27046
- results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
27596
+ results = await searchSearXNG(query, this.config.searxngUrl, maxResults, this.config.guardSearxngUrl);
27047
27597
  anyBackendReachable = true;
27048
27598
  if (results.length > 0) return this.formatResults(query, results);
27049
27599
  errors.push("SearXNG: returned 0 results");
@@ -27120,7 +27670,7 @@ var GlobTool = class extends BaseTool {
27120
27670
  };
27121
27671
  async execute(input, _options) {
27122
27672
  const pattern = input["pattern"];
27123
- const searchPath = input["path"] ? path35__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27673
+ const searchPath = input["path"] ? path36__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27124
27674
  const matches = await glob.glob(pattern, {
27125
27675
  cwd: searchPath,
27126
27676
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -27133,7 +27683,7 @@ var GlobTool = class extends BaseTool {
27133
27683
  const withMtime = await Promise.all(
27134
27684
  matches.map(async (rel) => {
27135
27685
  try {
27136
- const stat = await fs11__default.default.stat(path35__default.default.join(searchPath, rel));
27686
+ const stat = await fs11__default.default.stat(path36__default.default.join(searchPath, rel));
27137
27687
  return { rel, mtime: stat.mtimeMs };
27138
27688
  } catch {
27139
27689
  return { rel, mtime: 0 };
@@ -27182,7 +27732,7 @@ var GrepTool = class extends BaseTool {
27182
27732
  };
27183
27733
  async execute(input, _options) {
27184
27734
  const pattern = input["pattern"];
27185
- const searchPath = input["path"] ? path35__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27735
+ const searchPath = input["path"] ? path36__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27186
27736
  const globPattern = input["glob"];
27187
27737
  const outputMode = input["output_mode"] ?? "content";
27188
27738
  const context = input["context"] ?? 0;
@@ -27236,12 +27786,12 @@ var GrepTool = class extends BaseTool {
27236
27786
  nodir: true
27237
27787
  });
27238
27788
  } catch {
27239
- files = [path35__default.default.relative(searchPath, searchPath) || "."];
27789
+ files = [path36__default.default.relative(searchPath, searchPath) || "."];
27240
27790
  }
27241
27791
  const results = [];
27242
27792
  let totalCount = 0;
27243
27793
  for (const rel of files) {
27244
- const abs = path35__default.default.join(searchPath, rel);
27794
+ const abs = path36__default.default.join(searchPath, rel);
27245
27795
  let content;
27246
27796
  try {
27247
27797
  content = await fs11__default.default.readFile(abs, "utf-8");
@@ -27282,92 +27832,6 @@ Total: ${totalCount} matches`);
27282
27832
  return results.join("\n");
27283
27833
  }
27284
27834
  };
27285
- var SsrfBlockedError = class extends Error {
27286
- constructor(message) {
27287
- super(message);
27288
- this.name = "SsrfBlockedError";
27289
- }
27290
- };
27291
- var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
27292
- var MAX_REDIRECTS2 = 5;
27293
- function allowLocal() {
27294
- return process.env["CASCADE_ALLOW_LOCAL_FETCH"] === "1";
27295
- }
27296
- function isPrivateAddress(ip) {
27297
- const type = net__default.default.isIP(ip);
27298
- if (type === 4) return isPrivateIPv4(ip);
27299
- if (type === 6) return isPrivateIPv6(ip);
27300
- return true;
27301
- }
27302
- function isPrivateIPv4(ip) {
27303
- const parts = ip.split(".").map((p) => Number(p));
27304
- if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return true;
27305
- const [a, b] = parts;
27306
- if (a === 0) return true;
27307
- if (a === 10) return true;
27308
- if (a === 127) return true;
27309
- if (a === 169 && b === 254) return true;
27310
- if (a === 172 && b >= 16 && b <= 31) return true;
27311
- if (a === 192 && b === 168) return true;
27312
- if (a === 100 && b >= 64 && b <= 127) return true;
27313
- if (a >= 224) return true;
27314
- return false;
27315
- }
27316
- function isPrivateIPv6(ip) {
27317
- const lower = ip.toLowerCase().replace(/^\[|\]$/g, "");
27318
- if (lower === "::1" || lower === "::") return true;
27319
- if (lower.startsWith("fe80")) return true;
27320
- if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
27321
- const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower);
27322
- if (mapped) return isPrivateIPv4(mapped[1]);
27323
- return false;
27324
- }
27325
- async function assertPublicUrl(rawUrl) {
27326
- let url;
27327
- try {
27328
- url = new URL(rawUrl);
27329
- } catch {
27330
- throw new SsrfBlockedError(`Invalid URL: ${rawUrl}`);
27331
- }
27332
- if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
27333
- throw new SsrfBlockedError(`Blocked URL scheme "${url.protocol}" \u2014 only http and https are allowed.`);
27334
- }
27335
- if (allowLocal()) return url;
27336
- const host = url.hostname.replace(/^\[|\]$/g, "");
27337
- if (net__default.default.isIP(host)) {
27338
- if (isPrivateAddress(host)) {
27339
- throw new SsrfBlockedError(`Blocked request to non-public address ${host}.`);
27340
- }
27341
- return url;
27342
- }
27343
- if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
27344
- throw new SsrfBlockedError(`Blocked request to local hostname "${host}".`);
27345
- }
27346
- let addresses;
27347
- try {
27348
- const records = await dns2__default.default.lookup(host, { all: true });
27349
- addresses = records.map((r) => r.address);
27350
- } catch {
27351
- throw new SsrfBlockedError(`Could not resolve host "${host}".`);
27352
- }
27353
- if (addresses.length === 0 || addresses.some((addr) => isPrivateAddress(addr))) {
27354
- throw new SsrfBlockedError(`Blocked request to "${host}" \u2014 resolves to a non-public address.`);
27355
- }
27356
- return url;
27357
- }
27358
- async function safeFetch(rawUrl, init = {}) {
27359
- let currentUrl = (await assertPublicUrl(rawUrl)).toString();
27360
- for (let i = 0; i <= MAX_REDIRECTS2; i++) {
27361
- const resp = await fetch(currentUrl, { ...init, redirect: "manual" });
27362
- if (resp.status < 300 || resp.status >= 400) return resp;
27363
- const location = resp.headers.get("location");
27364
- if (!location) return resp;
27365
- const next = new URL(location, currentUrl);
27366
- await assertPublicUrl(next.toString());
27367
- currentUrl = next.toString();
27368
- }
27369
- throw new SsrfBlockedError(`Too many redirects (>${MAX_REDIRECTS2}).`);
27370
- }
27371
27835
 
27372
27836
  // src/tools/web-fetch.ts
27373
27837
  var MAX_CHARS = 5e4;
@@ -27640,10 +28104,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
27640
28104
  }
27641
28105
  isIgnored(filePath) {
27642
28106
  if (!filePath) return false;
27643
- const abs = path35__default.default.resolve(this.workspaceRoot, filePath);
27644
- const rel = path35__default.default.relative(this.workspaceRoot, abs);
27645
- if (!rel || rel.startsWith("..") || path35__default.default.isAbsolute(rel)) return true;
27646
- const posixRel = rel.split(path35__default.default.sep).join("/");
28107
+ const abs = path36__default.default.resolve(this.workspaceRoot, filePath);
28108
+ const rel = path36__default.default.relative(this.workspaceRoot, abs);
28109
+ if (!rel || rel.startsWith("..") || path36__default.default.isAbsolute(rel)) return true;
28110
+ const posixRel = rel.split(path36__default.default.sep).join("/");
27647
28111
  return this.ignoreMatcher.ignores(posixRel);
27648
28112
  }
27649
28113
  };
@@ -27877,22 +28341,22 @@ var FileMcpOAuthStore = class {
27877
28341
  filePath;
27878
28342
  load() {
27879
28343
  try {
27880
- return JSON.parse(fs32__default.default.readFileSync(this.filePath, "utf-8"));
28344
+ return JSON.parse(fs33__default.default.readFileSync(this.filePath, "utf-8"));
27881
28345
  } catch {
27882
28346
  return void 0;
27883
28347
  }
27884
28348
  }
27885
28349
  save(state) {
27886
- fs32__default.default.mkdirSync(path35__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
27887
- fs32__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
28350
+ fs33__default.default.mkdirSync(path36__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
28351
+ fs33__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
27888
28352
  try {
27889
- fs32__default.default.chmodSync(this.filePath, 384);
28353
+ fs33__default.default.chmodSync(this.filePath, 384);
27890
28354
  } catch {
27891
28355
  }
27892
28356
  }
27893
28357
  clear() {
27894
28358
  try {
27895
- fs32__default.default.rmSync(this.filePath);
28359
+ fs33__default.default.rmSync(this.filePath);
27896
28360
  } catch {
27897
28361
  }
27898
28362
  }
@@ -28712,7 +29176,7 @@ var TaskAnalyzer = class {
28712
29176
 
28713
29177
  // src/core/router/model-performance-tracker.ts
28714
29178
  init_pricing();
28715
- var DEFAULT_STATS_FILE = path35__default.default.join(os10__default.default.homedir(), ".cascade", "model-perf.json");
29179
+ var DEFAULT_STATS_FILE = path36__default.default.join(os10__default.default.homedir(), ".cascade", "model-perf.json");
28716
29180
  var ModelPerformanceTracker = class _ModelPerformanceTracker {
28717
29181
  stats = /* @__PURE__ */ new Map();
28718
29182
  featureStats = /* @__PURE__ */ new Map();
@@ -28752,7 +29216,7 @@ var ModelPerformanceTracker = class _ModelPerformanceTracker {
28752
29216
  async save() {
28753
29217
  if (this.readOnly) return;
28754
29218
  try {
28755
- await fs11__default.default.mkdir(path35__default.default.dirname(this.statsFile), { recursive: true });
29219
+ await fs11__default.default.mkdir(path36__default.default.dirname(this.statsFile), { recursive: true });
28756
29220
  const modelsObj = {};
28757
29221
  const featuresObj = {};
28758
29222
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -29385,7 +29849,7 @@ Required capability: ${description.slice(0, 300)}`;
29385
29849
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
29386
29850
  async loadPersistedTools() {
29387
29851
  if (!this.workspacePath || !this.persistEnabled) return;
29388
- const file = path35__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
29852
+ const file = path36__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
29389
29853
  try {
29390
29854
  const raw = await fs11__default.default.readFile(file, "utf-8");
29391
29855
  const specs = JSON.parse(raw);
@@ -29409,8 +29873,8 @@ Required capability: ${description.slice(0, 300)}`;
29409
29873
  }
29410
29874
  async persist() {
29411
29875
  if (!this.workspacePath || !this.persistEnabled) return;
29412
- const dir = path35__default.default.join(this.workspacePath, ".cascade");
29413
- const file = path35__default.default.join(dir, DYNAMIC_TOOLS_FILE);
29876
+ const dir = path36__default.default.join(this.workspacePath, ".cascade");
29877
+ const file = path36__default.default.join(dir, DYNAMIC_TOOLS_FILE);
29414
29878
  try {
29415
29879
  await fs11__default.default.mkdir(dir, { recursive: true });
29416
29880
  await fs11__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -29430,12 +29894,12 @@ var WorldStateDB = class {
29430
29894
  constructor(workspacePath, debugMode = false) {
29431
29895
  this.workspacePath = workspacePath;
29432
29896
  this.debugMode = debugMode;
29433
- const cascadeDir = path35__default.default.join(workspacePath, ".cascade");
29434
- if (!fs32__default.default.existsSync(cascadeDir)) {
29435
- fs32__default.default.mkdirSync(cascadeDir, { recursive: true });
29897
+ const cascadeDir = path36__default.default.join(workspacePath, ".cascade");
29898
+ if (!fs33__default.default.existsSync(cascadeDir)) {
29899
+ fs33__default.default.mkdirSync(cascadeDir, { recursive: true });
29436
29900
  }
29437
- this.keyPath = path35__default.default.join(cascadeDir, "world_state.key");
29438
- this.dbPath = path35__default.default.join(cascadeDir, "world_state.db");
29901
+ this.keyPath = path36__default.default.join(cascadeDir, "world_state.key");
29902
+ this.dbPath = path36__default.default.join(cascadeDir, "world_state.db");
29439
29903
  this.initEncryptionKey();
29440
29904
  this.db = new Database2__default.default(this.dbPath);
29441
29905
  this.db.pragma("journal_mode = WAL");
@@ -29492,11 +29956,11 @@ var WorldStateDB = class {
29492
29956
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
29493
29957
  }
29494
29958
  initEncryptionKey() {
29495
- if (fs32__default.default.existsSync(this.keyPath)) {
29496
- this.encryptionKey = fs32__default.default.readFileSync(this.keyPath);
29959
+ if (fs33__default.default.existsSync(this.keyPath)) {
29960
+ this.encryptionKey = fs33__default.default.readFileSync(this.keyPath);
29497
29961
  } else {
29498
29962
  this.encryptionKey = crypto__default.default.randomBytes(32);
29499
- fs32__default.default.writeFileSync(this.keyPath, this.encryptionKey);
29963
+ fs33__default.default.writeFileSync(this.keyPath, this.encryptionKey);
29500
29964
  }
29501
29965
  }
29502
29966
  encrypt(text) {
@@ -29782,9 +30246,9 @@ var WorldStateDB = class {
29782
30246
  dumpDebugIfNeeded() {
29783
30247
  if (!this.debugMode) return;
29784
30248
  try {
29785
- const dumpPath = path35__default.default.join(os10__default.default.tmpdir(), "cascade_world_state_debug.json");
30249
+ const dumpPath = path36__default.default.join(os10__default.default.tmpdir(), "cascade_world_state_debug.json");
29786
30250
  const entries = this.getAllEntries();
29787
- fs32__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
30251
+ fs33__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
29788
30252
  } catch (err) {
29789
30253
  console.error("Failed to dump debug world state", err);
29790
30254
  }
@@ -29840,11 +30304,11 @@ var ResumeStore = class {
29840
30304
  version: RESUME_SCHEMA_VERSION,
29841
30305
  createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
29842
30306
  };
29843
- await fs32.promises.mkdir(this.dir, { recursive: true });
29844
- const finalPath = path35__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
30307
+ await fs33.promises.mkdir(this.dir, { recursive: true });
30308
+ const finalPath = path36__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
29845
30309
  const tempPath = `${finalPath}.${process.pid}.tmp`;
29846
- await fs32.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
29847
- await fs32.promises.rename(tempPath, finalPath);
30310
+ await fs33.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
30311
+ await fs33.promises.rename(tempPath, finalPath);
29848
30312
  await this.prune();
29849
30313
  return true;
29850
30314
  } catch {
@@ -29855,7 +30319,7 @@ var ResumeStore = class {
29855
30319
  async list() {
29856
30320
  let entries;
29857
30321
  try {
29858
- entries = await fs32.promises.readdir(this.dir);
30322
+ entries = await fs33.promises.readdir(this.dir);
29859
30323
  } catch {
29860
30324
  return [];
29861
30325
  }
@@ -29864,7 +30328,7 @@ var ResumeStore = class {
29864
30328
  const checkpoints = [];
29865
30329
  for (const candidate of candidates2) {
29866
30330
  try {
29867
- const raw = await fs32.promises.readFile(path35__default.default.join(this.dir, candidate.name), "utf-8");
30331
+ const raw = await fs33.promises.readFile(path36__default.default.join(this.dir, candidate.name), "utf-8");
29868
30332
  const parsed = JSON.parse(raw);
29869
30333
  if (isCheckpoint(parsed)) checkpoints.push(parsed);
29870
30334
  } catch {
@@ -29898,12 +30362,12 @@ var ResumeStore = class {
29898
30362
  const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
29899
30363
  if (!target) return null;
29900
30364
  try {
29901
- const entries = await fs32.promises.readdir(this.dir);
30365
+ const entries = await fs33.promises.readdir(this.dir);
29902
30366
  const wanted = safeId(target.taskId);
29903
30367
  const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
29904
30368
  if (!source) return null;
29905
30369
  const claimId = `${this.now()}-${safeId(target.taskId)}`;
29906
- await fs32.promises.rename(path35__default.default.join(this.dir, source), path35__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
30370
+ await fs33.promises.rename(path36__default.default.join(this.dir, source), path36__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
29907
30371
  return { checkpoint: target, claimId };
29908
30372
  } catch {
29909
30373
  return null;
@@ -29912,9 +30376,9 @@ var ResumeStore = class {
29912
30376
  /** Give a claim back, so the work stays recoverable after a failed start. */
29913
30377
  async release(claimId) {
29914
30378
  try {
29915
- await fs32.promises.rename(
29916
- path35__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
29917
- path35__default.default.join(this.dir, `${claimId}.json`)
30379
+ await fs33.promises.rename(
30380
+ path36__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
30381
+ path36__default.default.join(this.dir, `${claimId}.json`)
29918
30382
  );
29919
30383
  } catch {
29920
30384
  }
@@ -29922,7 +30386,7 @@ var ResumeStore = class {
29922
30386
  /** Discard a claim for good — the resumed run has taken ownership. */
29923
30387
  async settle(claimId) {
29924
30388
  try {
29925
- await fs32.promises.rm(path35__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
30389
+ await fs33.promises.rm(path36__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
29926
30390
  } catch {
29927
30391
  }
29928
30392
  }
@@ -29944,15 +30408,15 @@ var ResumeStore = class {
29944
30408
  async reclaimStale() {
29945
30409
  let reclaimed = 0;
29946
30410
  try {
29947
- const entries = await fs32.promises.readdir(this.dir);
30411
+ const entries = await fs33.promises.readdir(this.dir);
29948
30412
  const cutoff = this.now() - this.claimTimeoutMs;
29949
30413
  for (const name of entries) {
29950
30414
  if (!name.endsWith(CLAIM_SUFFIX)) continue;
29951
30415
  const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
29952
30416
  if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
29953
- await fs32.promises.rename(
29954
- path35__default.default.join(this.dir, name),
29955
- path35__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
30417
+ await fs33.promises.rename(
30418
+ path36__default.default.join(this.dir, name),
30419
+ path36__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
29956
30420
  );
29957
30421
  reclaimed++;
29958
30422
  }
@@ -29963,12 +30427,12 @@ var ResumeStore = class {
29963
30427
  /** Remove every checkpoint for a task id. */
29964
30428
  async delete(taskId) {
29965
30429
  try {
29966
- const entries = await fs32.promises.readdir(this.dir);
30430
+ const entries = await fs33.promises.readdir(this.dir);
29967
30431
  const wanted = safeId(taskId);
29968
30432
  await Promise.all(entries.map(async (name) => {
29969
30433
  const match = FILE_PATTERN.exec(name);
29970
30434
  if (match?.[2] === wanted) {
29971
- await fs32.promises.rm(path35__default.default.join(this.dir, name), { force: true });
30435
+ await fs33.promises.rm(path36__default.default.join(this.dir, name), { force: true });
29972
30436
  }
29973
30437
  }));
29974
30438
  } catch {
@@ -29977,13 +30441,13 @@ var ResumeStore = class {
29977
30441
  /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
29978
30442
  async prune() {
29979
30443
  try {
29980
- const entries = await fs32.promises.readdir(this.dir);
30444
+ const entries = await fs33.promises.readdir(this.dir);
29981
30445
  const cutoff = this.now() - this.maxAgeMs;
29982
30446
  const files = entries.map((name) => ({ name, match: FILE_PATTERN.exec(name) })).filter((e) => e.match !== null).map((e) => ({ name: e.name, timestamp: Number(e.match[1]) })).sort((a, b) => b.timestamp - a.timestamp);
29983
30447
  const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
29984
30448
  const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
29985
30449
  await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
29986
- (name) => fs32.promises.rm(path35__default.default.join(this.dir, name), { force: true })
30450
+ (name) => fs33.promises.rm(path36__default.default.join(this.dir, name), { force: true })
29987
30451
  ));
29988
30452
  } catch {
29989
30453
  }
@@ -30360,7 +30824,7 @@ var WorkspaceIndex = class {
30360
30824
  isIgnored;
30361
30825
  chunker;
30362
30826
  constructor(opts) {
30363
- this.root = path35__default.default.resolve(opts.root);
30827
+ this.root = path36__default.default.resolve(opts.root);
30364
30828
  this.db = opts.db;
30365
30829
  this.namespace = opts.namespace ?? "code";
30366
30830
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -30392,14 +30856,14 @@ var WorkspaceIndex = class {
30392
30856
  return;
30393
30857
  }
30394
30858
  for (const entry of entries) {
30395
- const abs = path35__default.default.join(dir, entry.name);
30859
+ const abs = path36__default.default.join(dir, entry.name);
30396
30860
  if (entry.isDirectory()) {
30397
30861
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
30398
30862
  await walk(abs);
30399
30863
  } else if (entry.isFile()) {
30400
- const ext = path35__default.default.extname(entry.name).slice(1).toLowerCase();
30864
+ const ext = path36__default.default.extname(entry.name).slice(1).toLowerCase();
30401
30865
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
30402
- out.push(path35__default.default.relative(this.root, abs));
30866
+ out.push(path36__default.default.relative(this.root, abs));
30403
30867
  }
30404
30868
  }
30405
30869
  };
@@ -30429,7 +30893,7 @@ var WorkspaceIndex = class {
30429
30893
  const entries = [];
30430
30894
  for (const rel of rels) {
30431
30895
  try {
30432
- const abs = path35__default.default.join(this.root, rel);
30896
+ const abs = path36__default.default.join(this.root, rel);
30433
30897
  const stat = await fs11__default.default.stat(abs);
30434
30898
  if (stat.size > this.maxFileBytes) continue;
30435
30899
  const buf = await fs11__default.default.readFile(abs);
@@ -30834,6 +31298,127 @@ var CurrentPageTool = class extends BaseTool {
30834
31298
  }
30835
31299
  };
30836
31300
 
31301
+ // src/tools/browser-control.ts
31302
+ var BrowserControlTool = class extends BaseTool {
31303
+ name = "browser_control";
31304
+ description = "Act on the web page the user has open in the built-in browser: navigate, click an element, type into a field, press a key, or wait for something to appear. This drives the REAL browser the user is signed into and can see, so actions have real consequences \u2014 prefer read_current_page when you only need to look. CSS selectors are matched against the live page; call extract_text first if you need to find one. There is one browser: if another part of this run is using it, this call waits its turn, so a slow reply means queued, not stuck.";
31305
+ inputSchema = {
31306
+ type: "object",
31307
+ properties: {
31308
+ action: {
31309
+ type: "string",
31310
+ enum: ["navigate", "click", "fill", "press", "wait_for", "extract_text"],
31311
+ description: "What to do."
31312
+ },
31313
+ url: { type: "string", description: "For navigate: the http(s) address to open." },
31314
+ selector: {
31315
+ type: "string",
31316
+ description: "CSS selector for click, fill and wait_for. Optional for extract_text (defaults to the whole page)."
31317
+ },
31318
+ value: { type: "string", description: "For fill: the text to type." },
31319
+ key: { type: "string", description: 'For press: a key name such as "Enter", "Tab" or "Escape".' },
31320
+ timeoutMs: { type: "number", description: "For wait_for: how long to wait before giving up (default 10000, max 30000)." }
31321
+ },
31322
+ required: ["action"]
31323
+ };
31324
+ controller;
31325
+ release;
31326
+ /** One-way. See `revoke`. */
31327
+ revoked = false;
31328
+ constructor(controller, release) {
31329
+ super();
31330
+ this.controller = controller;
31331
+ this.release = release;
31332
+ }
31333
+ /**
31334
+ * A worker has finished. Ends its hold on the browser.
31335
+ *
31336
+ * Called from the tier's terminal path rather than by the model, because a
31337
+ * model cannot be relied on to announce that it is done — which is why the
31338
+ * host cannot use idle time as the boundary instead.
31339
+ */
31340
+ releaseActor(actorId) {
31341
+ this.release?.(actorId);
31342
+ }
31343
+ /**
31344
+ * Permanently disable this tool, whatever the registry still holds.
31345
+ *
31346
+ * Registration-time gates cannot express "this run turned out to be
31347
+ * unattended": a host that wires the controller and only then declares the
31348
+ * run unattended has already registered the tool, and a check that ran at
31349
+ * registration has no second chance. Enforcing at EXECUTION time makes the
31350
+ * guarantee independent of call order, which is the whole point of stating
31351
+ * the invariant rather than relying on the scheduler's current sequence.
31352
+ *
31353
+ * One-way on purpose. A safety gate that can be re-opened by a later call is
31354
+ * a gate that a refactor re-opens by accident.
31355
+ */
31356
+ revoke() {
31357
+ this.revoked = true;
31358
+ }
31359
+ // Acting on a session the user is signed into is the highest-consequence
31360
+ // thing in the tool set — higher than shell, arguably, because the blast
31361
+ // radius is someone else's server rather than this machine.
31362
+ isDangerous() {
31363
+ return true;
31364
+ }
31365
+ async execute(input, options) {
31366
+ if (this.revoked) {
31367
+ return "Error: browser control is not available in this run. It requires a person watching who can stop it, and nobody is.";
31368
+ }
31369
+ const kind = input["action"];
31370
+ if (!kind) return "Error: action is required.";
31371
+ if (options?.signal?.aborted) return "Error: the run was cancelled before this action ran.";
31372
+ const missing = requiredFieldFor(kind, input);
31373
+ if (missing) return `Error: "${kind}" needs ${missing}.`;
31374
+ const action = {
31375
+ kind,
31376
+ ...typeof input["url"] === "string" ? { url: input["url"] } : {},
31377
+ ...typeof input["selector"] === "string" ? { selector: input["selector"] } : {},
31378
+ ...typeof input["value"] === "string" ? { value: input["value"] } : {},
31379
+ ...typeof input["key"] === "string" ? { key: input["key"] } : {},
31380
+ ...typeof input["timeoutMs"] === "number" ? { timeoutMs: Math.min(Math.max(input["timeoutMs"], 0), 3e4) } : {}
31381
+ };
31382
+ let outcome;
31383
+ try {
31384
+ outcome = await this.controller(action, {
31385
+ // `sessionId` is the run. The host scopes both revocation and its
31386
+ // single-owner lease on it, so passing it is not telemetry — drop it
31387
+ // and a Stop in one run silently stops every later one.
31388
+ sessionId: options?.sessionId ?? "",
31389
+ actorId: options?.tierId ?? options?.sessionId ?? "",
31390
+ ...options?.signal ? { signal: options.signal } : {}
31391
+ });
31392
+ } catch (err) {
31393
+ return `Error: the browser could not perform "${kind}" \u2014 ${err instanceof Error ? err.message : String(err)}`;
31394
+ }
31395
+ const where = outcome.url ? `
31396
+ Page: ${outcome.title ? `${outcome.title} \u2014 ` : ""}${outcome.url}` : "";
31397
+ return `${outcome.ok ? "" : "Failed: "}${outcome.detail}${where}`;
31398
+ }
31399
+ };
31400
+ function requiredFieldFor(kind, input) {
31401
+ const has = (k) => typeof input[k] === "string" && input[k].length > 0;
31402
+ const given = (k) => typeof input[k] === "string";
31403
+ switch (kind) {
31404
+ case "navigate":
31405
+ return has("url") ? null : "a url";
31406
+ case "click":
31407
+ return has("selector") ? null : "a selector";
31408
+ case "fill":
31409
+ return has("selector") ? given("value") ? null : "a value" : "a selector";
31410
+ case "press":
31411
+ return has("key") ? null : "a key";
31412
+ case "wait_for":
31413
+ return has("selector") ? null : "a selector";
31414
+ // extract_text works with or without a selector.
31415
+ case "extract_text":
31416
+ return null;
31417
+ default:
31418
+ return null;
31419
+ }
31420
+ }
31421
+
30837
31422
  // src/core/cascade.ts
30838
31423
  var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
30839
31424
  function buildContextualPrompt(prompt, history = []) {
@@ -30919,13 +31504,13 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
30919
31504
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
30920
31505
  try {
30921
31506
  this.router.setDeadModelStore(new DeadModelStore(
30922
- fileDeadModelPersistence(path35__default.default.join(workspacePath, ".cascade", "dead-models.json"))
31507
+ fileDeadModelPersistence(path36__default.default.join(workspacePath, ".cascade", "dead-models.json"))
30923
31508
  ));
30924
31509
  } catch {
30925
31510
  }
30926
31511
  try {
30927
31512
  this.resumeStore = new ResumeStore({
30928
- dir: path35__default.default.join(workspacePath, ".cascade", "resume")
31513
+ dir: path36__default.default.join(workspacePath, ".cascade", "resume")
30929
31514
  });
30930
31515
  } catch {
30931
31516
  }
@@ -31013,8 +31598,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
31013
31598
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
31014
31599
  return;
31015
31600
  }
31016
- const dbPath = ci.dbPath || path35__default.default.join(this.workspacePath, ".cascade", "code-index.db");
31017
- fs32__default.default.mkdirSync(path35__default.default.dirname(dbPath), { recursive: true });
31601
+ const dbPath = ci.dbPath || path36__default.default.join(this.workspacePath, ".cascade", "code-index.db");
31602
+ fs33__default.default.mkdirSync(path36__default.default.dirname(dbPath), { recursive: true });
31018
31603
  const db = new Database2__default.default(dbPath);
31019
31604
  db.pragma("journal_mode = WAL");
31020
31605
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -31326,6 +31911,49 @@ ${input}` }],
31326
31911
  if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
31327
31912
  this.toolRegistry.register(new CurrentPageTool(provider));
31328
31913
  }
31914
+ /**
31915
+ * True when nobody is watching this run — a scheduled/cron task, which
31916
+ * auto-approves every tool because there is no one to ask.
31917
+ *
31918
+ * That auto-approval is fine for ordinary tools and disqualifying for this
31919
+ * one: `browser_control` drives the signed-in browser, and "every run asks
31920
+ * first" is the promise the Settings copy makes. The property used to hold
31921
+ * only because `runScheduledTask` happens not to call `setBrowserController`
31922
+ * — a guarantee resting on an ABSENT call, which is exactly what a later
31923
+ * refactor adds without noticing. Stated and enforced instead.
31924
+ */
31925
+ unattended = false;
31926
+ /**
31927
+ * Declare that no human is watching this run, so capabilities that depend on
31928
+ * a person being able to see and stop them are refused outright.
31929
+ */
31930
+ setUnattended(on) {
31931
+ this.unattended = on;
31932
+ if (!on) return;
31933
+ const tool = this.toolRegistry.getTool("browser_control");
31934
+ tool?.revoke?.();
31935
+ }
31936
+ /**
31937
+ * Wire the host's browser so a run can ACT on the open page, not just read it.
31938
+ *
31939
+ * Two gates before this registers anything, and both matter:
31940
+ *
31941
+ * - `tools.agentBrowserControl` must be on. Unlike read_current_page, which
31942
+ * reaches nothing the user has not already opened themselves, this drives
31943
+ * the session they are signed into. That is opt-in, not a default.
31944
+ * - `disabledTools` still removes it, the same as every other tool.
31945
+ * - The run must not be UNATTENDED. See `setUnattended`.
31946
+ *
31947
+ * Host-supplied like the page provider: the tool exists only where there is a
31948
+ * real browser to drive, so it never has to be refused in the CLI or a hosted
31949
+ * run — it is simply not there.
31950
+ */
31951
+ setBrowserController(controller, release) {
31952
+ if (this.unattended) return;
31953
+ if (!this.config.tools?.agentBrowserControl) return;
31954
+ if ((this.config.tools?.disabledTools ?? []).includes("browser_control")) return;
31955
+ this.toolRegistry.register(new BrowserControlTool(controller, release));
31956
+ }
31329
31957
  registerMediaTools(workspacePath) {
31330
31958
  const denied = new Set(this.config.tools?.disabledTools ?? []);
31331
31959
  const permitted = (name) => !denied.has(name);
@@ -31931,6 +32559,7 @@ ${prompt}` : prompt;
31931
32559
  this.router.setRunSignal(options.signal);
31932
32560
  const startMs = Date.now();
31933
32561
  const taskId = crypto.randomUUID();
32562
+ this.emit("run:started", { taskId });
31934
32563
  this.decisionLog = [];
31935
32564
  this.currentRunCompleted = [];
31936
32565
  this.wroteCheckpointThisRun = false;
@@ -32482,14 +33111,14 @@ ${finalOutput}` : ""
32482
33111
  init_constants();
32483
33112
  var CLOUD_SESSION_FILE = "cloud-session.json";
32484
33113
  function globalDir(homeDir = os10__default.default.homedir()) {
32485
- return path35__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
33114
+ return path36__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
32486
33115
  }
32487
33116
  function cloudSessionPath(dir = globalDir()) {
32488
- return path35__default.default.join(dir, CLOUD_SESSION_FILE);
33117
+ return path36__default.default.join(dir, CLOUD_SESSION_FILE);
32489
33118
  }
32490
33119
  function loadCloudSession(dir = globalDir()) {
32491
33120
  try {
32492
- const raw = fs32__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
33121
+ const raw = fs33__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
32493
33122
  const parsed = JSON.parse(raw);
32494
33123
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
32495
33124
  return parsed;
@@ -32498,17 +33127,17 @@ function loadCloudSession(dir = globalDir()) {
32498
33127
  }
32499
33128
  }
32500
33129
  function saveCloudSession(session, dir = globalDir()) {
32501
- fs32__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
33130
+ fs33__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
32502
33131
  const file = cloudSessionPath(dir);
32503
- fs32__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
33132
+ fs33__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
32504
33133
  try {
32505
- fs32__default.default.chmodSync(file, 384);
33134
+ fs33__default.default.chmodSync(file, 384);
32506
33135
  } catch {
32507
33136
  }
32508
33137
  }
32509
33138
  function clearCloudSession(dir = globalDir()) {
32510
33139
  try {
32511
- fs32__default.default.rmSync(cloudSessionPath(dir));
33140
+ fs33__default.default.rmSync(cloudSessionPath(dir));
32512
33141
  } catch {
32513
33142
  }
32514
33143
  }
@@ -32680,16 +33309,16 @@ var CloudClient = class _CloudClient {
32680
33309
  return (await this.refresh()).accessToken;
32681
33310
  }
32682
33311
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
32683
- async authed(path40, init = {}) {
32684
- const call = (token) => fetch(this.url(path40), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
33312
+ async authed(path41, init = {}) {
33313
+ const call = (token) => fetch(this.url(path41), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
32685
33314
  let res = await call(await this.accessToken());
32686
33315
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
32687
33316
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
32688
33317
  const text = await res.text();
32689
33318
  return text ? JSON.parse(text) : {};
32690
33319
  }
32691
- authedGet(path40) {
32692
- return this.authed(path40);
33320
+ authedGet(path41) {
33321
+ return this.authed(path41);
32693
33322
  }
32694
33323
  async fetchMe(token) {
32695
33324
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -34463,9 +35092,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
34463
35092
  }
34464
35093
  } catch {
34465
35094
  }
34466
- const configPath = path35__default.default.join(workspacePath, ".cascade", "config.json");
35095
+ const configPath = path36__default.default.join(workspacePath, ".cascade", "config.json");
34467
35096
  try {
34468
- await fs11__default.default.mkdir(path35__default.default.dirname(configPath), { recursive: true });
35097
+ await fs11__default.default.mkdir(path36__default.default.dirname(configPath), { recursive: true });
34469
35098
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
34470
35099
  } catch (err) {
34471
35100
  const msg = err instanceof Error ? err.message : String(err);
@@ -34517,9 +35146,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
34517
35146
  if (msg.includes("non-text parts")) return;
34518
35147
  originalLog(...args);
34519
35148
  };
34520
- const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
35149
+ const store = new MemoryStore(path36__default.default.join(workspacePath, CASCADE_DB_FILE));
34521
35150
  storeRef.current = store;
34522
- globalStoreRef.current = new MemoryStore(path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
35151
+ globalStoreRef.current = new MemoryStore(path36__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
34523
35152
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
34524
35153
  setIdentities(identityRows);
34525
35154
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -34599,7 +35228,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
34599
35228
  onThemeChange: (name) => setTheme(getTheme(name)),
34600
35229
  onExport: async (fmt) => {
34601
35230
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
34602
- const exportPath = path35__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
35231
+ const exportPath = path36__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
34603
35232
  if (fmt === "json") {
34604
35233
  await fs11__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
34605
35234
  } else {
@@ -35519,7 +36148,7 @@ function stringifySlashOutput(val) {
35519
36148
  }
35520
36149
  async function searchSessionsAndMessages(query, workspacePath) {
35521
36150
  if (!query) return "Usage: /search <query>";
35522
- const dbPath = path35__default.default.join(workspacePath, CASCADE_DB_FILE);
36151
+ const dbPath = path36__default.default.join(workspacePath, CASCADE_DB_FILE);
35523
36152
  try {
35524
36153
  await fs11__default.default.access(dbPath);
35525
36154
  } catch {
@@ -35553,7 +36182,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
35553
36182
  async function diagnoseRuntime(config, workspacePath) {
35554
36183
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
35555
36184
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
35556
- const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
36185
+ const store = new MemoryStore(path36__default.default.join(workspacePath, CASCADE_DB_FILE));
35557
36186
  try {
35558
36187
  const sessions2 = store.listSessions(void 0, 3);
35559
36188
  return [
@@ -35572,7 +36201,7 @@ async function diagnoseRuntime(config, workspacePath) {
35572
36201
  }
35573
36202
  async function showRecentLogs(args, workspacePath) {
35574
36203
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
35575
- const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
36204
+ const store = new MemoryStore(path36__default.default.join(workspacePath, CASCADE_DB_FILE));
35576
36205
  try {
35577
36206
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
35578
36207
  if (!logs.length) return "No recent runtime logs.";
@@ -35584,7 +36213,7 @@ async function showRecentLogs(args, workspacePath) {
35584
36213
  async function loadSessionSnapshot(args, workspacePath) {
35585
36214
  const sessionId = args[0];
35586
36215
  if (!sessionId) return "Usage: /resume <sessionId>";
35587
- const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
36216
+ const store = new MemoryStore(path36__default.default.join(workspacePath, CASCADE_DB_FILE));
35588
36217
  try {
35589
36218
  const session = store.getSession(sessionId);
35590
36219
  if (!session) return `Session not found: ${sessionId}`;
@@ -36004,9 +36633,9 @@ function SetupWizard({ workspacePath, onComplete }) {
36004
36633
  ...anyAuto ? { cascadeAuto: true } : {}
36005
36634
  };
36006
36635
  const config = CascadeConfigSchema.parse(rawConfig);
36007
- const configDir = path35__default.default.join(workspacePath, ".cascade");
36636
+ const configDir = path36__default.default.join(workspacePath, ".cascade");
36008
36637
  await fs11__default.default.mkdir(configDir, { recursive: true });
36009
- const configPath = path35__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
36638
+ const configPath = path36__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
36010
36639
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
36011
36640
  savedConfigRef.current = config;
36012
36641
  dispatchRef.current({ type: "GO_DONE" });
@@ -36347,14 +36976,14 @@ function printTelemetryBanner() {
36347
36976
  async function initCommand(workspacePath = process.cwd()) {
36348
36977
  const spin = ora__default.default({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
36349
36978
  try {
36350
- const configDir = path35__default.default.join(workspacePath, ".cascade");
36979
+ const configDir = path36__default.default.join(workspacePath, ".cascade");
36351
36980
  await fs11__default.default.mkdir(configDir, { recursive: true });
36352
- const mdPath = path35__default.default.join(workspacePath, "CASCADE.md");
36981
+ const mdPath = path36__default.default.join(workspacePath, "CASCADE.md");
36353
36982
  if (!await fileExists(mdPath)) {
36354
36983
  await createDefaultCascadeMd(workspacePath);
36355
36984
  spin.succeed(chalk8__default.default.green("Created CASCADE.md"));
36356
36985
  }
36357
- const ignorePath = path35__default.default.join(workspacePath, ".cascadeignore");
36986
+ const ignorePath = path36__default.default.join(workspacePath, ".cascadeignore");
36358
36987
  if (!await fileExists(ignorePath)) {
36359
36988
  await createDefaultIgnoreFile(workspacePath);
36360
36989
  spin.succeed(chalk8__default.default.green("Created .cascadeignore"));
@@ -36363,7 +36992,7 @@ async function initCommand(workspacePath = process.cwd()) {
36363
36992
  console.log();
36364
36993
  console.log(chalk8__default.default.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
36365
36994
  console.log();
36366
- const configPath = path35__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
36995
+ const configPath = path36__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
36367
36996
  if (await fileExists(configPath)) {
36368
36997
  console.log(chalk8__default.default.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
36369
36998
  console.log();
@@ -36514,8 +37143,8 @@ function fromEnv(env) {
36514
37143
  return out;
36515
37144
  }
36516
37145
  async function fromClaudeCode(home, env) {
36517
- const dir = str(env["CLAUDE_CONFIG_DIR"]) ?? path35__default.default.join(home, ".claude");
36518
- const file = path35__default.default.join(dir, ".credentials.json");
37146
+ const dir = str(env["CLAUDE_CONFIG_DIR"]) ?? path36__default.default.join(home, ".claude");
37147
+ const file = path36__default.default.join(dir, ".credentials.json");
36519
37148
  const data = await readJson(file);
36520
37149
  if (!data) return [];
36521
37150
  const found = [];
@@ -36539,7 +37168,7 @@ async function fromClaudeCode(home, env) {
36539
37168
  return found;
36540
37169
  }
36541
37170
  async function fromCodex(home) {
36542
- const file = path35__default.default.join(home, ".codex", "auth.json");
37171
+ const file = path36__default.default.join(home, ".codex", "auth.json");
36543
37172
  const data = await readJson(file);
36544
37173
  if (!data) return [];
36545
37174
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -36562,7 +37191,7 @@ async function fromCodex(home) {
36562
37191
  return [];
36563
37192
  }
36564
37193
  async function fromGemini(home) {
36565
- const file = path35__default.default.join(home, ".gemini", "oauth_creds.json");
37194
+ const file = path36__default.default.join(home, ".gemini", "oauth_creds.json");
36566
37195
  const data = await readJson(file);
36567
37196
  const accessToken = str(data?.["access_token"]);
36568
37197
  if (!accessToken) return [];
@@ -36578,7 +37207,7 @@ async function fromGemini(home) {
36578
37207
  }
36579
37208
  async function fromCopilot(home) {
36580
37209
  for (const name of ["apps.json", "hosts.json"]) {
36581
- const file = path35__default.default.join(home, ".config", "github-copilot", name);
37210
+ const file = path36__default.default.join(home, ".config", "github-copilot", name);
36582
37211
  const data = await readJson(file);
36583
37212
  if (!data) continue;
36584
37213
  for (const value of Object.values(data)) {
@@ -36879,7 +37508,7 @@ async function doctorCommand() {
36879
37508
  checks.push({
36880
37509
  label: "Cascade config",
36881
37510
  ok: true,
36882
- detail: `Loaded ${path35__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
37511
+ detail: `Loaded ${path36__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
36883
37512
  });
36884
37513
  const providers = [
36885
37514
  { type: "anthropic", name: "Anthropic" },
@@ -36989,7 +37618,7 @@ async function checkEndpoint(url) {
36989
37618
  }
36990
37619
  }
36991
37620
  async function indexCommand(dirPath) {
36992
- const workspace = path35__default.default.resolve(dirPath || process.cwd());
37621
+ const workspace = path36__default.default.resolve(dirPath || process.cwd());
36993
37622
  const cm = new ConfigManager(workspace);
36994
37623
  await cm.load();
36995
37624
  const config = cm.getConfig();
@@ -37000,8 +37629,8 @@ async function indexCommand(dirPath) {
37000
37629
  process.exitCode = 1;
37001
37630
  return;
37002
37631
  }
37003
- const dbPath = config.codeIndex?.dbPath || path35__default.default.join(workspace, ".cascade", "code-index.db");
37004
- fs32__default.default.mkdirSync(path35__default.default.dirname(dbPath), { recursive: true });
37632
+ const dbPath = config.codeIndex?.dbPath || path36__default.default.join(workspace, ".cascade", "code-index.db");
37633
+ fs33__default.default.mkdirSync(path36__default.default.dirname(dbPath), { recursive: true });
37005
37634
  const db = new Database2__default.default(dbPath);
37006
37635
  db.pragma("journal_mode = WAL");
37007
37636
  const complete = chatCompleterFromProviders(config.providers);
@@ -37533,7 +38162,7 @@ function openBrowser2(url) {
37533
38162
  }
37534
38163
  function storePathFor(name) {
37535
38164
  const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
37536
- return path35__default.default.join(os10__default.default.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
38165
+ return path36__default.default.join(os10__default.default.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
37537
38166
  }
37538
38167
  function defaultName(url) {
37539
38168
  try {
@@ -37828,6 +38457,7 @@ function applySettingsPayload(config, data) {
37828
38457
  if (["auto", "T1", "T2", "T3"].includes(a["forceTier"])) config.routing = { ...config.routing ?? {}, forceTier: a["forceTier"] };
37829
38458
  if (typeof a["benchmarksLive"] === "boolean") config.benchmarks = { ...config.benchmarks ?? {}, live: a["benchmarksLive"] };
37830
38459
  if (["isolate", "worker", "auto"].includes(a["dynamicToolSandbox"])) config.tools = { ...config.tools ?? {}, dynamicToolSandbox: a["dynamicToolSandbox"] };
38460
+ if (typeof a["agentBrowserControl"] === "boolean") config.tools = { ...config.tools ?? {}, agentBrowserControl: a["agentBrowserControl"] };
37831
38461
  if (typeof a["factsExtraction"] === "boolean") config.knowledge = { ...config.knowledge ?? {}, factsExtraction: a["factsExtraction"] };
37832
38462
  if (typeof a["rememberSessions"] === "boolean") config.memory = { ...config.memory ?? {}, rememberSessions: a["rememberSessions"] };
37833
38463
  if (typeof a["enableToolCreation"] === "boolean") config.enableToolCreation = a["enableToolCreation"];
@@ -37885,6 +38515,7 @@ function settingsSnapshot(config) {
37885
38515
  forceTier: config.routing?.forceTier,
37886
38516
  benchmarksLive: config.benchmarks?.live,
37887
38517
  dynamicToolSandbox: config.tools?.dynamicToolSandbox,
38518
+ agentBrowserControl: config.tools?.agentBrowserControl,
37888
38519
  factsExtraction: config.knowledge?.factsExtraction,
37889
38520
  rememberSessions: config.memory?.rememberSessions,
37890
38521
  enableToolCreation: config.enableToolCreation,
@@ -37920,8 +38551,8 @@ function replaceInPlace(target, source) {
37920
38551
  }
37921
38552
  function writeConfigFile(configPath, config) {
37922
38553
  try {
37923
- fs32__default.default.mkdirSync(path35__default.default.dirname(configPath), { recursive: true });
37924
- fs32__default.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
38554
+ fs33__default.default.mkdirSync(path36__default.default.dirname(configPath), { recursive: true });
38555
+ fs33__default.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
37925
38556
  return { ok: true };
37926
38557
  } catch (err) {
37927
38558
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
@@ -38337,7 +38968,7 @@ function aggregateCostStats(sessions2, opts = {}) {
38337
38968
  }
38338
38969
 
38339
38970
  // src/dashboard/server.ts
38340
- var __dirname$1 = path35__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
38971
+ var __dirname$1 = path36__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
38341
38972
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
38342
38973
  var MASK = "***";
38343
38974
  function masked(key, value) {
@@ -38395,6 +39026,14 @@ var DashboardServer = class {
38395
39026
  * always failing.
38396
39027
  */
38397
39028
  currentPageProvider;
39029
+ /**
39030
+ * Supplied by the desktop shell so a run can ACT on that page — click, type,
39031
+ * submit — not just read it. Same reasoning as the provider above: absent in
39032
+ * the CLI, so the tool is not registered rather than present and failing.
39033
+ * Cascade applies the `tools.agentBrowserControl` gate on top of this.
39034
+ */
39035
+ browserController;
39036
+ browserActorRelease;
38398
39037
  activeControllers = /* @__PURE__ */ new Map();
38399
39038
  /**
38400
39039
  * Run taskIds per chat session — file snapshots are keyed by the run's
@@ -38456,6 +39095,7 @@ var DashboardServer = class {
38456
39095
  });
38457
39096
  this.socket.onConfigUpdate(async (data) => {
38458
39097
  const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
39098
+ if (result.ok) this.settingsChangeHook?.(this.config);
38459
39099
  const refused = result.refused.map((r) => ({
38460
39100
  type: r.type,
38461
39101
  reason: r.reason,
@@ -38479,6 +39119,7 @@ var DashboardServer = class {
38479
39119
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
38480
39120
  this.activeSessions.set(sessionId, cascade);
38481
39121
  if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
39122
+ if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
38482
39123
  cascade.on("stream:token", (e) => {
38483
39124
  this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
38484
39125
  });
@@ -38502,13 +39143,16 @@ var DashboardServer = class {
38502
39143
  if (ev.requestId) this.clearPendingEscalation(ev.requestId);
38503
39144
  this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
38504
39145
  });
39146
+ cascade.on("run:started", (e) => {
39147
+ const id = e.taskId;
39148
+ if (id) this.recordSessionTask(sessionId, id);
39149
+ });
38505
39150
  try {
38506
39151
  const result = await cascade.run({
38507
39152
  prompt: runPrompt,
38508
39153
  signal: abortController.signal,
38509
39154
  approvalCallback: this.makeApprovalCallback(sessionId)
38510
39155
  });
38511
- this.recordSessionTask(sessionId, result.taskId);
38512
39156
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
38513
39157
  this.captureWhy(sessionId, cascade, result);
38514
39158
  this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
@@ -38574,6 +39218,49 @@ var DashboardServer = class {
38574
39218
  setCurrentPageProvider(provider) {
38575
39219
  this.currentPageProvider = provider;
38576
39220
  }
39221
+ /** Let runs act on that view, not just read it (desktop only). */
39222
+ setBrowserController(controller, release) {
39223
+ this.browserController = controller;
39224
+ this.browserActorRelease = release;
39225
+ }
39226
+ /**
39227
+ * Told whenever settings are committed through THIS server.
39228
+ *
39229
+ * Settings reach the live config by two routes — the desktop's Electron IPC
39230
+ * handler and this server's `config:update` socket — and both run the same
39231
+ * `commitSettings`. The desktop mirrors `tools.agentBrowserControl` into its
39232
+ * browser module after its own route, but had no way to learn about the
39233
+ * other, so a socket write left the module's copy stale: an enable the module
39234
+ * still refused, or worse a disable the module still honoured for a run
39235
+ * already holding the tool. This is the same sibling-writer drift
39236
+ * `settings-payload.ts` exists to prevent, one layer up.
39237
+ */
39238
+ settingsChangeHook;
39239
+ onSettingsChanged(hook) {
39240
+ this.settingsChangeHook = hook;
39241
+ }
39242
+ /**
39243
+ * Told when a run ends, so a host holding run-scoped state can release it.
39244
+ *
39245
+ * The desktop uses this to retire a run's browser Stop control: that has to
39246
+ * outlive each individual action (the gap between two actions is when the
39247
+ * user most wants it) but must not outlive the run.
39248
+ */
39249
+ /**
39250
+ * Fired when a run ends, carrying BOTH identifiers explicitly.
39251
+ *
39252
+ * They are different things and conflating them was a live bug: a chat
39253
+ * `sessionId` can contain many runs (hence `sessionTaskIds`), while
39254
+ * `Cascade.run()` mints a fresh random `taskId` per run — and it is the
39255
+ * taskId that tools see as `ToolExecuteOptions.sessionId`. Passing only the
39256
+ * chat id meant browser cleanup keyed on the task id matched nothing, so a
39257
+ * finished run kept its Stop banner armed and held its browser lease until
39258
+ * the deadlock ceiling.
39259
+ */
39260
+ runEndedHook;
39261
+ onRunEnded(hook) {
39262
+ this.runEndedHook = hook;
39263
+ }
38577
39264
  async start() {
38578
39265
  const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
38579
39266
  if (!isLoopback) {
@@ -38656,7 +39343,7 @@ var DashboardServer = class {
38656
39343
  * silently and still typecheck.
38657
39344
  */
38658
39345
  persistConfig(config) {
38659
- const result = writeConfigFile(path35__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE), config);
39346
+ const result = writeConfigFile(path36__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE), config);
38660
39347
  if (!result.ok) console.warn(`[dashboard] Failed to persist config: ${result.error}`);
38661
39348
  return result;
38662
39349
  }
@@ -38675,7 +39362,7 @@ var DashboardServer = class {
38675
39362
  */
38676
39363
  syncGlobalCredentials() {
38677
39364
  try {
38678
- saveGlobalCredentials(path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
39365
+ saveGlobalCredentials(path36__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
38679
39366
  } catch (err) {
38680
39367
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
38681
39368
  }
@@ -38690,15 +39377,15 @@ var DashboardServer = class {
38690
39377
  resolveDashboardSecret() {
38691
39378
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
38692
39379
  if (fromConfig) return fromConfig;
38693
- const secretPath = path35__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
39380
+ const secretPath = path36__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
38694
39381
  try {
38695
- if (fs32__default.default.existsSync(secretPath)) {
38696
- const existing = fs32__default.default.readFileSync(secretPath, "utf-8").trim();
39382
+ if (fs33__default.default.existsSync(secretPath)) {
39383
+ const existing = fs33__default.default.readFileSync(secretPath, "utf-8").trim();
38697
39384
  if (existing.length >= 16) return existing;
38698
39385
  }
38699
39386
  const generated = crypto.randomUUID();
38700
- fs32__default.default.mkdirSync(path35__default.default.dirname(secretPath), { recursive: true });
38701
- fs32__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
39387
+ fs33__default.default.mkdirSync(path36__default.default.dirname(secretPath), { recursive: true });
39388
+ fs33__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
38702
39389
  if (this.config.dashboard.auth) {
38703
39390
  console.warn(
38704
39391
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -38725,7 +39412,7 @@ var DashboardServer = class {
38725
39412
  // ── Setup ─────────────────────────────────────
38726
39413
  getGlobalStore() {
38727
39414
  if (!this.globalStore) {
38728
- const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39415
+ const globalDbPath = path36__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
38729
39416
  this.globalStore = new MemoryStore(globalDbPath);
38730
39417
  }
38731
39418
  return this.globalStore;
@@ -38764,6 +39451,8 @@ var DashboardServer = class {
38764
39451
  }
38765
39452
  /** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
38766
39453
  persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
39454
+ const endedTaskId = result?.taskId ?? this.sessionTaskIds.get(sessionId)?.at(-1);
39455
+ this.runEndedHook?.({ sessionId, ...endedTaskId ? { taskId: endedTaskId } : {} });
38767
39456
  try {
38768
39457
  if (reply && reply.trim()) {
38769
39458
  this.store.addMessage({ id: crypto.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -38857,6 +39546,7 @@ var DashboardServer = class {
38857
39546
  const prompt = task.prompt;
38858
39547
  const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
38859
39548
  const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
39549
+ cascade.setUnattended(true);
38860
39550
  this.activeSessions.set(sessionId, cascade);
38861
39551
  cascade.on("tier:status", (e) => {
38862
39552
  this.socket.broadcast("tier:status", { sessionId, ...e });
@@ -38864,13 +39554,16 @@ var DashboardServer = class {
38864
39554
  cascade.on("peer:message", (e) => {
38865
39555
  this.socket.emitPeerMessage(e);
38866
39556
  });
39557
+ cascade.on("run:started", (e) => {
39558
+ const id = e.taskId;
39559
+ if (id) this.recordSessionTask(sessionId, id);
39560
+ });
38867
39561
  try {
38868
39562
  const result = await cascade.run({
38869
39563
  prompt,
38870
39564
  identityId: task.identityId,
38871
39565
  approvalCallback: async () => ({ approved: true, always: false })
38872
39566
  });
38873
- this.recordSessionTask(sessionId, result.taskId);
38874
39567
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
38875
39568
  this.captureWhy(sessionId, cascade, result);
38876
39569
  this.socket.broadcast("session:complete", { sessionId, result });
@@ -39105,12 +39798,12 @@ ${prompt}`;
39105
39798
  }
39106
39799
  }
39107
39800
  watchRuntimeChanges() {
39108
- const workspaceDbPath = path35__default.default.join(this.workspacePath, CASCADE_DB_FILE);
39109
- const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39801
+ const workspaceDbPath = path36__default.default.join(this.workspacePath, CASCADE_DB_FILE);
39802
+ const globalDbPath = path36__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39110
39803
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
39111
39804
  for (const watchPath of watchPaths) {
39112
- if (!fs32__default.default.existsSync(watchPath)) continue;
39113
- fs32__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
39805
+ if (!fs33__default.default.existsSync(watchPath)) continue;
39806
+ fs33__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
39114
39807
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
39115
39808
  });
39116
39809
  }
@@ -39232,7 +39925,7 @@ ${prompt}`;
39232
39925
  const sessionId = req.params.id;
39233
39926
  this.store.deleteSession(sessionId);
39234
39927
  this.store.deleteRuntimeSession(sessionId);
39235
- const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39928
+ const globalDbPath = path36__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39236
39929
  const globalStore = new MemoryStore(globalDbPath);
39237
39930
  try {
39238
39931
  globalStore.deleteRuntimeSession(sessionId);
@@ -39325,7 +40018,7 @@ ${prompt}`;
39325
40018
  });
39326
40019
  this.app.delete("/api/sessions", auth2, (req, res) => {
39327
40020
  const body = req.body;
39328
- const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
40021
+ const globalDbPath = path36__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39329
40022
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
39330
40023
  const globalStore = new MemoryStore(globalDbPath);
39331
40024
  try {
@@ -39348,7 +40041,7 @@ ${prompt}`;
39348
40041
  });
39349
40042
  this.app.delete("/api/runtime", auth2, (_req, res) => {
39350
40043
  this.store.deleteAllRuntimeNodes();
39351
- const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
40044
+ const globalDbPath = path36__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39352
40045
  const globalStore = new MemoryStore(globalDbPath);
39353
40046
  try {
39354
40047
  globalStore.deleteAllRuntimeNodes();
@@ -39433,12 +40126,12 @@ ${prompt}`;
39433
40126
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
39434
40127
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
39435
40128
  try {
39436
- const configPath = path35__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
39437
- const existing = fs32__default.default.existsSync(configPath) ? JSON.parse(fs32__default.default.readFileSync(configPath, "utf-8")) : {};
40129
+ const configPath = path36__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
40130
+ const existing = fs33__default.default.existsSync(configPath) ? JSON.parse(fs33__default.default.readFileSync(configPath, "utf-8")) : {};
39438
40131
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
39439
40132
  const tmp = configPath + ".tmp";
39440
- fs32__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
39441
- fs32__default.default.renameSync(tmp, configPath);
40133
+ fs33__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
40134
+ fs33__default.default.renameSync(tmp, configPath);
39442
40135
  res.json({ ok: true });
39443
40136
  } catch (err) {
39444
40137
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -39466,7 +40159,7 @@ ${prompt}`;
39466
40159
  this.app.get("/api/runtime", auth2, (req, res) => {
39467
40160
  const scope = req.query["scope"] ?? "workspace";
39468
40161
  if (scope === "global") {
39469
- const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
40162
+ const globalDbPath = path36__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39470
40163
  const globalStore = new MemoryStore(globalDbPath);
39471
40164
  try {
39472
40165
  res.json({
@@ -39503,6 +40196,7 @@ ${prompt}`;
39503
40196
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
39504
40197
  this.activeSessions.set(sessionId, cascade);
39505
40198
  if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
40199
+ if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
39506
40200
  cascade.on("stream:token", (e) => {
39507
40201
  this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
39508
40202
  });
@@ -39526,13 +40220,16 @@ ${prompt}`;
39526
40220
  if (ev.requestId) this.clearPendingEscalation(ev.requestId);
39527
40221
  this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
39528
40222
  });
40223
+ cascade.on("run:started", (e) => {
40224
+ const id = e.taskId;
40225
+ if (id) this.recordSessionTask(sessionId, id);
40226
+ });
39529
40227
  try {
39530
40228
  const result = await cascade.run({
39531
40229
  prompt: runPrompt,
39532
40230
  identityId: body.identityId,
39533
40231
  approvalCallback: this.makeApprovalCallback(sessionId)
39534
40232
  });
39535
- this.recordSessionTask(sessionId, result.taskId);
39536
40233
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
39537
40234
  this.captureWhy(sessionId, cascade, result);
39538
40235
  this.socket.broadcast("cost:update", {
@@ -39807,13 +40504,13 @@ ${prompt}`;
39807
40504
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
39808
40505
  }
39809
40506
  });
39810
- const prodPath = path35__default.default.resolve(__dirname$1, "../web/dist");
39811
- const devPath = path35__default.default.resolve(__dirname$1, "../../web/dist");
39812
- const webDistPath = fs32__default.default.existsSync(prodPath) ? prodPath : devPath;
39813
- if (fs32__default.default.existsSync(webDistPath)) {
40507
+ const prodPath = path36__default.default.resolve(__dirname$1, "../web/dist");
40508
+ const devPath = path36__default.default.resolve(__dirname$1, "../../web/dist");
40509
+ const webDistPath = fs33__default.default.existsSync(prodPath) ? prodPath : devPath;
40510
+ if (fs33__default.default.existsSync(webDistPath)) {
39814
40511
  this.app.use(express__default.default.static(webDistPath));
39815
40512
  this.app.get("*", (_req, res) => {
39816
- res.sendFile(path35__default.default.join(webDistPath, "index.html"));
40513
+ res.sendFile(path36__default.default.join(webDistPath, "index.html"));
39817
40514
  });
39818
40515
  } else {
39819
40516
  this.app.get("/", (_req, res) => {
@@ -39834,7 +40531,7 @@ init_constants();
39834
40531
  async function dashboardCommand(config, workspacePath = process.cwd()) {
39835
40532
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
39836
40533
  const spin = ora__default.default({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
39837
- const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
40534
+ const store = new MemoryStore(path36__default.default.join(workspacePath, CASCADE_DB_FILE));
39838
40535
  const server = new DashboardServer(config, store, workspacePath);
39839
40536
  server.watchRuntimeChanges();
39840
40537
  const gThis = globalThis;
@@ -39866,7 +40563,7 @@ init_constants();
39866
40563
  function makeIdentityCommand(workspacePath = process.cwd()) {
39867
40564
  const identity = new commander.Command("identity").alias("id").description("Manage Cascade identities");
39868
40565
  identity.command("list").description("List all available identities").action(() => {
39869
- const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
40566
+ const store = new MemoryStore(path36__default.default.join(workspacePath, CASCADE_DB_FILE));
39870
40567
  try {
39871
40568
  const identities = store.listIdentities();
39872
40569
  console.log(chalk8__default.default.bold("\n Identities:"));
@@ -39886,7 +40583,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
39886
40583
  }
39887
40584
  });
39888
40585
  identity.command("create <name>").description("Create a new identity").option("-d, --desc <text>", "Description of the identity").option("-s, --system <text>", "System prompt").option("--default", "Set as default identity").action((name, options) => {
39889
- const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
40586
+ const store = new MemoryStore(path36__default.default.join(workspacePath, CASCADE_DB_FILE));
39890
40587
  try {
39891
40588
  if (options.default) {
39892
40589
  const existingDefault = store.getDefaultIdentity();
@@ -39912,7 +40609,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
39912
40609
  }
39913
40610
  });
39914
40611
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
39915
- const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
40612
+ const store = new MemoryStore(path36__default.default.join(workspacePath, CASCADE_DB_FILE));
39916
40613
  try {
39917
40614
  const identities = store.listIdentities();
39918
40615
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
@@ -40061,8 +40758,8 @@ async function exportCommand(options = {}) {
40061
40758
  let store;
40062
40759
  try {
40063
40760
  const workspacePath = options.workspacePath ?? process.cwd();
40064
- const workspaceDbPath = path35__default.default.join(workspacePath, CASCADE_DB_FILE);
40065
- const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
40761
+ const workspaceDbPath = path36__default.default.join(workspacePath, CASCADE_DB_FILE);
40762
+ const globalDbPath = path36__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
40066
40763
  let dbPath = globalDbPath;
40067
40764
  try {
40068
40765
  await fs11__default.default.access(workspaceDbPath);
@@ -40104,7 +40801,7 @@ async function exportCommand(options = {}) {
40104
40801
  const ext = format === "json" ? ".json" : ".md";
40105
40802
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
40106
40803
  const defaultFile = `cascade-export-${safeName}${ext}`;
40107
- const outPath = options.output ? path35__default.default.resolve(options.output) : path35__default.default.join(process.cwd(), defaultFile);
40804
+ const outPath = options.output ? path36__default.default.resolve(options.output) : path36__default.default.join(process.cwd(), defaultFile);
40108
40805
  await fs11__default.default.writeFile(outPath, content, "utf-8");
40109
40806
  spin.succeed(chalk8__default.default.green(`Exported to ${chalk8__default.default.white(outPath)}`));
40110
40807
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
@@ -40244,7 +40941,7 @@ var SHOW_CURSOR = "\x1B[?25h";
40244
40941
  function writeNow(stdout, text) {
40245
40942
  if (typeof stdout.fd === "number") {
40246
40943
  try {
40247
- fs32__default.default.writeSync(stdout.fd, text);
40944
+ fs33__default.default.writeSync(stdout.fd, text);
40248
40945
  return;
40249
40946
  } catch {
40250
40947
  }
@@ -40267,11 +40964,11 @@ function restoreTerminal(stdin, stdout) {
40267
40964
  dotenv__default.default.config();
40268
40965
  function warnIfBuildIsStale() {
40269
40966
  try {
40270
- const here = path35__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
40967
+ const here = path36__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
40271
40968
  for (const rel of ["..", "../.."]) {
40272
- const pkgPath = path35__default.default.join(here, rel, "package.json");
40273
- if (!fs32__default.default.existsSync(pkgPath)) continue;
40274
- const pkg = JSON.parse(fs32__default.default.readFileSync(pkgPath, "utf-8"));
40969
+ const pkgPath = path36__default.default.join(here, rel, "package.json");
40970
+ if (!fs33__default.default.existsSync(pkgPath)) continue;
40971
+ const pkg = JSON.parse(fs33__default.default.readFileSync(pkgPath, "utf-8"));
40275
40972
  if (pkg.name !== "cascade-ai") continue;
40276
40973
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
40277
40974
  console.error(