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.js CHANGED
@@ -1,4 +1,4 @@
1
- import dns from 'dns';
1
+ import dnsCallback from 'dns';
2
2
  import http, { createServer } from 'http';
3
3
  import https from 'https';
4
4
  import zlib from 'zlib';
@@ -7,8 +7,8 @@ import Anthropic from '@anthropic-ai/sdk';
7
7
  import OpenAI, { AzureOpenAI } from 'openai';
8
8
  import { HarmBlockThreshold, HarmCategory, GoogleGenAI } from '@google/genai';
9
9
  import Database2 from 'better-sqlite3';
10
- import path35, { dirname } from 'path';
11
- import fs32, { mkdirSync, writeFileSync, existsSync, readFileSync, promises } from 'fs';
10
+ import path36, { dirname } from 'path';
11
+ import fs33, { mkdirSync, writeFileSync, existsSync, readFileSync, promises } from 'fs';
12
12
  import os10 from 'os';
13
13
  import crypto, { randomUUID, randomBytes, webcrypto, timingSafeEqual, createHash } from 'crypto';
14
14
  import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
@@ -30,6 +30,7 @@ import { simpleGit } from 'simple-git';
30
30
  import PDFDocument from 'pdfkit';
31
31
  import dns2 from 'dns/promises';
32
32
  import net from 'net';
33
+ import { Agent } from 'undici';
33
34
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
34
35
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
35
36
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
@@ -1795,7 +1796,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1795
1796
  var init_constants = __esm({
1796
1797
  "src/constants.ts"() {
1797
1798
  init_pricing();
1798
- CASCADE_VERSION = "0.76.0";
1799
+ CASCADE_VERSION = "0.78.0";
1799
1800
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1800
1801
  CASCADE_DB_FILE = ".cascade/memory.db";
1801
1802
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -1808,64 +1809,88 @@ var init_constants = __esm({
1808
1809
  DEFAULT_CONTEXT_LIMIT = 2e5;
1809
1810
  DEFAULT_AUTO_SUMMARIZE_AT = 15e4;
1810
1811
  MODELS = {
1811
- // Anthropic
1812
- "claude-opus-4": {
1813
- id: "claude-opus-4-5",
1814
- name: "Claude Opus 4",
1812
+ // Anthropic — active Claude API models (2026-08-27).
1813
+ "claude-fable-5": {
1814
+ id: "claude-fable-5",
1815
+ name: "Claude Fable 5",
1815
1816
  provider: "anthropic",
1816
- contextWindow: 2e5,
1817
+ contextWindow: 1e6,
1817
1818
  isVisionCapable: true,
1818
- inputCostPer1kTokens: 0.015,
1819
- outputCostPer1kTokens: 0.075,
1820
- maxOutputTokens: 32e3,
1819
+ inputCostPer1kTokens: 0.01,
1820
+ outputCostPer1kTokens: 0.05,
1821
+ maxOutputTokens: 128e3,
1821
1822
  supportsStreaming: true,
1822
1823
  isLocal: false
1823
1824
  },
1824
- "claude-sonnet-4": {
1825
- id: "claude-sonnet-4-5",
1826
- name: "Claude Sonnet 4",
1825
+ "claude-opus-5": {
1826
+ id: "claude-opus-5",
1827
+ name: "Claude Opus 5",
1827
1828
  provider: "anthropic",
1828
- contextWindow: 2e5,
1829
+ contextWindow: 1e6,
1829
1830
  isVisionCapable: true,
1830
- inputCostPer1kTokens: 3e-3,
1831
- outputCostPer1kTokens: 0.015,
1832
- maxOutputTokens: 16e3,
1831
+ inputCostPer1kTokens: 5e-3,
1832
+ outputCostPer1kTokens: 0.025,
1833
+ maxOutputTokens: 128e3,
1833
1834
  supportsStreaming: true,
1834
1835
  isLocal: false
1835
1836
  },
1836
- "claude-haiku-3-5": {
1837
- id: "claude-haiku-3-5-20251001",
1838
- name: "Claude Haiku 3.5",
1837
+ "claude-opus-4-8": {
1838
+ id: "claude-opus-4-8",
1839
+ name: "Claude Opus 4.8",
1839
1840
  provider: "anthropic",
1840
- contextWindow: 2e5,
1841
+ contextWindow: 1e6,
1841
1842
  isVisionCapable: true,
1842
- inputCostPer1kTokens: 8e-4,
1843
- outputCostPer1kTokens: 4e-3,
1844
- maxOutputTokens: 8e3,
1843
+ inputCostPer1kTokens: 5e-3,
1844
+ outputCostPer1kTokens: 0.025,
1845
+ maxOutputTokens: 128e3,
1845
1846
  supportsStreaming: true,
1846
1847
  isLocal: false
1847
1848
  },
1848
- "claude-haiku-4-5": {
1849
- id: "claude-haiku-4-5-20251001",
1850
- name: "Claude Haiku 4.5",
1849
+ "claude-opus-4-7": {
1850
+ id: "claude-opus-4-7",
1851
+ name: "Claude Opus 4.7",
1851
1852
  provider: "anthropic",
1852
- contextWindow: 2e5,
1853
+ contextWindow: 1e6,
1853
1854
  isVisionCapable: true,
1854
- inputCostPer1kTokens: 8e-4,
1855
- outputCostPer1kTokens: 4e-3,
1856
- maxOutputTokens: 8e3,
1855
+ inputCostPer1kTokens: 5e-3,
1856
+ outputCostPer1kTokens: 0.025,
1857
+ maxOutputTokens: 128e3,
1857
1858
  supportsStreaming: true,
1858
1859
  isLocal: false
1859
1860
  },
1860
- "claude-opus-4-8": {
1861
- id: "claude-opus-4-8",
1862
- name: "Claude Opus 4.8",
1861
+ "claude-opus-4-6": {
1862
+ id: "claude-opus-4-6",
1863
+ name: "Claude Opus 4.6",
1864
+ provider: "anthropic",
1865
+ contextWindow: 1e6,
1866
+ isVisionCapable: true,
1867
+ inputCostPer1kTokens: 5e-3,
1868
+ outputCostPer1kTokens: 0.025,
1869
+ maxOutputTokens: 128e3,
1870
+ supportsStreaming: true,
1871
+ isLocal: false
1872
+ },
1873
+ "claude-opus-4-5": {
1874
+ id: "claude-opus-4-5-20251101",
1875
+ name: "Claude Opus 4.5",
1863
1876
  provider: "anthropic",
1864
1877
  contextWindow: 2e5,
1865
1878
  isVisionCapable: true,
1866
- inputCostPer1kTokens: 0.015,
1867
- outputCostPer1kTokens: 0.075,
1868
- maxOutputTokens: 32e3,
1879
+ inputCostPer1kTokens: 5e-3,
1880
+ outputCostPer1kTokens: 0.025,
1881
+ maxOutputTokens: 64e3,
1882
+ supportsStreaming: true,
1883
+ isLocal: false
1884
+ },
1885
+ "claude-sonnet-5": {
1886
+ id: "claude-sonnet-5",
1887
+ name: "Claude Sonnet 5",
1888
+ provider: "anthropic",
1889
+ contextWindow: 1e6,
1890
+ isVisionCapable: true,
1891
+ inputCostPer1kTokens: 2e-3,
1892
+ outputCostPer1kTokens: 0.01,
1893
+ maxOutputTokens: 128e3,
1869
1894
  supportsStreaming: true,
1870
1895
  isLocal: false
1871
1896
  },
@@ -1873,76 +1898,135 @@ var init_constants = __esm({
1873
1898
  id: "claude-sonnet-4-6",
1874
1899
  name: "Claude Sonnet 4.6",
1875
1900
  provider: "anthropic",
1901
+ contextWindow: 1e6,
1902
+ isVisionCapable: true,
1903
+ inputCostPer1kTokens: 3e-3,
1904
+ outputCostPer1kTokens: 0.015,
1905
+ maxOutputTokens: 64e3,
1906
+ supportsStreaming: true,
1907
+ isLocal: false
1908
+ },
1909
+ "claude-sonnet-4-5": {
1910
+ id: "claude-sonnet-4-5-20250929",
1911
+ name: "Claude Sonnet 4.5",
1912
+ provider: "anthropic",
1876
1913
  contextWindow: 2e5,
1877
1914
  isVisionCapable: true,
1878
1915
  inputCostPer1kTokens: 3e-3,
1879
1916
  outputCostPer1kTokens: 0.015,
1880
- maxOutputTokens: 16e3,
1917
+ maxOutputTokens: 64e3,
1881
1918
  supportsStreaming: true,
1882
1919
  isLocal: false
1883
1920
  },
1884
- // OpenAI
1885
- "gpt-4o": {
1886
- id: "gpt-4o",
1887
- name: "GPT-4o",
1921
+ "claude-haiku-4-5": {
1922
+ id: "claude-haiku-4-5-20251001",
1923
+ name: "Claude Haiku 4.5",
1924
+ provider: "anthropic",
1925
+ contextWindow: 2e5,
1926
+ isVisionCapable: true,
1927
+ inputCostPer1kTokens: 1e-3,
1928
+ outputCostPer1kTokens: 5e-3,
1929
+ maxOutputTokens: 64e3,
1930
+ supportsStreaming: true,
1931
+ isLocal: false
1932
+ },
1933
+ // OpenAI — active chat-capable models used by Cascade's Chat Completions path.
1934
+ "gpt-5.6-sol": {
1935
+ id: "gpt-5.6-sol",
1936
+ name: "GPT-5.6 Sol",
1888
1937
  provider: "openai",
1889
- contextWindow: 128e3,
1938
+ contextWindow: 105e4,
1939
+ isVisionCapable: true,
1940
+ inputCostPer1kTokens: 4e-3,
1941
+ outputCostPer1kTokens: 0.02,
1942
+ maxOutputTokens: 128e3,
1943
+ supportsStreaming: true,
1944
+ isLocal: false
1945
+ },
1946
+ "gpt-5.6-terra": {
1947
+ id: "gpt-5.6-terra",
1948
+ name: "GPT-5.6 Terra",
1949
+ provider: "openai",
1950
+ contextWindow: 105e4,
1951
+ isVisionCapable: true,
1952
+ inputCostPer1kTokens: 2e-3,
1953
+ outputCostPer1kTokens: 0.012,
1954
+ maxOutputTokens: 128e3,
1955
+ supportsStreaming: true,
1956
+ isLocal: false
1957
+ },
1958
+ "gpt-5.6-luna": {
1959
+ id: "gpt-5.6-luna",
1960
+ name: "GPT-5.6 Luna",
1961
+ provider: "openai",
1962
+ contextWindow: 105e4,
1963
+ isVisionCapable: true,
1964
+ inputCostPer1kTokens: 2e-4,
1965
+ outputCostPer1kTokens: 12e-4,
1966
+ maxOutputTokens: 128e3,
1967
+ supportsStreaming: true,
1968
+ isLocal: false
1969
+ },
1970
+ "gpt-5.5": {
1971
+ id: "gpt-5.5",
1972
+ name: "GPT-5.5",
1973
+ provider: "openai",
1974
+ contextWindow: 105e4,
1890
1975
  isVisionCapable: true,
1891
1976
  inputCostPer1kTokens: 5e-3,
1892
- outputCostPer1kTokens: 0.015,
1893
- maxOutputTokens: 16e3,
1977
+ outputCostPer1kTokens: 0.03,
1978
+ maxOutputTokens: 128e3,
1894
1979
  supportsStreaming: true,
1895
1980
  isLocal: false
1896
1981
  },
1897
- "gpt-4o-mini": {
1898
- id: "gpt-4o-mini",
1899
- name: "GPT-4o Mini",
1982
+ "gpt-5.4": {
1983
+ id: "gpt-5.4",
1984
+ name: "GPT-5.4",
1900
1985
  provider: "openai",
1901
- contextWindow: 128e3,
1986
+ contextWindow: 105e4,
1902
1987
  isVisionCapable: true,
1903
- inputCostPer1kTokens: 15e-5,
1904
- outputCostPer1kTokens: 6e-4,
1905
- maxOutputTokens: 16e3,
1988
+ inputCostPer1kTokens: 25e-4,
1989
+ outputCostPer1kTokens: 0.015,
1990
+ maxOutputTokens: 128e3,
1906
1991
  supportsStreaming: true,
1907
1992
  isLocal: false
1908
1993
  },
1909
- "gpt-4.1": {
1910
- id: "gpt-4.1",
1911
- name: "GPT-4.1",
1994
+ "gpt-5.4-mini": {
1995
+ id: "gpt-5.4-mini",
1996
+ name: "GPT-5.4 Mini",
1912
1997
  provider: "openai",
1913
- contextWindow: 1047576,
1998
+ contextWindow: 4e5,
1914
1999
  isVisionCapable: true,
1915
- inputCostPer1kTokens: 2e-3,
1916
- outputCostPer1kTokens: 8e-3,
1917
- maxOutputTokens: 32768,
2000
+ inputCostPer1kTokens: 75e-5,
2001
+ outputCostPer1kTokens: 45e-4,
2002
+ maxOutputTokens: 128e3,
1918
2003
  supportsStreaming: true,
1919
2004
  isLocal: false
1920
2005
  },
1921
- "gpt-4.1-mini": {
1922
- id: "gpt-4.1-mini",
1923
- name: "GPT-4.1 Mini",
2006
+ "gpt-5.4-nano": {
2007
+ id: "gpt-5.4-nano",
2008
+ name: "GPT-5.4 Nano",
1924
2009
  provider: "openai",
1925
- contextWindow: 1047576,
2010
+ contextWindow: 4e5,
1926
2011
  isVisionCapable: true,
1927
- inputCostPer1kTokens: 4e-4,
1928
- outputCostPer1kTokens: 16e-4,
1929
- maxOutputTokens: 32768,
2012
+ inputCostPer1kTokens: 2e-4,
2013
+ outputCostPer1kTokens: 125e-5,
2014
+ maxOutputTokens: 128e3,
1930
2015
  supportsStreaming: true,
1931
2016
  isLocal: false
1932
2017
  },
1933
- "gpt-4.1-nano": {
1934
- id: "gpt-4.1-nano",
1935
- name: "GPT-4.1 Nano",
2018
+ "gpt-5.2": {
2019
+ id: "gpt-5.2",
2020
+ name: "GPT-5.2",
1936
2021
  provider: "openai",
1937
- contextWindow: 1047576,
2022
+ contextWindow: 4e5,
1938
2023
  isVisionCapable: true,
1939
- inputCostPer1kTokens: 1e-4,
1940
- outputCostPer1kTokens: 4e-4,
1941
- maxOutputTokens: 32768,
2024
+ inputCostPer1kTokens: 175e-5,
2025
+ outputCostPer1kTokens: 0.014,
2026
+ maxOutputTokens: 128e3,
1942
2027
  supportsStreaming: true,
1943
2028
  isLocal: false
1944
2029
  },
1945
- // GPT-5 family (offline baseline pricing; the live fetch keeps it current).
1946
2030
  "gpt-5": {
1947
2031
  id: "gpt-5",
1948
2032
  name: "GPT-5",
@@ -1979,79 +2063,149 @@ var init_constants = __esm({
1979
2063
  supportsStreaming: true,
1980
2064
  isLocal: false
1981
2065
  },
1982
- // GPT-5 point releases — genuinely distinct capability, so they route as their
1983
- // own families (not folded into gpt-5). Offline baseline pricing; live fetch
1984
- // keeps it current. gpt-5.5 "Spud" (Apr 2026) is the current SWE-bench leader.
1985
- "gpt-5.5": {
1986
- id: "gpt-5.5",
1987
- name: "GPT-5.5",
2066
+ "gpt-4.1": {
2067
+ id: "gpt-4.1",
2068
+ name: "GPT-4.1",
1988
2069
  provider: "openai",
1989
- contextWindow: 4e5,
2070
+ contextWindow: 1047576,
1990
2071
  isVisionCapable: true,
1991
- inputCostPer1kTokens: 25e-4,
1992
- outputCostPer1kTokens: 0.02,
1993
- maxOutputTokens: 128e3,
2072
+ inputCostPer1kTokens: 2e-3,
2073
+ outputCostPer1kTokens: 8e-3,
2074
+ maxOutputTokens: 32768,
1994
2075
  supportsStreaming: true,
1995
2076
  isLocal: false
1996
2077
  },
1997
- "gpt-5.4": {
1998
- id: "gpt-5.4",
1999
- name: "GPT-5.4",
2078
+ "gpt-4.1-mini": {
2079
+ id: "gpt-4.1-mini",
2080
+ name: "GPT-4.1 Mini",
2000
2081
  provider: "openai",
2001
- contextWindow: 4e5,
2082
+ contextWindow: 1047576,
2002
2083
  isVisionCapable: true,
2003
- inputCostPer1kTokens: 125e-5,
2084
+ inputCostPer1kTokens: 4e-4,
2085
+ outputCostPer1kTokens: 16e-4,
2086
+ maxOutputTokens: 32768,
2087
+ supportsStreaming: true,
2088
+ isLocal: false
2089
+ },
2090
+ "gpt-4.1-nano": {
2091
+ id: "gpt-4.1-nano",
2092
+ name: "GPT-4.1 Nano",
2093
+ provider: "openai",
2094
+ contextWindow: 1047576,
2095
+ isVisionCapable: true,
2096
+ inputCostPer1kTokens: 1e-4,
2097
+ outputCostPer1kTokens: 4e-4,
2098
+ maxOutputTokens: 32768,
2099
+ supportsStreaming: true,
2100
+ isLocal: false
2101
+ },
2102
+ "gpt-4o": {
2103
+ id: "gpt-4o",
2104
+ name: "GPT-4o",
2105
+ provider: "openai",
2106
+ contextWindow: 128e3,
2107
+ isVisionCapable: true,
2108
+ inputCostPer1kTokens: 25e-4,
2004
2109
  outputCostPer1kTokens: 0.01,
2005
- maxOutputTokens: 128e3,
2110
+ maxOutputTokens: 16e3,
2006
2111
  supportsStreaming: true,
2007
2112
  isLocal: false
2008
2113
  },
2009
- "gpt-5.4-mini": {
2010
- id: "gpt-5.4-mini",
2011
- name: "GPT-5.4 Mini",
2114
+ "gpt-4o-mini": {
2115
+ id: "gpt-4o-mini",
2116
+ name: "GPT-4o Mini",
2012
2117
  provider: "openai",
2013
- contextWindow: 4e5,
2118
+ contextWindow: 128e3,
2014
2119
  isVisionCapable: true,
2015
- inputCostPer1kTokens: 25e-5,
2016
- outputCostPer1kTokens: 2e-3,
2017
- maxOutputTokens: 128e3,
2120
+ inputCostPer1kTokens: 15e-5,
2121
+ outputCostPer1kTokens: 6e-4,
2122
+ maxOutputTokens: 16e3,
2018
2123
  supportsStreaming: true,
2019
2124
  isLocal: false
2020
2125
  },
2021
- // Google
2022
- "gemini-2.0-flash": {
2023
- id: "gemini-2.0-flash",
2024
- name: "Gemini 2.0 Flash",
2126
+ // Google Gemini — active GA + preview chat models. Gemini 2.0/1.5 are no
2127
+ // longer advertised because the Gemini API shut those generations down.
2128
+ "gemini-3.7-flash": {
2129
+ id: "gemini-3.7-flash",
2130
+ name: "Gemini 3.7 Flash",
2025
2131
  provider: "gemini",
2026
- contextWindow: 1e6,
2132
+ contextWindow: 1048576,
2027
2133
  isVisionCapable: true,
2028
- inputCostPer1kTokens: 1e-4,
2029
- outputCostPer1kTokens: 4e-4,
2030
- maxOutputTokens: 8e3,
2134
+ inputCostPer1kTokens: 75e-5,
2135
+ outputCostPer1kTokens: 375e-5,
2136
+ maxOutputTokens: 65536,
2031
2137
  supportsStreaming: true,
2032
2138
  isLocal: false
2033
2139
  },
2034
- "gemini-1.5-pro": {
2035
- id: "gemini-1.5-pro",
2036
- name: "Gemini 1.5 Pro",
2140
+ "gemini-3.6-flash": {
2141
+ id: "gemini-3.6-flash",
2142
+ name: "Gemini 3.6 Flash",
2037
2143
  provider: "gemini",
2038
- contextWindow: 1e6,
2144
+ contextWindow: 1048576,
2039
2145
  isVisionCapable: true,
2040
- inputCostPer1kTokens: 125e-5,
2041
- outputCostPer1kTokens: 5e-3,
2042
- maxOutputTokens: 8e3,
2146
+ inputCostPer1kTokens: 75e-5,
2147
+ outputCostPer1kTokens: 375e-5,
2148
+ maxOutputTokens: 65536,
2043
2149
  supportsStreaming: true,
2044
2150
  isLocal: false
2045
2151
  },
2046
- "gemini-2.0-flash-lite": {
2047
- id: "gemini-2.0-flash-lite",
2048
- name: "Gemini 2.0 Flash-Lite",
2152
+ "gemini-3.5-flash": {
2153
+ id: "gemini-3.5-flash",
2154
+ name: "Gemini 3.5 Flash",
2049
2155
  provider: "gemini",
2050
- contextWindow: 1e6,
2156
+ contextWindow: 1048576,
2051
2157
  isVisionCapable: true,
2052
- inputCostPer1kTokens: 5e-5,
2053
- outputCostPer1kTokens: 2e-4,
2054
- maxOutputTokens: 8e3,
2158
+ inputCostPer1kTokens: 15e-4,
2159
+ outputCostPer1kTokens: 9e-3,
2160
+ maxOutputTokens: 65536,
2161
+ supportsStreaming: true,
2162
+ isLocal: false
2163
+ },
2164
+ "gemini-3.5-flash-lite": {
2165
+ id: "gemini-3.5-flash-lite",
2166
+ name: "Gemini 3.5 Flash-Lite",
2167
+ provider: "gemini",
2168
+ contextWindow: 1048576,
2169
+ isVisionCapable: true,
2170
+ inputCostPer1kTokens: 3e-4,
2171
+ outputCostPer1kTokens: 25e-4,
2172
+ maxOutputTokens: 65536,
2173
+ supportsStreaming: true,
2174
+ isLocal: false
2175
+ },
2176
+ "gemini-3.1-flash-lite": {
2177
+ id: "gemini-3.1-flash-lite",
2178
+ name: "Gemini 3.1 Flash-Lite",
2179
+ provider: "gemini",
2180
+ contextWindow: 1048576,
2181
+ isVisionCapable: true,
2182
+ inputCostPer1kTokens: 25e-5,
2183
+ outputCostPer1kTokens: 15e-4,
2184
+ maxOutputTokens: 65536,
2185
+ supportsStreaming: true,
2186
+ isLocal: false
2187
+ },
2188
+ "gemini-3.1-pro-preview": {
2189
+ id: "gemini-3.1-pro-preview",
2190
+ name: "Gemini 3.1 Pro Preview",
2191
+ provider: "gemini",
2192
+ contextWindow: 1048576,
2193
+ isVisionCapable: true,
2194
+ inputCostPer1kTokens: 2e-3,
2195
+ outputCostPer1kTokens: 0.012,
2196
+ maxOutputTokens: 65536,
2197
+ supportsStreaming: true,
2198
+ isLocal: false
2199
+ },
2200
+ "gemini-3-flash-preview": {
2201
+ id: "gemini-3-flash-preview",
2202
+ name: "Gemini 3 Flash Preview",
2203
+ provider: "gemini",
2204
+ contextWindow: 1048576,
2205
+ isVisionCapable: true,
2206
+ inputCostPer1kTokens: 5e-4,
2207
+ outputCostPer1kTokens: 3e-3,
2208
+ maxOutputTokens: 65536,
2055
2209
  supportsStreaming: true,
2056
2210
  isLocal: false
2057
2211
  },
@@ -2059,7 +2213,7 @@ var init_constants = __esm({
2059
2213
  id: "gemini-2.5-pro",
2060
2214
  name: "Gemini 2.5 Pro",
2061
2215
  provider: "gemini",
2062
- contextWindow: 1e6,
2216
+ contextWindow: 1048576,
2063
2217
  isVisionCapable: true,
2064
2218
  inputCostPer1kTokens: 125e-5,
2065
2219
  outputCostPer1kTokens: 0.01,
@@ -2071,10 +2225,22 @@ var init_constants = __esm({
2071
2225
  id: "gemini-2.5-flash",
2072
2226
  name: "Gemini 2.5 Flash",
2073
2227
  provider: "gemini",
2074
- contextWindow: 1e6,
2228
+ contextWindow: 1048576,
2075
2229
  isVisionCapable: true,
2076
- inputCostPer1kTokens: 15e-5,
2077
- outputCostPer1kTokens: 6e-4,
2230
+ inputCostPer1kTokens: 3e-4,
2231
+ outputCostPer1kTokens: 25e-4,
2232
+ maxOutputTokens: 65536,
2233
+ supportsStreaming: true,
2234
+ isLocal: false
2235
+ },
2236
+ "gemini-2.5-flash-lite": {
2237
+ id: "gemini-2.5-flash-lite",
2238
+ name: "Gemini 2.5 Flash-Lite",
2239
+ provider: "gemini",
2240
+ contextWindow: 1048576,
2241
+ isVisionCapable: true,
2242
+ inputCostPer1kTokens: 1e-4,
2243
+ outputCostPer1kTokens: 4e-4,
2078
2244
  maxOutputTokens: 65536,
2079
2245
  supportsStreaming: true,
2080
2246
  isLocal: false
@@ -2144,43 +2310,57 @@ var init_constants = __esm({
2144
2310
  _m.pricingUnknown = false;
2145
2311
  }
2146
2312
  T1_MODEL_PRIORITY = [
2313
+ "gpt-5.6-sol",
2314
+ "claude-fable-5",
2315
+ "claude-opus-5",
2147
2316
  "gpt-5.5",
2148
2317
  "gpt-5.4",
2318
+ "gemini-3.1-pro-preview",
2319
+ "claude-opus-4-8",
2320
+ "claude-sonnet-5",
2321
+ "gemini-3.7-flash",
2322
+ "gpt-5.2",
2149
2323
  "gpt-5",
2150
- "claude-opus-4",
2151
- "claude-sonnet-4",
2152
2324
  "gemini-2.5-pro",
2153
2325
  "gpt-4.1",
2154
- "gpt-4o",
2155
- "gemini-1.5-pro"
2326
+ "gpt-4o"
2156
2327
  ];
2157
2328
  T2_MODEL_PRIORITY = [
2329
+ "gpt-5.6-terra",
2330
+ "claude-sonnet-5",
2331
+ "gemini-3.7-flash",
2332
+ "gemini-3.6-flash",
2158
2333
  "gpt-5.4-mini",
2159
2334
  "gpt-5-mini",
2160
- "claude-sonnet-4",
2335
+ "claude-sonnet-4-6",
2161
2336
  "claude-haiku-4-5",
2162
- "claude-haiku-3-5",
2337
+ "gemini-3.5-flash",
2163
2338
  "gemini-2.5-flash",
2164
2339
  "gpt-4.1-mini",
2165
2340
  "gpt-4o-mini",
2166
- "gemini-2.0-flash",
2167
2341
  "llama3:70b"
2168
2342
  ];
2169
2343
  T3_MODEL_PRIORITY = [
2344
+ "gpt-5.6-luna",
2345
+ "gpt-5.4-nano",
2346
+ "gemini-3.5-flash-lite",
2347
+ "gemini-3.1-flash-lite",
2348
+ "claude-haiku-4-5",
2170
2349
  "gpt-5-nano",
2350
+ "gemini-2.5-flash-lite",
2171
2351
  "llama3.2:3b",
2172
2352
  "mistral:7b",
2173
- "claude-haiku-4-5",
2174
- "claude-haiku-3-5",
2175
2353
  "gpt-4.1-nano",
2176
2354
  "gpt-4o-mini",
2177
- "gemini-2.5-flash",
2178
- "gemini-2.0-flash"
2355
+ "gemini-2.5-flash"
2179
2356
  ];
2180
2357
  VISION_MODEL_PRIORITY = [
2181
- "claude-sonnet-4",
2358
+ "gpt-5.6-sol",
2359
+ "claude-fable-5",
2360
+ "claude-sonnet-5",
2361
+ "gemini-3.7-flash",
2182
2362
  "gpt-4o",
2183
- "gemini-2.0-flash",
2363
+ "gemini-2.5-flash",
2184
2364
  "llava"
2185
2365
  ];
2186
2366
  COMPLEXITY_T2_COUNT = {
@@ -2237,8 +2417,8 @@ function normalizeEndpoint(url) {
2237
2417
  if (!raw) return "";
2238
2418
  try {
2239
2419
  const u = new URL(raw);
2240
- const path40 = stripTrailingSlashes(`${u.pathname}${u.search}`);
2241
- return `${u.protocol}//${u.host.toLowerCase()}${path40}`;
2420
+ const path41 = stripTrailingSlashes(`${u.pathname}${u.search}`);
2421
+ return `${u.protocol}//${u.host.toLowerCase()}${path41}`;
2242
2422
  } catch {
2243
2423
  return raw.toLowerCase();
2244
2424
  }
@@ -2370,7 +2550,7 @@ var CLIENT_VERSION_SEGMENT, REDIRECT_STATUSES, NULL_BODY_STATUSES, MAX_REDIRECTS
2370
2550
  var init_net = __esm({
2371
2551
  "src/utils/net.ts"() {
2372
2552
  try {
2373
- dns.setDefaultResultOrder("ipv4first");
2553
+ dnsCallback.setDefaultResultOrder("ipv4first");
2374
2554
  } catch {
2375
2555
  }
2376
2556
  CLIENT_VERSION_SEGMENT = {
@@ -3102,6 +3282,56 @@ var init_openai = __esm({
3102
3282
  }
3103
3283
  });
3104
3284
 
3285
+ // src/providers/cloud-model-catalog.json
3286
+ var cloud_model_catalog_default;
3287
+ var init_cloud_model_catalog = __esm({
3288
+ "src/providers/cloud-model-catalog.json"() {
3289
+ cloud_model_catalog_default = {
3290
+ schemaVersion: 1,
3291
+ verifiedAt: "2026-08-27",
3292
+ 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.",
3293
+ 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.",
3294
+ models: [
3295
+ { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
3296
+ { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
3297
+ { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
3298
+ { id: "gpt-5.5", name: "GPT-5.5", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
3299
+ { id: "gpt-5.4", name: "GPT-5.4", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
3300
+ { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
3301
+ { id: "gpt-5.4-nano", name: "GPT-5.4 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
3302
+ { id: "gpt-5", name: "GPT-5", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
3303
+ { id: "gpt-5-mini", name: "GPT-5 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
3304
+ { id: "gpt-5-nano", name: "GPT-5 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
3305
+ { id: "gpt-4.1", name: "GPT-4.1", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
3306
+ { id: "gpt-4.1-mini", name: "GPT-4.1 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
3307
+ { id: "gpt-4.1-nano", name: "GPT-4.1 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
3308
+ { id: "gpt-4o", name: "GPT-4o", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 128e3, maxOutputTokens: 16384, vision: true, toolUse: true },
3309
+ { id: "gpt-4o-mini", name: "GPT-4o Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 128e3, maxOutputTokens: 16384, vision: true, toolUse: true },
3310
+ { id: "claude-fable-5", name: "Claude Fable 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3311
+ { id: "claude-opus-5", name: "Claude Opus 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3312
+ { id: "claude-opus-4-8", name: "Claude Opus 4.8", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3313
+ { id: "claude-opus-4-7", name: "Claude Opus 4.7", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3314
+ { id: "claude-opus-4-6", name: "Claude Opus 4.6", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, vision: true, toolUse: true },
3315
+ { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, vision: true, toolUse: true },
3316
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3317
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
3318
+ { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, vision: true, toolUse: true },
3319
+ { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, maxOutputTokens: 64e3, vision: true, toolUse: true },
3320
+ { id: "gemini-3.7-flash", name: "Gemini 3.7 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3321
+ { id: "gemini-3.6-flash", name: "Gemini 3.6 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3322
+ { id: "gemini-3.5-flash", name: "Gemini 3.5 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3323
+ { id: "gemini-3.5-flash-lite", name: "Gemini 3.5 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3324
+ { id: "gemini-3.1-flash-lite", name: "Gemini 3.1 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3325
+ { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro", providers: ["gemini"], lifecycle: "preview", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3326
+ { id: "gemini-3-flash-preview", name: "Gemini 3 Flash", providers: ["gemini"], lifecycle: "preview", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3327
+ { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3328
+ { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
3329
+ { id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true }
3330
+ ]
3331
+ };
3332
+ }
3333
+ });
3334
+
3105
3335
  // src/providers/azure.ts
3106
3336
  var azure_exports = {};
3107
3337
  __export(azure_exports, {
@@ -3110,36 +3340,81 @@ __export(azure_exports, {
3110
3340
  azureModelForDeployment: () => azureModelForDeployment,
3111
3341
  inferAzureBaseModel: () => inferAzureBaseModel
3112
3342
  });
3343
+ function compactModelId(id) {
3344
+ return id.toLowerCase().replace(/[^a-z0-9.]/g, "");
3345
+ }
3346
+ function catalogEntry(id) {
3347
+ const n = id.toLowerCase();
3348
+ return CLOUD_MODELS.find((m) => m.id.toLowerCase() === n);
3349
+ }
3113
3350
  function inferAzureBaseModel(deploymentName) {
3114
3351
  const n = deploymentName.toLowerCase();
3115
- for (const [re, base] of AZURE_BASE_MODEL_RULES) if (re.test(n)) return base;
3352
+ const compact = compactModelId(n);
3353
+ const hasBareGpt56Alias = compact.includes("gpt5.6");
3354
+ for (const model of AZURE_CATALOG) {
3355
+ if (hasBareGpt56Alias && model.id === "gpt-5") continue;
3356
+ if (n.includes(model.id.toLowerCase()) || compact.includes(compactModelId(model.id))) {
3357
+ return model.id;
3358
+ }
3359
+ }
3360
+ if (hasBareGpt56Alias) return "gpt-5.6-sol";
3361
+ const gpt5At = compact.indexOf("gpt5");
3362
+ if (gpt5At !== -1) {
3363
+ const suffix = compact.slice(gpt5At + "gpt5".length);
3364
+ if (suffix.includes("nano")) return "gpt-5-nano";
3365
+ if (suffix.includes("mini")) return "gpt-5-mini";
3366
+ return "gpt-5";
3367
+ }
3368
+ if (compact.includes("gpt4.1nano")) return "gpt-4.1-nano";
3369
+ if (compact.includes("gpt4.1mini")) return "gpt-4.1-mini";
3370
+ if (compact.includes("gpt4.1")) return "gpt-4.1";
3371
+ if (compact.includes("gpt4omini")) return "gpt-4o-mini";
3372
+ if (compact.includes("gpt4o")) return "gpt-4o";
3116
3373
  return null;
3117
3374
  }
3375
+ function catalogModelInfo(baseModelId) {
3376
+ const entry = catalogEntry(baseModelId);
3377
+ if (!entry?.contextWindow) return void 0;
3378
+ return {
3379
+ id: entry.id,
3380
+ name: entry.name,
3381
+ provider: "openai",
3382
+ contextWindow: entry.contextWindow,
3383
+ isVisionCapable: entry.vision ?? false,
3384
+ inputCostPer1kTokens: 0,
3385
+ outputCostPer1kTokens: 0,
3386
+ maxOutputTokens: entry.maxOutputTokens ?? 16e3,
3387
+ supportsStreaming: true,
3388
+ isLocal: false,
3389
+ supportsToolUse: entry.toolUse ?? true,
3390
+ pricingUnknown: true
3391
+ };
3392
+ }
3118
3393
  function azureModelForDeployment(cfg) {
3119
3394
  if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
3120
3395
  const id = cfg.deploymentName.trim();
3121
3396
  const name = cfg.label?.trim() || id;
3122
3397
  const baseModelId = cfg.model?.trim() || inferAzureBaseModel(id) || void 0;
3123
- const base = baseModelId ? MODELS[baseModelId] : void 0;
3124
- if (base) {
3398
+ const base = baseModelId ? MODELS[baseModelId] ?? catalogModelInfo(baseModelId) : void 0;
3399
+ if (base && baseModelId) {
3125
3400
  const azurePrice = resolvePricing(
3126
3401
  { id, provider: "azure", isLocal: false, baseModelId },
3127
3402
  { region: cfg.region }
3128
3403
  );
3404
+ const exactAzurePrice = !azurePrice.unknown && !azurePrice.estimatedFromProvider;
3405
+ const nativeToolsOnThisTransport = /^gpt-5\.6-(?:sol|terra|luna)$/i.test(baseModelId) ? false : base.supportsToolUse ?? true;
3129
3406
  return {
3130
3407
  ...base,
3131
3408
  id,
3132
- // callable deployment name
3133
3409
  name,
3134
3410
  provider: "azure",
3135
3411
  baseModelId,
3136
- // real identity for benchmark + live pricing
3137
- supportsToolUse: base.supportsToolUse ?? true,
3138
- ...azurePrice.unknown ? {} : {
3412
+ supportsToolUse: nativeToolsOnThisTransport,
3413
+ ...!azurePrice.unknown ? {
3139
3414
  inputCostPer1kTokens: azurePrice.input,
3140
3415
  outputCostPer1kTokens: azurePrice.output,
3141
- pricingUnknown: false
3142
- }
3416
+ pricingUnknown: !exactAzurePrice
3417
+ } : { pricingUnknown: true }
3143
3418
  };
3144
3419
  }
3145
3420
  return {
@@ -3154,39 +3429,34 @@ function azureModelForDeployment(cfg) {
3154
3429
  maxOutputTokens: 16e3,
3155
3430
  supportsStreaming: true,
3156
3431
  isLocal: false,
3157
- supportsToolUse: true
3432
+ supportsToolUse: true,
3433
+ pricingUnknown: true
3158
3434
  };
3159
3435
  }
3160
- var DEFAULT_AZURE_API_VERSION, AZURE_BASE_MODEL_RULES, AZURE_BASE_MODELS, AzureOpenAIProvider;
3436
+ function stripTrailingSlashes3(value) {
3437
+ let end = value.length;
3438
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
3439
+ return value.slice(0, end);
3440
+ }
3441
+ var DEFAULT_AZURE_API_VERSION, CLOUD_MODELS, AZURE_CATALOG, AZURE_BASE_MODELS, AzureOpenAIProvider;
3161
3442
  var init_azure = __esm({
3162
3443
  "src/providers/azure.ts"() {
3163
3444
  init_constants();
3164
3445
  init_openai();
3165
3446
  init_pricing();
3447
+ init_cloud_model_catalog();
3166
3448
  DEFAULT_AZURE_API_VERSION = "2024-12-01-preview";
3167
- AZURE_BASE_MODEL_RULES = [
3168
- [/gpt-?5\.5/, "gpt-5.5"],
3169
- [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
3170
- [/gpt-?5\.4/, "gpt-5.4"],
3171
- [/gpt-?5.*nano/, "gpt-5-nano"],
3172
- [/gpt-?5.*mini/, "gpt-5-mini"],
3173
- [/gpt-?5/, "gpt-5"],
3174
- [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
3175
- [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
3176
- [/gpt-?4\.1/, "gpt-4.1"],
3177
- [/gpt-?4o-mini/, "gpt-4o-mini"],
3178
- [/gpt-?4o/, "gpt-4o"]
3179
- ];
3180
- AZURE_BASE_MODELS = AZURE_BASE_MODEL_RULES.map(([, id]) => id);
3449
+ CLOUD_MODELS = cloud_model_catalog_default.models;
3450
+ 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));
3451
+ AZURE_BASE_MODELS = AZURE_CATALOG.map((m) => m.id);
3181
3452
  AzureOpenAIProvider = class extends OpenAIProvider {
3182
3453
  constructor(config, model) {
3183
3454
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
3184
- const endpoint = rawUrl.replace(/\/+$/, "");
3455
+ const endpoint = stripTrailingSlashes3(rawUrl);
3185
3456
  super(
3186
3457
  {
3187
3458
  ...config,
3188
3459
  baseUrl: endpoint
3189
- // Kept for superclass compatibility if it reads it
3190
3460
  },
3191
3461
  model
3192
3462
  );
@@ -4170,16 +4440,16 @@ var require_isarray = __commonJS({
4170
4440
  }
4171
4441
  });
4172
4442
 
4173
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/stream.js
4443
+ // node_modules/readable-stream/lib/internal/streams/stream.js
4174
4444
  var require_stream = __commonJS({
4175
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/stream.js"(exports, module) {
4445
+ "node_modules/readable-stream/lib/internal/streams/stream.js"(exports, module) {
4176
4446
  module.exports = __require("stream");
4177
4447
  }
4178
4448
  });
4179
4449
 
4180
- // node_modules/jszip/node_modules/safe-buffer/index.js
4450
+ // node_modules/readable-stream/node_modules/safe-buffer/index.js
4181
4451
  var require_safe_buffer = __commonJS({
4182
- "node_modules/jszip/node_modules/safe-buffer/index.js"(exports, module) {
4452
+ "node_modules/readable-stream/node_modules/safe-buffer/index.js"(exports, module) {
4183
4453
  var buffer = __require("buffer");
4184
4454
  var Buffer2 = buffer.Buffer;
4185
4455
  function copyProps(src, dst) {
@@ -4297,7 +4567,7 @@ var require_util = __commonJS({
4297
4567
  typeof arg === "undefined";
4298
4568
  }
4299
4569
  exports.isPrimitive = isPrimitive;
4300
- exports.isBuffer = Buffer.isBuffer;
4570
+ exports.isBuffer = __require("buffer").Buffer.isBuffer;
4301
4571
  function objectToString(o) {
4302
4572
  return Object.prototype.toString.call(o);
4303
4573
  }
@@ -4350,9 +4620,9 @@ var require_inherits = __commonJS({
4350
4620
  }
4351
4621
  });
4352
4622
 
4353
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/BufferList.js
4623
+ // node_modules/readable-stream/lib/internal/streams/BufferList.js
4354
4624
  var require_BufferList = __commonJS({
4355
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/BufferList.js"(exports, module) {
4625
+ "node_modules/readable-stream/lib/internal/streams/BufferList.js"(exports, module) {
4356
4626
  function _classCallCheck(instance, Constructor) {
4357
4627
  if (!(instance instanceof Constructor)) {
4358
4628
  throw new TypeError("Cannot call a class as a function");
@@ -4427,9 +4697,9 @@ var require_BufferList = __commonJS({
4427
4697
  }
4428
4698
  });
4429
4699
 
4430
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/destroy.js
4700
+ // node_modules/readable-stream/lib/internal/streams/destroy.js
4431
4701
  var require_destroy = __commonJS({
4432
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/destroy.js"(exports, module) {
4702
+ "node_modules/readable-stream/lib/internal/streams/destroy.js"(exports, module) {
4433
4703
  var pna = require_process_nextick_args();
4434
4704
  function destroy(err, cb) {
4435
4705
  var _this = this;
@@ -4502,9 +4772,9 @@ var require_node = __commonJS({
4502
4772
  }
4503
4773
  });
4504
4774
 
4505
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_writable.js
4775
+ // node_modules/readable-stream/lib/_stream_writable.js
4506
4776
  var require_stream_writable = __commonJS({
4507
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_writable.js"(exports, module) {
4777
+ "node_modules/readable-stream/lib/_stream_writable.js"(exports, module) {
4508
4778
  var pna = require_process_nextick_args();
4509
4779
  module.exports = Writable;
4510
4780
  function CorkedRequest(state) {
@@ -4941,9 +5211,9 @@ var require_stream_writable = __commonJS({
4941
5211
  }
4942
5212
  });
4943
5213
 
4944
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_duplex.js
5214
+ // node_modules/readable-stream/lib/_stream_duplex.js
4945
5215
  var require_stream_duplex = __commonJS({
4946
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_duplex.js"(exports, module) {
5216
+ "node_modules/readable-stream/lib/_stream_duplex.js"(exports, module) {
4947
5217
  var pna = require_process_nextick_args();
4948
5218
  var objectKeys = Object.keys || function(obj) {
4949
5219
  var keys2 = [];
@@ -5017,10 +5287,67 @@ var require_stream_duplex = __commonJS({
5017
5287
  }
5018
5288
  });
5019
5289
 
5020
- // node_modules/jszip/node_modules/string_decoder/lib/string_decoder.js
5290
+ // node_modules/string_decoder/node_modules/safe-buffer/index.js
5291
+ var require_safe_buffer2 = __commonJS({
5292
+ "node_modules/string_decoder/node_modules/safe-buffer/index.js"(exports, module) {
5293
+ var buffer = __require("buffer");
5294
+ var Buffer2 = buffer.Buffer;
5295
+ function copyProps(src, dst) {
5296
+ for (var key in src) {
5297
+ dst[key] = src[key];
5298
+ }
5299
+ }
5300
+ if (Buffer2.from && Buffer2.alloc && Buffer2.allocUnsafe && Buffer2.allocUnsafeSlow) {
5301
+ module.exports = buffer;
5302
+ } else {
5303
+ copyProps(buffer, exports);
5304
+ exports.Buffer = SafeBuffer;
5305
+ }
5306
+ function SafeBuffer(arg, encodingOrOffset, length) {
5307
+ return Buffer2(arg, encodingOrOffset, length);
5308
+ }
5309
+ copyProps(Buffer2, SafeBuffer);
5310
+ SafeBuffer.from = function(arg, encodingOrOffset, length) {
5311
+ if (typeof arg === "number") {
5312
+ throw new TypeError("Argument must not be a number");
5313
+ }
5314
+ return Buffer2(arg, encodingOrOffset, length);
5315
+ };
5316
+ SafeBuffer.alloc = function(size, fill, encoding) {
5317
+ if (typeof size !== "number") {
5318
+ throw new TypeError("Argument must be a number");
5319
+ }
5320
+ var buf = Buffer2(size);
5321
+ if (fill !== void 0) {
5322
+ if (typeof encoding === "string") {
5323
+ buf.fill(fill, encoding);
5324
+ } else {
5325
+ buf.fill(fill);
5326
+ }
5327
+ } else {
5328
+ buf.fill(0);
5329
+ }
5330
+ return buf;
5331
+ };
5332
+ SafeBuffer.allocUnsafe = function(size) {
5333
+ if (typeof size !== "number") {
5334
+ throw new TypeError("Argument must be a number");
5335
+ }
5336
+ return Buffer2(size);
5337
+ };
5338
+ SafeBuffer.allocUnsafeSlow = function(size) {
5339
+ if (typeof size !== "number") {
5340
+ throw new TypeError("Argument must be a number");
5341
+ }
5342
+ return buffer.SlowBuffer(size);
5343
+ };
5344
+ }
5345
+ });
5346
+
5347
+ // node_modules/string_decoder/lib/string_decoder.js
5021
5348
  var require_string_decoder = __commonJS({
5022
- "node_modules/jszip/node_modules/string_decoder/lib/string_decoder.js"(exports) {
5023
- var Buffer2 = require_safe_buffer().Buffer;
5349
+ "node_modules/string_decoder/lib/string_decoder.js"(exports) {
5350
+ var Buffer2 = require_safe_buffer2().Buffer;
5024
5351
  var isEncoding = Buffer2.isEncoding || function(encoding) {
5025
5352
  encoding = "" + encoding;
5026
5353
  switch (encoding && encoding.toLowerCase()) {
@@ -5254,9 +5581,9 @@ var require_string_decoder = __commonJS({
5254
5581
  }
5255
5582
  });
5256
5583
 
5257
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_readable.js
5584
+ // node_modules/readable-stream/lib/_stream_readable.js
5258
5585
  var require_stream_readable = __commonJS({
5259
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_readable.js"(exports, module) {
5586
+ "node_modules/readable-stream/lib/_stream_readable.js"(exports, module) {
5260
5587
  var pna = require_process_nextick_args();
5261
5588
  module.exports = Readable2;
5262
5589
  var isArray = require_isarray();
@@ -5939,9 +6266,9 @@ var require_stream_readable = __commonJS({
5939
6266
  }
5940
6267
  });
5941
6268
 
5942
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_transform.js
6269
+ // node_modules/readable-stream/lib/_stream_transform.js
5943
6270
  var require_stream_transform = __commonJS({
5944
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_transform.js"(exports, module) {
6271
+ "node_modules/readable-stream/lib/_stream_transform.js"(exports, module) {
5945
6272
  module.exports = Transform;
5946
6273
  var Duplex = require_stream_duplex();
5947
6274
  var util = Object.create(require_util());
@@ -6038,9 +6365,9 @@ var require_stream_transform = __commonJS({
6038
6365
  }
6039
6366
  });
6040
6367
 
6041
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_passthrough.js
6368
+ // node_modules/readable-stream/lib/_stream_passthrough.js
6042
6369
  var require_stream_passthrough = __commonJS({
6043
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_passthrough.js"(exports, module) {
6370
+ "node_modules/readable-stream/lib/_stream_passthrough.js"(exports, module) {
6044
6371
  module.exports = PassThrough;
6045
6372
  var Transform = require_stream_transform();
6046
6373
  var util = Object.create(require_util());
@@ -6056,9 +6383,9 @@ var require_stream_passthrough = __commonJS({
6056
6383
  }
6057
6384
  });
6058
6385
 
6059
- // node_modules/jszip/node_modules/readable-stream/readable.js
6386
+ // node_modules/readable-stream/readable.js
6060
6387
  var require_readable = __commonJS({
6061
- "node_modules/jszip/node_modules/readable-stream/readable.js"(exports, module) {
6388
+ "node_modules/readable-stream/readable.js"(exports, module) {
6062
6389
  var Stream = __require("stream");
6063
6390
  if (process.env.READABLE_STREAM === "disable" && Stream) {
6064
6391
  module.exports = Stream;
@@ -6967,8 +7294,8 @@ var require_utils = __commonJS({
6967
7294
  var result = transform[inputType][outputType](input);
6968
7295
  return result;
6969
7296
  };
6970
- exports.resolve = function(path40) {
6971
- var parts = path40.split("/");
7297
+ exports.resolve = function(path41) {
7298
+ var parts = path41.split("/");
6972
7299
  var result = [];
6973
7300
  for (var index = 0; index < parts.length; index++) {
6974
7301
  var part = parts[index];
@@ -12783,18 +13110,18 @@ var require_object = __commonJS({
12783
13110
  var object = new ZipObject(name, zipObjectContent, o);
12784
13111
  this.files[name] = object;
12785
13112
  };
12786
- var parentFolder = function(path40) {
12787
- if (path40.slice(-1) === "/") {
12788
- path40 = path40.substring(0, path40.length - 1);
13113
+ var parentFolder = function(path41) {
13114
+ if (path41.slice(-1) === "/") {
13115
+ path41 = path41.substring(0, path41.length - 1);
12789
13116
  }
12790
- var lastSlash = path40.lastIndexOf("/");
12791
- return lastSlash > 0 ? path40.substring(0, lastSlash) : "";
13117
+ var lastSlash = path41.lastIndexOf("/");
13118
+ return lastSlash > 0 ? path41.substring(0, lastSlash) : "";
12792
13119
  };
12793
- var forceTrailingSlash = function(path40) {
12794
- if (path40.slice(-1) !== "/") {
12795
- path40 += "/";
13120
+ var forceTrailingSlash = function(path41) {
13121
+ if (path41.slice(-1) !== "/") {
13122
+ path41 += "/";
12796
13123
  }
12797
- return path40;
13124
+ return path41;
12798
13125
  };
12799
13126
  var folderAdd = function(name, createFolders) {
12800
13127
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -13793,12 +14120,12 @@ var init_audit_logger = __esm({
13793
14120
  constructor(workspacePath, debugMode = false) {
13794
14121
  this.workspacePath = workspacePath;
13795
14122
  this.debugMode = debugMode;
13796
- const cascadeDir = path35.join(workspacePath, ".cascade");
13797
- if (!fs32.existsSync(cascadeDir)) {
13798
- fs32.mkdirSync(cascadeDir, { recursive: true });
14123
+ const cascadeDir = path36.join(workspacePath, ".cascade");
14124
+ if (!fs33.existsSync(cascadeDir)) {
14125
+ fs33.mkdirSync(cascadeDir, { recursive: true });
13799
14126
  }
13800
- this.keyPath = path35.join(cascadeDir, "audit_log.key");
13801
- this.dbPath = path35.join(cascadeDir, "audit_log.db");
14127
+ this.keyPath = path36.join(cascadeDir, "audit_log.key");
14128
+ this.dbPath = path36.join(cascadeDir, "audit_log.db");
13802
14129
  this.initEncryptionKey();
13803
14130
  this.db = new Database2(this.dbPath);
13804
14131
  this.db.pragma("journal_mode = WAL");
@@ -13828,11 +14155,11 @@ var init_audit_logger = __esm({
13828
14155
  return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
13829
14156
  }
13830
14157
  initEncryptionKey() {
13831
- if (fs32.existsSync(this.keyPath)) {
13832
- this.encryptionKey = fs32.readFileSync(this.keyPath);
14158
+ if (fs33.existsSync(this.keyPath)) {
14159
+ this.encryptionKey = fs33.readFileSync(this.keyPath);
13833
14160
  } else {
13834
14161
  this.encryptionKey = crypto.randomBytes(32);
13835
- fs32.writeFileSync(this.keyPath, this.encryptionKey);
14162
+ fs33.writeFileSync(this.keyPath, this.encryptionKey);
13836
14163
  }
13837
14164
  }
13838
14165
  encrypt(text) {
@@ -13910,9 +14237,9 @@ var init_audit_logger = __esm({
13910
14237
  dumpDebugIfNeeded() {
13911
14238
  if (!this.debugMode) return;
13912
14239
  try {
13913
- const dumpPath = path35.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
14240
+ const dumpPath = path36.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
13914
14241
  const entries = this.getAllLogs();
13915
- fs32.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
14242
+ fs33.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
13916
14243
  } catch (err) {
13917
14244
  console.error("Failed to dump debug audit logs", err);
13918
14245
  }
@@ -13967,7 +14294,7 @@ var Keystore = class {
13967
14294
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
13968
14295
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
13969
14296
  this.backend = "keytar";
13970
- if (password && fs32.existsSync(this.storePath)) {
14297
+ if (password && fs33.existsSync(this.storePath)) {
13971
14298
  try {
13972
14299
  const fileEntries = this.decryptFile(password);
13973
14300
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -13986,7 +14313,7 @@ var Keystore = class {
13986
14313
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
13987
14314
  );
13988
14315
  }
13989
- if (!fs32.existsSync(this.storePath)) {
14316
+ if (!fs33.existsSync(this.storePath)) {
13990
14317
  const salt = crypto.randomBytes(SALT_LEN);
13991
14318
  this.masterKey = this.deriveKey(password, salt);
13992
14319
  this.writeWithSalt({}, salt);
@@ -14000,7 +14327,7 @@ var Keystore = class {
14000
14327
  }
14001
14328
  /** Synchronous legacy unlock kept for AES-only environments. */
14002
14329
  unlockSync(password) {
14003
- if (!fs32.existsSync(this.storePath)) {
14330
+ if (!fs33.existsSync(this.storePath)) {
14004
14331
  const salt = crypto.randomBytes(SALT_LEN);
14005
14332
  this.masterKey = this.deriveKey(password, salt);
14006
14333
  this.writeWithSalt({}, salt);
@@ -14058,7 +14385,7 @@ var Keystore = class {
14058
14385
  }
14059
14386
  }
14060
14387
  decryptFile(password, knownSalt) {
14061
- if (!fs32.existsSync(this.storePath)) return {};
14388
+ if (!fs33.existsSync(this.storePath)) return {};
14062
14389
  try {
14063
14390
  const { salt, ciphertext, iv, tag } = this.readRaw();
14064
14391
  const useSalt = knownSalt ?? salt;
@@ -14080,8 +14407,8 @@ var Keystore = class {
14080
14407
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
14081
14408
  const tag = cipher.getAuthTag();
14082
14409
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
14083
- fs32.mkdirSync(path35.dirname(this.storePath), { recursive: true });
14084
- fs32.writeFileSync(this.storePath, out, { mode: 384 });
14410
+ fs33.mkdirSync(path36.dirname(this.storePath), { recursive: true });
14411
+ fs33.writeFileSync(this.storePath, out, { mode: 384 });
14085
14412
  }
14086
14413
  writeWithSalt(data, salt) {
14087
14414
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -14091,11 +14418,11 @@ var Keystore = class {
14091
14418
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
14092
14419
  const tag = cipher.getAuthTag();
14093
14420
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
14094
- fs32.mkdirSync(path35.dirname(this.storePath), { recursive: true });
14095
- fs32.writeFileSync(this.storePath, out, { mode: 384 });
14421
+ fs33.mkdirSync(path36.dirname(this.storePath), { recursive: true });
14422
+ fs33.writeFileSync(this.storePath, out, { mode: 384 });
14096
14423
  }
14097
14424
  readRaw() {
14098
- const buf = fs32.readFileSync(this.storePath);
14425
+ const buf = fs33.readFileSync(this.storePath);
14099
14426
  let offset = 0;
14100
14427
  const salt = buf.subarray(offset, offset + SALT_LEN);
14101
14428
  offset += SALT_LEN;
@@ -14128,7 +14455,7 @@ var CascadeIgnore = class {
14128
14455
  ]);
14129
14456
  }
14130
14457
  async load(workspacePath) {
14131
- const filePath = path35.join(workspacePath, ".cascadeignore");
14458
+ const filePath = path36.join(workspacePath, ".cascadeignore");
14132
14459
  try {
14133
14460
  const content = await fs11.readFile(filePath, "utf-8");
14134
14461
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -14139,7 +14466,7 @@ var CascadeIgnore = class {
14139
14466
  }
14140
14467
  isIgnored(filePath, workspacePath) {
14141
14468
  try {
14142
- const relative = workspacePath ? path35.relative(workspacePath, filePath) : filePath;
14469
+ const relative = workspacePath ? path36.relative(workspacePath, filePath) : filePath;
14143
14470
  return this.ig.ignores(relative);
14144
14471
  } catch {
14145
14472
  return false;
@@ -14150,7 +14477,7 @@ var CascadeIgnore = class {
14150
14477
  }
14151
14478
  };
14152
14479
  async function createDefaultIgnoreFile(workspacePath) {
14153
- const filePath = path35.join(workspacePath, ".cascadeignore");
14480
+ const filePath = path36.join(workspacePath, ".cascadeignore");
14154
14481
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
14155
14482
  # Syntax identical to .gitignore
14156
14483
 
@@ -14180,7 +14507,7 @@ Thumbs.db
14180
14507
  await fs11.writeFile(filePath, content, "utf-8");
14181
14508
  }
14182
14509
  async function loadCascadeMd(workspacePath) {
14183
- const filePath = path35.join(workspacePath, "CASCADE.md");
14510
+ const filePath = path36.join(workspacePath, "CASCADE.md");
14184
14511
  try {
14185
14512
  const raw = await fs11.readFile(filePath, "utf-8");
14186
14513
  return parseCascadeMd(raw);
@@ -14209,7 +14536,7 @@ ${raw.trim()}`;
14209
14536
  return { raw, sections, systemPrompt };
14210
14537
  }
14211
14538
  async function createDefaultCascadeMd(workspacePath) {
14212
- const filePath = path35.join(workspacePath, "CASCADE.md");
14539
+ const filePath = path36.join(workspacePath, "CASCADE.md");
14213
14540
  const content = `# Cascade Project Instructions
14214
14541
 
14215
14542
  This file contains project-specific instructions for the Cascade AI agent.
@@ -14244,7 +14571,7 @@ List any areas the agent should not touch.
14244
14571
  var MemoryStore = class _MemoryStore {
14245
14572
  db;
14246
14573
  constructor(dbPath) {
14247
- fs32.mkdirSync(path35.dirname(dbPath), { recursive: true });
14574
+ fs33.mkdirSync(path36.dirname(dbPath), { recursive: true });
14248
14575
  try {
14249
14576
  this.db = new Database2(dbPath, { timeout: 5e3 });
14250
14577
  this.db.pragma("journal_mode = WAL");
@@ -15142,13 +15469,38 @@ var WebSearchConfigSchema = z.object({
15142
15469
  /** Tavily API key — get one at https://tavily.com */
15143
15470
  tavilyApiKey: z.string().optional(),
15144
15471
  /** Max results per search (default 5) */
15145
- maxResults: z.number().default(5)
15472
+ maxResults: z.number().default(5),
15473
+ /**
15474
+ * Send `searxngUrl` through the SSRF guard. Off for a local run, where the
15475
+ * URL is the machine owner's own config and a self-hosted SearXNG is
15476
+ * routinely on a private address; on for a host that accepts it from a
15477
+ * request. See `src/tools/web-search.ts`.
15478
+ *
15479
+ * It has to be named HERE, not just on the TypeScript interfaces: the config
15480
+ * passes through `CascadeConfigSchema.parse()` on its way into a run, and
15481
+ * zod drops every key this object does not declare.
15482
+ */
15483
+ guardSearxngUrl: z.boolean().optional()
15146
15484
  });
15147
15485
  var ToolsConfigSchema = z.object({
15148
15486
  shellAllowlist: z.array(z.string()).default([]),
15149
15487
  shellBlocklist: z.array(z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
15150
15488
  requireApprovalFor: z.array(z.string()).default([]),
15151
15489
  browserEnabled: z.boolean().default(false),
15490
+ /**
15491
+ * Let a run ACT on the page open in the desktop's built-in browser — click,
15492
+ * type, submit — not just read it.
15493
+ *
15494
+ * Distinct from `browserEnabled`, which turns on the headless Playwright
15495
+ * tool. That one drives a throwaway browser with an empty cookie jar; this
15496
+ * one drives the session the user is signed into, which is a different order
15497
+ * of consequence and so gets its own switch rather than riding on that one.
15498
+ *
15499
+ * Off by default. Turning it on does not hand anything over silently: the
15500
+ * tool still goes through the approval chain, and the browser panel carries a
15501
+ * stop control for the run that is acting.
15502
+ */
15503
+ agentBrowserControl: z.boolean().default(false),
15152
15504
  mcpServers: z.array(McpServerConfigSchema).optional(),
15153
15505
  mcpTrusted: z.array(z.string()).optional(),
15154
15506
  /** Web search backends — at least one should be configured for best results */
@@ -15551,7 +15903,7 @@ function credentialEndpointsConflict(type, rowEndpoint, credentialEndpoint) {
15551
15903
 
15552
15904
  // src/config/global-credentials.ts
15553
15905
  function credentialsPath(globalDir2) {
15554
- return path35.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
15906
+ return path36.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
15555
15907
  }
15556
15908
  function providerKey(p) {
15557
15909
  return p.type;
@@ -15581,7 +15933,7 @@ function isPersistable(p) {
15581
15933
  }
15582
15934
  function loadGlobalCredentials(globalDir2) {
15583
15935
  try {
15584
- const raw = fs32.readFileSync(credentialsPath(globalDir2), "utf-8");
15936
+ const raw = fs33.readFileSync(credentialsPath(globalDir2), "utf-8");
15585
15937
  const parsed = JSON.parse(raw);
15586
15938
  if (!Array.isArray(parsed.providers)) return [];
15587
15939
  return parsed.providers.filter(
@@ -15593,13 +15945,13 @@ function loadGlobalCredentials(globalDir2) {
15593
15945
  }
15594
15946
  function saveGlobalCredentials(globalDir2, providers) {
15595
15947
  const filePath = credentialsPath(globalDir2);
15596
- fs32.mkdirSync(globalDir2, { recursive: true, mode: 448 });
15948
+ fs33.mkdirSync(globalDir2, { recursive: true, mode: 448 });
15597
15949
  const body = { version: 1, providers: providers.filter(isPersistable) };
15598
15950
  const tmp = `${filePath}.tmp`;
15599
- fs32.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
15600
- fs32.renameSync(tmp, filePath);
15951
+ fs33.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
15952
+ fs33.renameSync(tmp, filePath);
15601
15953
  try {
15602
- fs32.chmodSync(filePath, 384);
15954
+ fs33.chmodSync(filePath, 384);
15603
15955
  } catch {
15604
15956
  }
15605
15957
  }
@@ -16004,7 +16356,7 @@ var ConfigManager = class {
16004
16356
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
16005
16357
  constructor(workspacePath = process.cwd(), globalDirOverride) {
16006
16358
  this.workspacePath = workspacePath;
16007
- this.globalDir = globalDirOverride ?? path35.join(os10.homedir(), GLOBAL_CONFIG_DIR);
16359
+ this.globalDir = globalDirOverride ?? path36.join(os10.homedir(), GLOBAL_CONFIG_DIR);
16008
16360
  }
16009
16361
  async load() {
16010
16362
  this.config = await this.loadConfig();
@@ -16018,8 +16370,8 @@ var ConfigManager = class {
16018
16370
  this.ignore = new CascadeIgnore();
16019
16371
  await this.ignore.load(this.workspacePath);
16020
16372
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
16021
- this.keystore = new Keystore(path35.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
16022
- this.store = new MemoryStore(path35.join(this.workspacePath, CASCADE_DB_FILE));
16373
+ this.keystore = new Keystore(path36.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
16374
+ this.store = new MemoryStore(path36.join(this.workspacePath, CASCADE_DB_FILE));
16023
16375
  const globalRevoked = stripRevokedCredentials(loadGlobalCredentials(this.globalDir));
16024
16376
  const globalCreds = filterRetiredCredentials(globalRevoked.kept);
16025
16377
  if (globalRevoked.removed > 0) this.revokedCredentials += globalRevoked.removed;
@@ -16092,8 +16444,8 @@ var ConfigManager = class {
16092
16444
  * never reached the disk (see `commitSettings`).
16093
16445
  */
16094
16446
  async save(config = this.config) {
16095
- const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
16096
- await fs11.mkdir(path35.dirname(configPath), { recursive: true });
16447
+ const configPath = path36.join(this.workspacePath, CASCADE_CONFIG_FILE);
16448
+ await fs11.mkdir(path36.dirname(configPath), { recursive: true });
16097
16449
  await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
16098
16450
  try {
16099
16451
  saveGlobalCredentials(this.globalDir, config.providers);
@@ -16143,7 +16495,7 @@ var ConfigManager = class {
16143
16495
  this.revokedCredentials = 0;
16144
16496
  this.revokedPins = [];
16145
16497
  this.revokedNotice = void 0;
16146
- const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
16498
+ const configPath = path36.join(this.workspacePath, CASCADE_CONFIG_FILE);
16147
16499
  try {
16148
16500
  const raw = await fs11.readFile(configPath, "utf-8");
16149
16501
  const parsed = JSON.parse(raw);
@@ -16237,7 +16589,7 @@ var ConfigManager = class {
16237
16589
  return onResource;
16238
16590
  }
16239
16591
  async persistClearedPins(tiers) {
16240
- const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
16592
+ const configPath = path36.join(this.workspacePath, CASCADE_CONFIG_FILE);
16241
16593
  try {
16242
16594
  const raw = JSON.parse(await fs11.readFile(configPath, "utf-8"));
16243
16595
  const models = raw["models"];
@@ -17961,7 +18313,7 @@ init_pricing();
17961
18313
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
17962
18314
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
17963
18315
  var FETCH_TIMEOUT_MS = 8e3;
17964
- var DEFAULT_CACHE_FILE = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
18316
+ var DEFAULT_CACHE_FILE = path36.join(os10.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
17965
18317
  function normalizeModelId2(id) {
17966
18318
  let s = id.toLowerCase();
17967
18319
  const slash = s.lastIndexOf("/");
@@ -18108,7 +18460,7 @@ var LiveDataProvider = class {
18108
18460
  }
18109
18461
  async saveCache() {
18110
18462
  try {
18111
- await fs11.mkdir(path35.dirname(this.opts.cacheFile), { recursive: true });
18463
+ await fs11.mkdir(path36.dirname(this.opts.cacheFile), { recursive: true });
18112
18464
  const cache = {
18113
18465
  fetchedAt: this.fetchedAt,
18114
18466
  snapshot: this.snapshot ?? void 0,
@@ -18215,9 +18567,15 @@ var FAMILY_BENCHMARKS = {
18215
18567
  "claude-opus": { code: 95, analysis: 92, creative: 90, data: 88 },
18216
18568
  "claude-sonnet": { code: 93, analysis: 88, creative: 87, data: 85 },
18217
18569
  "claude-haiku": { code: 80, analysis: 75, creative: 76, data: 72 },
18218
- // OpenAI GPT-5 family — current flagship; strongest all-round. Offline
18219
- // baseline (the live fetch refreshes these). Distinct point releases carry
18220
- // their own scores: gpt-5.5 "Spud" leads SWE-bench (88.7%); gpt-5.4 ~80%.
18570
+ // OpenAI GPT-5 family. GPT-5.6 is split by durable capability tier instead
18571
+ // of being folded into generic gpt-5: Sol, Terra and Luna have materially
18572
+ // different benchmark/cost envelopes and should compete independently.
18573
+ // Scores are the committed normalized snapshot; provenance is in
18574
+ // scripts/benchmarks/sources/openai-gpt-5.6-2026-08.json.
18575
+ "gpt-5.6-sol": { code: 100, analysis: 98, creative: 88, data: 88 },
18576
+ "gpt-5.6-terra": { code: 97, analysis: 92, creative: 82, data: 85 },
18577
+ "gpt-5.6-luna": { code: 93, analysis: 85, creative: 74, data: 60 },
18578
+ // Older GPT-5 point releases remain distinct where public data supports it.
18221
18579
  "gpt-5.5": { code: 97, analysis: 96, creative: 94, data: 95 },
18222
18580
  "gpt-5.4": { code: 94, analysis: 94, creative: 92, data: 92 },
18223
18581
  "gpt-5.4-mini": { code: 86, analysis: 85, creative: 85, data: 83 },
@@ -18253,13 +18611,19 @@ var FAMILY_BENCHMARKS = {
18253
18611
  "llama-small": { code: 55, analysis: 56, creative: 60, data: 54 }
18254
18612
  };
18255
18613
  var FAMILY_MATCHERS = [
18614
+ [/fable/i, "claude-opus"],
18256
18615
  [/opus/i, "claude-opus"],
18257
18616
  [/sonnet/i, "claude-sonnet"],
18258
18617
  [/haiku/i, "claude-haiku"],
18259
- // GPT-5 family — ordered most-specific first. Distinct point releases
18260
- // (gpt-5.5, gpt-5.4, gpt-5.4-mini) match their own family; nano/mini before
18261
- // the base; unrecognised gpt-5.x still fold into the gpt-5 base.
18618
+ // GPT-5 family — ordered most-specific first. GPT-5.6 durable tiers carry
18619
+ // their own profiles. 5.4 Nano intentionally maps to the nano class rather
18620
+ // than being swallowed by the generic 5.4 rule.
18621
+ [/gpt-?5\.6.*sol/i, "gpt-5.6-sol"],
18622
+ [/gpt-?5\.6.*terra/i, "gpt-5.6-terra"],
18623
+ [/gpt-?5\.6.*luna/i, "gpt-5.6-luna"],
18624
+ [/gpt-?5\.6(?:\s|$)/i, "gpt-5.6-sol"],
18262
18625
  [/gpt-?5\.5/i, "gpt-5.5"],
18626
+ [/gpt-?5\.4.*nano/i, "gpt-5-nano"],
18263
18627
  [/gpt-?5\.4.*mini/i, "gpt-5.4-mini"],
18264
18628
  [/gpt-?5\.4/i, "gpt-5.4"],
18265
18629
  [/gpt-?5.*nano/i, "gpt-5-nano"],
@@ -21061,6 +21425,23 @@ Now execute your subtask using this context where relevant.`
21061
21425
  this.setStatus("FAILED", finalOutput);
21062
21426
  this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput, "FAILED");
21063
21427
  return this.buildResult("ESCALATED", finalOutput, { checksRun, passed, failed }, issues, correctionAttempts);
21428
+ } finally {
21429
+ this.releaseBrowserControl();
21430
+ }
21431
+ }
21432
+ /**
21433
+ * End this worker's hold on the shared browser.
21434
+ *
21435
+ * Duck-typed rather than imported: `browser_control` is registered only by a
21436
+ * host that HAS a browser (the desktop), so in the CLI and hosted runs there
21437
+ * is no such tool and this is a no-op. Importing the class to instanceof-check
21438
+ * it would couple the worker to a tool that usually is not there.
21439
+ */
21440
+ releaseBrowserControl() {
21441
+ try {
21442
+ const tool = this.toolRegistry.getTool?.("browser_control");
21443
+ tool?.releaseActor?.(this.id);
21444
+ } catch {
21064
21445
  }
21065
21446
  }
21066
21447
  sendToPeer(toId, content) {
@@ -21319,8 +21700,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
21319
21700
  // Media generation can run for a minute; without this a cancelled run
21320
21701
  // still pays for an image nobody will see.
21321
21702
  ...this.signal ? { signal: this.signal } : {},
21322
- saveSnapshot: async (path40, content) => {
21323
- this.store?.addFileSnapshot(this.taskId, path40, content);
21703
+ saveSnapshot: async (path41, content) => {
21704
+ this.store?.addFileSnapshot(this.taskId, path41, content);
21324
21705
  },
21325
21706
  sendPeerSync: (to, syncType, content) => {
21326
21707
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -21523,7 +21904,7 @@ ${assignment.expectedOutput}`;
21523
21904
  const { promisify: promisify4 } = await import('util');
21524
21905
  const execFileAsync2 = promisify4(execFile3);
21525
21906
  for (const artifactPath of artifactPaths) {
21526
- const absolutePath = path35.resolve(this.artifactRoot(), artifactPath);
21907
+ const absolutePath = path36.resolve(this.artifactRoot(), artifactPath);
21527
21908
  try {
21528
21909
  const stat = await fs11.stat(absolutePath);
21529
21910
  if (!stat.isFile()) {
@@ -21558,7 +21939,7 @@ ${assignment.expectedOutput}`;
21558
21939
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
21559
21940
  continue;
21560
21941
  }
21561
- const ext = path35.extname(absolutePath).toLowerCase();
21942
+ const ext = path36.extname(absolutePath).toLowerCase();
21562
21943
  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;
21563
21944
  if (check) {
21564
21945
  try {
@@ -21650,7 +22031,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
21650
22031
  async checkAcceptance(assignment) {
21651
22032
  const criteria = assignment.acceptance ?? [];
21652
22033
  if (!criteria.length) return [];
21653
- const resolve2 = (target) => path35.resolve(this.artifactRoot(), target);
22034
+ const resolve2 = (target) => path36.resolve(this.artifactRoot(), target);
21654
22035
  const workerCanWriteFiles = canProduceFiles(this.tools.map((t) => t.name));
21655
22036
  return evaluateAcceptance(criteria, assignment.files ?? [], {
21656
22037
  stat: async (target) => {
@@ -24961,16 +25342,16 @@ function resolveInWorkspace(workspaceRoot, input) {
24961
25342
  if (typeof input !== "string" || input.length === 0) {
24962
25343
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
24963
25344
  }
24964
- const root = path35.resolve(workspaceRoot);
24965
- const abs = path35.isAbsolute(input) ? path35.resolve(input) : path35.resolve(root, input);
24966
- const rel = path35.relative(root, abs);
24967
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path35.isAbsolute(rel)) {
25345
+ const root = path36.resolve(workspaceRoot);
25346
+ const abs = path36.isAbsolute(input) ? path36.resolve(input) : path36.resolve(root, input);
25347
+ const rel = path36.relative(root, abs);
25348
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path36.isAbsolute(rel)) {
24968
25349
  throw new WorkspaceSandboxError(input, root);
24969
25350
  }
24970
25351
  try {
24971
- const real = fs32.realpathSync(abs);
24972
- const realRel = path35.relative(root, real);
24973
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path35.isAbsolute(realRel))) {
25352
+ const real = fs33.realpathSync(abs);
25353
+ const realRel = path36.relative(root, real);
25354
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path36.isAbsolute(realRel))) {
24974
25355
  throw new WorkspaceSandboxError(input, root);
24975
25356
  }
24976
25357
  } catch (e) {
@@ -25519,9 +25900,9 @@ async function animatePptx(bytes, scheme) {
25519
25900
  const zip = await import_jszip.default.loadAsync(bytes);
25520
25901
  const slidePaths = Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n));
25521
25902
  if (slidePaths.length === 0) return bytes;
25522
- for (const path40 of slidePaths) {
25523
- const xml = await zip.file(path40).async("string");
25524
- zip.file(path40, animateSlideXml(xml, scheme));
25903
+ for (const path41 of slidePaths) {
25904
+ const xml = await zip.file(path41).async("string");
25905
+ zip.file(path41, animateSlideXml(xml, scheme));
25525
25906
  }
25526
25907
  return await zip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
25527
25908
  } catch {
@@ -25902,7 +26283,7 @@ var GenerateDocumentTool = class extends BaseTool {
25902
26283
  }
25903
26284
  }
25904
26285
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
25905
- await fs11.mkdir(path35.dirname(absPath), { recursive: true });
26286
+ await fs11.mkdir(path36.dirname(absPath), { recursive: true });
25906
26287
  await fs11.writeFile(absPath, bytes);
25907
26288
  const charts = countCharts(content);
25908
26289
  return [
@@ -26123,7 +26504,7 @@ var FileWriteTool = class extends BaseTool {
26123
26504
  } catch {
26124
26505
  }
26125
26506
  }
26126
- await fs11.mkdir(path35.dirname(absPath), { recursive: true });
26507
+ await fs11.mkdir(path36.dirname(absPath), { recursive: true });
26127
26508
  await fs11.writeFile(absPath, content, "utf-8");
26128
26509
  return `Written ${content.length} characters to ${filePath}`;
26129
26510
  }
@@ -26481,12 +26862,10 @@ ${data.description}`;
26481
26862
  }
26482
26863
  }
26483
26864
  };
26484
-
26485
- // src/tools/browser.ts
26486
26865
  var BROWSER_LAUNCH_TIMEOUT_MS = 15e3;
26487
26866
  var BrowserTool = class extends BaseTool {
26488
26867
  name = "browser";
26489
- description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Only available with multimodal models.";
26868
+ description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Screenshots are saved to a file for image_analyze to read.";
26490
26869
  inputSchema = {
26491
26870
  type: "object",
26492
26871
  properties: {
@@ -26553,7 +26932,14 @@ var BrowserTool = class extends BaseTool {
26553
26932
  }
26554
26933
  case "screenshot": {
26555
26934
  const buf = await page2.screenshot({ type: "png" });
26556
- return `data:image/png;base64,${buf.toString("base64")}`;
26935
+ const rel = path36.join(".cascade", "screenshots", `screenshot-${Date.now()}-${randomUUID().slice(0, 8)}.png`);
26936
+ const abs = resolveInWorkspace(this.workspaceRoot, rel);
26937
+ await fs11.mkdir(path36.dirname(abs), { recursive: true });
26938
+ await fs11.writeFile(abs, buf);
26939
+ return [
26940
+ `Screenshot saved to ${abs} (${Math.round(buf.byteLength / 1024)} KB).`,
26941
+ `Call image_analyze with that path to see what is on it \u2014 that needs a vision-capable model.`
26942
+ ].join(" ");
26557
26943
  }
26558
26944
  case "evaluate": {
26559
26945
  const result = await page2.evaluate(input["script"]);
@@ -26621,7 +27007,7 @@ var ImageAnalyzeTool = class extends BaseTool {
26621
27007
  };
26622
27008
  async function fileToImageAttachment(filePath) {
26623
27009
  const data = await fs11.readFile(filePath);
26624
- const ext = path35.extname(filePath).toLowerCase();
27010
+ const ext = path36.extname(filePath).toLowerCase();
26625
27011
  const mimeMap = {
26626
27012
  ".jpg": "image/jpeg",
26627
27013
  ".jpeg": "image/jpeg",
@@ -26655,14 +27041,14 @@ var PDFCreateTool = class extends BaseTool {
26655
27041
  const filePath = input["path"];
26656
27042
  const content = input["content"];
26657
27043
  const title = input["title"];
26658
- const dir = path35.dirname(filePath);
26659
- if (!fs32.existsSync(dir)) {
26660
- fs32.mkdirSync(dir, { recursive: true });
27044
+ const dir = path36.dirname(filePath);
27045
+ if (!fs33.existsSync(dir)) {
27046
+ fs33.mkdirSync(dir, { recursive: true });
26661
27047
  }
26662
27048
  return new Promise((resolve2, reject) => {
26663
27049
  try {
26664
27050
  const doc = new PDFDocument({ margin: 50 });
26665
- const stream = fs32.createWriteStream(filePath);
27051
+ const stream = fs33.createWriteStream(filePath);
26666
27052
  doc.pipe(stream);
26667
27053
  if (title) {
26668
27054
  doc.info["Title"] = title;
@@ -26740,22 +27126,22 @@ var CodeInterpreterTool = class extends BaseTool {
26740
27126
  }
26741
27127
  cmdPrefix = NODE_CMD;
26742
27128
  }
26743
- const tmpDir = path35.join(this.workspaceRoot, ".cascade", "tmp");
26744
- if (!fs32.existsSync(tmpDir)) {
26745
- fs32.mkdirSync(tmpDir, { recursive: true });
27129
+ const tmpDir = path36.join(this.workspaceRoot, ".cascade", "tmp");
27130
+ if (!fs33.existsSync(tmpDir)) {
27131
+ fs33.mkdirSync(tmpDir, { recursive: true });
26746
27132
  }
26747
27133
  const extension = language === "python" ? "py" : "js";
26748
27134
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
26749
- const filePath = path35.join(tmpDir, fileName);
26750
- fs32.writeFileSync(filePath, code, "utf-8");
27135
+ const filePath = path36.join(tmpDir, fileName);
27136
+ fs33.writeFileSync(filePath, code, "utf-8");
26751
27137
  const execArgs = [filePath, ...args];
26752
27138
  return new Promise((resolve2) => {
26753
27139
  const startMs = Date.now();
26754
27140
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
26755
27141
  const duration = Date.now() - startMs;
26756
27142
  try {
26757
- if (fs32.existsSync(filePath)) {
26758
- fs32.unlinkSync(filePath);
27143
+ if (fs33.existsSync(filePath)) {
27144
+ fs33.unlinkSync(filePath);
26759
27145
  }
26760
27146
  } catch (cleanupErr) {
26761
27147
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -26835,18 +27221,178 @@ ${formatted}`;
26835
27221
  return `Unknown action: ${action}`;
26836
27222
  }
26837
27223
  };
27224
+ var SsrfBlockedError = class extends Error {
27225
+ constructor(message) {
27226
+ super(message);
27227
+ this.name = "SsrfBlockedError";
27228
+ }
27229
+ };
27230
+ var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
27231
+ var MAX_REDIRECTS2 = 5;
27232
+ function allowLocal() {
27233
+ return process.env["CASCADE_ALLOW_LOCAL_FETCH"] === "1";
27234
+ }
27235
+ function isPrivateAddress(ip) {
27236
+ const type = net.isIP(ip);
27237
+ if (type === 4) return isPrivateIPv4(ip);
27238
+ if (type === 6) return isPrivateIPv6(ip);
27239
+ return true;
27240
+ }
27241
+ function isPrivateIPv4(ip) {
27242
+ const parts = ip.split(".").map((p) => Number(p));
27243
+ if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return true;
27244
+ const [a, b] = parts;
27245
+ if (a === 0) return true;
27246
+ if (a === 10) return true;
27247
+ if (a === 127) return true;
27248
+ if (a === 169 && b === 254) return true;
27249
+ if (a === 172 && b >= 16 && b <= 31) return true;
27250
+ if (a === 192 && b === 168) return true;
27251
+ if (a === 100 && b >= 64 && b <= 127) return true;
27252
+ if (a >= 224) return true;
27253
+ return false;
27254
+ }
27255
+ function ipv6Groups(ip) {
27256
+ let s = ip.toLowerCase().replace(/^\[|\]$/g, "");
27257
+ const zone = s.indexOf("%");
27258
+ if (zone !== -1) s = s.slice(0, zone);
27259
+ const dotted = /^(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
27260
+ if (dotted) {
27261
+ const o = [dotted[2], dotted[3], dotted[4], dotted[5]].map((p) => Number(p));
27262
+ if (o.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
27263
+ s = `${dotted[1]}${(o[0] << 8 | o[1]).toString(16)}:${(o[2] << 8 | o[3]).toString(16)}`;
27264
+ }
27265
+ const halves = s.split("::");
27266
+ if (halves.length > 2) return null;
27267
+ const split = (part) => part ? part.split(":") : [];
27268
+ const head = split(halves[0] ?? "");
27269
+ const tail = halves.length === 2 ? split(halves[1] ?? "") : [];
27270
+ const zeros = halves.length === 2 ? 8 - head.length - tail.length : 0;
27271
+ if (zeros < 0) return null;
27272
+ const groups = [...head, ...Array(zeros).fill("0"), ...tail];
27273
+ if (groups.length !== 8) return null;
27274
+ const out = [];
27275
+ for (const g of groups) {
27276
+ if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
27277
+ out.push(parseInt(g, 16));
27278
+ }
27279
+ return out;
27280
+ }
27281
+ function embeddedIPv4(g) {
27282
+ const quad = (hi, lo) => `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
27283
+ const leadingZero = (n) => g.slice(0, n).every((x) => x === 0);
27284
+ if (leadingZero(5) && g[5] === 65535) return quad(g[6], g[7]);
27285
+ if (leadingZero(6)) return quad(g[6], g[7]);
27286
+ if (g[0] === 100 && g[1] === 65435) {
27287
+ if (g[2] === 1) {
27288
+ return quad(g[3], (g[4] & 255) << 8 | g[5] >> 8);
27289
+ }
27290
+ if (g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0) return quad(g[6], g[7]);
27291
+ return null;
27292
+ }
27293
+ if (g[0] === 8194) return quad(g[1], g[2]);
27294
+ return null;
27295
+ }
27296
+ function isPrivateIPv6(ip) {
27297
+ const g = ipv6Groups(ip);
27298
+ if (!g) return true;
27299
+ const v4 = embeddedIPv4(g);
27300
+ if (v4) return isPrivateIPv4(v4);
27301
+ if (g[0] === 100 && g[1] === 65435) return true;
27302
+ if (g.every((x) => x === 0)) return true;
27303
+ if (leading7Zero(g) && g[7] === 1) return true;
27304
+ if ((g[0] & 65472) === 65152) return true;
27305
+ if ((g[0] & 65024) === 64512) return true;
27306
+ if ((g[0] & 65280) === 65280) return true;
27307
+ return false;
27308
+ }
27309
+ function leading7Zero(g) {
27310
+ return g.slice(0, 7).every((x) => x === 0);
27311
+ }
27312
+ async function assertPublicUrl(rawUrl) {
27313
+ let url;
27314
+ try {
27315
+ url = new URL(rawUrl);
27316
+ } catch {
27317
+ throw new SsrfBlockedError(`Invalid URL: ${rawUrl}`);
27318
+ }
27319
+ if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
27320
+ throw new SsrfBlockedError(`Blocked URL scheme "${url.protocol}" \u2014 only http and https are allowed.`);
27321
+ }
27322
+ if (allowLocal()) return url;
27323
+ const host = url.hostname.replace(/^\[|\]$/g, "");
27324
+ if (net.isIP(host)) {
27325
+ if (isPrivateAddress(host)) {
27326
+ throw new SsrfBlockedError(`Blocked request to non-public address ${host}.`);
27327
+ }
27328
+ return url;
27329
+ }
27330
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
27331
+ throw new SsrfBlockedError(`Blocked request to local hostname "${host}".`);
27332
+ }
27333
+ let addresses;
27334
+ try {
27335
+ const records = await dns2.lookup(host, { all: true });
27336
+ addresses = records.map((r) => r.address);
27337
+ } catch {
27338
+ throw new SsrfBlockedError(`Could not resolve host "${host}".`);
27339
+ }
27340
+ if (addresses.length === 0 || addresses.some((addr) => isPrivateAddress(addr))) {
27341
+ throw new SsrfBlockedError(`Blocked request to "${host}" \u2014 resolves to a non-public address.`);
27342
+ }
27343
+ return url;
27344
+ }
27345
+ var ssrfAgent = new Agent({
27346
+ connect: {
27347
+ lookup(hostname, options, callback) {
27348
+ if (allowLocal()) {
27349
+ dnsCallback.lookup(hostname, options, callback);
27350
+ return;
27351
+ }
27352
+ dnsCallback.lookup(hostname, { ...options, all: true }, (err, addresses) => {
27353
+ if (err) return callback(err, "", 0);
27354
+ const list = Array.isArray(addresses) ? addresses : [addresses];
27355
+ const bad = list.find((a) => isPrivateAddress(a.address));
27356
+ if (bad) {
27357
+ return callback(
27358
+ new SsrfBlockedError(`Blocked connection to "${hostname}" \u2014 resolved to non-public address ${bad.address}.`),
27359
+ "",
27360
+ 0
27361
+ );
27362
+ }
27363
+ if (options.all) return callback(null, list);
27364
+ const first = list[0];
27365
+ return callback(null, first.address, first.family);
27366
+ });
27367
+ }
27368
+ }
27369
+ });
27370
+ async function safeFetch(rawUrl, init = {}) {
27371
+ let currentUrl = (await assertPublicUrl(rawUrl)).toString();
27372
+ for (let i = 0; i <= MAX_REDIRECTS2; i++) {
27373
+ const resp = await fetch(currentUrl, { ...init, redirect: "manual", dispatcher: ssrfAgent });
27374
+ if (resp.status < 300 || resp.status >= 400) return resp;
27375
+ const location = resp.headers.get("location");
27376
+ if (!location) return resp;
27377
+ const next = new URL(location, currentUrl);
27378
+ await assertPublicUrl(next.toString());
27379
+ currentUrl = next.toString();
27380
+ }
27381
+ throw new SsrfBlockedError(`Too many redirects (>${MAX_REDIRECTS2}).`);
27382
+ }
26838
27383
 
26839
27384
  // src/tools/web-search.ts
26840
- async function searchSearXNG(query, baseUrl, maxResults) {
27385
+ async function searchSearXNG(query, baseUrl, maxResults, guardUrl = false) {
26841
27386
  const url = new URL("/search", baseUrl);
26842
27387
  url.searchParams.set("q", query);
26843
27388
  url.searchParams.set("format", "json");
26844
27389
  url.searchParams.set("categories", "general");
26845
27390
  url.searchParams.set("engines", "google,bing,duckduckgo");
26846
- const resp = await fetch(url.toString(), {
27391
+ const init = {
26847
27392
  headers: { "User-Agent": "Cascade-AI/1.0 WebSearchTool" },
26848
27393
  signal: AbortSignal.timeout(1e4)
26849
- });
27394
+ };
27395
+ const resp = guardUrl ? await safeFetch(url.toString(), init) : await fetch(url.toString(), init);
26850
27396
  if (!resp.ok) {
26851
27397
  throw new Error(`SearXNG returned HTTP ${resp.status}`);
26852
27398
  }
@@ -26975,7 +27521,11 @@ var WebSearchTool = class extends BaseTool {
26975
27521
  searxngUrl: config.searxngUrl ?? process.env["SEARXNG_URL"],
26976
27522
  braveApiKey: config.braveApiKey ?? process.env["BRAVE_SEARCH_API_KEY"],
26977
27523
  tavilyApiKey: config.tavilyApiKey ?? process.env["TAVILY_API_KEY"],
26978
- maxResults: config.maxResults ?? 5
27524
+ maxResults: config.maxResults ?? 5,
27525
+ // Note this reads only from `config`, never from the environment: the
27526
+ // guard tracks the trustworthiness of the URL the host passed in, and an
27527
+ // env var says nothing about that.
27528
+ guardSearxngUrl: config.guardSearxngUrl ?? false
26979
27529
  };
26980
27530
  }
26981
27531
  async execute(input, _options) {
@@ -26990,7 +27540,7 @@ var WebSearchTool = class extends BaseTool {
26990
27540
  let anyBackendReachable = false;
26991
27541
  if (this.config.searxngUrl) {
26992
27542
  try {
26993
- results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
27543
+ results = await searchSearXNG(query, this.config.searxngUrl, maxResults, this.config.guardSearxngUrl);
26994
27544
  anyBackendReachable = true;
26995
27545
  if (results.length > 0) return this.formatResults(query, results);
26996
27546
  errors.push("SearXNG: returned 0 results");
@@ -27067,7 +27617,7 @@ var GlobTool = class extends BaseTool {
27067
27617
  };
27068
27618
  async execute(input, _options) {
27069
27619
  const pattern = input["pattern"];
27070
- const searchPath = input["path"] ? path35.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27620
+ const searchPath = input["path"] ? path36.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27071
27621
  const matches = await glob(pattern, {
27072
27622
  cwd: searchPath,
27073
27623
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -27080,7 +27630,7 @@ var GlobTool = class extends BaseTool {
27080
27630
  const withMtime = await Promise.all(
27081
27631
  matches.map(async (rel) => {
27082
27632
  try {
27083
- const stat = await fs11.stat(path35.join(searchPath, rel));
27633
+ const stat = await fs11.stat(path36.join(searchPath, rel));
27084
27634
  return { rel, mtime: stat.mtimeMs };
27085
27635
  } catch {
27086
27636
  return { rel, mtime: 0 };
@@ -27129,7 +27679,7 @@ var GrepTool = class extends BaseTool {
27129
27679
  };
27130
27680
  async execute(input, _options) {
27131
27681
  const pattern = input["pattern"];
27132
- const searchPath = input["path"] ? path35.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27682
+ const searchPath = input["path"] ? path36.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27133
27683
  const globPattern = input["glob"];
27134
27684
  const outputMode = input["output_mode"] ?? "content";
27135
27685
  const context = input["context"] ?? 0;
@@ -27183,12 +27733,12 @@ var GrepTool = class extends BaseTool {
27183
27733
  nodir: true
27184
27734
  });
27185
27735
  } catch {
27186
- files = [path35.relative(searchPath, searchPath) || "."];
27736
+ files = [path36.relative(searchPath, searchPath) || "."];
27187
27737
  }
27188
27738
  const results = [];
27189
27739
  let totalCount = 0;
27190
27740
  for (const rel of files) {
27191
- const abs = path35.join(searchPath, rel);
27741
+ const abs = path36.join(searchPath, rel);
27192
27742
  let content;
27193
27743
  try {
27194
27744
  content = await fs11.readFile(abs, "utf-8");
@@ -27229,92 +27779,6 @@ Total: ${totalCount} matches`);
27229
27779
  return results.join("\n");
27230
27780
  }
27231
27781
  };
27232
- var SsrfBlockedError = class extends Error {
27233
- constructor(message) {
27234
- super(message);
27235
- this.name = "SsrfBlockedError";
27236
- }
27237
- };
27238
- var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
27239
- var MAX_REDIRECTS2 = 5;
27240
- function allowLocal() {
27241
- return process.env["CASCADE_ALLOW_LOCAL_FETCH"] === "1";
27242
- }
27243
- function isPrivateAddress(ip) {
27244
- const type = net.isIP(ip);
27245
- if (type === 4) return isPrivateIPv4(ip);
27246
- if (type === 6) return isPrivateIPv6(ip);
27247
- return true;
27248
- }
27249
- function isPrivateIPv4(ip) {
27250
- const parts = ip.split(".").map((p) => Number(p));
27251
- if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return true;
27252
- const [a, b] = parts;
27253
- if (a === 0) return true;
27254
- if (a === 10) return true;
27255
- if (a === 127) return true;
27256
- if (a === 169 && b === 254) return true;
27257
- if (a === 172 && b >= 16 && b <= 31) return true;
27258
- if (a === 192 && b === 168) return true;
27259
- if (a === 100 && b >= 64 && b <= 127) return true;
27260
- if (a >= 224) return true;
27261
- return false;
27262
- }
27263
- function isPrivateIPv6(ip) {
27264
- const lower = ip.toLowerCase().replace(/^\[|\]$/g, "");
27265
- if (lower === "::1" || lower === "::") return true;
27266
- if (lower.startsWith("fe80")) return true;
27267
- if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
27268
- const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower);
27269
- if (mapped) return isPrivateIPv4(mapped[1]);
27270
- return false;
27271
- }
27272
- async function assertPublicUrl(rawUrl) {
27273
- let url;
27274
- try {
27275
- url = new URL(rawUrl);
27276
- } catch {
27277
- throw new SsrfBlockedError(`Invalid URL: ${rawUrl}`);
27278
- }
27279
- if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
27280
- throw new SsrfBlockedError(`Blocked URL scheme "${url.protocol}" \u2014 only http and https are allowed.`);
27281
- }
27282
- if (allowLocal()) return url;
27283
- const host = url.hostname.replace(/^\[|\]$/g, "");
27284
- if (net.isIP(host)) {
27285
- if (isPrivateAddress(host)) {
27286
- throw new SsrfBlockedError(`Blocked request to non-public address ${host}.`);
27287
- }
27288
- return url;
27289
- }
27290
- if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
27291
- throw new SsrfBlockedError(`Blocked request to local hostname "${host}".`);
27292
- }
27293
- let addresses;
27294
- try {
27295
- const records = await dns2.lookup(host, { all: true });
27296
- addresses = records.map((r) => r.address);
27297
- } catch {
27298
- throw new SsrfBlockedError(`Could not resolve host "${host}".`);
27299
- }
27300
- if (addresses.length === 0 || addresses.some((addr) => isPrivateAddress(addr))) {
27301
- throw new SsrfBlockedError(`Blocked request to "${host}" \u2014 resolves to a non-public address.`);
27302
- }
27303
- return url;
27304
- }
27305
- async function safeFetch(rawUrl, init = {}) {
27306
- let currentUrl = (await assertPublicUrl(rawUrl)).toString();
27307
- for (let i = 0; i <= MAX_REDIRECTS2; i++) {
27308
- const resp = await fetch(currentUrl, { ...init, redirect: "manual" });
27309
- if (resp.status < 300 || resp.status >= 400) return resp;
27310
- const location = resp.headers.get("location");
27311
- if (!location) return resp;
27312
- const next = new URL(location, currentUrl);
27313
- await assertPublicUrl(next.toString());
27314
- currentUrl = next.toString();
27315
- }
27316
- throw new SsrfBlockedError(`Too many redirects (>${MAX_REDIRECTS2}).`);
27317
- }
27318
27782
 
27319
27783
  // src/tools/web-fetch.ts
27320
27784
  var MAX_CHARS = 5e4;
@@ -27587,10 +28051,10 @@ var ToolRegistry = class extends EventEmitter {
27587
28051
  }
27588
28052
  isIgnored(filePath) {
27589
28053
  if (!filePath) return false;
27590
- const abs = path35.resolve(this.workspaceRoot, filePath);
27591
- const rel = path35.relative(this.workspaceRoot, abs);
27592
- if (!rel || rel.startsWith("..") || path35.isAbsolute(rel)) return true;
27593
- const posixRel = rel.split(path35.sep).join("/");
28054
+ const abs = path36.resolve(this.workspaceRoot, filePath);
28055
+ const rel = path36.relative(this.workspaceRoot, abs);
28056
+ if (!rel || rel.startsWith("..") || path36.isAbsolute(rel)) return true;
28057
+ const posixRel = rel.split(path36.sep).join("/");
27594
28058
  return this.ignoreMatcher.ignores(posixRel);
27595
28059
  }
27596
28060
  };
@@ -27824,22 +28288,22 @@ var FileMcpOAuthStore = class {
27824
28288
  filePath;
27825
28289
  load() {
27826
28290
  try {
27827
- return JSON.parse(fs32.readFileSync(this.filePath, "utf-8"));
28291
+ return JSON.parse(fs33.readFileSync(this.filePath, "utf-8"));
27828
28292
  } catch {
27829
28293
  return void 0;
27830
28294
  }
27831
28295
  }
27832
28296
  save(state) {
27833
- fs32.mkdirSync(path35.dirname(this.filePath), { recursive: true, mode: 448 });
27834
- fs32.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
28297
+ fs33.mkdirSync(path36.dirname(this.filePath), { recursive: true, mode: 448 });
28298
+ fs33.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
27835
28299
  try {
27836
- fs32.chmodSync(this.filePath, 384);
28300
+ fs33.chmodSync(this.filePath, 384);
27837
28301
  } catch {
27838
28302
  }
27839
28303
  }
27840
28304
  clear() {
27841
28305
  try {
27842
- fs32.rmSync(this.filePath);
28306
+ fs33.rmSync(this.filePath);
27843
28307
  } catch {
27844
28308
  }
27845
28309
  }
@@ -28659,7 +29123,7 @@ var TaskAnalyzer = class {
28659
29123
 
28660
29124
  // src/core/router/model-performance-tracker.ts
28661
29125
  init_pricing();
28662
- var DEFAULT_STATS_FILE = path35.join(os10.homedir(), ".cascade", "model-perf.json");
29126
+ var DEFAULT_STATS_FILE = path36.join(os10.homedir(), ".cascade", "model-perf.json");
28663
29127
  var ModelPerformanceTracker = class _ModelPerformanceTracker {
28664
29128
  stats = /* @__PURE__ */ new Map();
28665
29129
  featureStats = /* @__PURE__ */ new Map();
@@ -28699,7 +29163,7 @@ var ModelPerformanceTracker = class _ModelPerformanceTracker {
28699
29163
  async save() {
28700
29164
  if (this.readOnly) return;
28701
29165
  try {
28702
- await fs11.mkdir(path35.dirname(this.statsFile), { recursive: true });
29166
+ await fs11.mkdir(path36.dirname(this.statsFile), { recursive: true });
28703
29167
  const modelsObj = {};
28704
29168
  const featuresObj = {};
28705
29169
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -29332,7 +29796,7 @@ Required capability: ${description.slice(0, 300)}`;
29332
29796
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
29333
29797
  async loadPersistedTools() {
29334
29798
  if (!this.workspacePath || !this.persistEnabled) return;
29335
- const file = path35.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
29799
+ const file = path36.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
29336
29800
  try {
29337
29801
  const raw = await fs11.readFile(file, "utf-8");
29338
29802
  const specs = JSON.parse(raw);
@@ -29356,8 +29820,8 @@ Required capability: ${description.slice(0, 300)}`;
29356
29820
  }
29357
29821
  async persist() {
29358
29822
  if (!this.workspacePath || !this.persistEnabled) return;
29359
- const dir = path35.join(this.workspacePath, ".cascade");
29360
- const file = path35.join(dir, DYNAMIC_TOOLS_FILE);
29823
+ const dir = path36.join(this.workspacePath, ".cascade");
29824
+ const file = path36.join(dir, DYNAMIC_TOOLS_FILE);
29361
29825
  try {
29362
29826
  await fs11.mkdir(dir, { recursive: true });
29363
29827
  await fs11.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -29377,12 +29841,12 @@ var WorldStateDB = class {
29377
29841
  constructor(workspacePath, debugMode = false) {
29378
29842
  this.workspacePath = workspacePath;
29379
29843
  this.debugMode = debugMode;
29380
- const cascadeDir = path35.join(workspacePath, ".cascade");
29381
- if (!fs32.existsSync(cascadeDir)) {
29382
- fs32.mkdirSync(cascadeDir, { recursive: true });
29844
+ const cascadeDir = path36.join(workspacePath, ".cascade");
29845
+ if (!fs33.existsSync(cascadeDir)) {
29846
+ fs33.mkdirSync(cascadeDir, { recursive: true });
29383
29847
  }
29384
- this.keyPath = path35.join(cascadeDir, "world_state.key");
29385
- this.dbPath = path35.join(cascadeDir, "world_state.db");
29848
+ this.keyPath = path36.join(cascadeDir, "world_state.key");
29849
+ this.dbPath = path36.join(cascadeDir, "world_state.db");
29386
29850
  this.initEncryptionKey();
29387
29851
  this.db = new Database2(this.dbPath);
29388
29852
  this.db.pragma("journal_mode = WAL");
@@ -29439,11 +29903,11 @@ var WorldStateDB = class {
29439
29903
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
29440
29904
  }
29441
29905
  initEncryptionKey() {
29442
- if (fs32.existsSync(this.keyPath)) {
29443
- this.encryptionKey = fs32.readFileSync(this.keyPath);
29906
+ if (fs33.existsSync(this.keyPath)) {
29907
+ this.encryptionKey = fs33.readFileSync(this.keyPath);
29444
29908
  } else {
29445
29909
  this.encryptionKey = crypto.randomBytes(32);
29446
- fs32.writeFileSync(this.keyPath, this.encryptionKey);
29910
+ fs33.writeFileSync(this.keyPath, this.encryptionKey);
29447
29911
  }
29448
29912
  }
29449
29913
  encrypt(text) {
@@ -29729,9 +30193,9 @@ var WorldStateDB = class {
29729
30193
  dumpDebugIfNeeded() {
29730
30194
  if (!this.debugMode) return;
29731
30195
  try {
29732
- const dumpPath = path35.join(os10.tmpdir(), "cascade_world_state_debug.json");
30196
+ const dumpPath = path36.join(os10.tmpdir(), "cascade_world_state_debug.json");
29733
30197
  const entries = this.getAllEntries();
29734
- fs32.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
30198
+ fs33.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
29735
30199
  } catch (err) {
29736
30200
  console.error("Failed to dump debug world state", err);
29737
30201
  }
@@ -29788,7 +30252,7 @@ var ResumeStore = class {
29788
30252
  createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
29789
30253
  };
29790
30254
  await promises.mkdir(this.dir, { recursive: true });
29791
- const finalPath = path35.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
30255
+ const finalPath = path36.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
29792
30256
  const tempPath = `${finalPath}.${process.pid}.tmp`;
29793
30257
  await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
29794
30258
  await promises.rename(tempPath, finalPath);
@@ -29811,7 +30275,7 @@ var ResumeStore = class {
29811
30275
  const checkpoints = [];
29812
30276
  for (const candidate of candidates2) {
29813
30277
  try {
29814
- const raw = await promises.readFile(path35.join(this.dir, candidate.name), "utf-8");
30278
+ const raw = await promises.readFile(path36.join(this.dir, candidate.name), "utf-8");
29815
30279
  const parsed = JSON.parse(raw);
29816
30280
  if (isCheckpoint(parsed)) checkpoints.push(parsed);
29817
30281
  } catch {
@@ -29850,7 +30314,7 @@ var ResumeStore = class {
29850
30314
  const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
29851
30315
  if (!source) return null;
29852
30316
  const claimId = `${this.now()}-${safeId(target.taskId)}`;
29853
- await promises.rename(path35.join(this.dir, source), path35.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
30317
+ await promises.rename(path36.join(this.dir, source), path36.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
29854
30318
  return { checkpoint: target, claimId };
29855
30319
  } catch {
29856
30320
  return null;
@@ -29860,8 +30324,8 @@ var ResumeStore = class {
29860
30324
  async release(claimId) {
29861
30325
  try {
29862
30326
  await promises.rename(
29863
- path35.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
29864
- path35.join(this.dir, `${claimId}.json`)
30327
+ path36.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
30328
+ path36.join(this.dir, `${claimId}.json`)
29865
30329
  );
29866
30330
  } catch {
29867
30331
  }
@@ -29869,7 +30333,7 @@ var ResumeStore = class {
29869
30333
  /** Discard a claim for good — the resumed run has taken ownership. */
29870
30334
  async settle(claimId) {
29871
30335
  try {
29872
- await promises.rm(path35.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
30336
+ await promises.rm(path36.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
29873
30337
  } catch {
29874
30338
  }
29875
30339
  }
@@ -29898,8 +30362,8 @@ var ResumeStore = class {
29898
30362
  const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
29899
30363
  if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
29900
30364
  await promises.rename(
29901
- path35.join(this.dir, name),
29902
- path35.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
30365
+ path36.join(this.dir, name),
30366
+ path36.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
29903
30367
  );
29904
30368
  reclaimed++;
29905
30369
  }
@@ -29915,7 +30379,7 @@ var ResumeStore = class {
29915
30379
  await Promise.all(entries.map(async (name) => {
29916
30380
  const match = FILE_PATTERN.exec(name);
29917
30381
  if (match?.[2] === wanted) {
29918
- await promises.rm(path35.join(this.dir, name), { force: true });
30382
+ await promises.rm(path36.join(this.dir, name), { force: true });
29919
30383
  }
29920
30384
  }));
29921
30385
  } catch {
@@ -29930,7 +30394,7 @@ var ResumeStore = class {
29930
30394
  const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
29931
30395
  const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
29932
30396
  await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
29933
- (name) => promises.rm(path35.join(this.dir, name), { force: true })
30397
+ (name) => promises.rm(path36.join(this.dir, name), { force: true })
29934
30398
  ));
29935
30399
  } catch {
29936
30400
  }
@@ -30307,7 +30771,7 @@ var WorkspaceIndex = class {
30307
30771
  isIgnored;
30308
30772
  chunker;
30309
30773
  constructor(opts) {
30310
- this.root = path35.resolve(opts.root);
30774
+ this.root = path36.resolve(opts.root);
30311
30775
  this.db = opts.db;
30312
30776
  this.namespace = opts.namespace ?? "code";
30313
30777
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -30339,14 +30803,14 @@ var WorkspaceIndex = class {
30339
30803
  return;
30340
30804
  }
30341
30805
  for (const entry of entries) {
30342
- const abs = path35.join(dir, entry.name);
30806
+ const abs = path36.join(dir, entry.name);
30343
30807
  if (entry.isDirectory()) {
30344
30808
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
30345
30809
  await walk(abs);
30346
30810
  } else if (entry.isFile()) {
30347
- const ext = path35.extname(entry.name).slice(1).toLowerCase();
30811
+ const ext = path36.extname(entry.name).slice(1).toLowerCase();
30348
30812
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
30349
- out.push(path35.relative(this.root, abs));
30813
+ out.push(path36.relative(this.root, abs));
30350
30814
  }
30351
30815
  }
30352
30816
  };
@@ -30376,7 +30840,7 @@ var WorkspaceIndex = class {
30376
30840
  const entries = [];
30377
30841
  for (const rel of rels) {
30378
30842
  try {
30379
- const abs = path35.join(this.root, rel);
30843
+ const abs = path36.join(this.root, rel);
30380
30844
  const stat = await fs11.stat(abs);
30381
30845
  if (stat.size > this.maxFileBytes) continue;
30382
30846
  const buf = await fs11.readFile(abs);
@@ -30781,6 +31245,127 @@ var CurrentPageTool = class extends BaseTool {
30781
31245
  }
30782
31246
  };
30783
31247
 
31248
+ // src/tools/browser-control.ts
31249
+ var BrowserControlTool = class extends BaseTool {
31250
+ name = "browser_control";
31251
+ 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.";
31252
+ inputSchema = {
31253
+ type: "object",
31254
+ properties: {
31255
+ action: {
31256
+ type: "string",
31257
+ enum: ["navigate", "click", "fill", "press", "wait_for", "extract_text"],
31258
+ description: "What to do."
31259
+ },
31260
+ url: { type: "string", description: "For navigate: the http(s) address to open." },
31261
+ selector: {
31262
+ type: "string",
31263
+ description: "CSS selector for click, fill and wait_for. Optional for extract_text (defaults to the whole page)."
31264
+ },
31265
+ value: { type: "string", description: "For fill: the text to type." },
31266
+ key: { type: "string", description: 'For press: a key name such as "Enter", "Tab" or "Escape".' },
31267
+ timeoutMs: { type: "number", description: "For wait_for: how long to wait before giving up (default 10000, max 30000)." }
31268
+ },
31269
+ required: ["action"]
31270
+ };
31271
+ controller;
31272
+ release;
31273
+ /** One-way. See `revoke`. */
31274
+ revoked = false;
31275
+ constructor(controller, release) {
31276
+ super();
31277
+ this.controller = controller;
31278
+ this.release = release;
31279
+ }
31280
+ /**
31281
+ * A worker has finished. Ends its hold on the browser.
31282
+ *
31283
+ * Called from the tier's terminal path rather than by the model, because a
31284
+ * model cannot be relied on to announce that it is done — which is why the
31285
+ * host cannot use idle time as the boundary instead.
31286
+ */
31287
+ releaseActor(actorId) {
31288
+ this.release?.(actorId);
31289
+ }
31290
+ /**
31291
+ * Permanently disable this tool, whatever the registry still holds.
31292
+ *
31293
+ * Registration-time gates cannot express "this run turned out to be
31294
+ * unattended": a host that wires the controller and only then declares the
31295
+ * run unattended has already registered the tool, and a check that ran at
31296
+ * registration has no second chance. Enforcing at EXECUTION time makes the
31297
+ * guarantee independent of call order, which is the whole point of stating
31298
+ * the invariant rather than relying on the scheduler's current sequence.
31299
+ *
31300
+ * One-way on purpose. A safety gate that can be re-opened by a later call is
31301
+ * a gate that a refactor re-opens by accident.
31302
+ */
31303
+ revoke() {
31304
+ this.revoked = true;
31305
+ }
31306
+ // Acting on a session the user is signed into is the highest-consequence
31307
+ // thing in the tool set — higher than shell, arguably, because the blast
31308
+ // radius is someone else's server rather than this machine.
31309
+ isDangerous() {
31310
+ return true;
31311
+ }
31312
+ async execute(input, options) {
31313
+ if (this.revoked) {
31314
+ return "Error: browser control is not available in this run. It requires a person watching who can stop it, and nobody is.";
31315
+ }
31316
+ const kind = input["action"];
31317
+ if (!kind) return "Error: action is required.";
31318
+ if (options?.signal?.aborted) return "Error: the run was cancelled before this action ran.";
31319
+ const missing = requiredFieldFor(kind, input);
31320
+ if (missing) return `Error: "${kind}" needs ${missing}.`;
31321
+ const action = {
31322
+ kind,
31323
+ ...typeof input["url"] === "string" ? { url: input["url"] } : {},
31324
+ ...typeof input["selector"] === "string" ? { selector: input["selector"] } : {},
31325
+ ...typeof input["value"] === "string" ? { value: input["value"] } : {},
31326
+ ...typeof input["key"] === "string" ? { key: input["key"] } : {},
31327
+ ...typeof input["timeoutMs"] === "number" ? { timeoutMs: Math.min(Math.max(input["timeoutMs"], 0), 3e4) } : {}
31328
+ };
31329
+ let outcome;
31330
+ try {
31331
+ outcome = await this.controller(action, {
31332
+ // `sessionId` is the run. The host scopes both revocation and its
31333
+ // single-owner lease on it, so passing it is not telemetry — drop it
31334
+ // and a Stop in one run silently stops every later one.
31335
+ sessionId: options?.sessionId ?? "",
31336
+ actorId: options?.tierId ?? options?.sessionId ?? "",
31337
+ ...options?.signal ? { signal: options.signal } : {}
31338
+ });
31339
+ } catch (err) {
31340
+ return `Error: the browser could not perform "${kind}" \u2014 ${err instanceof Error ? err.message : String(err)}`;
31341
+ }
31342
+ const where = outcome.url ? `
31343
+ Page: ${outcome.title ? `${outcome.title} \u2014 ` : ""}${outcome.url}` : "";
31344
+ return `${outcome.ok ? "" : "Failed: "}${outcome.detail}${where}`;
31345
+ }
31346
+ };
31347
+ function requiredFieldFor(kind, input) {
31348
+ const has = (k) => typeof input[k] === "string" && input[k].length > 0;
31349
+ const given = (k) => typeof input[k] === "string";
31350
+ switch (kind) {
31351
+ case "navigate":
31352
+ return has("url") ? null : "a url";
31353
+ case "click":
31354
+ return has("selector") ? null : "a selector";
31355
+ case "fill":
31356
+ return has("selector") ? given("value") ? null : "a value" : "a selector";
31357
+ case "press":
31358
+ return has("key") ? null : "a key";
31359
+ case "wait_for":
31360
+ return has("selector") ? null : "a selector";
31361
+ // extract_text works with or without a selector.
31362
+ case "extract_text":
31363
+ return null;
31364
+ default:
31365
+ return null;
31366
+ }
31367
+ }
31368
+
30784
31369
  // src/core/cascade.ts
30785
31370
  var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
30786
31371
  function buildContextualPrompt(prompt, history = []) {
@@ -30866,13 +31451,13 @@ var Cascade = class _Cascade extends EventEmitter {
30866
31451
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
30867
31452
  try {
30868
31453
  this.router.setDeadModelStore(new DeadModelStore(
30869
- fileDeadModelPersistence(path35.join(workspacePath, ".cascade", "dead-models.json"))
31454
+ fileDeadModelPersistence(path36.join(workspacePath, ".cascade", "dead-models.json"))
30870
31455
  ));
30871
31456
  } catch {
30872
31457
  }
30873
31458
  try {
30874
31459
  this.resumeStore = new ResumeStore({
30875
- dir: path35.join(workspacePath, ".cascade", "resume")
31460
+ dir: path36.join(workspacePath, ".cascade", "resume")
30876
31461
  });
30877
31462
  } catch {
30878
31463
  }
@@ -30960,8 +31545,8 @@ var Cascade = class _Cascade extends EventEmitter {
30960
31545
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
30961
31546
  return;
30962
31547
  }
30963
- const dbPath = ci.dbPath || path35.join(this.workspacePath, ".cascade", "code-index.db");
30964
- fs32.mkdirSync(path35.dirname(dbPath), { recursive: true });
31548
+ const dbPath = ci.dbPath || path36.join(this.workspacePath, ".cascade", "code-index.db");
31549
+ fs33.mkdirSync(path36.dirname(dbPath), { recursive: true });
30965
31550
  const db = new Database2(dbPath);
30966
31551
  db.pragma("journal_mode = WAL");
30967
31552
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -31273,6 +31858,49 @@ ${input}` }],
31273
31858
  if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
31274
31859
  this.toolRegistry.register(new CurrentPageTool(provider));
31275
31860
  }
31861
+ /**
31862
+ * True when nobody is watching this run — a scheduled/cron task, which
31863
+ * auto-approves every tool because there is no one to ask.
31864
+ *
31865
+ * That auto-approval is fine for ordinary tools and disqualifying for this
31866
+ * one: `browser_control` drives the signed-in browser, and "every run asks
31867
+ * first" is the promise the Settings copy makes. The property used to hold
31868
+ * only because `runScheduledTask` happens not to call `setBrowserController`
31869
+ * — a guarantee resting on an ABSENT call, which is exactly what a later
31870
+ * refactor adds without noticing. Stated and enforced instead.
31871
+ */
31872
+ unattended = false;
31873
+ /**
31874
+ * Declare that no human is watching this run, so capabilities that depend on
31875
+ * a person being able to see and stop them are refused outright.
31876
+ */
31877
+ setUnattended(on) {
31878
+ this.unattended = on;
31879
+ if (!on) return;
31880
+ const tool = this.toolRegistry.getTool("browser_control");
31881
+ tool?.revoke?.();
31882
+ }
31883
+ /**
31884
+ * Wire the host's browser so a run can ACT on the open page, not just read it.
31885
+ *
31886
+ * Two gates before this registers anything, and both matter:
31887
+ *
31888
+ * - `tools.agentBrowserControl` must be on. Unlike read_current_page, which
31889
+ * reaches nothing the user has not already opened themselves, this drives
31890
+ * the session they are signed into. That is opt-in, not a default.
31891
+ * - `disabledTools` still removes it, the same as every other tool.
31892
+ * - The run must not be UNATTENDED. See `setUnattended`.
31893
+ *
31894
+ * Host-supplied like the page provider: the tool exists only where there is a
31895
+ * real browser to drive, so it never has to be refused in the CLI or a hosted
31896
+ * run — it is simply not there.
31897
+ */
31898
+ setBrowserController(controller, release) {
31899
+ if (this.unattended) return;
31900
+ if (!this.config.tools?.agentBrowserControl) return;
31901
+ if ((this.config.tools?.disabledTools ?? []).includes("browser_control")) return;
31902
+ this.toolRegistry.register(new BrowserControlTool(controller, release));
31903
+ }
31276
31904
  registerMediaTools(workspacePath) {
31277
31905
  const denied = new Set(this.config.tools?.disabledTools ?? []);
31278
31906
  const permitted = (name) => !denied.has(name);
@@ -31878,6 +32506,7 @@ ${prompt}` : prompt;
31878
32506
  this.router.setRunSignal(options.signal);
31879
32507
  const startMs = Date.now();
31880
32508
  const taskId = randomUUID();
32509
+ this.emit("run:started", { taskId });
31881
32510
  this.decisionLog = [];
31882
32511
  this.currentRunCompleted = [];
31883
32512
  this.wroteCheckpointThisRun = false;
@@ -32429,14 +33058,14 @@ ${finalOutput}` : ""
32429
33058
  init_constants();
32430
33059
  var CLOUD_SESSION_FILE = "cloud-session.json";
32431
33060
  function globalDir(homeDir = os10.homedir()) {
32432
- return path35.join(homeDir, GLOBAL_CONFIG_DIR);
33061
+ return path36.join(homeDir, GLOBAL_CONFIG_DIR);
32433
33062
  }
32434
33063
  function cloudSessionPath(dir = globalDir()) {
32435
- return path35.join(dir, CLOUD_SESSION_FILE);
33064
+ return path36.join(dir, CLOUD_SESSION_FILE);
32436
33065
  }
32437
33066
  function loadCloudSession(dir = globalDir()) {
32438
33067
  try {
32439
- const raw = fs32.readFileSync(cloudSessionPath(dir), "utf-8");
33068
+ const raw = fs33.readFileSync(cloudSessionPath(dir), "utf-8");
32440
33069
  const parsed = JSON.parse(raw);
32441
33070
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
32442
33071
  return parsed;
@@ -32445,17 +33074,17 @@ function loadCloudSession(dir = globalDir()) {
32445
33074
  }
32446
33075
  }
32447
33076
  function saveCloudSession(session, dir = globalDir()) {
32448
- fs32.mkdirSync(dir, { recursive: true, mode: 448 });
33077
+ fs33.mkdirSync(dir, { recursive: true, mode: 448 });
32449
33078
  const file = cloudSessionPath(dir);
32450
- fs32.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
33079
+ fs33.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
32451
33080
  try {
32452
- fs32.chmodSync(file, 384);
33081
+ fs33.chmodSync(file, 384);
32453
33082
  } catch {
32454
33083
  }
32455
33084
  }
32456
33085
  function clearCloudSession(dir = globalDir()) {
32457
33086
  try {
32458
- fs32.rmSync(cloudSessionPath(dir));
33087
+ fs33.rmSync(cloudSessionPath(dir));
32459
33088
  } catch {
32460
33089
  }
32461
33090
  }
@@ -32627,16 +33256,16 @@ var CloudClient = class _CloudClient {
32627
33256
  return (await this.refresh()).accessToken;
32628
33257
  }
32629
33258
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
32630
- async authed(path40, init = {}) {
32631
- const call = (token) => fetch(this.url(path40), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
33259
+ async authed(path41, init = {}) {
33260
+ const call = (token) => fetch(this.url(path41), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
32632
33261
  let res = await call(await this.accessToken());
32633
33262
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
32634
33263
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
32635
33264
  const text = await res.text();
32636
33265
  return text ? JSON.parse(text) : {};
32637
33266
  }
32638
- authedGet(path40) {
32639
- return this.authed(path40);
33267
+ authedGet(path41) {
33268
+ return this.authed(path41);
32640
33269
  }
32641
33270
  async fetchMe(token) {
32642
33271
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -34410,9 +35039,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
34410
35039
  }
34411
35040
  } catch {
34412
35041
  }
34413
- const configPath = path35.join(workspacePath, ".cascade", "config.json");
35042
+ const configPath = path36.join(workspacePath, ".cascade", "config.json");
34414
35043
  try {
34415
- await fs11.mkdir(path35.dirname(configPath), { recursive: true });
35044
+ await fs11.mkdir(path36.dirname(configPath), { recursive: true });
34416
35045
  await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
34417
35046
  } catch (err) {
34418
35047
  const msg = err instanceof Error ? err.message : String(err);
@@ -34464,9 +35093,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
34464
35093
  if (msg.includes("non-text parts")) return;
34465
35094
  originalLog(...args);
34466
35095
  };
34467
- const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
35096
+ const store = new MemoryStore(path36.join(workspacePath, CASCADE_DB_FILE));
34468
35097
  storeRef.current = store;
34469
- globalStoreRef.current = new MemoryStore(path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
35098
+ globalStoreRef.current = new MemoryStore(path36.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
34470
35099
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
34471
35100
  setIdentities(identityRows);
34472
35101
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -34546,7 +35175,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
34546
35175
  onThemeChange: (name) => setTheme(getTheme(name)),
34547
35176
  onExport: async (fmt) => {
34548
35177
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
34549
- const exportPath = path35.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
35178
+ const exportPath = path36.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
34550
35179
  if (fmt === "json") {
34551
35180
  await fs11.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
34552
35181
  } else {
@@ -35466,7 +36095,7 @@ function stringifySlashOutput(val) {
35466
36095
  }
35467
36096
  async function searchSessionsAndMessages(query, workspacePath) {
35468
36097
  if (!query) return "Usage: /search <query>";
35469
- const dbPath = path35.join(workspacePath, CASCADE_DB_FILE);
36098
+ const dbPath = path36.join(workspacePath, CASCADE_DB_FILE);
35470
36099
  try {
35471
36100
  await fs11.access(dbPath);
35472
36101
  } catch {
@@ -35500,7 +36129,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
35500
36129
  async function diagnoseRuntime(config, workspacePath) {
35501
36130
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
35502
36131
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
35503
- const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
36132
+ const store = new MemoryStore(path36.join(workspacePath, CASCADE_DB_FILE));
35504
36133
  try {
35505
36134
  const sessions2 = store.listSessions(void 0, 3);
35506
36135
  return [
@@ -35519,7 +36148,7 @@ async function diagnoseRuntime(config, workspacePath) {
35519
36148
  }
35520
36149
  async function showRecentLogs(args, workspacePath) {
35521
36150
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
35522
- const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
36151
+ const store = new MemoryStore(path36.join(workspacePath, CASCADE_DB_FILE));
35523
36152
  try {
35524
36153
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
35525
36154
  if (!logs.length) return "No recent runtime logs.";
@@ -35531,7 +36160,7 @@ async function showRecentLogs(args, workspacePath) {
35531
36160
  async function loadSessionSnapshot(args, workspacePath) {
35532
36161
  const sessionId = args[0];
35533
36162
  if (!sessionId) return "Usage: /resume <sessionId>";
35534
- const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
36163
+ const store = new MemoryStore(path36.join(workspacePath, CASCADE_DB_FILE));
35535
36164
  try {
35536
36165
  const session = store.getSession(sessionId);
35537
36166
  if (!session) return `Session not found: ${sessionId}`;
@@ -35951,9 +36580,9 @@ function SetupWizard({ workspacePath, onComplete }) {
35951
36580
  ...anyAuto ? { cascadeAuto: true } : {}
35952
36581
  };
35953
36582
  const config = CascadeConfigSchema.parse(rawConfig);
35954
- const configDir = path35.join(workspacePath, ".cascade");
36583
+ const configDir = path36.join(workspacePath, ".cascade");
35955
36584
  await fs11.mkdir(configDir, { recursive: true });
35956
- const configPath = path35.join(workspacePath, CASCADE_CONFIG_FILE);
36585
+ const configPath = path36.join(workspacePath, CASCADE_CONFIG_FILE);
35957
36586
  await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
35958
36587
  savedConfigRef.current = config;
35959
36588
  dispatchRef.current({ type: "GO_DONE" });
@@ -36294,14 +36923,14 @@ function printTelemetryBanner() {
36294
36923
  async function initCommand(workspacePath = process.cwd()) {
36295
36924
  const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
36296
36925
  try {
36297
- const configDir = path35.join(workspacePath, ".cascade");
36926
+ const configDir = path36.join(workspacePath, ".cascade");
36298
36927
  await fs11.mkdir(configDir, { recursive: true });
36299
- const mdPath = path35.join(workspacePath, "CASCADE.md");
36928
+ const mdPath = path36.join(workspacePath, "CASCADE.md");
36300
36929
  if (!await fileExists(mdPath)) {
36301
36930
  await createDefaultCascadeMd(workspacePath);
36302
36931
  spin.succeed(chalk8.green("Created CASCADE.md"));
36303
36932
  }
36304
- const ignorePath = path35.join(workspacePath, ".cascadeignore");
36933
+ const ignorePath = path36.join(workspacePath, ".cascadeignore");
36305
36934
  if (!await fileExists(ignorePath)) {
36306
36935
  await createDefaultIgnoreFile(workspacePath);
36307
36936
  spin.succeed(chalk8.green("Created .cascadeignore"));
@@ -36310,7 +36939,7 @@ async function initCommand(workspacePath = process.cwd()) {
36310
36939
  console.log();
36311
36940
  console.log(chalk8.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
36312
36941
  console.log();
36313
- const configPath = path35.join(workspacePath, CASCADE_CONFIG_FILE);
36942
+ const configPath = path36.join(workspacePath, CASCADE_CONFIG_FILE);
36314
36943
  if (await fileExists(configPath)) {
36315
36944
  console.log(chalk8.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
36316
36945
  console.log();
@@ -36461,8 +37090,8 @@ function fromEnv(env) {
36461
37090
  return out;
36462
37091
  }
36463
37092
  async function fromClaudeCode(home, env) {
36464
- const dir = str(env["CLAUDE_CONFIG_DIR"]) ?? path35.join(home, ".claude");
36465
- const file = path35.join(dir, ".credentials.json");
37093
+ const dir = str(env["CLAUDE_CONFIG_DIR"]) ?? path36.join(home, ".claude");
37094
+ const file = path36.join(dir, ".credentials.json");
36466
37095
  const data = await readJson(file);
36467
37096
  if (!data) return [];
36468
37097
  const found = [];
@@ -36486,7 +37115,7 @@ async function fromClaudeCode(home, env) {
36486
37115
  return found;
36487
37116
  }
36488
37117
  async function fromCodex(home) {
36489
- const file = path35.join(home, ".codex", "auth.json");
37118
+ const file = path36.join(home, ".codex", "auth.json");
36490
37119
  const data = await readJson(file);
36491
37120
  if (!data) return [];
36492
37121
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -36509,7 +37138,7 @@ async function fromCodex(home) {
36509
37138
  return [];
36510
37139
  }
36511
37140
  async function fromGemini(home) {
36512
- const file = path35.join(home, ".gemini", "oauth_creds.json");
37141
+ const file = path36.join(home, ".gemini", "oauth_creds.json");
36513
37142
  const data = await readJson(file);
36514
37143
  const accessToken = str(data?.["access_token"]);
36515
37144
  if (!accessToken) return [];
@@ -36525,7 +37154,7 @@ async function fromGemini(home) {
36525
37154
  }
36526
37155
  async function fromCopilot(home) {
36527
37156
  for (const name of ["apps.json", "hosts.json"]) {
36528
- const file = path35.join(home, ".config", "github-copilot", name);
37157
+ const file = path36.join(home, ".config", "github-copilot", name);
36529
37158
  const data = await readJson(file);
36530
37159
  if (!data) continue;
36531
37160
  for (const value of Object.values(data)) {
@@ -36826,7 +37455,7 @@ async function doctorCommand() {
36826
37455
  checks.push({
36827
37456
  label: "Cascade config",
36828
37457
  ok: true,
36829
- detail: `Loaded ${path35.join(process.cwd(), CASCADE_CONFIG_FILE)}`
37458
+ detail: `Loaded ${path36.join(process.cwd(), CASCADE_CONFIG_FILE)}`
36830
37459
  });
36831
37460
  const providers = [
36832
37461
  { type: "anthropic", name: "Anthropic" },
@@ -36936,7 +37565,7 @@ async function checkEndpoint(url) {
36936
37565
  }
36937
37566
  }
36938
37567
  async function indexCommand(dirPath) {
36939
- const workspace = path35.resolve(dirPath || process.cwd());
37568
+ const workspace = path36.resolve(dirPath || process.cwd());
36940
37569
  const cm = new ConfigManager(workspace);
36941
37570
  await cm.load();
36942
37571
  const config = cm.getConfig();
@@ -36947,8 +37576,8 @@ async function indexCommand(dirPath) {
36947
37576
  process.exitCode = 1;
36948
37577
  return;
36949
37578
  }
36950
- const dbPath = config.codeIndex?.dbPath || path35.join(workspace, ".cascade", "code-index.db");
36951
- fs32.mkdirSync(path35.dirname(dbPath), { recursive: true });
37579
+ const dbPath = config.codeIndex?.dbPath || path36.join(workspace, ".cascade", "code-index.db");
37580
+ fs33.mkdirSync(path36.dirname(dbPath), { recursive: true });
36952
37581
  const db = new Database2(dbPath);
36953
37582
  db.pragma("journal_mode = WAL");
36954
37583
  const complete = chatCompleterFromProviders(config.providers);
@@ -37480,7 +38109,7 @@ function openBrowser2(url) {
37480
38109
  }
37481
38110
  function storePathFor(name) {
37482
38111
  const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
37483
- return path35.join(os10.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
38112
+ return path36.join(os10.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
37484
38113
  }
37485
38114
  function defaultName(url) {
37486
38115
  try {
@@ -37775,6 +38404,7 @@ function applySettingsPayload(config, data) {
37775
38404
  if (["auto", "T1", "T2", "T3"].includes(a["forceTier"])) config.routing = { ...config.routing ?? {}, forceTier: a["forceTier"] };
37776
38405
  if (typeof a["benchmarksLive"] === "boolean") config.benchmarks = { ...config.benchmarks ?? {}, live: a["benchmarksLive"] };
37777
38406
  if (["isolate", "worker", "auto"].includes(a["dynamicToolSandbox"])) config.tools = { ...config.tools ?? {}, dynamicToolSandbox: a["dynamicToolSandbox"] };
38407
+ if (typeof a["agentBrowserControl"] === "boolean") config.tools = { ...config.tools ?? {}, agentBrowserControl: a["agentBrowserControl"] };
37778
38408
  if (typeof a["factsExtraction"] === "boolean") config.knowledge = { ...config.knowledge ?? {}, factsExtraction: a["factsExtraction"] };
37779
38409
  if (typeof a["rememberSessions"] === "boolean") config.memory = { ...config.memory ?? {}, rememberSessions: a["rememberSessions"] };
37780
38410
  if (typeof a["enableToolCreation"] === "boolean") config.enableToolCreation = a["enableToolCreation"];
@@ -37832,6 +38462,7 @@ function settingsSnapshot(config) {
37832
38462
  forceTier: config.routing?.forceTier,
37833
38463
  benchmarksLive: config.benchmarks?.live,
37834
38464
  dynamicToolSandbox: config.tools?.dynamicToolSandbox,
38465
+ agentBrowserControl: config.tools?.agentBrowserControl,
37835
38466
  factsExtraction: config.knowledge?.factsExtraction,
37836
38467
  rememberSessions: config.memory?.rememberSessions,
37837
38468
  enableToolCreation: config.enableToolCreation,
@@ -37867,8 +38498,8 @@ function replaceInPlace(target, source) {
37867
38498
  }
37868
38499
  function writeConfigFile(configPath, config) {
37869
38500
  try {
37870
- fs32.mkdirSync(path35.dirname(configPath), { recursive: true });
37871
- fs32.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
38501
+ fs33.mkdirSync(path36.dirname(configPath), { recursive: true });
38502
+ fs33.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
37872
38503
  return { ok: true };
37873
38504
  } catch (err) {
37874
38505
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
@@ -38284,7 +38915,7 @@ function aggregateCostStats(sessions2, opts = {}) {
38284
38915
  }
38285
38916
 
38286
38917
  // src/dashboard/server.ts
38287
- var __dirname$1 = path35.dirname(fileURLToPath(import.meta.url));
38918
+ var __dirname$1 = path36.dirname(fileURLToPath(import.meta.url));
38288
38919
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
38289
38920
  var MASK = "***";
38290
38921
  function masked(key, value) {
@@ -38342,6 +38973,14 @@ var DashboardServer = class {
38342
38973
  * always failing.
38343
38974
  */
38344
38975
  currentPageProvider;
38976
+ /**
38977
+ * Supplied by the desktop shell so a run can ACT on that page — click, type,
38978
+ * submit — not just read it. Same reasoning as the provider above: absent in
38979
+ * the CLI, so the tool is not registered rather than present and failing.
38980
+ * Cascade applies the `tools.agentBrowserControl` gate on top of this.
38981
+ */
38982
+ browserController;
38983
+ browserActorRelease;
38345
38984
  activeControllers = /* @__PURE__ */ new Map();
38346
38985
  /**
38347
38986
  * Run taskIds per chat session — file snapshots are keyed by the run's
@@ -38403,6 +39042,7 @@ var DashboardServer = class {
38403
39042
  });
38404
39043
  this.socket.onConfigUpdate(async (data) => {
38405
39044
  const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
39045
+ if (result.ok) this.settingsChangeHook?.(this.config);
38406
39046
  const refused = result.refused.map((r) => ({
38407
39047
  type: r.type,
38408
39048
  reason: r.reason,
@@ -38426,6 +39066,7 @@ var DashboardServer = class {
38426
39066
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
38427
39067
  this.activeSessions.set(sessionId, cascade);
38428
39068
  if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
39069
+ if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
38429
39070
  cascade.on("stream:token", (e) => {
38430
39071
  this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
38431
39072
  });
@@ -38449,13 +39090,16 @@ var DashboardServer = class {
38449
39090
  if (ev.requestId) this.clearPendingEscalation(ev.requestId);
38450
39091
  this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
38451
39092
  });
39093
+ cascade.on("run:started", (e) => {
39094
+ const id = e.taskId;
39095
+ if (id) this.recordSessionTask(sessionId, id);
39096
+ });
38452
39097
  try {
38453
39098
  const result = await cascade.run({
38454
39099
  prompt: runPrompt,
38455
39100
  signal: abortController.signal,
38456
39101
  approvalCallback: this.makeApprovalCallback(sessionId)
38457
39102
  });
38458
- this.recordSessionTask(sessionId, result.taskId);
38459
39103
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
38460
39104
  this.captureWhy(sessionId, cascade, result);
38461
39105
  this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
@@ -38521,6 +39165,49 @@ var DashboardServer = class {
38521
39165
  setCurrentPageProvider(provider) {
38522
39166
  this.currentPageProvider = provider;
38523
39167
  }
39168
+ /** Let runs act on that view, not just read it (desktop only). */
39169
+ setBrowserController(controller, release) {
39170
+ this.browserController = controller;
39171
+ this.browserActorRelease = release;
39172
+ }
39173
+ /**
39174
+ * Told whenever settings are committed through THIS server.
39175
+ *
39176
+ * Settings reach the live config by two routes — the desktop's Electron IPC
39177
+ * handler and this server's `config:update` socket — and both run the same
39178
+ * `commitSettings`. The desktop mirrors `tools.agentBrowserControl` into its
39179
+ * browser module after its own route, but had no way to learn about the
39180
+ * other, so a socket write left the module's copy stale: an enable the module
39181
+ * still refused, or worse a disable the module still honoured for a run
39182
+ * already holding the tool. This is the same sibling-writer drift
39183
+ * `settings-payload.ts` exists to prevent, one layer up.
39184
+ */
39185
+ settingsChangeHook;
39186
+ onSettingsChanged(hook) {
39187
+ this.settingsChangeHook = hook;
39188
+ }
39189
+ /**
39190
+ * Told when a run ends, so a host holding run-scoped state can release it.
39191
+ *
39192
+ * The desktop uses this to retire a run's browser Stop control: that has to
39193
+ * outlive each individual action (the gap between two actions is when the
39194
+ * user most wants it) but must not outlive the run.
39195
+ */
39196
+ /**
39197
+ * Fired when a run ends, carrying BOTH identifiers explicitly.
39198
+ *
39199
+ * They are different things and conflating them was a live bug: a chat
39200
+ * `sessionId` can contain many runs (hence `sessionTaskIds`), while
39201
+ * `Cascade.run()` mints a fresh random `taskId` per run — and it is the
39202
+ * taskId that tools see as `ToolExecuteOptions.sessionId`. Passing only the
39203
+ * chat id meant browser cleanup keyed on the task id matched nothing, so a
39204
+ * finished run kept its Stop banner armed and held its browser lease until
39205
+ * the deadlock ceiling.
39206
+ */
39207
+ runEndedHook;
39208
+ onRunEnded(hook) {
39209
+ this.runEndedHook = hook;
39210
+ }
38524
39211
  async start() {
38525
39212
  const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
38526
39213
  if (!isLoopback) {
@@ -38603,7 +39290,7 @@ var DashboardServer = class {
38603
39290
  * silently and still typecheck.
38604
39291
  */
38605
39292
  persistConfig(config) {
38606
- const result = writeConfigFile(path35.join(this.workspacePath, CASCADE_CONFIG_FILE), config);
39293
+ const result = writeConfigFile(path36.join(this.workspacePath, CASCADE_CONFIG_FILE), config);
38607
39294
  if (!result.ok) console.warn(`[dashboard] Failed to persist config: ${result.error}`);
38608
39295
  return result;
38609
39296
  }
@@ -38622,7 +39309,7 @@ var DashboardServer = class {
38622
39309
  */
38623
39310
  syncGlobalCredentials() {
38624
39311
  try {
38625
- saveGlobalCredentials(path35.join(os10.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
39312
+ saveGlobalCredentials(path36.join(os10.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
38626
39313
  } catch (err) {
38627
39314
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
38628
39315
  }
@@ -38637,15 +39324,15 @@ var DashboardServer = class {
38637
39324
  resolveDashboardSecret() {
38638
39325
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
38639
39326
  if (fromConfig) return fromConfig;
38640
- const secretPath = path35.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
39327
+ const secretPath = path36.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
38641
39328
  try {
38642
- if (fs32.existsSync(secretPath)) {
38643
- const existing = fs32.readFileSync(secretPath, "utf-8").trim();
39329
+ if (fs33.existsSync(secretPath)) {
39330
+ const existing = fs33.readFileSync(secretPath, "utf-8").trim();
38644
39331
  if (existing.length >= 16) return existing;
38645
39332
  }
38646
39333
  const generated = randomUUID();
38647
- fs32.mkdirSync(path35.dirname(secretPath), { recursive: true });
38648
- fs32.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
39334
+ fs33.mkdirSync(path36.dirname(secretPath), { recursive: true });
39335
+ fs33.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
38649
39336
  if (this.config.dashboard.auth) {
38650
39337
  console.warn(
38651
39338
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -38672,7 +39359,7 @@ var DashboardServer = class {
38672
39359
  // ── Setup ─────────────────────────────────────
38673
39360
  getGlobalStore() {
38674
39361
  if (!this.globalStore) {
38675
- const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39362
+ const globalDbPath = path36.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
38676
39363
  this.globalStore = new MemoryStore(globalDbPath);
38677
39364
  }
38678
39365
  return this.globalStore;
@@ -38711,6 +39398,8 @@ var DashboardServer = class {
38711
39398
  }
38712
39399
  /** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
38713
39400
  persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
39401
+ const endedTaskId = result?.taskId ?? this.sessionTaskIds.get(sessionId)?.at(-1);
39402
+ this.runEndedHook?.({ sessionId, ...endedTaskId ? { taskId: endedTaskId } : {} });
38714
39403
  try {
38715
39404
  if (reply && reply.trim()) {
38716
39405
  this.store.addMessage({ id: randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -38804,6 +39493,7 @@ var DashboardServer = class {
38804
39493
  const prompt = task.prompt;
38805
39494
  const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
38806
39495
  const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
39496
+ cascade.setUnattended(true);
38807
39497
  this.activeSessions.set(sessionId, cascade);
38808
39498
  cascade.on("tier:status", (e) => {
38809
39499
  this.socket.broadcast("tier:status", { sessionId, ...e });
@@ -38811,13 +39501,16 @@ var DashboardServer = class {
38811
39501
  cascade.on("peer:message", (e) => {
38812
39502
  this.socket.emitPeerMessage(e);
38813
39503
  });
39504
+ cascade.on("run:started", (e) => {
39505
+ const id = e.taskId;
39506
+ if (id) this.recordSessionTask(sessionId, id);
39507
+ });
38814
39508
  try {
38815
39509
  const result = await cascade.run({
38816
39510
  prompt,
38817
39511
  identityId: task.identityId,
38818
39512
  approvalCallback: async () => ({ approved: true, always: false })
38819
39513
  });
38820
- this.recordSessionTask(sessionId, result.taskId);
38821
39514
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
38822
39515
  this.captureWhy(sessionId, cascade, result);
38823
39516
  this.socket.broadcast("session:complete", { sessionId, result });
@@ -39052,12 +39745,12 @@ ${prompt}`;
39052
39745
  }
39053
39746
  }
39054
39747
  watchRuntimeChanges() {
39055
- const workspaceDbPath = path35.join(this.workspacePath, CASCADE_DB_FILE);
39056
- const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39748
+ const workspaceDbPath = path36.join(this.workspacePath, CASCADE_DB_FILE);
39749
+ const globalDbPath = path36.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39057
39750
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
39058
39751
  for (const watchPath of watchPaths) {
39059
- if (!fs32.existsSync(watchPath)) continue;
39060
- fs32.watchFile(watchPath, { interval: 3e3 }, () => {
39752
+ if (!fs33.existsSync(watchPath)) continue;
39753
+ fs33.watchFile(watchPath, { interval: 3e3 }, () => {
39061
39754
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
39062
39755
  });
39063
39756
  }
@@ -39179,7 +39872,7 @@ ${prompt}`;
39179
39872
  const sessionId = req.params.id;
39180
39873
  this.store.deleteSession(sessionId);
39181
39874
  this.store.deleteRuntimeSession(sessionId);
39182
- const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39875
+ const globalDbPath = path36.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39183
39876
  const globalStore = new MemoryStore(globalDbPath);
39184
39877
  try {
39185
39878
  globalStore.deleteRuntimeSession(sessionId);
@@ -39272,7 +39965,7 @@ ${prompt}`;
39272
39965
  });
39273
39966
  this.app.delete("/api/sessions", auth2, (req, res) => {
39274
39967
  const body = req.body;
39275
- const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39968
+ const globalDbPath = path36.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39276
39969
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
39277
39970
  const globalStore = new MemoryStore(globalDbPath);
39278
39971
  try {
@@ -39295,7 +39988,7 @@ ${prompt}`;
39295
39988
  });
39296
39989
  this.app.delete("/api/runtime", auth2, (_req, res) => {
39297
39990
  this.store.deleteAllRuntimeNodes();
39298
- const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39991
+ const globalDbPath = path36.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39299
39992
  const globalStore = new MemoryStore(globalDbPath);
39300
39993
  try {
39301
39994
  globalStore.deleteAllRuntimeNodes();
@@ -39380,12 +40073,12 @@ ${prompt}`;
39380
40073
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
39381
40074
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
39382
40075
  try {
39383
- const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
39384
- const existing = fs32.existsSync(configPath) ? JSON.parse(fs32.readFileSync(configPath, "utf-8")) : {};
40076
+ const configPath = path36.join(this.workspacePath, CASCADE_CONFIG_FILE);
40077
+ const existing = fs33.existsSync(configPath) ? JSON.parse(fs33.readFileSync(configPath, "utf-8")) : {};
39385
40078
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
39386
40079
  const tmp = configPath + ".tmp";
39387
- fs32.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
39388
- fs32.renameSync(tmp, configPath);
40080
+ fs33.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
40081
+ fs33.renameSync(tmp, configPath);
39389
40082
  res.json({ ok: true });
39390
40083
  } catch (err) {
39391
40084
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -39413,7 +40106,7 @@ ${prompt}`;
39413
40106
  this.app.get("/api/runtime", auth2, (req, res) => {
39414
40107
  const scope = req.query["scope"] ?? "workspace";
39415
40108
  if (scope === "global") {
39416
- const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
40109
+ const globalDbPath = path36.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
39417
40110
  const globalStore = new MemoryStore(globalDbPath);
39418
40111
  try {
39419
40112
  res.json({
@@ -39450,6 +40143,7 @@ ${prompt}`;
39450
40143
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
39451
40144
  this.activeSessions.set(sessionId, cascade);
39452
40145
  if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
40146
+ if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
39453
40147
  cascade.on("stream:token", (e) => {
39454
40148
  this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
39455
40149
  });
@@ -39473,13 +40167,16 @@ ${prompt}`;
39473
40167
  if (ev.requestId) this.clearPendingEscalation(ev.requestId);
39474
40168
  this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
39475
40169
  });
40170
+ cascade.on("run:started", (e) => {
40171
+ const id = e.taskId;
40172
+ if (id) this.recordSessionTask(sessionId, id);
40173
+ });
39476
40174
  try {
39477
40175
  const result = await cascade.run({
39478
40176
  prompt: runPrompt,
39479
40177
  identityId: body.identityId,
39480
40178
  approvalCallback: this.makeApprovalCallback(sessionId)
39481
40179
  });
39482
- this.recordSessionTask(sessionId, result.taskId);
39483
40180
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
39484
40181
  this.captureWhy(sessionId, cascade, result);
39485
40182
  this.socket.broadcast("cost:update", {
@@ -39754,13 +40451,13 @@ ${prompt}`;
39754
40451
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
39755
40452
  }
39756
40453
  });
39757
- const prodPath = path35.resolve(__dirname$1, "../web/dist");
39758
- const devPath = path35.resolve(__dirname$1, "../../web/dist");
39759
- const webDistPath = fs32.existsSync(prodPath) ? prodPath : devPath;
39760
- if (fs32.existsSync(webDistPath)) {
40454
+ const prodPath = path36.resolve(__dirname$1, "../web/dist");
40455
+ const devPath = path36.resolve(__dirname$1, "../../web/dist");
40456
+ const webDistPath = fs33.existsSync(prodPath) ? prodPath : devPath;
40457
+ if (fs33.existsSync(webDistPath)) {
39761
40458
  this.app.use(express.static(webDistPath));
39762
40459
  this.app.get("*", (_req, res) => {
39763
- res.sendFile(path35.join(webDistPath, "index.html"));
40460
+ res.sendFile(path36.join(webDistPath, "index.html"));
39764
40461
  });
39765
40462
  } else {
39766
40463
  this.app.get("/", (_req, res) => {
@@ -39781,7 +40478,7 @@ init_constants();
39781
40478
  async function dashboardCommand(config, workspacePath = process.cwd()) {
39782
40479
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
39783
40480
  const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
39784
- const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
40481
+ const store = new MemoryStore(path36.join(workspacePath, CASCADE_DB_FILE));
39785
40482
  const server = new DashboardServer(config, store, workspacePath);
39786
40483
  server.watchRuntimeChanges();
39787
40484
  const gThis = globalThis;
@@ -39813,7 +40510,7 @@ init_constants();
39813
40510
  function makeIdentityCommand(workspacePath = process.cwd()) {
39814
40511
  const identity = new Command("identity").alias("id").description("Manage Cascade identities");
39815
40512
  identity.command("list").description("List all available identities").action(() => {
39816
- const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
40513
+ const store = new MemoryStore(path36.join(workspacePath, CASCADE_DB_FILE));
39817
40514
  try {
39818
40515
  const identities = store.listIdentities();
39819
40516
  console.log(chalk8.bold("\n Identities:"));
@@ -39833,7 +40530,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
39833
40530
  }
39834
40531
  });
39835
40532
  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) => {
39836
- const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
40533
+ const store = new MemoryStore(path36.join(workspacePath, CASCADE_DB_FILE));
39837
40534
  try {
39838
40535
  if (options.default) {
39839
40536
  const existingDefault = store.getDefaultIdentity();
@@ -39859,7 +40556,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
39859
40556
  }
39860
40557
  });
39861
40558
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
39862
- const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
40559
+ const store = new MemoryStore(path36.join(workspacePath, CASCADE_DB_FILE));
39863
40560
  try {
39864
40561
  const identities = store.listIdentities();
39865
40562
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
@@ -40008,8 +40705,8 @@ async function exportCommand(options = {}) {
40008
40705
  let store;
40009
40706
  try {
40010
40707
  const workspacePath = options.workspacePath ?? process.cwd();
40011
- const workspaceDbPath = path35.join(workspacePath, CASCADE_DB_FILE);
40012
- const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
40708
+ const workspaceDbPath = path36.join(workspacePath, CASCADE_DB_FILE);
40709
+ const globalDbPath = path36.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
40013
40710
  let dbPath = globalDbPath;
40014
40711
  try {
40015
40712
  await fs11.access(workspaceDbPath);
@@ -40051,7 +40748,7 @@ async function exportCommand(options = {}) {
40051
40748
  const ext = format === "json" ? ".json" : ".md";
40052
40749
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
40053
40750
  const defaultFile = `cascade-export-${safeName}${ext}`;
40054
- const outPath = options.output ? path35.resolve(options.output) : path35.join(process.cwd(), defaultFile);
40751
+ const outPath = options.output ? path36.resolve(options.output) : path36.join(process.cwd(), defaultFile);
40055
40752
  await fs11.writeFile(outPath, content, "utf-8");
40056
40753
  spin.succeed(chalk8.green(`Exported to ${chalk8.white(outPath)}`));
40057
40754
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
@@ -40191,7 +40888,7 @@ var SHOW_CURSOR = "\x1B[?25h";
40191
40888
  function writeNow(stdout, text) {
40192
40889
  if (typeof stdout.fd === "number") {
40193
40890
  try {
40194
- fs32.writeSync(stdout.fd, text);
40891
+ fs33.writeSync(stdout.fd, text);
40195
40892
  return;
40196
40893
  } catch {
40197
40894
  }
@@ -40214,11 +40911,11 @@ function restoreTerminal(stdin, stdout) {
40214
40911
  dotenv.config();
40215
40912
  function warnIfBuildIsStale() {
40216
40913
  try {
40217
- const here = path35.dirname(fileURLToPath(import.meta.url));
40914
+ const here = path36.dirname(fileURLToPath(import.meta.url));
40218
40915
  for (const rel of ["..", "../.."]) {
40219
- const pkgPath = path35.join(here, rel, "package.json");
40220
- if (!fs32.existsSync(pkgPath)) continue;
40221
- const pkg = JSON.parse(fs32.readFileSync(pkgPath, "utf-8"));
40916
+ const pkgPath = path36.join(here, rel, "package.json");
40917
+ if (!fs33.existsSync(pkgPath)) continue;
40918
+ const pkg = JSON.parse(fs33.readFileSync(pkgPath, "utf-8"));
40222
40919
  if (pkg.name !== "cascade-ai") continue;
40223
40920
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
40224
40921
  console.error(