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/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import Database2 from 'better-sqlite3';
2
- import path27, { dirname } from 'path';
3
- import fs26, { promises, mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
2
+ import path28, { dirname } from 'path';
3
+ import fs27, { promises, mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
4
4
  import os6 from 'os';
5
5
  import crypto, { randomUUID, randomBytes, createHash, timingSafeEqual, webcrypto } from 'crypto';
6
6
  import EventEmitter, { setMaxListeners } from 'events';
7
7
  import { glob } from 'glob';
8
8
  import Anthropic from '@anthropic-ai/sdk';
9
- import dns from 'dns';
9
+ import dnsCallback from 'dns';
10
10
  import http, { createServer } from 'http';
11
11
  import https from 'https';
12
12
  import zlib from 'zlib';
@@ -23,6 +23,7 @@ import { simpleGit } from 'simple-git';
23
23
  import PDFDocument from 'pdfkit';
24
24
  import dns2 from 'dns/promises';
25
25
  import net from 'net';
26
+ import { Agent } from 'undici';
26
27
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
27
28
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
28
29
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
@@ -132,16 +133,16 @@ var require_isarray = __commonJS({
132
133
  }
133
134
  });
134
135
 
135
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/stream.js
136
+ // node_modules/readable-stream/lib/internal/streams/stream.js
136
137
  var require_stream = __commonJS({
137
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/stream.js"(exports, module) {
138
+ "node_modules/readable-stream/lib/internal/streams/stream.js"(exports, module) {
138
139
  module.exports = __require("stream");
139
140
  }
140
141
  });
141
142
 
142
- // node_modules/jszip/node_modules/safe-buffer/index.js
143
+ // node_modules/readable-stream/node_modules/safe-buffer/index.js
143
144
  var require_safe_buffer = __commonJS({
144
- "node_modules/jszip/node_modules/safe-buffer/index.js"(exports, module) {
145
+ "node_modules/readable-stream/node_modules/safe-buffer/index.js"(exports, module) {
145
146
  var buffer = __require("buffer");
146
147
  var Buffer2 = buffer.Buffer;
147
148
  function copyProps(src, dst) {
@@ -259,7 +260,7 @@ var require_util = __commonJS({
259
260
  typeof arg === "undefined";
260
261
  }
261
262
  exports.isPrimitive = isPrimitive;
262
- exports.isBuffer = Buffer.isBuffer;
263
+ exports.isBuffer = __require("buffer").Buffer.isBuffer;
263
264
  function objectToString(o) {
264
265
  return Object.prototype.toString.call(o);
265
266
  }
@@ -312,9 +313,9 @@ var require_inherits = __commonJS({
312
313
  }
313
314
  });
314
315
 
315
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/BufferList.js
316
+ // node_modules/readable-stream/lib/internal/streams/BufferList.js
316
317
  var require_BufferList = __commonJS({
317
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/BufferList.js"(exports, module) {
318
+ "node_modules/readable-stream/lib/internal/streams/BufferList.js"(exports, module) {
318
319
  function _classCallCheck(instance, Constructor) {
319
320
  if (!(instance instanceof Constructor)) {
320
321
  throw new TypeError("Cannot call a class as a function");
@@ -389,9 +390,9 @@ var require_BufferList = __commonJS({
389
390
  }
390
391
  });
391
392
 
392
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/destroy.js
393
+ // node_modules/readable-stream/lib/internal/streams/destroy.js
393
394
  var require_destroy = __commonJS({
394
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/destroy.js"(exports, module) {
395
+ "node_modules/readable-stream/lib/internal/streams/destroy.js"(exports, module) {
395
396
  var pna = require_process_nextick_args();
396
397
  function destroy(err, cb) {
397
398
  var _this = this;
@@ -464,9 +465,9 @@ var require_node = __commonJS({
464
465
  }
465
466
  });
466
467
 
467
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_writable.js
468
+ // node_modules/readable-stream/lib/_stream_writable.js
468
469
  var require_stream_writable = __commonJS({
469
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_writable.js"(exports, module) {
470
+ "node_modules/readable-stream/lib/_stream_writable.js"(exports, module) {
470
471
  var pna = require_process_nextick_args();
471
472
  module.exports = Writable;
472
473
  function CorkedRequest(state) {
@@ -903,9 +904,9 @@ var require_stream_writable = __commonJS({
903
904
  }
904
905
  });
905
906
 
906
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_duplex.js
907
+ // node_modules/readable-stream/lib/_stream_duplex.js
907
908
  var require_stream_duplex = __commonJS({
908
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_duplex.js"(exports, module) {
909
+ "node_modules/readable-stream/lib/_stream_duplex.js"(exports, module) {
909
910
  var pna = require_process_nextick_args();
910
911
  var objectKeys = Object.keys || function(obj) {
911
912
  var keys2 = [];
@@ -979,10 +980,67 @@ var require_stream_duplex = __commonJS({
979
980
  }
980
981
  });
981
982
 
982
- // node_modules/jszip/node_modules/string_decoder/lib/string_decoder.js
983
+ // node_modules/string_decoder/node_modules/safe-buffer/index.js
984
+ var require_safe_buffer2 = __commonJS({
985
+ "node_modules/string_decoder/node_modules/safe-buffer/index.js"(exports, module) {
986
+ var buffer = __require("buffer");
987
+ var Buffer2 = buffer.Buffer;
988
+ function copyProps(src, dst) {
989
+ for (var key in src) {
990
+ dst[key] = src[key];
991
+ }
992
+ }
993
+ if (Buffer2.from && Buffer2.alloc && Buffer2.allocUnsafe && Buffer2.allocUnsafeSlow) {
994
+ module.exports = buffer;
995
+ } else {
996
+ copyProps(buffer, exports);
997
+ exports.Buffer = SafeBuffer;
998
+ }
999
+ function SafeBuffer(arg, encodingOrOffset, length) {
1000
+ return Buffer2(arg, encodingOrOffset, length);
1001
+ }
1002
+ copyProps(Buffer2, SafeBuffer);
1003
+ SafeBuffer.from = function(arg, encodingOrOffset, length) {
1004
+ if (typeof arg === "number") {
1005
+ throw new TypeError("Argument must not be a number");
1006
+ }
1007
+ return Buffer2(arg, encodingOrOffset, length);
1008
+ };
1009
+ SafeBuffer.alloc = function(size, fill, encoding) {
1010
+ if (typeof size !== "number") {
1011
+ throw new TypeError("Argument must be a number");
1012
+ }
1013
+ var buf = Buffer2(size);
1014
+ if (fill !== void 0) {
1015
+ if (typeof encoding === "string") {
1016
+ buf.fill(fill, encoding);
1017
+ } else {
1018
+ buf.fill(fill);
1019
+ }
1020
+ } else {
1021
+ buf.fill(0);
1022
+ }
1023
+ return buf;
1024
+ };
1025
+ SafeBuffer.allocUnsafe = function(size) {
1026
+ if (typeof size !== "number") {
1027
+ throw new TypeError("Argument must be a number");
1028
+ }
1029
+ return Buffer2(size);
1030
+ };
1031
+ SafeBuffer.allocUnsafeSlow = function(size) {
1032
+ if (typeof size !== "number") {
1033
+ throw new TypeError("Argument must be a number");
1034
+ }
1035
+ return buffer.SlowBuffer(size);
1036
+ };
1037
+ }
1038
+ });
1039
+
1040
+ // node_modules/string_decoder/lib/string_decoder.js
983
1041
  var require_string_decoder = __commonJS({
984
- "node_modules/jszip/node_modules/string_decoder/lib/string_decoder.js"(exports) {
985
- var Buffer2 = require_safe_buffer().Buffer;
1042
+ "node_modules/string_decoder/lib/string_decoder.js"(exports) {
1043
+ var Buffer2 = require_safe_buffer2().Buffer;
986
1044
  var isEncoding = Buffer2.isEncoding || function(encoding) {
987
1045
  encoding = "" + encoding;
988
1046
  switch (encoding && encoding.toLowerCase()) {
@@ -1216,9 +1274,9 @@ var require_string_decoder = __commonJS({
1216
1274
  }
1217
1275
  });
1218
1276
 
1219
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_readable.js
1277
+ // node_modules/readable-stream/lib/_stream_readable.js
1220
1278
  var require_stream_readable = __commonJS({
1221
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_readable.js"(exports, module) {
1279
+ "node_modules/readable-stream/lib/_stream_readable.js"(exports, module) {
1222
1280
  var pna = require_process_nextick_args();
1223
1281
  module.exports = Readable2;
1224
1282
  var isArray = require_isarray();
@@ -1901,9 +1959,9 @@ var require_stream_readable = __commonJS({
1901
1959
  }
1902
1960
  });
1903
1961
 
1904
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_transform.js
1962
+ // node_modules/readable-stream/lib/_stream_transform.js
1905
1963
  var require_stream_transform = __commonJS({
1906
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_transform.js"(exports, module) {
1964
+ "node_modules/readable-stream/lib/_stream_transform.js"(exports, module) {
1907
1965
  module.exports = Transform;
1908
1966
  var Duplex = require_stream_duplex();
1909
1967
  var util = Object.create(require_util());
@@ -2000,9 +2058,9 @@ var require_stream_transform = __commonJS({
2000
2058
  }
2001
2059
  });
2002
2060
 
2003
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_passthrough.js
2061
+ // node_modules/readable-stream/lib/_stream_passthrough.js
2004
2062
  var require_stream_passthrough = __commonJS({
2005
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_passthrough.js"(exports, module) {
2063
+ "node_modules/readable-stream/lib/_stream_passthrough.js"(exports, module) {
2006
2064
  module.exports = PassThrough;
2007
2065
  var Transform = require_stream_transform();
2008
2066
  var util = Object.create(require_util());
@@ -2018,9 +2076,9 @@ var require_stream_passthrough = __commonJS({
2018
2076
  }
2019
2077
  });
2020
2078
 
2021
- // node_modules/jszip/node_modules/readable-stream/readable.js
2079
+ // node_modules/readable-stream/readable.js
2022
2080
  var require_readable = __commonJS({
2023
- "node_modules/jszip/node_modules/readable-stream/readable.js"(exports, module) {
2081
+ "node_modules/readable-stream/readable.js"(exports, module) {
2024
2082
  var Stream = __require("stream");
2025
2083
  if (process.env.READABLE_STREAM === "disable" && Stream) {
2026
2084
  module.exports = Stream;
@@ -2929,8 +2987,8 @@ var require_utils = __commonJS({
2929
2987
  var result = transform[inputType][outputType](input);
2930
2988
  return result;
2931
2989
  };
2932
- exports.resolve = function(path29) {
2933
- var parts = path29.split("/");
2990
+ exports.resolve = function(path30) {
2991
+ var parts = path30.split("/");
2934
2992
  var result = [];
2935
2993
  for (var index = 0; index < parts.length; index++) {
2936
2994
  var part = parts[index];
@@ -8745,18 +8803,18 @@ var require_object = __commonJS({
8745
8803
  var object = new ZipObject(name, zipObjectContent, o);
8746
8804
  this.files[name] = object;
8747
8805
  };
8748
- var parentFolder = function(path29) {
8749
- if (path29.slice(-1) === "/") {
8750
- path29 = path29.substring(0, path29.length - 1);
8806
+ var parentFolder = function(path30) {
8807
+ if (path30.slice(-1) === "/") {
8808
+ path30 = path30.substring(0, path30.length - 1);
8751
8809
  }
8752
- var lastSlash = path29.lastIndexOf("/");
8753
- return lastSlash > 0 ? path29.substring(0, lastSlash) : "";
8810
+ var lastSlash = path30.lastIndexOf("/");
8811
+ return lastSlash > 0 ? path30.substring(0, lastSlash) : "";
8754
8812
  };
8755
- var forceTrailingSlash = function(path29) {
8756
- if (path29.slice(-1) !== "/") {
8757
- path29 += "/";
8813
+ var forceTrailingSlash = function(path30) {
8814
+ if (path30.slice(-1) !== "/") {
8815
+ path30 += "/";
8758
8816
  }
8759
- return path29;
8817
+ return path30;
8760
8818
  };
8761
8819
  var folderAdd = function(name, createFolders) {
8762
8820
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -9755,12 +9813,12 @@ var init_audit_logger = __esm({
9755
9813
  constructor(workspacePath, debugMode = false) {
9756
9814
  this.workspacePath = workspacePath;
9757
9815
  this.debugMode = debugMode;
9758
- const cascadeDir = path27.join(workspacePath, ".cascade");
9759
- if (!fs26.existsSync(cascadeDir)) {
9760
- fs26.mkdirSync(cascadeDir, { recursive: true });
9816
+ const cascadeDir = path28.join(workspacePath, ".cascade");
9817
+ if (!fs27.existsSync(cascadeDir)) {
9818
+ fs27.mkdirSync(cascadeDir, { recursive: true });
9761
9819
  }
9762
- this.keyPath = path27.join(cascadeDir, "audit_log.key");
9763
- this.dbPath = path27.join(cascadeDir, "audit_log.db");
9820
+ this.keyPath = path28.join(cascadeDir, "audit_log.key");
9821
+ this.dbPath = path28.join(cascadeDir, "audit_log.db");
9764
9822
  this.initEncryptionKey();
9765
9823
  this.db = new Database2(this.dbPath);
9766
9824
  this.db.pragma("journal_mode = WAL");
@@ -9790,11 +9848,11 @@ var init_audit_logger = __esm({
9790
9848
  return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
9791
9849
  }
9792
9850
  initEncryptionKey() {
9793
- if (fs26.existsSync(this.keyPath)) {
9794
- this.encryptionKey = fs26.readFileSync(this.keyPath);
9851
+ if (fs27.existsSync(this.keyPath)) {
9852
+ this.encryptionKey = fs27.readFileSync(this.keyPath);
9795
9853
  } else {
9796
9854
  this.encryptionKey = crypto.randomBytes(32);
9797
- fs26.writeFileSync(this.keyPath, this.encryptionKey);
9855
+ fs27.writeFileSync(this.keyPath, this.encryptionKey);
9798
9856
  }
9799
9857
  }
9800
9858
  encrypt(text) {
@@ -9872,9 +9930,9 @@ var init_audit_logger = __esm({
9872
9930
  dumpDebugIfNeeded() {
9873
9931
  if (!this.debugMode) return;
9874
9932
  try {
9875
- const dumpPath = path27.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
9933
+ const dumpPath = path28.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
9876
9934
  const entries = this.getAllLogs();
9877
- fs26.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9935
+ fs27.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9878
9936
  } catch (err) {
9879
9937
  console.error("Failed to dump debug audit logs", err);
9880
9938
  }
@@ -11565,7 +11623,7 @@ function reconcilePrice(model, live) {
11565
11623
  }
11566
11624
 
11567
11625
  // src/constants.ts
11568
- var CASCADE_VERSION = "0.76.0";
11626
+ var CASCADE_VERSION = "0.78.0";
11569
11627
  var CASCADE_CONFIG_DIR = ".cascade";
11570
11628
  var CASCADE_MD_FILE = "CASCADE.md";
11571
11629
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -11586,64 +11644,88 @@ var DEFAULT_AUTO_SUMMARIZE_AT = 15e4;
11586
11644
  var DEFAULT_MAX_SESSION_MESSAGES = 1e3;
11587
11645
  var DEFAULT_RETENTION_DAYS = 90;
11588
11646
  var MODELS = {
11589
- // Anthropic
11590
- "claude-opus-4": {
11591
- id: "claude-opus-4-5",
11592
- name: "Claude Opus 4",
11647
+ // Anthropic — active Claude API models (2026-08-27).
11648
+ "claude-fable-5": {
11649
+ id: "claude-fable-5",
11650
+ name: "Claude Fable 5",
11593
11651
  provider: "anthropic",
11594
- contextWindow: 2e5,
11652
+ contextWindow: 1e6,
11595
11653
  isVisionCapable: true,
11596
- inputCostPer1kTokens: 0.015,
11597
- outputCostPer1kTokens: 0.075,
11598
- maxOutputTokens: 32e3,
11654
+ inputCostPer1kTokens: 0.01,
11655
+ outputCostPer1kTokens: 0.05,
11656
+ maxOutputTokens: 128e3,
11599
11657
  supportsStreaming: true,
11600
11658
  isLocal: false
11601
11659
  },
11602
- "claude-sonnet-4": {
11603
- id: "claude-sonnet-4-5",
11604
- name: "Claude Sonnet 4",
11660
+ "claude-opus-5": {
11661
+ id: "claude-opus-5",
11662
+ name: "Claude Opus 5",
11605
11663
  provider: "anthropic",
11606
- contextWindow: 2e5,
11664
+ contextWindow: 1e6,
11607
11665
  isVisionCapable: true,
11608
- inputCostPer1kTokens: 3e-3,
11609
- outputCostPer1kTokens: 0.015,
11610
- maxOutputTokens: 16e3,
11666
+ inputCostPer1kTokens: 5e-3,
11667
+ outputCostPer1kTokens: 0.025,
11668
+ maxOutputTokens: 128e3,
11611
11669
  supportsStreaming: true,
11612
11670
  isLocal: false
11613
11671
  },
11614
- "claude-haiku-3-5": {
11615
- id: "claude-haiku-3-5-20251001",
11616
- name: "Claude Haiku 3.5",
11672
+ "claude-opus-4-8": {
11673
+ id: "claude-opus-4-8",
11674
+ name: "Claude Opus 4.8",
11617
11675
  provider: "anthropic",
11618
- contextWindow: 2e5,
11676
+ contextWindow: 1e6,
11619
11677
  isVisionCapable: true,
11620
- inputCostPer1kTokens: 8e-4,
11621
- outputCostPer1kTokens: 4e-3,
11622
- maxOutputTokens: 8e3,
11678
+ inputCostPer1kTokens: 5e-3,
11679
+ outputCostPer1kTokens: 0.025,
11680
+ maxOutputTokens: 128e3,
11623
11681
  supportsStreaming: true,
11624
11682
  isLocal: false
11625
11683
  },
11626
- "claude-haiku-4-5": {
11627
- id: "claude-haiku-4-5-20251001",
11628
- name: "Claude Haiku 4.5",
11684
+ "claude-opus-4-7": {
11685
+ id: "claude-opus-4-7",
11686
+ name: "Claude Opus 4.7",
11629
11687
  provider: "anthropic",
11630
- contextWindow: 2e5,
11688
+ contextWindow: 1e6,
11631
11689
  isVisionCapable: true,
11632
- inputCostPer1kTokens: 8e-4,
11633
- outputCostPer1kTokens: 4e-3,
11634
- maxOutputTokens: 8e3,
11690
+ inputCostPer1kTokens: 5e-3,
11691
+ outputCostPer1kTokens: 0.025,
11692
+ maxOutputTokens: 128e3,
11635
11693
  supportsStreaming: true,
11636
11694
  isLocal: false
11637
11695
  },
11638
- "claude-opus-4-8": {
11639
- id: "claude-opus-4-8",
11640
- name: "Claude Opus 4.8",
11696
+ "claude-opus-4-6": {
11697
+ id: "claude-opus-4-6",
11698
+ name: "Claude Opus 4.6",
11699
+ provider: "anthropic",
11700
+ contextWindow: 1e6,
11701
+ isVisionCapable: true,
11702
+ inputCostPer1kTokens: 5e-3,
11703
+ outputCostPer1kTokens: 0.025,
11704
+ maxOutputTokens: 128e3,
11705
+ supportsStreaming: true,
11706
+ isLocal: false
11707
+ },
11708
+ "claude-opus-4-5": {
11709
+ id: "claude-opus-4-5-20251101",
11710
+ name: "Claude Opus 4.5",
11641
11711
  provider: "anthropic",
11642
11712
  contextWindow: 2e5,
11643
11713
  isVisionCapable: true,
11644
- inputCostPer1kTokens: 0.015,
11645
- outputCostPer1kTokens: 0.075,
11646
- maxOutputTokens: 32e3,
11714
+ inputCostPer1kTokens: 5e-3,
11715
+ outputCostPer1kTokens: 0.025,
11716
+ maxOutputTokens: 64e3,
11717
+ supportsStreaming: true,
11718
+ isLocal: false
11719
+ },
11720
+ "claude-sonnet-5": {
11721
+ id: "claude-sonnet-5",
11722
+ name: "Claude Sonnet 5",
11723
+ provider: "anthropic",
11724
+ contextWindow: 1e6,
11725
+ isVisionCapable: true,
11726
+ inputCostPer1kTokens: 2e-3,
11727
+ outputCostPer1kTokens: 0.01,
11728
+ maxOutputTokens: 128e3,
11647
11729
  supportsStreaming: true,
11648
11730
  isLocal: false
11649
11731
  },
@@ -11651,76 +11733,135 @@ var MODELS = {
11651
11733
  id: "claude-sonnet-4-6",
11652
11734
  name: "Claude Sonnet 4.6",
11653
11735
  provider: "anthropic",
11736
+ contextWindow: 1e6,
11737
+ isVisionCapable: true,
11738
+ inputCostPer1kTokens: 3e-3,
11739
+ outputCostPer1kTokens: 0.015,
11740
+ maxOutputTokens: 64e3,
11741
+ supportsStreaming: true,
11742
+ isLocal: false
11743
+ },
11744
+ "claude-sonnet-4-5": {
11745
+ id: "claude-sonnet-4-5-20250929",
11746
+ name: "Claude Sonnet 4.5",
11747
+ provider: "anthropic",
11654
11748
  contextWindow: 2e5,
11655
11749
  isVisionCapable: true,
11656
11750
  inputCostPer1kTokens: 3e-3,
11657
11751
  outputCostPer1kTokens: 0.015,
11658
- maxOutputTokens: 16e3,
11752
+ maxOutputTokens: 64e3,
11659
11753
  supportsStreaming: true,
11660
11754
  isLocal: false
11661
11755
  },
11662
- // OpenAI
11663
- "gpt-4o": {
11664
- id: "gpt-4o",
11665
- name: "GPT-4o",
11756
+ "claude-haiku-4-5": {
11757
+ id: "claude-haiku-4-5-20251001",
11758
+ name: "Claude Haiku 4.5",
11759
+ provider: "anthropic",
11760
+ contextWindow: 2e5,
11761
+ isVisionCapable: true,
11762
+ inputCostPer1kTokens: 1e-3,
11763
+ outputCostPer1kTokens: 5e-3,
11764
+ maxOutputTokens: 64e3,
11765
+ supportsStreaming: true,
11766
+ isLocal: false
11767
+ },
11768
+ // OpenAI — active chat-capable models used by Cascade's Chat Completions path.
11769
+ "gpt-5.6-sol": {
11770
+ id: "gpt-5.6-sol",
11771
+ name: "GPT-5.6 Sol",
11666
11772
  provider: "openai",
11667
- contextWindow: 128e3,
11773
+ contextWindow: 105e4,
11774
+ isVisionCapable: true,
11775
+ inputCostPer1kTokens: 4e-3,
11776
+ outputCostPer1kTokens: 0.02,
11777
+ maxOutputTokens: 128e3,
11778
+ supportsStreaming: true,
11779
+ isLocal: false
11780
+ },
11781
+ "gpt-5.6-terra": {
11782
+ id: "gpt-5.6-terra",
11783
+ name: "GPT-5.6 Terra",
11784
+ provider: "openai",
11785
+ contextWindow: 105e4,
11786
+ isVisionCapable: true,
11787
+ inputCostPer1kTokens: 2e-3,
11788
+ outputCostPer1kTokens: 0.012,
11789
+ maxOutputTokens: 128e3,
11790
+ supportsStreaming: true,
11791
+ isLocal: false
11792
+ },
11793
+ "gpt-5.6-luna": {
11794
+ id: "gpt-5.6-luna",
11795
+ name: "GPT-5.6 Luna",
11796
+ provider: "openai",
11797
+ contextWindow: 105e4,
11798
+ isVisionCapable: true,
11799
+ inputCostPer1kTokens: 2e-4,
11800
+ outputCostPer1kTokens: 12e-4,
11801
+ maxOutputTokens: 128e3,
11802
+ supportsStreaming: true,
11803
+ isLocal: false
11804
+ },
11805
+ "gpt-5.5": {
11806
+ id: "gpt-5.5",
11807
+ name: "GPT-5.5",
11808
+ provider: "openai",
11809
+ contextWindow: 105e4,
11668
11810
  isVisionCapable: true,
11669
11811
  inputCostPer1kTokens: 5e-3,
11670
- outputCostPer1kTokens: 0.015,
11671
- maxOutputTokens: 16e3,
11812
+ outputCostPer1kTokens: 0.03,
11813
+ maxOutputTokens: 128e3,
11672
11814
  supportsStreaming: true,
11673
11815
  isLocal: false
11674
11816
  },
11675
- "gpt-4o-mini": {
11676
- id: "gpt-4o-mini",
11677
- name: "GPT-4o Mini",
11817
+ "gpt-5.4": {
11818
+ id: "gpt-5.4",
11819
+ name: "GPT-5.4",
11678
11820
  provider: "openai",
11679
- contextWindow: 128e3,
11821
+ contextWindow: 105e4,
11680
11822
  isVisionCapable: true,
11681
- inputCostPer1kTokens: 15e-5,
11682
- outputCostPer1kTokens: 6e-4,
11683
- maxOutputTokens: 16e3,
11823
+ inputCostPer1kTokens: 25e-4,
11824
+ outputCostPer1kTokens: 0.015,
11825
+ maxOutputTokens: 128e3,
11684
11826
  supportsStreaming: true,
11685
11827
  isLocal: false
11686
11828
  },
11687
- "gpt-4.1": {
11688
- id: "gpt-4.1",
11689
- name: "GPT-4.1",
11829
+ "gpt-5.4-mini": {
11830
+ id: "gpt-5.4-mini",
11831
+ name: "GPT-5.4 Mini",
11690
11832
  provider: "openai",
11691
- contextWindow: 1047576,
11833
+ contextWindow: 4e5,
11692
11834
  isVisionCapable: true,
11693
- inputCostPer1kTokens: 2e-3,
11694
- outputCostPer1kTokens: 8e-3,
11695
- maxOutputTokens: 32768,
11835
+ inputCostPer1kTokens: 75e-5,
11836
+ outputCostPer1kTokens: 45e-4,
11837
+ maxOutputTokens: 128e3,
11696
11838
  supportsStreaming: true,
11697
11839
  isLocal: false
11698
11840
  },
11699
- "gpt-4.1-mini": {
11700
- id: "gpt-4.1-mini",
11701
- name: "GPT-4.1 Mini",
11841
+ "gpt-5.4-nano": {
11842
+ id: "gpt-5.4-nano",
11843
+ name: "GPT-5.4 Nano",
11702
11844
  provider: "openai",
11703
- contextWindow: 1047576,
11845
+ contextWindow: 4e5,
11704
11846
  isVisionCapable: true,
11705
- inputCostPer1kTokens: 4e-4,
11706
- outputCostPer1kTokens: 16e-4,
11707
- maxOutputTokens: 32768,
11847
+ inputCostPer1kTokens: 2e-4,
11848
+ outputCostPer1kTokens: 125e-5,
11849
+ maxOutputTokens: 128e3,
11708
11850
  supportsStreaming: true,
11709
11851
  isLocal: false
11710
11852
  },
11711
- "gpt-4.1-nano": {
11712
- id: "gpt-4.1-nano",
11713
- name: "GPT-4.1 Nano",
11853
+ "gpt-5.2": {
11854
+ id: "gpt-5.2",
11855
+ name: "GPT-5.2",
11714
11856
  provider: "openai",
11715
- contextWindow: 1047576,
11857
+ contextWindow: 4e5,
11716
11858
  isVisionCapable: true,
11717
- inputCostPer1kTokens: 1e-4,
11718
- outputCostPer1kTokens: 4e-4,
11719
- maxOutputTokens: 32768,
11859
+ inputCostPer1kTokens: 175e-5,
11860
+ outputCostPer1kTokens: 0.014,
11861
+ maxOutputTokens: 128e3,
11720
11862
  supportsStreaming: true,
11721
11863
  isLocal: false
11722
11864
  },
11723
- // GPT-5 family (offline baseline pricing; the live fetch keeps it current).
11724
11865
  "gpt-5": {
11725
11866
  id: "gpt-5",
11726
11867
  name: "GPT-5",
@@ -11757,79 +11898,149 @@ var MODELS = {
11757
11898
  supportsStreaming: true,
11758
11899
  isLocal: false
11759
11900
  },
11760
- // GPT-5 point releases — genuinely distinct capability, so they route as their
11761
- // own families (not folded into gpt-5). Offline baseline pricing; live fetch
11762
- // keeps it current. gpt-5.5 "Spud" (Apr 2026) is the current SWE-bench leader.
11763
- "gpt-5.5": {
11764
- id: "gpt-5.5",
11765
- name: "GPT-5.5",
11901
+ "gpt-4.1": {
11902
+ id: "gpt-4.1",
11903
+ name: "GPT-4.1",
11766
11904
  provider: "openai",
11767
- contextWindow: 4e5,
11905
+ contextWindow: 1047576,
11768
11906
  isVisionCapable: true,
11769
- inputCostPer1kTokens: 25e-4,
11770
- outputCostPer1kTokens: 0.02,
11771
- maxOutputTokens: 128e3,
11907
+ inputCostPer1kTokens: 2e-3,
11908
+ outputCostPer1kTokens: 8e-3,
11909
+ maxOutputTokens: 32768,
11772
11910
  supportsStreaming: true,
11773
11911
  isLocal: false
11774
11912
  },
11775
- "gpt-5.4": {
11776
- id: "gpt-5.4",
11777
- name: "GPT-5.4",
11913
+ "gpt-4.1-mini": {
11914
+ id: "gpt-4.1-mini",
11915
+ name: "GPT-4.1 Mini",
11778
11916
  provider: "openai",
11779
- contextWindow: 4e5,
11917
+ contextWindow: 1047576,
11780
11918
  isVisionCapable: true,
11781
- inputCostPer1kTokens: 125e-5,
11919
+ inputCostPer1kTokens: 4e-4,
11920
+ outputCostPer1kTokens: 16e-4,
11921
+ maxOutputTokens: 32768,
11922
+ supportsStreaming: true,
11923
+ isLocal: false
11924
+ },
11925
+ "gpt-4.1-nano": {
11926
+ id: "gpt-4.1-nano",
11927
+ name: "GPT-4.1 Nano",
11928
+ provider: "openai",
11929
+ contextWindow: 1047576,
11930
+ isVisionCapable: true,
11931
+ inputCostPer1kTokens: 1e-4,
11932
+ outputCostPer1kTokens: 4e-4,
11933
+ maxOutputTokens: 32768,
11934
+ supportsStreaming: true,
11935
+ isLocal: false
11936
+ },
11937
+ "gpt-4o": {
11938
+ id: "gpt-4o",
11939
+ name: "GPT-4o",
11940
+ provider: "openai",
11941
+ contextWindow: 128e3,
11942
+ isVisionCapable: true,
11943
+ inputCostPer1kTokens: 25e-4,
11782
11944
  outputCostPer1kTokens: 0.01,
11783
- maxOutputTokens: 128e3,
11945
+ maxOutputTokens: 16e3,
11784
11946
  supportsStreaming: true,
11785
11947
  isLocal: false
11786
11948
  },
11787
- "gpt-5.4-mini": {
11788
- id: "gpt-5.4-mini",
11789
- name: "GPT-5.4 Mini",
11949
+ "gpt-4o-mini": {
11950
+ id: "gpt-4o-mini",
11951
+ name: "GPT-4o Mini",
11790
11952
  provider: "openai",
11791
- contextWindow: 4e5,
11953
+ contextWindow: 128e3,
11792
11954
  isVisionCapable: true,
11793
- inputCostPer1kTokens: 25e-5,
11794
- outputCostPer1kTokens: 2e-3,
11795
- maxOutputTokens: 128e3,
11955
+ inputCostPer1kTokens: 15e-5,
11956
+ outputCostPer1kTokens: 6e-4,
11957
+ maxOutputTokens: 16e3,
11796
11958
  supportsStreaming: true,
11797
11959
  isLocal: false
11798
11960
  },
11799
- // Google
11800
- "gemini-2.0-flash": {
11801
- id: "gemini-2.0-flash",
11802
- name: "Gemini 2.0 Flash",
11961
+ // Google Gemini — active GA + preview chat models. Gemini 2.0/1.5 are no
11962
+ // longer advertised because the Gemini API shut those generations down.
11963
+ "gemini-3.7-flash": {
11964
+ id: "gemini-3.7-flash",
11965
+ name: "Gemini 3.7 Flash",
11803
11966
  provider: "gemini",
11804
- contextWindow: 1e6,
11967
+ contextWindow: 1048576,
11805
11968
  isVisionCapable: true,
11806
- inputCostPer1kTokens: 1e-4,
11807
- outputCostPer1kTokens: 4e-4,
11808
- maxOutputTokens: 8e3,
11969
+ inputCostPer1kTokens: 75e-5,
11970
+ outputCostPer1kTokens: 375e-5,
11971
+ maxOutputTokens: 65536,
11809
11972
  supportsStreaming: true,
11810
11973
  isLocal: false
11811
11974
  },
11812
- "gemini-1.5-pro": {
11813
- id: "gemini-1.5-pro",
11814
- name: "Gemini 1.5 Pro",
11975
+ "gemini-3.6-flash": {
11976
+ id: "gemini-3.6-flash",
11977
+ name: "Gemini 3.6 Flash",
11815
11978
  provider: "gemini",
11816
- contextWindow: 1e6,
11979
+ contextWindow: 1048576,
11817
11980
  isVisionCapable: true,
11818
- inputCostPer1kTokens: 125e-5,
11819
- outputCostPer1kTokens: 5e-3,
11820
- maxOutputTokens: 8e3,
11981
+ inputCostPer1kTokens: 75e-5,
11982
+ outputCostPer1kTokens: 375e-5,
11983
+ maxOutputTokens: 65536,
11821
11984
  supportsStreaming: true,
11822
11985
  isLocal: false
11823
11986
  },
11824
- "gemini-2.0-flash-lite": {
11825
- id: "gemini-2.0-flash-lite",
11826
- name: "Gemini 2.0 Flash-Lite",
11987
+ "gemini-3.5-flash": {
11988
+ id: "gemini-3.5-flash",
11989
+ name: "Gemini 3.5 Flash",
11827
11990
  provider: "gemini",
11828
- contextWindow: 1e6,
11991
+ contextWindow: 1048576,
11829
11992
  isVisionCapable: true,
11830
- inputCostPer1kTokens: 5e-5,
11831
- outputCostPer1kTokens: 2e-4,
11832
- maxOutputTokens: 8e3,
11993
+ inputCostPer1kTokens: 15e-4,
11994
+ outputCostPer1kTokens: 9e-3,
11995
+ maxOutputTokens: 65536,
11996
+ supportsStreaming: true,
11997
+ isLocal: false
11998
+ },
11999
+ "gemini-3.5-flash-lite": {
12000
+ id: "gemini-3.5-flash-lite",
12001
+ name: "Gemini 3.5 Flash-Lite",
12002
+ provider: "gemini",
12003
+ contextWindow: 1048576,
12004
+ isVisionCapable: true,
12005
+ inputCostPer1kTokens: 3e-4,
12006
+ outputCostPer1kTokens: 25e-4,
12007
+ maxOutputTokens: 65536,
12008
+ supportsStreaming: true,
12009
+ isLocal: false
12010
+ },
12011
+ "gemini-3.1-flash-lite": {
12012
+ id: "gemini-3.1-flash-lite",
12013
+ name: "Gemini 3.1 Flash-Lite",
12014
+ provider: "gemini",
12015
+ contextWindow: 1048576,
12016
+ isVisionCapable: true,
12017
+ inputCostPer1kTokens: 25e-5,
12018
+ outputCostPer1kTokens: 15e-4,
12019
+ maxOutputTokens: 65536,
12020
+ supportsStreaming: true,
12021
+ isLocal: false
12022
+ },
12023
+ "gemini-3.1-pro-preview": {
12024
+ id: "gemini-3.1-pro-preview",
12025
+ name: "Gemini 3.1 Pro Preview",
12026
+ provider: "gemini",
12027
+ contextWindow: 1048576,
12028
+ isVisionCapable: true,
12029
+ inputCostPer1kTokens: 2e-3,
12030
+ outputCostPer1kTokens: 0.012,
12031
+ maxOutputTokens: 65536,
12032
+ supportsStreaming: true,
12033
+ isLocal: false
12034
+ },
12035
+ "gemini-3-flash-preview": {
12036
+ id: "gemini-3-flash-preview",
12037
+ name: "Gemini 3 Flash Preview",
12038
+ provider: "gemini",
12039
+ contextWindow: 1048576,
12040
+ isVisionCapable: true,
12041
+ inputCostPer1kTokens: 5e-4,
12042
+ outputCostPer1kTokens: 3e-3,
12043
+ maxOutputTokens: 65536,
11833
12044
  supportsStreaming: true,
11834
12045
  isLocal: false
11835
12046
  },
@@ -11837,7 +12048,7 @@ var MODELS = {
11837
12048
  id: "gemini-2.5-pro",
11838
12049
  name: "Gemini 2.5 Pro",
11839
12050
  provider: "gemini",
11840
- contextWindow: 1e6,
12051
+ contextWindow: 1048576,
11841
12052
  isVisionCapable: true,
11842
12053
  inputCostPer1kTokens: 125e-5,
11843
12054
  outputCostPer1kTokens: 0.01,
@@ -11849,10 +12060,22 @@ var MODELS = {
11849
12060
  id: "gemini-2.5-flash",
11850
12061
  name: "Gemini 2.5 Flash",
11851
12062
  provider: "gemini",
11852
- contextWindow: 1e6,
12063
+ contextWindow: 1048576,
11853
12064
  isVisionCapable: true,
11854
- inputCostPer1kTokens: 15e-5,
11855
- outputCostPer1kTokens: 6e-4,
12065
+ inputCostPer1kTokens: 3e-4,
12066
+ outputCostPer1kTokens: 25e-4,
12067
+ maxOutputTokens: 65536,
12068
+ supportsStreaming: true,
12069
+ isLocal: false
12070
+ },
12071
+ "gemini-2.5-flash-lite": {
12072
+ id: "gemini-2.5-flash-lite",
12073
+ name: "Gemini 2.5 Flash-Lite",
12074
+ provider: "gemini",
12075
+ contextWindow: 1048576,
12076
+ isVisionCapable: true,
12077
+ inputCostPer1kTokens: 1e-4,
12078
+ outputCostPer1kTokens: 4e-4,
11856
12079
  maxOutputTokens: 65536,
11857
12080
  supportsStreaming: true,
11858
12081
  isLocal: false
@@ -11922,43 +12145,57 @@ for (const _m of Object.values(MODELS)) {
11922
12145
  _m.pricingUnknown = false;
11923
12146
  }
11924
12147
  var T1_MODEL_PRIORITY = [
12148
+ "gpt-5.6-sol",
12149
+ "claude-fable-5",
12150
+ "claude-opus-5",
11925
12151
  "gpt-5.5",
11926
12152
  "gpt-5.4",
12153
+ "gemini-3.1-pro-preview",
12154
+ "claude-opus-4-8",
12155
+ "claude-sonnet-5",
12156
+ "gemini-3.7-flash",
12157
+ "gpt-5.2",
11927
12158
  "gpt-5",
11928
- "claude-opus-4",
11929
- "claude-sonnet-4",
11930
12159
  "gemini-2.5-pro",
11931
12160
  "gpt-4.1",
11932
- "gpt-4o",
11933
- "gemini-1.5-pro"
12161
+ "gpt-4o"
11934
12162
  ];
11935
12163
  var T2_MODEL_PRIORITY = [
12164
+ "gpt-5.6-terra",
12165
+ "claude-sonnet-5",
12166
+ "gemini-3.7-flash",
12167
+ "gemini-3.6-flash",
11936
12168
  "gpt-5.4-mini",
11937
12169
  "gpt-5-mini",
11938
- "claude-sonnet-4",
12170
+ "claude-sonnet-4-6",
11939
12171
  "claude-haiku-4-5",
11940
- "claude-haiku-3-5",
12172
+ "gemini-3.5-flash",
11941
12173
  "gemini-2.5-flash",
11942
12174
  "gpt-4.1-mini",
11943
12175
  "gpt-4o-mini",
11944
- "gemini-2.0-flash",
11945
12176
  "llama3:70b"
11946
12177
  ];
11947
12178
  var T3_MODEL_PRIORITY = [
12179
+ "gpt-5.6-luna",
12180
+ "gpt-5.4-nano",
12181
+ "gemini-3.5-flash-lite",
12182
+ "gemini-3.1-flash-lite",
12183
+ "claude-haiku-4-5",
11948
12184
  "gpt-5-nano",
12185
+ "gemini-2.5-flash-lite",
11949
12186
  "llama3.2:3b",
11950
12187
  "mistral:7b",
11951
- "claude-haiku-4-5",
11952
- "claude-haiku-3-5",
11953
12188
  "gpt-4.1-nano",
11954
12189
  "gpt-4o-mini",
11955
- "gemini-2.5-flash",
11956
- "gemini-2.0-flash"
12190
+ "gemini-2.5-flash"
11957
12191
  ];
11958
12192
  var VISION_MODEL_PRIORITY = [
11959
- "claude-sonnet-4",
12193
+ "gpt-5.6-sol",
12194
+ "claude-fable-5",
12195
+ "claude-sonnet-5",
12196
+ "gemini-3.7-flash",
11960
12197
  "gpt-4o",
11961
- "gemini-2.0-flash",
12198
+ "gemini-2.5-flash",
11962
12199
  "llava"
11963
12200
  ];
11964
12201
  var COMPLEXITY_T2_COUNT = {
@@ -12139,7 +12376,7 @@ function isChatModel(id, methods) {
12139
12376
  return !NON_CHAT.test(id);
12140
12377
  }
12141
12378
  try {
12142
- dns.setDefaultResultOrder("ipv4first");
12379
+ dnsCallback.setDefaultResultOrder("ipv4first");
12143
12380
  } catch {
12144
12381
  }
12145
12382
  function preferIpv4Host(url) {
@@ -12155,8 +12392,8 @@ function normalizeEndpoint(url) {
12155
12392
  if (!raw) return "";
12156
12393
  try {
12157
12394
  const u = new URL(raw);
12158
- const path29 = stripTrailingSlashes(`${u.pathname}${u.search}`);
12159
- return `${u.protocol}//${u.host.toLowerCase()}${path29}`;
12395
+ const path30 = stripTrailingSlashes(`${u.pathname}${u.search}`);
12396
+ return `${u.protocol}//${u.host.toLowerCase()}${path30}`;
12160
12397
  } catch {
12161
12398
  return raw.toLowerCase();
12162
12399
  }
@@ -12823,52 +13060,127 @@ var OpenAIProvider = class extends BaseProvider {
12823
13060
  }
12824
13061
  };
12825
13062
 
13063
+ // src/providers/cloud-model-catalog.json
13064
+ var cloud_model_catalog_default = {
13065
+ models: [
13066
+ { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
13067
+ { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
13068
+ { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
13069
+ { id: "gpt-5.5", name: "GPT-5.5", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
13070
+ { id: "gpt-5.4", name: "GPT-5.4", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
13071
+ { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
13072
+ { id: "gpt-5.4-nano", name: "GPT-5.4 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
13073
+ { id: "gpt-5", name: "GPT-5", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
13074
+ { id: "gpt-5-mini", name: "GPT-5 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
13075
+ { id: "gpt-5-nano", name: "GPT-5 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
13076
+ { id: "gpt-4.1", name: "GPT-4.1", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
13077
+ { id: "gpt-4.1-mini", name: "GPT-4.1 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
13078
+ { id: "gpt-4.1-nano", name: "GPT-4.1 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
13079
+ { id: "gpt-4o", name: "GPT-4o", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 128e3, maxOutputTokens: 16384, vision: true, toolUse: true },
13080
+ { id: "gpt-4o-mini", name: "GPT-4o Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 128e3, maxOutputTokens: 16384, vision: true, toolUse: true },
13081
+ { id: "claude-fable-5", name: "Claude Fable 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13082
+ { id: "claude-opus-5", name: "Claude Opus 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13083
+ { id: "claude-opus-4-8", name: "Claude Opus 4.8", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13084
+ { id: "claude-opus-4-7", name: "Claude Opus 4.7", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13085
+ { id: "claude-opus-4-6", name: "Claude Opus 4.6", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, vision: true, toolUse: true },
13086
+ { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, vision: true, toolUse: true },
13087
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13088
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13089
+ { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, vision: true, toolUse: true },
13090
+ { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, maxOutputTokens: 64e3, vision: true, toolUse: true },
13091
+ { id: "gemini-3.7-flash", name: "Gemini 3.7 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13092
+ { id: "gemini-3.6-flash", name: "Gemini 3.6 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13093
+ { id: "gemini-3.5-flash", name: "Gemini 3.5 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13094
+ { id: "gemini-3.5-flash-lite", name: "Gemini 3.5 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13095
+ { id: "gemini-3.1-flash-lite", name: "Gemini 3.1 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13096
+ { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro", providers: ["gemini"], lifecycle: "preview", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13097
+ { id: "gemini-3-flash-preview", name: "Gemini 3 Flash", providers: ["gemini"], lifecycle: "preview", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13098
+ { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13099
+ { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13100
+ { id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true }
13101
+ ]
13102
+ };
13103
+
12826
13104
  // src/providers/azure.ts
12827
13105
  var DEFAULT_AZURE_API_VERSION = "2024-12-01-preview";
12828
- var AZURE_BASE_MODEL_RULES = [
12829
- [/gpt-?5\.5/, "gpt-5.5"],
12830
- [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
12831
- [/gpt-?5\.4/, "gpt-5.4"],
12832
- [/gpt-?5.*nano/, "gpt-5-nano"],
12833
- [/gpt-?5.*mini/, "gpt-5-mini"],
12834
- [/gpt-?5/, "gpt-5"],
12835
- [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
12836
- [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
12837
- [/gpt-?4\.1/, "gpt-4.1"],
12838
- [/gpt-?4o-mini/, "gpt-4o-mini"],
12839
- [/gpt-?4o/, "gpt-4o"]
12840
- ];
12841
- var AZURE_BASE_MODELS = AZURE_BASE_MODEL_RULES.map(([, id]) => id);
13106
+ var CLOUD_MODELS = cloud_model_catalog_default.models;
13107
+ var 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));
13108
+ var AZURE_BASE_MODELS = AZURE_CATALOG.map((m) => m.id);
13109
+ function compactModelId(id) {
13110
+ return id.toLowerCase().replace(/[^a-z0-9.]/g, "");
13111
+ }
13112
+ function catalogEntry(id) {
13113
+ const n = id.toLowerCase();
13114
+ return CLOUD_MODELS.find((m) => m.id.toLowerCase() === n);
13115
+ }
12842
13116
  function inferAzureBaseModel(deploymentName) {
12843
13117
  const n = deploymentName.toLowerCase();
12844
- for (const [re, base] of AZURE_BASE_MODEL_RULES) if (re.test(n)) return base;
13118
+ const compact = compactModelId(n);
13119
+ const hasBareGpt56Alias = compact.includes("gpt5.6");
13120
+ for (const model of AZURE_CATALOG) {
13121
+ if (hasBareGpt56Alias && model.id === "gpt-5") continue;
13122
+ if (n.includes(model.id.toLowerCase()) || compact.includes(compactModelId(model.id))) {
13123
+ return model.id;
13124
+ }
13125
+ }
13126
+ if (hasBareGpt56Alias) return "gpt-5.6-sol";
13127
+ const gpt5At = compact.indexOf("gpt5");
13128
+ if (gpt5At !== -1) {
13129
+ const suffix = compact.slice(gpt5At + "gpt5".length);
13130
+ if (suffix.includes("nano")) return "gpt-5-nano";
13131
+ if (suffix.includes("mini")) return "gpt-5-mini";
13132
+ return "gpt-5";
13133
+ }
13134
+ if (compact.includes("gpt4.1nano")) return "gpt-4.1-nano";
13135
+ if (compact.includes("gpt4.1mini")) return "gpt-4.1-mini";
13136
+ if (compact.includes("gpt4.1")) return "gpt-4.1";
13137
+ if (compact.includes("gpt4omini")) return "gpt-4o-mini";
13138
+ if (compact.includes("gpt4o")) return "gpt-4o";
12845
13139
  return null;
12846
13140
  }
13141
+ function catalogModelInfo(baseModelId) {
13142
+ const entry = catalogEntry(baseModelId);
13143
+ if (!entry?.contextWindow) return void 0;
13144
+ return {
13145
+ id: entry.id,
13146
+ name: entry.name,
13147
+ provider: "openai",
13148
+ contextWindow: entry.contextWindow,
13149
+ isVisionCapable: entry.vision ?? false,
13150
+ inputCostPer1kTokens: 0,
13151
+ outputCostPer1kTokens: 0,
13152
+ maxOutputTokens: entry.maxOutputTokens ?? 16e3,
13153
+ supportsStreaming: true,
13154
+ isLocal: false,
13155
+ supportsToolUse: entry.toolUse ?? true,
13156
+ pricingUnknown: true
13157
+ };
13158
+ }
12847
13159
  function azureModelForDeployment(cfg) {
12848
13160
  if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
12849
13161
  const id = cfg.deploymentName.trim();
12850
13162
  const name = cfg.label?.trim() || id;
12851
13163
  const baseModelId = cfg.model?.trim() || inferAzureBaseModel(id) || void 0;
12852
- const base = baseModelId ? MODELS[baseModelId] : void 0;
12853
- if (base) {
13164
+ const base = baseModelId ? MODELS[baseModelId] ?? catalogModelInfo(baseModelId) : void 0;
13165
+ if (base && baseModelId) {
12854
13166
  const azurePrice = resolvePricing(
12855
13167
  { id, provider: "azure", isLocal: false, baseModelId },
12856
13168
  { region: cfg.region }
12857
13169
  );
13170
+ const exactAzurePrice = !azurePrice.unknown && !azurePrice.estimatedFromProvider;
13171
+ const nativeToolsOnThisTransport = /^gpt-5\.6-(?:sol|terra|luna)$/i.test(baseModelId) ? false : base.supportsToolUse ?? true;
12858
13172
  return {
12859
13173
  ...base,
12860
13174
  id,
12861
- // callable deployment name
12862
13175
  name,
12863
13176
  provider: "azure",
12864
13177
  baseModelId,
12865
- // real identity for benchmark + live pricing
12866
- supportsToolUse: base.supportsToolUse ?? true,
12867
- ...azurePrice.unknown ? {} : {
13178
+ supportsToolUse: nativeToolsOnThisTransport,
13179
+ ...!azurePrice.unknown ? {
12868
13180
  inputCostPer1kTokens: azurePrice.input,
12869
13181
  outputCostPer1kTokens: azurePrice.output,
12870
- pricingUnknown: false
12871
- }
13182
+ pricingUnknown: !exactAzurePrice
13183
+ } : { pricingUnknown: true }
12872
13184
  };
12873
13185
  }
12874
13186
  return {
@@ -12883,18 +13195,23 @@ function azureModelForDeployment(cfg) {
12883
13195
  maxOutputTokens: 16e3,
12884
13196
  supportsStreaming: true,
12885
13197
  isLocal: false,
12886
- supportsToolUse: true
13198
+ supportsToolUse: true,
13199
+ pricingUnknown: true
12887
13200
  };
12888
13201
  }
13202
+ function stripTrailingSlashes3(value) {
13203
+ let end = value.length;
13204
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
13205
+ return value.slice(0, end);
13206
+ }
12889
13207
  var AzureOpenAIProvider = class extends OpenAIProvider {
12890
13208
  constructor(config, model) {
12891
13209
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
12892
- const endpoint = rawUrl.replace(/\/+$/, "");
13210
+ const endpoint = stripTrailingSlashes3(rawUrl);
12893
13211
  super(
12894
13212
  {
12895
13213
  ...config,
12896
13214
  baseUrl: endpoint
12897
- // Kept for superclass compatibility if it reads it
12898
13215
  },
12899
13216
  model
12900
13217
  );
@@ -14637,7 +14954,7 @@ var Keystore = class {
14637
14954
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
14638
14955
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
14639
14956
  this.backend = "keytar";
14640
- if (password && fs26.existsSync(this.storePath)) {
14957
+ if (password && fs27.existsSync(this.storePath)) {
14641
14958
  try {
14642
14959
  const fileEntries = this.decryptFile(password);
14643
14960
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -14656,7 +14973,7 @@ var Keystore = class {
14656
14973
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
14657
14974
  );
14658
14975
  }
14659
- if (!fs26.existsSync(this.storePath)) {
14976
+ if (!fs27.existsSync(this.storePath)) {
14660
14977
  const salt = crypto.randomBytes(SALT_LEN);
14661
14978
  this.masterKey = this.deriveKey(password, salt);
14662
14979
  this.writeWithSalt({}, salt);
@@ -14670,7 +14987,7 @@ var Keystore = class {
14670
14987
  }
14671
14988
  /** Synchronous legacy unlock kept for AES-only environments. */
14672
14989
  unlockSync(password) {
14673
- if (!fs26.existsSync(this.storePath)) {
14990
+ if (!fs27.existsSync(this.storePath)) {
14674
14991
  const salt = crypto.randomBytes(SALT_LEN);
14675
14992
  this.masterKey = this.deriveKey(password, salt);
14676
14993
  this.writeWithSalt({}, salt);
@@ -14728,7 +15045,7 @@ var Keystore = class {
14728
15045
  }
14729
15046
  }
14730
15047
  decryptFile(password, knownSalt) {
14731
- if (!fs26.existsSync(this.storePath)) return {};
15048
+ if (!fs27.existsSync(this.storePath)) return {};
14732
15049
  try {
14733
15050
  const { salt, ciphertext, iv, tag } = this.readRaw();
14734
15051
  const useSalt = knownSalt ?? salt;
@@ -14750,8 +15067,8 @@ var Keystore = class {
14750
15067
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
14751
15068
  const tag = cipher.getAuthTag();
14752
15069
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
14753
- fs26.mkdirSync(path27.dirname(this.storePath), { recursive: true });
14754
- fs26.writeFileSync(this.storePath, out, { mode: 384 });
15070
+ fs27.mkdirSync(path28.dirname(this.storePath), { recursive: true });
15071
+ fs27.writeFileSync(this.storePath, out, { mode: 384 });
14755
15072
  }
14756
15073
  writeWithSalt(data, salt) {
14757
15074
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -14761,11 +15078,11 @@ var Keystore = class {
14761
15078
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
14762
15079
  const tag = cipher.getAuthTag();
14763
15080
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
14764
- fs26.mkdirSync(path27.dirname(this.storePath), { recursive: true });
14765
- fs26.writeFileSync(this.storePath, out, { mode: 384 });
15081
+ fs27.mkdirSync(path28.dirname(this.storePath), { recursive: true });
15082
+ fs27.writeFileSync(this.storePath, out, { mode: 384 });
14766
15083
  }
14767
15084
  readRaw() {
14768
- const buf = fs26.readFileSync(this.storePath);
15085
+ const buf = fs27.readFileSync(this.storePath);
14769
15086
  let offset = 0;
14770
15087
  const salt = buf.subarray(offset, offset + SALT_LEN);
14771
15088
  offset += SALT_LEN;
@@ -14798,7 +15115,7 @@ var CascadeIgnore = class {
14798
15115
  ]);
14799
15116
  }
14800
15117
  async load(workspacePath) {
14801
- const filePath = path27.join(workspacePath, ".cascadeignore");
15118
+ const filePath = path28.join(workspacePath, ".cascadeignore");
14802
15119
  try {
14803
15120
  const content = await fs11.readFile(filePath, "utf-8");
14804
15121
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -14809,7 +15126,7 @@ var CascadeIgnore = class {
14809
15126
  }
14810
15127
  isIgnored(filePath, workspacePath) {
14811
15128
  try {
14812
- const relative = workspacePath ? path27.relative(workspacePath, filePath) : filePath;
15129
+ const relative = workspacePath ? path28.relative(workspacePath, filePath) : filePath;
14813
15130
  return this.ig.ignores(relative);
14814
15131
  } catch {
14815
15132
  return false;
@@ -14820,7 +15137,7 @@ var CascadeIgnore = class {
14820
15137
  }
14821
15138
  };
14822
15139
  async function loadCascadeMd(workspacePath) {
14823
- const filePath = path27.join(workspacePath, "CASCADE.md");
15140
+ const filePath = path28.join(workspacePath, "CASCADE.md");
14824
15141
  try {
14825
15142
  const raw = await fs11.readFile(filePath, "utf-8");
14826
15143
  return parseCascadeMd(raw);
@@ -14851,7 +15168,7 @@ ${raw.trim()}`;
14851
15168
  var MemoryStore = class _MemoryStore {
14852
15169
  db;
14853
15170
  constructor(dbPath) {
14854
- fs26.mkdirSync(path27.dirname(dbPath), { recursive: true });
15171
+ fs27.mkdirSync(path28.dirname(dbPath), { recursive: true });
14855
15172
  try {
14856
15173
  this.db = new Database2(dbPath, { timeout: 5e3 });
14857
15174
  this.db.pragma("journal_mode = WAL");
@@ -15749,13 +16066,38 @@ var WebSearchConfigSchema = z.object({
15749
16066
  /** Tavily API key — get one at https://tavily.com */
15750
16067
  tavilyApiKey: z.string().optional(),
15751
16068
  /** Max results per search (default 5) */
15752
- maxResults: z.number().default(5)
16069
+ maxResults: z.number().default(5),
16070
+ /**
16071
+ * Send `searxngUrl` through the SSRF guard. Off for a local run, where the
16072
+ * URL is the machine owner's own config and a self-hosted SearXNG is
16073
+ * routinely on a private address; on for a host that accepts it from a
16074
+ * request. See `src/tools/web-search.ts`.
16075
+ *
16076
+ * It has to be named HERE, not just on the TypeScript interfaces: the config
16077
+ * passes through `CascadeConfigSchema.parse()` on its way into a run, and
16078
+ * zod drops every key this object does not declare.
16079
+ */
16080
+ guardSearxngUrl: z.boolean().optional()
15753
16081
  });
15754
16082
  var ToolsConfigSchema = z.object({
15755
16083
  shellAllowlist: z.array(z.string()).default([]),
15756
16084
  shellBlocklist: z.array(z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
15757
16085
  requireApprovalFor: z.array(z.string()).default([]),
15758
16086
  browserEnabled: z.boolean().default(false),
16087
+ /**
16088
+ * Let a run ACT on the page open in the desktop's built-in browser — click,
16089
+ * type, submit — not just read it.
16090
+ *
16091
+ * Distinct from `browserEnabled`, which turns on the headless Playwright
16092
+ * tool. That one drives a throwaway browser with an empty cookie jar; this
16093
+ * one drives the session the user is signed into, which is a different order
16094
+ * of consequence and so gets its own switch rather than riding on that one.
16095
+ *
16096
+ * Off by default. Turning it on does not hand anything over silently: the
16097
+ * tool still goes through the approval chain, and the browser panel carries a
16098
+ * stop control for the run that is acting.
16099
+ */
16100
+ agentBrowserControl: z.boolean().default(false),
15759
16101
  mcpServers: z.array(McpServerConfigSchema).optional(),
15760
16102
  mcpTrusted: z.array(z.string()).optional(),
15761
16103
  /** Web search backends — at least one should be configured for best results */
@@ -16153,7 +16495,7 @@ function credentialEndpointsConflict(type, rowEndpoint, credentialEndpoint) {
16153
16495
 
16154
16496
  // src/config/global-credentials.ts
16155
16497
  function credentialsPath(globalDir2) {
16156
- return path27.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
16498
+ return path28.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
16157
16499
  }
16158
16500
  function providerKey(p) {
16159
16501
  return p.type;
@@ -16183,7 +16525,7 @@ function isPersistable(p) {
16183
16525
  }
16184
16526
  function loadGlobalCredentials(globalDir2) {
16185
16527
  try {
16186
- const raw = fs26.readFileSync(credentialsPath(globalDir2), "utf-8");
16528
+ const raw = fs27.readFileSync(credentialsPath(globalDir2), "utf-8");
16187
16529
  const parsed = JSON.parse(raw);
16188
16530
  if (!Array.isArray(parsed.providers)) return [];
16189
16531
  return parsed.providers.filter(
@@ -16195,13 +16537,13 @@ function loadGlobalCredentials(globalDir2) {
16195
16537
  }
16196
16538
  function saveGlobalCredentials(globalDir2, providers) {
16197
16539
  const filePath = credentialsPath(globalDir2);
16198
- fs26.mkdirSync(globalDir2, { recursive: true, mode: 448 });
16540
+ fs27.mkdirSync(globalDir2, { recursive: true, mode: 448 });
16199
16541
  const body = { version: 1, providers: providers.filter(isPersistable) };
16200
16542
  const tmp = `${filePath}.tmp`;
16201
- fs26.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
16202
- fs26.renameSync(tmp, filePath);
16543
+ fs27.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
16544
+ fs27.renameSync(tmp, filePath);
16203
16545
  try {
16204
- fs26.chmodSync(filePath, 384);
16546
+ fs27.chmodSync(filePath, 384);
16205
16547
  } catch {
16206
16548
  }
16207
16549
  }
@@ -16602,7 +16944,7 @@ var ConfigManager = class {
16602
16944
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
16603
16945
  constructor(workspacePath = process.cwd(), globalDirOverride) {
16604
16946
  this.workspacePath = workspacePath;
16605
- this.globalDir = globalDirOverride ?? path27.join(os6.homedir(), GLOBAL_CONFIG_DIR);
16947
+ this.globalDir = globalDirOverride ?? path28.join(os6.homedir(), GLOBAL_CONFIG_DIR);
16606
16948
  }
16607
16949
  async load() {
16608
16950
  this.config = await this.loadConfig();
@@ -16616,8 +16958,8 @@ var ConfigManager = class {
16616
16958
  this.ignore = new CascadeIgnore();
16617
16959
  await this.ignore.load(this.workspacePath);
16618
16960
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
16619
- this.keystore = new Keystore(path27.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
16620
- this.store = new MemoryStore(path27.join(this.workspacePath, CASCADE_DB_FILE));
16961
+ this.keystore = new Keystore(path28.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
16962
+ this.store = new MemoryStore(path28.join(this.workspacePath, CASCADE_DB_FILE));
16621
16963
  const globalRevoked = stripRevokedCredentials(loadGlobalCredentials(this.globalDir));
16622
16964
  const globalCreds = filterRetiredCredentials(globalRevoked.kept);
16623
16965
  if (globalRevoked.removed > 0) this.revokedCredentials += globalRevoked.removed;
@@ -16690,8 +17032,8 @@ var ConfigManager = class {
16690
17032
  * never reached the disk (see `commitSettings`).
16691
17033
  */
16692
17034
  async save(config = this.config) {
16693
- const configPath = path27.join(this.workspacePath, CASCADE_CONFIG_FILE);
16694
- await fs11.mkdir(path27.dirname(configPath), { recursive: true });
17035
+ const configPath = path28.join(this.workspacePath, CASCADE_CONFIG_FILE);
17036
+ await fs11.mkdir(path28.dirname(configPath), { recursive: true });
16695
17037
  await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
16696
17038
  try {
16697
17039
  saveGlobalCredentials(this.globalDir, config.providers);
@@ -16741,7 +17083,7 @@ var ConfigManager = class {
16741
17083
  this.revokedCredentials = 0;
16742
17084
  this.revokedPins = [];
16743
17085
  this.revokedNotice = void 0;
16744
- const configPath = path27.join(this.workspacePath, CASCADE_CONFIG_FILE);
17086
+ const configPath = path28.join(this.workspacePath, CASCADE_CONFIG_FILE);
16745
17087
  try {
16746
17088
  const raw = await fs11.readFile(configPath, "utf-8");
16747
17089
  const parsed = JSON.parse(raw);
@@ -16835,7 +17177,7 @@ var ConfigManager = class {
16835
17177
  return onResource;
16836
17178
  }
16837
17179
  async persistClearedPins(tiers) {
16838
- const configPath = path27.join(this.workspacePath, CASCADE_CONFIG_FILE);
17180
+ const configPath = path28.join(this.workspacePath, CASCADE_CONFIG_FILE);
16839
17181
  try {
16840
17182
  const raw = JSON.parse(await fs11.readFile(configPath, "utf-8"));
16841
17183
  const models = raw["models"];
@@ -17429,7 +17771,7 @@ function computeDelegationSavings(stats, t1Model) {
17429
17771
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
17430
17772
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
17431
17773
  var FETCH_TIMEOUT_MS = 8e3;
17432
- var DEFAULT_CACHE_FILE = path27.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
17774
+ var DEFAULT_CACHE_FILE = path28.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
17433
17775
  function normalizeModelId2(id) {
17434
17776
  let s = id.toLowerCase();
17435
17777
  const slash = s.lastIndexOf("/");
@@ -17576,7 +17918,7 @@ var LiveDataProvider = class {
17576
17918
  }
17577
17919
  async saveCache() {
17578
17920
  try {
17579
- await fs11.mkdir(path27.dirname(this.opts.cacheFile), { recursive: true });
17921
+ await fs11.mkdir(path28.dirname(this.opts.cacheFile), { recursive: true });
17580
17922
  const cache = {
17581
17923
  fetchedAt: this.fetchedAt,
17582
17924
  snapshot: this.snapshot ?? void 0,
@@ -17683,9 +18025,15 @@ var FAMILY_BENCHMARKS = {
17683
18025
  "claude-opus": { code: 95, analysis: 92, creative: 90, data: 88 },
17684
18026
  "claude-sonnet": { code: 93, analysis: 88, creative: 87, data: 85 },
17685
18027
  "claude-haiku": { code: 80, analysis: 75, creative: 76, data: 72 },
17686
- // OpenAI GPT-5 family — current flagship; strongest all-round. Offline
17687
- // baseline (the live fetch refreshes these). Distinct point releases carry
17688
- // their own scores: gpt-5.5 "Spud" leads SWE-bench (88.7%); gpt-5.4 ~80%.
18028
+ // OpenAI GPT-5 family. GPT-5.6 is split by durable capability tier instead
18029
+ // of being folded into generic gpt-5: Sol, Terra and Luna have materially
18030
+ // different benchmark/cost envelopes and should compete independently.
18031
+ // Scores are the committed normalized snapshot; provenance is in
18032
+ // scripts/benchmarks/sources/openai-gpt-5.6-2026-08.json.
18033
+ "gpt-5.6-sol": { code: 100, analysis: 98, creative: 88, data: 88 },
18034
+ "gpt-5.6-terra": { code: 97, analysis: 92, creative: 82, data: 85 },
18035
+ "gpt-5.6-luna": { code: 93, analysis: 85, creative: 74, data: 60 },
18036
+ // Older GPT-5 point releases remain distinct where public data supports it.
17689
18037
  "gpt-5.5": { code: 97, analysis: 96, creative: 94, data: 95 },
17690
18038
  "gpt-5.4": { code: 94, analysis: 94, creative: 92, data: 92 },
17691
18039
  "gpt-5.4-mini": { code: 86, analysis: 85, creative: 85, data: 83 },
@@ -17721,13 +18069,19 @@ var FAMILY_BENCHMARKS = {
17721
18069
  "llama-small": { code: 55, analysis: 56, creative: 60, data: 54 }
17722
18070
  };
17723
18071
  var FAMILY_MATCHERS = [
18072
+ [/fable/i, "claude-opus"],
17724
18073
  [/opus/i, "claude-opus"],
17725
18074
  [/sonnet/i, "claude-sonnet"],
17726
18075
  [/haiku/i, "claude-haiku"],
17727
- // GPT-5 family — ordered most-specific first. Distinct point releases
17728
- // (gpt-5.5, gpt-5.4, gpt-5.4-mini) match their own family; nano/mini before
17729
- // the base; unrecognised gpt-5.x still fold into the gpt-5 base.
18076
+ // GPT-5 family — ordered most-specific first. GPT-5.6 durable tiers carry
18077
+ // their own profiles. 5.4 Nano intentionally maps to the nano class rather
18078
+ // than being swallowed by the generic 5.4 rule.
18079
+ [/gpt-?5\.6.*sol/i, "gpt-5.6-sol"],
18080
+ [/gpt-?5\.6.*terra/i, "gpt-5.6-terra"],
18081
+ [/gpt-?5\.6.*luna/i, "gpt-5.6-luna"],
18082
+ [/gpt-?5\.6(?:\s|$)/i, "gpt-5.6-sol"],
17730
18083
  [/gpt-?5\.5/i, "gpt-5.5"],
18084
+ [/gpt-?5\.4.*nano/i, "gpt-5-nano"],
17731
18085
  [/gpt-?5\.4.*mini/i, "gpt-5.4-mini"],
17732
18086
  [/gpt-?5\.4/i, "gpt-5.4"],
17733
18087
  [/gpt-?5.*nano/i, "gpt-5-nano"],
@@ -20528,6 +20882,23 @@ Now execute your subtask using this context where relevant.`
20528
20882
  this.setStatus("FAILED", finalOutput);
20529
20883
  this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput, "FAILED");
20530
20884
  return this.buildResult("ESCALATED", finalOutput, { checksRun, passed, failed }, issues, correctionAttempts);
20885
+ } finally {
20886
+ this.releaseBrowserControl();
20887
+ }
20888
+ }
20889
+ /**
20890
+ * End this worker's hold on the shared browser.
20891
+ *
20892
+ * Duck-typed rather than imported: `browser_control` is registered only by a
20893
+ * host that HAS a browser (the desktop), so in the CLI and hosted runs there
20894
+ * is no such tool and this is a no-op. Importing the class to instanceof-check
20895
+ * it would couple the worker to a tool that usually is not there.
20896
+ */
20897
+ releaseBrowserControl() {
20898
+ try {
20899
+ const tool = this.toolRegistry.getTool?.("browser_control");
20900
+ tool?.releaseActor?.(this.id);
20901
+ } catch {
20531
20902
  }
20532
20903
  }
20533
20904
  sendToPeer(toId, content) {
@@ -20786,8 +21157,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
20786
21157
  // Media generation can run for a minute; without this a cancelled run
20787
21158
  // still pays for an image nobody will see.
20788
21159
  ...this.signal ? { signal: this.signal } : {},
20789
- saveSnapshot: async (path29, content) => {
20790
- this.store?.addFileSnapshot(this.taskId, path29, content);
21160
+ saveSnapshot: async (path30, content) => {
21161
+ this.store?.addFileSnapshot(this.taskId, path30, content);
20791
21162
  },
20792
21163
  sendPeerSync: (to, syncType, content) => {
20793
21164
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -20990,7 +21361,7 @@ ${assignment.expectedOutput}`;
20990
21361
  const { promisify: promisify4 } = await import('util');
20991
21362
  const execFileAsync3 = promisify4(execFile4);
20992
21363
  for (const artifactPath of artifactPaths) {
20993
- const absolutePath = path27.resolve(this.artifactRoot(), artifactPath);
21364
+ const absolutePath = path28.resolve(this.artifactRoot(), artifactPath);
20994
21365
  try {
20995
21366
  const stat = await fs11.stat(absolutePath);
20996
21367
  if (!stat.isFile()) {
@@ -21025,7 +21396,7 @@ ${assignment.expectedOutput}`;
21025
21396
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
21026
21397
  continue;
21027
21398
  }
21028
- const ext = path27.extname(absolutePath).toLowerCase();
21399
+ const ext = path28.extname(absolutePath).toLowerCase();
21029
21400
  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;
21030
21401
  if (check) {
21031
21402
  try {
@@ -21117,7 +21488,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
21117
21488
  async checkAcceptance(assignment) {
21118
21489
  const criteria = assignment.acceptance ?? [];
21119
21490
  if (!criteria.length) return [];
21120
- const resolve2 = (target) => path27.resolve(this.artifactRoot(), target);
21491
+ const resolve2 = (target) => path28.resolve(this.artifactRoot(), target);
21121
21492
  const workerCanWriteFiles = canProduceFiles(this.tools.map((t) => t.name));
21122
21493
  return evaluateAcceptance(criteria, assignment.files ?? [], {
21123
21494
  stat: async (target) => {
@@ -24421,16 +24792,16 @@ function resolveInWorkspace(workspaceRoot, input) {
24421
24792
  if (typeof input !== "string" || input.length === 0) {
24422
24793
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
24423
24794
  }
24424
- const root = path27.resolve(workspaceRoot);
24425
- const abs = path27.isAbsolute(input) ? path27.resolve(input) : path27.resolve(root, input);
24426
- const rel = path27.relative(root, abs);
24427
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path27.isAbsolute(rel)) {
24795
+ const root = path28.resolve(workspaceRoot);
24796
+ const abs = path28.isAbsolute(input) ? path28.resolve(input) : path28.resolve(root, input);
24797
+ const rel = path28.relative(root, abs);
24798
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path28.isAbsolute(rel)) {
24428
24799
  throw new WorkspaceSandboxError(input, root);
24429
24800
  }
24430
24801
  try {
24431
- const real = fs26.realpathSync(abs);
24432
- const realRel = path27.relative(root, real);
24433
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path27.isAbsolute(realRel))) {
24802
+ const real = fs27.realpathSync(abs);
24803
+ const realRel = path28.relative(root, real);
24804
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path28.isAbsolute(realRel))) {
24434
24805
  throw new WorkspaceSandboxError(input, root);
24435
24806
  }
24436
24807
  } catch (e) {
@@ -24979,9 +25350,9 @@ async function animatePptx(bytes, scheme) {
24979
25350
  const zip = await import_jszip.default.loadAsync(bytes);
24980
25351
  const slidePaths = Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n));
24981
25352
  if (slidePaths.length === 0) return bytes;
24982
- for (const path29 of slidePaths) {
24983
- const xml = await zip.file(path29).async("string");
24984
- zip.file(path29, animateSlideXml(xml, scheme));
25353
+ for (const path30 of slidePaths) {
25354
+ const xml = await zip.file(path30).async("string");
25355
+ zip.file(path30, animateSlideXml(xml, scheme));
24985
25356
  }
24986
25357
  return await zip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
24987
25358
  } catch {
@@ -25367,7 +25738,7 @@ var GenerateDocumentTool = class extends BaseTool {
25367
25738
  }
25368
25739
  }
25369
25740
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
25370
- await fs11.mkdir(path27.dirname(absPath), { recursive: true });
25741
+ await fs11.mkdir(path28.dirname(absPath), { recursive: true });
25371
25742
  await fs11.writeFile(absPath, bytes);
25372
25743
  const charts = countCharts(content);
25373
25744
  return [
@@ -25585,7 +25956,7 @@ var FileWriteTool = class extends BaseTool {
25585
25956
  } catch {
25586
25957
  }
25587
25958
  }
25588
- await fs11.mkdir(path27.dirname(absPath), { recursive: true });
25959
+ await fs11.mkdir(path28.dirname(absPath), { recursive: true });
25589
25960
  await fs11.writeFile(absPath, content, "utf-8");
25590
25961
  return `Written ${content.length} characters to ${filePath}`;
25591
25962
  }
@@ -25943,12 +26314,10 @@ ${data.description}`;
25943
26314
  }
25944
26315
  }
25945
26316
  };
25946
-
25947
- // src/tools/browser.ts
25948
26317
  var BROWSER_LAUNCH_TIMEOUT_MS = 15e3;
25949
26318
  var BrowserTool = class extends BaseTool {
25950
26319
  name = "browser";
25951
- description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Only available with multimodal models.";
26320
+ description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Screenshots are saved to a file for image_analyze to read.";
25952
26321
  inputSchema = {
25953
26322
  type: "object",
25954
26323
  properties: {
@@ -26015,7 +26384,14 @@ var BrowserTool = class extends BaseTool {
26015
26384
  }
26016
26385
  case "screenshot": {
26017
26386
  const buf = await page2.screenshot({ type: "png" });
26018
- return `data:image/png;base64,${buf.toString("base64")}`;
26387
+ const rel = path28.join(".cascade", "screenshots", `screenshot-${Date.now()}-${randomUUID().slice(0, 8)}.png`);
26388
+ const abs = resolveInWorkspace(this.workspaceRoot, rel);
26389
+ await fs11.mkdir(path28.dirname(abs), { recursive: true });
26390
+ await fs11.writeFile(abs, buf);
26391
+ return [
26392
+ `Screenshot saved to ${abs} (${Math.round(buf.byteLength / 1024)} KB).`,
26393
+ `Call image_analyze with that path to see what is on it \u2014 that needs a vision-capable model.`
26394
+ ].join(" ");
26019
26395
  }
26020
26396
  case "evaluate": {
26021
26397
  const result = await page2.evaluate(input["script"]);
@@ -26083,7 +26459,7 @@ var ImageAnalyzeTool = class extends BaseTool {
26083
26459
  };
26084
26460
  async function fileToImageAttachment(filePath) {
26085
26461
  const data = await fs11.readFile(filePath);
26086
- const ext = path27.extname(filePath).toLowerCase();
26462
+ const ext = path28.extname(filePath).toLowerCase();
26087
26463
  const mimeMap = {
26088
26464
  ".jpg": "image/jpeg",
26089
26465
  ".jpeg": "image/jpeg",
@@ -26117,14 +26493,14 @@ var PDFCreateTool = class extends BaseTool {
26117
26493
  const filePath = input["path"];
26118
26494
  const content = input["content"];
26119
26495
  const title = input["title"];
26120
- const dir = path27.dirname(filePath);
26121
- if (!fs26.existsSync(dir)) {
26122
- fs26.mkdirSync(dir, { recursive: true });
26496
+ const dir = path28.dirname(filePath);
26497
+ if (!fs27.existsSync(dir)) {
26498
+ fs27.mkdirSync(dir, { recursive: true });
26123
26499
  }
26124
26500
  return new Promise((resolve2, reject) => {
26125
26501
  try {
26126
26502
  const doc = new PDFDocument({ margin: 50 });
26127
- const stream = fs26.createWriteStream(filePath);
26503
+ const stream = fs27.createWriteStream(filePath);
26128
26504
  doc.pipe(stream);
26129
26505
  if (title) {
26130
26506
  doc.info["Title"] = title;
@@ -26202,22 +26578,22 @@ var CodeInterpreterTool = class extends BaseTool {
26202
26578
  }
26203
26579
  cmdPrefix = NODE_CMD;
26204
26580
  }
26205
- const tmpDir = path27.join(this.workspaceRoot, ".cascade", "tmp");
26206
- if (!fs26.existsSync(tmpDir)) {
26207
- fs26.mkdirSync(tmpDir, { recursive: true });
26581
+ const tmpDir = path28.join(this.workspaceRoot, ".cascade", "tmp");
26582
+ if (!fs27.existsSync(tmpDir)) {
26583
+ fs27.mkdirSync(tmpDir, { recursive: true });
26208
26584
  }
26209
26585
  const extension = language === "python" ? "py" : "js";
26210
26586
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
26211
- const filePath = path27.join(tmpDir, fileName);
26212
- fs26.writeFileSync(filePath, code, "utf-8");
26587
+ const filePath = path28.join(tmpDir, fileName);
26588
+ fs27.writeFileSync(filePath, code, "utf-8");
26213
26589
  const execArgs = [filePath, ...args];
26214
26590
  return new Promise((resolve2) => {
26215
26591
  const startMs = Date.now();
26216
26592
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
26217
26593
  const duration = Date.now() - startMs;
26218
26594
  try {
26219
- if (fs26.existsSync(filePath)) {
26220
- fs26.unlinkSync(filePath);
26595
+ if (fs27.existsSync(filePath)) {
26596
+ fs27.unlinkSync(filePath);
26221
26597
  }
26222
26598
  } catch (cleanupErr) {
26223
26599
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -26297,18 +26673,178 @@ ${formatted}`;
26297
26673
  return `Unknown action: ${action}`;
26298
26674
  }
26299
26675
  };
26676
+ var SsrfBlockedError = class extends Error {
26677
+ constructor(message) {
26678
+ super(message);
26679
+ this.name = "SsrfBlockedError";
26680
+ }
26681
+ };
26682
+ var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
26683
+ var MAX_REDIRECTS2 = 5;
26684
+ function allowLocal() {
26685
+ return process.env["CASCADE_ALLOW_LOCAL_FETCH"] === "1";
26686
+ }
26687
+ function isPrivateAddress(ip) {
26688
+ const type = net.isIP(ip);
26689
+ if (type === 4) return isPrivateIPv4(ip);
26690
+ if (type === 6) return isPrivateIPv6(ip);
26691
+ return true;
26692
+ }
26693
+ function isPrivateIPv4(ip) {
26694
+ const parts = ip.split(".").map((p) => Number(p));
26695
+ if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return true;
26696
+ const [a, b] = parts;
26697
+ if (a === 0) return true;
26698
+ if (a === 10) return true;
26699
+ if (a === 127) return true;
26700
+ if (a === 169 && b === 254) return true;
26701
+ if (a === 172 && b >= 16 && b <= 31) return true;
26702
+ if (a === 192 && b === 168) return true;
26703
+ if (a === 100 && b >= 64 && b <= 127) return true;
26704
+ if (a >= 224) return true;
26705
+ return false;
26706
+ }
26707
+ function ipv6Groups(ip) {
26708
+ let s = ip.toLowerCase().replace(/^\[|\]$/g, "");
26709
+ const zone = s.indexOf("%");
26710
+ if (zone !== -1) s = s.slice(0, zone);
26711
+ const dotted = /^(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
26712
+ if (dotted) {
26713
+ const o = [dotted[2], dotted[3], dotted[4], dotted[5]].map((p) => Number(p));
26714
+ if (o.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
26715
+ s = `${dotted[1]}${(o[0] << 8 | o[1]).toString(16)}:${(o[2] << 8 | o[3]).toString(16)}`;
26716
+ }
26717
+ const halves = s.split("::");
26718
+ if (halves.length > 2) return null;
26719
+ const split = (part) => part ? part.split(":") : [];
26720
+ const head = split(halves[0] ?? "");
26721
+ const tail = halves.length === 2 ? split(halves[1] ?? "") : [];
26722
+ const zeros = halves.length === 2 ? 8 - head.length - tail.length : 0;
26723
+ if (zeros < 0) return null;
26724
+ const groups = [...head, ...Array(zeros).fill("0"), ...tail];
26725
+ if (groups.length !== 8) return null;
26726
+ const out = [];
26727
+ for (const g of groups) {
26728
+ if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
26729
+ out.push(parseInt(g, 16));
26730
+ }
26731
+ return out;
26732
+ }
26733
+ function embeddedIPv4(g) {
26734
+ const quad = (hi, lo) => `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
26735
+ const leadingZero = (n) => g.slice(0, n).every((x) => x === 0);
26736
+ if (leadingZero(5) && g[5] === 65535) return quad(g[6], g[7]);
26737
+ if (leadingZero(6)) return quad(g[6], g[7]);
26738
+ if (g[0] === 100 && g[1] === 65435) {
26739
+ if (g[2] === 1) {
26740
+ return quad(g[3], (g[4] & 255) << 8 | g[5] >> 8);
26741
+ }
26742
+ if (g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0) return quad(g[6], g[7]);
26743
+ return null;
26744
+ }
26745
+ if (g[0] === 8194) return quad(g[1], g[2]);
26746
+ return null;
26747
+ }
26748
+ function isPrivateIPv6(ip) {
26749
+ const g = ipv6Groups(ip);
26750
+ if (!g) return true;
26751
+ const v4 = embeddedIPv4(g);
26752
+ if (v4) return isPrivateIPv4(v4);
26753
+ if (g[0] === 100 && g[1] === 65435) return true;
26754
+ if (g.every((x) => x === 0)) return true;
26755
+ if (leading7Zero(g) && g[7] === 1) return true;
26756
+ if ((g[0] & 65472) === 65152) return true;
26757
+ if ((g[0] & 65024) === 64512) return true;
26758
+ if ((g[0] & 65280) === 65280) return true;
26759
+ return false;
26760
+ }
26761
+ function leading7Zero(g) {
26762
+ return g.slice(0, 7).every((x) => x === 0);
26763
+ }
26764
+ async function assertPublicUrl(rawUrl) {
26765
+ let url;
26766
+ try {
26767
+ url = new URL(rawUrl);
26768
+ } catch {
26769
+ throw new SsrfBlockedError(`Invalid URL: ${rawUrl}`);
26770
+ }
26771
+ if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
26772
+ throw new SsrfBlockedError(`Blocked URL scheme "${url.protocol}" \u2014 only http and https are allowed.`);
26773
+ }
26774
+ if (allowLocal()) return url;
26775
+ const host = url.hostname.replace(/^\[|\]$/g, "");
26776
+ if (net.isIP(host)) {
26777
+ if (isPrivateAddress(host)) {
26778
+ throw new SsrfBlockedError(`Blocked request to non-public address ${host}.`);
26779
+ }
26780
+ return url;
26781
+ }
26782
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
26783
+ throw new SsrfBlockedError(`Blocked request to local hostname "${host}".`);
26784
+ }
26785
+ let addresses;
26786
+ try {
26787
+ const records = await dns2.lookup(host, { all: true });
26788
+ addresses = records.map((r) => r.address);
26789
+ } catch {
26790
+ throw new SsrfBlockedError(`Could not resolve host "${host}".`);
26791
+ }
26792
+ if (addresses.length === 0 || addresses.some((addr) => isPrivateAddress(addr))) {
26793
+ throw new SsrfBlockedError(`Blocked request to "${host}" \u2014 resolves to a non-public address.`);
26794
+ }
26795
+ return url;
26796
+ }
26797
+ var ssrfAgent = new Agent({
26798
+ connect: {
26799
+ lookup(hostname, options, callback) {
26800
+ if (allowLocal()) {
26801
+ dnsCallback.lookup(hostname, options, callback);
26802
+ return;
26803
+ }
26804
+ dnsCallback.lookup(hostname, { ...options, all: true }, (err, addresses) => {
26805
+ if (err) return callback(err, "", 0);
26806
+ const list = Array.isArray(addresses) ? addresses : [addresses];
26807
+ const bad = list.find((a) => isPrivateAddress(a.address));
26808
+ if (bad) {
26809
+ return callback(
26810
+ new SsrfBlockedError(`Blocked connection to "${hostname}" \u2014 resolved to non-public address ${bad.address}.`),
26811
+ "",
26812
+ 0
26813
+ );
26814
+ }
26815
+ if (options.all) return callback(null, list);
26816
+ const first = list[0];
26817
+ return callback(null, first.address, first.family);
26818
+ });
26819
+ }
26820
+ }
26821
+ });
26822
+ async function safeFetch(rawUrl, init = {}) {
26823
+ let currentUrl = (await assertPublicUrl(rawUrl)).toString();
26824
+ for (let i = 0; i <= MAX_REDIRECTS2; i++) {
26825
+ const resp = await fetch(currentUrl, { ...init, redirect: "manual", dispatcher: ssrfAgent });
26826
+ if (resp.status < 300 || resp.status >= 400) return resp;
26827
+ const location = resp.headers.get("location");
26828
+ if (!location) return resp;
26829
+ const next = new URL(location, currentUrl);
26830
+ await assertPublicUrl(next.toString());
26831
+ currentUrl = next.toString();
26832
+ }
26833
+ throw new SsrfBlockedError(`Too many redirects (>${MAX_REDIRECTS2}).`);
26834
+ }
26300
26835
 
26301
26836
  // src/tools/web-search.ts
26302
- async function searchSearXNG(query, baseUrl, maxResults) {
26837
+ async function searchSearXNG(query, baseUrl, maxResults, guardUrl = false) {
26303
26838
  const url = new URL("/search", baseUrl);
26304
26839
  url.searchParams.set("q", query);
26305
26840
  url.searchParams.set("format", "json");
26306
26841
  url.searchParams.set("categories", "general");
26307
26842
  url.searchParams.set("engines", "google,bing,duckduckgo");
26308
- const resp = await fetch(url.toString(), {
26843
+ const init = {
26309
26844
  headers: { "User-Agent": "Cascade-AI/1.0 WebSearchTool" },
26310
26845
  signal: AbortSignal.timeout(1e4)
26311
- });
26846
+ };
26847
+ const resp = guardUrl ? await safeFetch(url.toString(), init) : await fetch(url.toString(), init);
26312
26848
  if (!resp.ok) {
26313
26849
  throw new Error(`SearXNG returned HTTP ${resp.status}`);
26314
26850
  }
@@ -26437,7 +26973,11 @@ var WebSearchTool = class extends BaseTool {
26437
26973
  searxngUrl: config.searxngUrl ?? process.env["SEARXNG_URL"],
26438
26974
  braveApiKey: config.braveApiKey ?? process.env["BRAVE_SEARCH_API_KEY"],
26439
26975
  tavilyApiKey: config.tavilyApiKey ?? process.env["TAVILY_API_KEY"],
26440
- maxResults: config.maxResults ?? 5
26976
+ maxResults: config.maxResults ?? 5,
26977
+ // Note this reads only from `config`, never from the environment: the
26978
+ // guard tracks the trustworthiness of the URL the host passed in, and an
26979
+ // env var says nothing about that.
26980
+ guardSearxngUrl: config.guardSearxngUrl ?? false
26441
26981
  };
26442
26982
  }
26443
26983
  async execute(input, _options) {
@@ -26452,7 +26992,7 @@ var WebSearchTool = class extends BaseTool {
26452
26992
  let anyBackendReachable = false;
26453
26993
  if (this.config.searxngUrl) {
26454
26994
  try {
26455
- results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
26995
+ results = await searchSearXNG(query, this.config.searxngUrl, maxResults, this.config.guardSearxngUrl);
26456
26996
  anyBackendReachable = true;
26457
26997
  if (results.length > 0) return this.formatResults(query, results);
26458
26998
  errors.push("SearXNG: returned 0 results");
@@ -26529,7 +27069,7 @@ var GlobTool = class extends BaseTool {
26529
27069
  };
26530
27070
  async execute(input, _options) {
26531
27071
  const pattern = input["pattern"];
26532
- const searchPath = input["path"] ? path27.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27072
+ const searchPath = input["path"] ? path28.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
26533
27073
  const matches = await glob(pattern, {
26534
27074
  cwd: searchPath,
26535
27075
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -26542,7 +27082,7 @@ var GlobTool = class extends BaseTool {
26542
27082
  const withMtime = await Promise.all(
26543
27083
  matches.map(async (rel) => {
26544
27084
  try {
26545
- const stat = await fs11.stat(path27.join(searchPath, rel));
27085
+ const stat = await fs11.stat(path28.join(searchPath, rel));
26546
27086
  return { rel, mtime: stat.mtimeMs };
26547
27087
  } catch {
26548
27088
  return { rel, mtime: 0 };
@@ -26591,7 +27131,7 @@ var GrepTool = class extends BaseTool {
26591
27131
  };
26592
27132
  async execute(input, _options) {
26593
27133
  const pattern = input["pattern"];
26594
- const searchPath = input["path"] ? path27.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27134
+ const searchPath = input["path"] ? path28.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
26595
27135
  const globPattern = input["glob"];
26596
27136
  const outputMode = input["output_mode"] ?? "content";
26597
27137
  const context = input["context"] ?? 0;
@@ -26645,12 +27185,12 @@ var GrepTool = class extends BaseTool {
26645
27185
  nodir: true
26646
27186
  });
26647
27187
  } catch {
26648
- files = [path27.relative(searchPath, searchPath) || "."];
27188
+ files = [path28.relative(searchPath, searchPath) || "."];
26649
27189
  }
26650
27190
  const results = [];
26651
27191
  let totalCount = 0;
26652
27192
  for (const rel of files) {
26653
- const abs = path27.join(searchPath, rel);
27193
+ const abs = path28.join(searchPath, rel);
26654
27194
  let content;
26655
27195
  try {
26656
27196
  content = await fs11.readFile(abs, "utf-8");
@@ -26691,92 +27231,6 @@ Total: ${totalCount} matches`);
26691
27231
  return results.join("\n");
26692
27232
  }
26693
27233
  };
26694
- var SsrfBlockedError = class extends Error {
26695
- constructor(message) {
26696
- super(message);
26697
- this.name = "SsrfBlockedError";
26698
- }
26699
- };
26700
- var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
26701
- var MAX_REDIRECTS2 = 5;
26702
- function allowLocal() {
26703
- return process.env["CASCADE_ALLOW_LOCAL_FETCH"] === "1";
26704
- }
26705
- function isPrivateAddress(ip) {
26706
- const type = net.isIP(ip);
26707
- if (type === 4) return isPrivateIPv4(ip);
26708
- if (type === 6) return isPrivateIPv6(ip);
26709
- return true;
26710
- }
26711
- function isPrivateIPv4(ip) {
26712
- const parts = ip.split(".").map((p) => Number(p));
26713
- if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return true;
26714
- const [a, b] = parts;
26715
- if (a === 0) return true;
26716
- if (a === 10) return true;
26717
- if (a === 127) return true;
26718
- if (a === 169 && b === 254) return true;
26719
- if (a === 172 && b >= 16 && b <= 31) return true;
26720
- if (a === 192 && b === 168) return true;
26721
- if (a === 100 && b >= 64 && b <= 127) return true;
26722
- if (a >= 224) return true;
26723
- return false;
26724
- }
26725
- function isPrivateIPv6(ip) {
26726
- const lower = ip.toLowerCase().replace(/^\[|\]$/g, "");
26727
- if (lower === "::1" || lower === "::") return true;
26728
- if (lower.startsWith("fe80")) return true;
26729
- if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
26730
- const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower);
26731
- if (mapped) return isPrivateIPv4(mapped[1]);
26732
- return false;
26733
- }
26734
- async function assertPublicUrl(rawUrl) {
26735
- let url;
26736
- try {
26737
- url = new URL(rawUrl);
26738
- } catch {
26739
- throw new SsrfBlockedError(`Invalid URL: ${rawUrl}`);
26740
- }
26741
- if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
26742
- throw new SsrfBlockedError(`Blocked URL scheme "${url.protocol}" \u2014 only http and https are allowed.`);
26743
- }
26744
- if (allowLocal()) return url;
26745
- const host = url.hostname.replace(/^\[|\]$/g, "");
26746
- if (net.isIP(host)) {
26747
- if (isPrivateAddress(host)) {
26748
- throw new SsrfBlockedError(`Blocked request to non-public address ${host}.`);
26749
- }
26750
- return url;
26751
- }
26752
- if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
26753
- throw new SsrfBlockedError(`Blocked request to local hostname "${host}".`);
26754
- }
26755
- let addresses;
26756
- try {
26757
- const records = await dns2.lookup(host, { all: true });
26758
- addresses = records.map((r) => r.address);
26759
- } catch {
26760
- throw new SsrfBlockedError(`Could not resolve host "${host}".`);
26761
- }
26762
- if (addresses.length === 0 || addresses.some((addr) => isPrivateAddress(addr))) {
26763
- throw new SsrfBlockedError(`Blocked request to "${host}" \u2014 resolves to a non-public address.`);
26764
- }
26765
- return url;
26766
- }
26767
- async function safeFetch(rawUrl, init = {}) {
26768
- let currentUrl = (await assertPublicUrl(rawUrl)).toString();
26769
- for (let i = 0; i <= MAX_REDIRECTS2; i++) {
26770
- const resp = await fetch(currentUrl, { ...init, redirect: "manual" });
26771
- if (resp.status < 300 || resp.status >= 400) return resp;
26772
- const location = resp.headers.get("location");
26773
- if (!location) return resp;
26774
- const next = new URL(location, currentUrl);
26775
- await assertPublicUrl(next.toString());
26776
- currentUrl = next.toString();
26777
- }
26778
- throw new SsrfBlockedError(`Too many redirects (>${MAX_REDIRECTS2}).`);
26779
- }
26780
27234
 
26781
27235
  // src/tools/web-fetch.ts
26782
27236
  var MAX_CHARS = 5e4;
@@ -27049,10 +27503,10 @@ var ToolRegistry = class extends EventEmitter {
27049
27503
  }
27050
27504
  isIgnored(filePath) {
27051
27505
  if (!filePath) return false;
27052
- const abs = path27.resolve(this.workspaceRoot, filePath);
27053
- const rel = path27.relative(this.workspaceRoot, abs);
27054
- if (!rel || rel.startsWith("..") || path27.isAbsolute(rel)) return true;
27055
- const posixRel = rel.split(path27.sep).join("/");
27506
+ const abs = path28.resolve(this.workspaceRoot, filePath);
27507
+ const rel = path28.relative(this.workspaceRoot, abs);
27508
+ if (!rel || rel.startsWith("..") || path28.isAbsolute(rel)) return true;
27509
+ const posixRel = rel.split(path28.sep).join("/");
27056
27510
  return this.ignoreMatcher.ignores(posixRel);
27057
27511
  }
27058
27512
  };
@@ -27286,22 +27740,22 @@ var FileMcpOAuthStore = class {
27286
27740
  filePath;
27287
27741
  load() {
27288
27742
  try {
27289
- return JSON.parse(fs26.readFileSync(this.filePath, "utf-8"));
27743
+ return JSON.parse(fs27.readFileSync(this.filePath, "utf-8"));
27290
27744
  } catch {
27291
27745
  return void 0;
27292
27746
  }
27293
27747
  }
27294
27748
  save(state) {
27295
- fs26.mkdirSync(path27.dirname(this.filePath), { recursive: true, mode: 448 });
27296
- fs26.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
27749
+ fs27.mkdirSync(path28.dirname(this.filePath), { recursive: true, mode: 448 });
27750
+ fs27.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
27297
27751
  try {
27298
- fs26.chmodSync(this.filePath, 384);
27752
+ fs27.chmodSync(this.filePath, 384);
27299
27753
  } catch {
27300
27754
  }
27301
27755
  }
27302
27756
  clear() {
27303
27757
  try {
27304
- fs26.rmSync(this.filePath);
27758
+ fs27.rmSync(this.filePath);
27305
27759
  } catch {
27306
27760
  }
27307
27761
  }
@@ -28139,7 +28593,7 @@ var TaskAnalyzer = class {
28139
28593
  analysisCache.clear();
28140
28594
  }
28141
28595
  };
28142
- var DEFAULT_STATS_FILE = path27.join(os6.homedir(), ".cascade", "model-perf.json");
28596
+ var DEFAULT_STATS_FILE = path28.join(os6.homedir(), ".cascade", "model-perf.json");
28143
28597
  var ModelPerformanceTracker = class _ModelPerformanceTracker {
28144
28598
  stats = /* @__PURE__ */ new Map();
28145
28599
  featureStats = /* @__PURE__ */ new Map();
@@ -28179,7 +28633,7 @@ var ModelPerformanceTracker = class _ModelPerformanceTracker {
28179
28633
  async save() {
28180
28634
  if (this.readOnly) return;
28181
28635
  try {
28182
- await fs11.mkdir(path27.dirname(this.statsFile), { recursive: true });
28636
+ await fs11.mkdir(path28.dirname(this.statsFile), { recursive: true });
28183
28637
  const modelsObj = {};
28184
28638
  const featuresObj = {};
28185
28639
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -28812,7 +29266,7 @@ Required capability: ${description.slice(0, 300)}`;
28812
29266
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
28813
29267
  async loadPersistedTools() {
28814
29268
  if (!this.workspacePath || !this.persistEnabled) return;
28815
- const file = path27.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
29269
+ const file = path28.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
28816
29270
  try {
28817
29271
  const raw = await fs11.readFile(file, "utf-8");
28818
29272
  const specs = JSON.parse(raw);
@@ -28836,8 +29290,8 @@ Required capability: ${description.slice(0, 300)}`;
28836
29290
  }
28837
29291
  async persist() {
28838
29292
  if (!this.workspacePath || !this.persistEnabled) return;
28839
- const dir = path27.join(this.workspacePath, ".cascade");
28840
- const file = path27.join(dir, DYNAMIC_TOOLS_FILE);
29293
+ const dir = path28.join(this.workspacePath, ".cascade");
29294
+ const file = path28.join(dir, DYNAMIC_TOOLS_FILE);
28841
29295
  try {
28842
29296
  await fs11.mkdir(dir, { recursive: true });
28843
29297
  await fs11.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -28857,12 +29311,12 @@ var WorldStateDB = class {
28857
29311
  constructor(workspacePath, debugMode = false) {
28858
29312
  this.workspacePath = workspacePath;
28859
29313
  this.debugMode = debugMode;
28860
- const cascadeDir = path27.join(workspacePath, ".cascade");
28861
- if (!fs26.existsSync(cascadeDir)) {
28862
- fs26.mkdirSync(cascadeDir, { recursive: true });
29314
+ const cascadeDir = path28.join(workspacePath, ".cascade");
29315
+ if (!fs27.existsSync(cascadeDir)) {
29316
+ fs27.mkdirSync(cascadeDir, { recursive: true });
28863
29317
  }
28864
- this.keyPath = path27.join(cascadeDir, "world_state.key");
28865
- this.dbPath = path27.join(cascadeDir, "world_state.db");
29318
+ this.keyPath = path28.join(cascadeDir, "world_state.key");
29319
+ this.dbPath = path28.join(cascadeDir, "world_state.db");
28866
29320
  this.initEncryptionKey();
28867
29321
  this.db = new Database2(this.dbPath);
28868
29322
  this.db.pragma("journal_mode = WAL");
@@ -28919,11 +29373,11 @@ var WorldStateDB = class {
28919
29373
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
28920
29374
  }
28921
29375
  initEncryptionKey() {
28922
- if (fs26.existsSync(this.keyPath)) {
28923
- this.encryptionKey = fs26.readFileSync(this.keyPath);
29376
+ if (fs27.existsSync(this.keyPath)) {
29377
+ this.encryptionKey = fs27.readFileSync(this.keyPath);
28924
29378
  } else {
28925
29379
  this.encryptionKey = crypto.randomBytes(32);
28926
- fs26.writeFileSync(this.keyPath, this.encryptionKey);
29380
+ fs27.writeFileSync(this.keyPath, this.encryptionKey);
28927
29381
  }
28928
29382
  }
28929
29383
  encrypt(text) {
@@ -29209,9 +29663,9 @@ var WorldStateDB = class {
29209
29663
  dumpDebugIfNeeded() {
29210
29664
  if (!this.debugMode) return;
29211
29665
  try {
29212
- const dumpPath = path27.join(os6.tmpdir(), "cascade_world_state_debug.json");
29666
+ const dumpPath = path28.join(os6.tmpdir(), "cascade_world_state_debug.json");
29213
29667
  const entries = this.getAllEntries();
29214
- fs26.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
29668
+ fs27.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
29215
29669
  } catch (err) {
29216
29670
  console.error("Failed to dump debug world state", err);
29217
29671
  }
@@ -29268,7 +29722,7 @@ var ResumeStore = class {
29268
29722
  createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
29269
29723
  };
29270
29724
  await promises.mkdir(this.dir, { recursive: true });
29271
- const finalPath = path27.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
29725
+ const finalPath = path28.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
29272
29726
  const tempPath = `${finalPath}.${process.pid}.tmp`;
29273
29727
  await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
29274
29728
  await promises.rename(tempPath, finalPath);
@@ -29291,7 +29745,7 @@ var ResumeStore = class {
29291
29745
  const checkpoints = [];
29292
29746
  for (const candidate of candidates) {
29293
29747
  try {
29294
- const raw = await promises.readFile(path27.join(this.dir, candidate.name), "utf-8");
29748
+ const raw = await promises.readFile(path28.join(this.dir, candidate.name), "utf-8");
29295
29749
  const parsed = JSON.parse(raw);
29296
29750
  if (isCheckpoint(parsed)) checkpoints.push(parsed);
29297
29751
  } catch {
@@ -29330,7 +29784,7 @@ var ResumeStore = class {
29330
29784
  const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
29331
29785
  if (!source) return null;
29332
29786
  const claimId = `${this.now()}-${safeId(target.taskId)}`;
29333
- await promises.rename(path27.join(this.dir, source), path27.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
29787
+ await promises.rename(path28.join(this.dir, source), path28.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
29334
29788
  return { checkpoint: target, claimId };
29335
29789
  } catch {
29336
29790
  return null;
@@ -29340,8 +29794,8 @@ var ResumeStore = class {
29340
29794
  async release(claimId) {
29341
29795
  try {
29342
29796
  await promises.rename(
29343
- path27.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
29344
- path27.join(this.dir, `${claimId}.json`)
29797
+ path28.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
29798
+ path28.join(this.dir, `${claimId}.json`)
29345
29799
  );
29346
29800
  } catch {
29347
29801
  }
@@ -29349,7 +29803,7 @@ var ResumeStore = class {
29349
29803
  /** Discard a claim for good — the resumed run has taken ownership. */
29350
29804
  async settle(claimId) {
29351
29805
  try {
29352
- await promises.rm(path27.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
29806
+ await promises.rm(path28.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
29353
29807
  } catch {
29354
29808
  }
29355
29809
  }
@@ -29378,8 +29832,8 @@ var ResumeStore = class {
29378
29832
  const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
29379
29833
  if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
29380
29834
  await promises.rename(
29381
- path27.join(this.dir, name),
29382
- path27.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
29835
+ path28.join(this.dir, name),
29836
+ path28.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
29383
29837
  );
29384
29838
  reclaimed++;
29385
29839
  }
@@ -29395,7 +29849,7 @@ var ResumeStore = class {
29395
29849
  await Promise.all(entries.map(async (name) => {
29396
29850
  const match = FILE_PATTERN.exec(name);
29397
29851
  if (match?.[2] === wanted) {
29398
- await promises.rm(path27.join(this.dir, name), { force: true });
29852
+ await promises.rm(path28.join(this.dir, name), { force: true });
29399
29853
  }
29400
29854
  }));
29401
29855
  } catch {
@@ -29410,7 +29864,7 @@ var ResumeStore = class {
29410
29864
  const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
29411
29865
  const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
29412
29866
  await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
29413
- (name) => promises.rm(path27.join(this.dir, name), { force: true })
29867
+ (name) => promises.rm(path28.join(this.dir, name), { force: true })
29414
29868
  ));
29415
29869
  } catch {
29416
29870
  }
@@ -29787,7 +30241,7 @@ var WorkspaceIndex = class {
29787
30241
  isIgnored;
29788
30242
  chunker;
29789
30243
  constructor(opts) {
29790
- this.root = path27.resolve(opts.root);
30244
+ this.root = path28.resolve(opts.root);
29791
30245
  this.db = opts.db;
29792
30246
  this.namespace = opts.namespace ?? "code";
29793
30247
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -29819,14 +30273,14 @@ var WorkspaceIndex = class {
29819
30273
  return;
29820
30274
  }
29821
30275
  for (const entry of entries) {
29822
- const abs = path27.join(dir, entry.name);
30276
+ const abs = path28.join(dir, entry.name);
29823
30277
  if (entry.isDirectory()) {
29824
30278
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
29825
30279
  await walk(abs);
29826
30280
  } else if (entry.isFile()) {
29827
- const ext = path27.extname(entry.name).slice(1).toLowerCase();
30281
+ const ext = path28.extname(entry.name).slice(1).toLowerCase();
29828
30282
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
29829
- out.push(path27.relative(this.root, abs));
30283
+ out.push(path28.relative(this.root, abs));
29830
30284
  }
29831
30285
  }
29832
30286
  };
@@ -29856,7 +30310,7 @@ var WorkspaceIndex = class {
29856
30310
  const entries = [];
29857
30311
  for (const rel of rels) {
29858
30312
  try {
29859
- const abs = path27.join(this.root, rel);
30313
+ const abs = path28.join(this.root, rel);
29860
30314
  const stat = await fs11.stat(abs);
29861
30315
  if (stat.size > this.maxFileBytes) continue;
29862
30316
  const buf = await fs11.readFile(abs);
@@ -30261,6 +30715,127 @@ var CurrentPageTool = class extends BaseTool {
30261
30715
  }
30262
30716
  };
30263
30717
 
30718
+ // src/tools/browser-control.ts
30719
+ var BrowserControlTool = class extends BaseTool {
30720
+ name = "browser_control";
30721
+ 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.";
30722
+ inputSchema = {
30723
+ type: "object",
30724
+ properties: {
30725
+ action: {
30726
+ type: "string",
30727
+ enum: ["navigate", "click", "fill", "press", "wait_for", "extract_text"],
30728
+ description: "What to do."
30729
+ },
30730
+ url: { type: "string", description: "For navigate: the http(s) address to open." },
30731
+ selector: {
30732
+ type: "string",
30733
+ description: "CSS selector for click, fill and wait_for. Optional for extract_text (defaults to the whole page)."
30734
+ },
30735
+ value: { type: "string", description: "For fill: the text to type." },
30736
+ key: { type: "string", description: 'For press: a key name such as "Enter", "Tab" or "Escape".' },
30737
+ timeoutMs: { type: "number", description: "For wait_for: how long to wait before giving up (default 10000, max 30000)." }
30738
+ },
30739
+ required: ["action"]
30740
+ };
30741
+ controller;
30742
+ release;
30743
+ /** One-way. See `revoke`. */
30744
+ revoked = false;
30745
+ constructor(controller, release) {
30746
+ super();
30747
+ this.controller = controller;
30748
+ this.release = release;
30749
+ }
30750
+ /**
30751
+ * A worker has finished. Ends its hold on the browser.
30752
+ *
30753
+ * Called from the tier's terminal path rather than by the model, because a
30754
+ * model cannot be relied on to announce that it is done — which is why the
30755
+ * host cannot use idle time as the boundary instead.
30756
+ */
30757
+ releaseActor(actorId) {
30758
+ this.release?.(actorId);
30759
+ }
30760
+ /**
30761
+ * Permanently disable this tool, whatever the registry still holds.
30762
+ *
30763
+ * Registration-time gates cannot express "this run turned out to be
30764
+ * unattended": a host that wires the controller and only then declares the
30765
+ * run unattended has already registered the tool, and a check that ran at
30766
+ * registration has no second chance. Enforcing at EXECUTION time makes the
30767
+ * guarantee independent of call order, which is the whole point of stating
30768
+ * the invariant rather than relying on the scheduler's current sequence.
30769
+ *
30770
+ * One-way on purpose. A safety gate that can be re-opened by a later call is
30771
+ * a gate that a refactor re-opens by accident.
30772
+ */
30773
+ revoke() {
30774
+ this.revoked = true;
30775
+ }
30776
+ // Acting on a session the user is signed into is the highest-consequence
30777
+ // thing in the tool set — higher than shell, arguably, because the blast
30778
+ // radius is someone else's server rather than this machine.
30779
+ isDangerous() {
30780
+ return true;
30781
+ }
30782
+ async execute(input, options) {
30783
+ if (this.revoked) {
30784
+ return "Error: browser control is not available in this run. It requires a person watching who can stop it, and nobody is.";
30785
+ }
30786
+ const kind = input["action"];
30787
+ if (!kind) return "Error: action is required.";
30788
+ if (options?.signal?.aborted) return "Error: the run was cancelled before this action ran.";
30789
+ const missing = requiredFieldFor(kind, input);
30790
+ if (missing) return `Error: "${kind}" needs ${missing}.`;
30791
+ const action = {
30792
+ kind,
30793
+ ...typeof input["url"] === "string" ? { url: input["url"] } : {},
30794
+ ...typeof input["selector"] === "string" ? { selector: input["selector"] } : {},
30795
+ ...typeof input["value"] === "string" ? { value: input["value"] } : {},
30796
+ ...typeof input["key"] === "string" ? { key: input["key"] } : {},
30797
+ ...typeof input["timeoutMs"] === "number" ? { timeoutMs: Math.min(Math.max(input["timeoutMs"], 0), 3e4) } : {}
30798
+ };
30799
+ let outcome;
30800
+ try {
30801
+ outcome = await this.controller(action, {
30802
+ // `sessionId` is the run. The host scopes both revocation and its
30803
+ // single-owner lease on it, so passing it is not telemetry — drop it
30804
+ // and a Stop in one run silently stops every later one.
30805
+ sessionId: options?.sessionId ?? "",
30806
+ actorId: options?.tierId ?? options?.sessionId ?? "",
30807
+ ...options?.signal ? { signal: options.signal } : {}
30808
+ });
30809
+ } catch (err) {
30810
+ return `Error: the browser could not perform "${kind}" \u2014 ${err instanceof Error ? err.message : String(err)}`;
30811
+ }
30812
+ const where = outcome.url ? `
30813
+ Page: ${outcome.title ? `${outcome.title} \u2014 ` : ""}${outcome.url}` : "";
30814
+ return `${outcome.ok ? "" : "Failed: "}${outcome.detail}${where}`;
30815
+ }
30816
+ };
30817
+ function requiredFieldFor(kind, input) {
30818
+ const has = (k) => typeof input[k] === "string" && input[k].length > 0;
30819
+ const given = (k) => typeof input[k] === "string";
30820
+ switch (kind) {
30821
+ case "navigate":
30822
+ return has("url") ? null : "a url";
30823
+ case "click":
30824
+ return has("selector") ? null : "a selector";
30825
+ case "fill":
30826
+ return has("selector") ? given("value") ? null : "a value" : "a selector";
30827
+ case "press":
30828
+ return has("key") ? null : "a key";
30829
+ case "wait_for":
30830
+ return has("selector") ? null : "a selector";
30831
+ // extract_text works with or without a selector.
30832
+ case "extract_text":
30833
+ return null;
30834
+ default:
30835
+ return null;
30836
+ }
30837
+ }
30838
+
30264
30839
  // src/core/cascade.ts
30265
30840
  var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
30266
30841
  function buildContextualPrompt(prompt, history = []) {
@@ -30346,13 +30921,13 @@ var Cascade = class _Cascade extends EventEmitter {
30346
30921
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
30347
30922
  try {
30348
30923
  this.router.setDeadModelStore(new DeadModelStore(
30349
- fileDeadModelPersistence(path27.join(workspacePath, ".cascade", "dead-models.json"))
30924
+ fileDeadModelPersistence(path28.join(workspacePath, ".cascade", "dead-models.json"))
30350
30925
  ));
30351
30926
  } catch {
30352
30927
  }
30353
30928
  try {
30354
30929
  this.resumeStore = new ResumeStore({
30355
- dir: path27.join(workspacePath, ".cascade", "resume")
30930
+ dir: path28.join(workspacePath, ".cascade", "resume")
30356
30931
  });
30357
30932
  } catch {
30358
30933
  }
@@ -30440,8 +31015,8 @@ var Cascade = class _Cascade extends EventEmitter {
30440
31015
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
30441
31016
  return;
30442
31017
  }
30443
- const dbPath = ci.dbPath || path27.join(this.workspacePath, ".cascade", "code-index.db");
30444
- fs26.mkdirSync(path27.dirname(dbPath), { recursive: true });
31018
+ const dbPath = ci.dbPath || path28.join(this.workspacePath, ".cascade", "code-index.db");
31019
+ fs27.mkdirSync(path28.dirname(dbPath), { recursive: true });
30445
31020
  const db = new Database2(dbPath);
30446
31021
  db.pragma("journal_mode = WAL");
30447
31022
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -30753,6 +31328,49 @@ ${input}` }],
30753
31328
  if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
30754
31329
  this.toolRegistry.register(new CurrentPageTool(provider));
30755
31330
  }
31331
+ /**
31332
+ * True when nobody is watching this run — a scheduled/cron task, which
31333
+ * auto-approves every tool because there is no one to ask.
31334
+ *
31335
+ * That auto-approval is fine for ordinary tools and disqualifying for this
31336
+ * one: `browser_control` drives the signed-in browser, and "every run asks
31337
+ * first" is the promise the Settings copy makes. The property used to hold
31338
+ * only because `runScheduledTask` happens not to call `setBrowserController`
31339
+ * — a guarantee resting on an ABSENT call, which is exactly what a later
31340
+ * refactor adds without noticing. Stated and enforced instead.
31341
+ */
31342
+ unattended = false;
31343
+ /**
31344
+ * Declare that no human is watching this run, so capabilities that depend on
31345
+ * a person being able to see and stop them are refused outright.
31346
+ */
31347
+ setUnattended(on) {
31348
+ this.unattended = on;
31349
+ if (!on) return;
31350
+ const tool = this.toolRegistry.getTool("browser_control");
31351
+ tool?.revoke?.();
31352
+ }
31353
+ /**
31354
+ * Wire the host's browser so a run can ACT on the open page, not just read it.
31355
+ *
31356
+ * Two gates before this registers anything, and both matter:
31357
+ *
31358
+ * - `tools.agentBrowserControl` must be on. Unlike read_current_page, which
31359
+ * reaches nothing the user has not already opened themselves, this drives
31360
+ * the session they are signed into. That is opt-in, not a default.
31361
+ * - `disabledTools` still removes it, the same as every other tool.
31362
+ * - The run must not be UNATTENDED. See `setUnattended`.
31363
+ *
31364
+ * Host-supplied like the page provider: the tool exists only where there is a
31365
+ * real browser to drive, so it never has to be refused in the CLI or a hosted
31366
+ * run — it is simply not there.
31367
+ */
31368
+ setBrowserController(controller, release) {
31369
+ if (this.unattended) return;
31370
+ if (!this.config.tools?.agentBrowserControl) return;
31371
+ if ((this.config.tools?.disabledTools ?? []).includes("browser_control")) return;
31372
+ this.toolRegistry.register(new BrowserControlTool(controller, release));
31373
+ }
30756
31374
  registerMediaTools(workspacePath) {
30757
31375
  const denied = new Set(this.config.tools?.disabledTools ?? []);
30758
31376
  const permitted = (name) => !denied.has(name);
@@ -31358,6 +31976,7 @@ ${prompt}` : prompt;
31358
31976
  this.router.setRunSignal(options.signal);
31359
31977
  const startMs = Date.now();
31360
31978
  const taskId = randomUUID();
31979
+ this.emit("run:started", { taskId });
31361
31980
  this.decisionLog = [];
31362
31981
  this.currentRunCompleted = [];
31363
31982
  this.wroteCheckpointThisRun = false;
@@ -32123,6 +32742,7 @@ function applySettingsPayload(config, data) {
32123
32742
  if (["auto", "T1", "T2", "T3"].includes(a["forceTier"])) config.routing = { ...config.routing ?? {}, forceTier: a["forceTier"] };
32124
32743
  if (typeof a["benchmarksLive"] === "boolean") config.benchmarks = { ...config.benchmarks ?? {}, live: a["benchmarksLive"] };
32125
32744
  if (["isolate", "worker", "auto"].includes(a["dynamicToolSandbox"])) config.tools = { ...config.tools ?? {}, dynamicToolSandbox: a["dynamicToolSandbox"] };
32745
+ if (typeof a["agentBrowserControl"] === "boolean") config.tools = { ...config.tools ?? {}, agentBrowserControl: a["agentBrowserControl"] };
32126
32746
  if (typeof a["factsExtraction"] === "boolean") config.knowledge = { ...config.knowledge ?? {}, factsExtraction: a["factsExtraction"] };
32127
32747
  if (typeof a["rememberSessions"] === "boolean") config.memory = { ...config.memory ?? {}, rememberSessions: a["rememberSessions"] };
32128
32748
  if (typeof a["enableToolCreation"] === "boolean") config.enableToolCreation = a["enableToolCreation"];
@@ -32180,6 +32800,7 @@ function settingsSnapshot(config) {
32180
32800
  forceTier: config.routing?.forceTier,
32181
32801
  benchmarksLive: config.benchmarks?.live,
32182
32802
  dynamicToolSandbox: config.tools?.dynamicToolSandbox,
32803
+ agentBrowserControl: config.tools?.agentBrowserControl,
32183
32804
  factsExtraction: config.knowledge?.factsExtraction,
32184
32805
  rememberSessions: config.memory?.rememberSessions,
32185
32806
  enableToolCreation: config.enableToolCreation,
@@ -32215,8 +32836,8 @@ function replaceInPlace(target, source) {
32215
32836
  }
32216
32837
  function writeConfigFile(configPath, config) {
32217
32838
  try {
32218
- fs26.mkdirSync(path27.dirname(configPath), { recursive: true });
32219
- fs26.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
32839
+ fs27.mkdirSync(path28.dirname(configPath), { recursive: true });
32840
+ fs27.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
32220
32841
  return { ok: true };
32221
32842
  } catch (err) {
32222
32843
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
@@ -32626,7 +33247,7 @@ function aggregateCostStats(sessions, opts = {}) {
32626
33247
  }
32627
33248
 
32628
33249
  // src/dashboard/server.ts
32629
- var __dirname$1 = path27.dirname(fileURLToPath(import.meta.url));
33250
+ var __dirname$1 = path28.dirname(fileURLToPath(import.meta.url));
32630
33251
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
32631
33252
  var MASK = "***";
32632
33253
  function masked(key, value) {
@@ -32684,6 +33305,14 @@ var DashboardServer = class {
32684
33305
  * always failing.
32685
33306
  */
32686
33307
  currentPageProvider;
33308
+ /**
33309
+ * Supplied by the desktop shell so a run can ACT on that page — click, type,
33310
+ * submit — not just read it. Same reasoning as the provider above: absent in
33311
+ * the CLI, so the tool is not registered rather than present and failing.
33312
+ * Cascade applies the `tools.agentBrowserControl` gate on top of this.
33313
+ */
33314
+ browserController;
33315
+ browserActorRelease;
32687
33316
  activeControllers = /* @__PURE__ */ new Map();
32688
33317
  /**
32689
33318
  * Run taskIds per chat session — file snapshots are keyed by the run's
@@ -32745,6 +33374,7 @@ var DashboardServer = class {
32745
33374
  });
32746
33375
  this.socket.onConfigUpdate(async (data) => {
32747
33376
  const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
33377
+ if (result.ok) this.settingsChangeHook?.(this.config);
32748
33378
  const refused = result.refused.map((r) => ({
32749
33379
  type: r.type,
32750
33380
  reason: r.reason,
@@ -32768,6 +33398,7 @@ var DashboardServer = class {
32768
33398
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
32769
33399
  this.activeSessions.set(sessionId, cascade);
32770
33400
  if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
33401
+ if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
32771
33402
  cascade.on("stream:token", (e) => {
32772
33403
  this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
32773
33404
  });
@@ -32791,13 +33422,16 @@ var DashboardServer = class {
32791
33422
  if (ev.requestId) this.clearPendingEscalation(ev.requestId);
32792
33423
  this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
32793
33424
  });
33425
+ cascade.on("run:started", (e) => {
33426
+ const id = e.taskId;
33427
+ if (id) this.recordSessionTask(sessionId, id);
33428
+ });
32794
33429
  try {
32795
33430
  const result = await cascade.run({
32796
33431
  prompt: runPrompt,
32797
33432
  signal: abortController.signal,
32798
33433
  approvalCallback: this.makeApprovalCallback(sessionId)
32799
33434
  });
32800
- this.recordSessionTask(sessionId, result.taskId);
32801
33435
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
32802
33436
  this.captureWhy(sessionId, cascade, result);
32803
33437
  this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
@@ -32863,6 +33497,49 @@ var DashboardServer = class {
32863
33497
  setCurrentPageProvider(provider) {
32864
33498
  this.currentPageProvider = provider;
32865
33499
  }
33500
+ /** Let runs act on that view, not just read it (desktop only). */
33501
+ setBrowserController(controller, release) {
33502
+ this.browserController = controller;
33503
+ this.browserActorRelease = release;
33504
+ }
33505
+ /**
33506
+ * Told whenever settings are committed through THIS server.
33507
+ *
33508
+ * Settings reach the live config by two routes — the desktop's Electron IPC
33509
+ * handler and this server's `config:update` socket — and both run the same
33510
+ * `commitSettings`. The desktop mirrors `tools.agentBrowserControl` into its
33511
+ * browser module after its own route, but had no way to learn about the
33512
+ * other, so a socket write left the module's copy stale: an enable the module
33513
+ * still refused, or worse a disable the module still honoured for a run
33514
+ * already holding the tool. This is the same sibling-writer drift
33515
+ * `settings-payload.ts` exists to prevent, one layer up.
33516
+ */
33517
+ settingsChangeHook;
33518
+ onSettingsChanged(hook) {
33519
+ this.settingsChangeHook = hook;
33520
+ }
33521
+ /**
33522
+ * Told when a run ends, so a host holding run-scoped state can release it.
33523
+ *
33524
+ * The desktop uses this to retire a run's browser Stop control: that has to
33525
+ * outlive each individual action (the gap between two actions is when the
33526
+ * user most wants it) but must not outlive the run.
33527
+ */
33528
+ /**
33529
+ * Fired when a run ends, carrying BOTH identifiers explicitly.
33530
+ *
33531
+ * They are different things and conflating them was a live bug: a chat
33532
+ * `sessionId` can contain many runs (hence `sessionTaskIds`), while
33533
+ * `Cascade.run()` mints a fresh random `taskId` per run — and it is the
33534
+ * taskId that tools see as `ToolExecuteOptions.sessionId`. Passing only the
33535
+ * chat id meant browser cleanup keyed on the task id matched nothing, so a
33536
+ * finished run kept its Stop banner armed and held its browser lease until
33537
+ * the deadlock ceiling.
33538
+ */
33539
+ runEndedHook;
33540
+ onRunEnded(hook) {
33541
+ this.runEndedHook = hook;
33542
+ }
32866
33543
  async start() {
32867
33544
  const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
32868
33545
  if (!isLoopback) {
@@ -32945,7 +33622,7 @@ var DashboardServer = class {
32945
33622
  * silently and still typecheck.
32946
33623
  */
32947
33624
  persistConfig(config) {
32948
- const result = writeConfigFile(path27.join(this.workspacePath, CASCADE_CONFIG_FILE), config);
33625
+ const result = writeConfigFile(path28.join(this.workspacePath, CASCADE_CONFIG_FILE), config);
32949
33626
  if (!result.ok) console.warn(`[dashboard] Failed to persist config: ${result.error}`);
32950
33627
  return result;
32951
33628
  }
@@ -32964,7 +33641,7 @@ var DashboardServer = class {
32964
33641
  */
32965
33642
  syncGlobalCredentials() {
32966
33643
  try {
32967
- saveGlobalCredentials(path27.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
33644
+ saveGlobalCredentials(path28.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
32968
33645
  } catch (err) {
32969
33646
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
32970
33647
  }
@@ -32979,15 +33656,15 @@ var DashboardServer = class {
32979
33656
  resolveDashboardSecret() {
32980
33657
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
32981
33658
  if (fromConfig) return fromConfig;
32982
- const secretPath = path27.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
33659
+ const secretPath = path28.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
32983
33660
  try {
32984
- if (fs26.existsSync(secretPath)) {
32985
- const existing = fs26.readFileSync(secretPath, "utf-8").trim();
33661
+ if (fs27.existsSync(secretPath)) {
33662
+ const existing = fs27.readFileSync(secretPath, "utf-8").trim();
32986
33663
  if (existing.length >= 16) return existing;
32987
33664
  }
32988
33665
  const generated = randomUUID();
32989
- fs26.mkdirSync(path27.dirname(secretPath), { recursive: true });
32990
- fs26.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
33666
+ fs27.mkdirSync(path28.dirname(secretPath), { recursive: true });
33667
+ fs27.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
32991
33668
  if (this.config.dashboard.auth) {
32992
33669
  console.warn(
32993
33670
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -33014,7 +33691,7 @@ var DashboardServer = class {
33014
33691
  // ── Setup ─────────────────────────────────────
33015
33692
  getGlobalStore() {
33016
33693
  if (!this.globalStore) {
33017
- const globalDbPath = path27.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33694
+ const globalDbPath = path28.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33018
33695
  this.globalStore = new MemoryStore(globalDbPath);
33019
33696
  }
33020
33697
  return this.globalStore;
@@ -33053,6 +33730,8 @@ var DashboardServer = class {
33053
33730
  }
33054
33731
  /** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
33055
33732
  persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
33733
+ const endedTaskId = result?.taskId ?? this.sessionTaskIds.get(sessionId)?.at(-1);
33734
+ this.runEndedHook?.({ sessionId, ...endedTaskId ? { taskId: endedTaskId } : {} });
33056
33735
  try {
33057
33736
  if (reply && reply.trim()) {
33058
33737
  this.store.addMessage({ id: randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -33146,6 +33825,7 @@ var DashboardServer = class {
33146
33825
  const prompt = task.prompt;
33147
33826
  const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
33148
33827
  const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
33828
+ cascade.setUnattended(true);
33149
33829
  this.activeSessions.set(sessionId, cascade);
33150
33830
  cascade.on("tier:status", (e) => {
33151
33831
  this.socket.broadcast("tier:status", { sessionId, ...e });
@@ -33153,13 +33833,16 @@ var DashboardServer = class {
33153
33833
  cascade.on("peer:message", (e) => {
33154
33834
  this.socket.emitPeerMessage(e);
33155
33835
  });
33836
+ cascade.on("run:started", (e) => {
33837
+ const id = e.taskId;
33838
+ if (id) this.recordSessionTask(sessionId, id);
33839
+ });
33156
33840
  try {
33157
33841
  const result = await cascade.run({
33158
33842
  prompt,
33159
33843
  identityId: task.identityId,
33160
33844
  approvalCallback: async () => ({ approved: true, always: false })
33161
33845
  });
33162
- this.recordSessionTask(sessionId, result.taskId);
33163
33846
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
33164
33847
  this.captureWhy(sessionId, cascade, result);
33165
33848
  this.socket.broadcast("session:complete", { sessionId, result });
@@ -33394,12 +34077,12 @@ ${prompt}`;
33394
34077
  }
33395
34078
  }
33396
34079
  watchRuntimeChanges() {
33397
- const workspaceDbPath = path27.join(this.workspacePath, CASCADE_DB_FILE);
33398
- const globalDbPath = path27.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
34080
+ const workspaceDbPath = path28.join(this.workspacePath, CASCADE_DB_FILE);
34081
+ const globalDbPath = path28.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33399
34082
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
33400
34083
  for (const watchPath of watchPaths) {
33401
- if (!fs26.existsSync(watchPath)) continue;
33402
- fs26.watchFile(watchPath, { interval: 3e3 }, () => {
34084
+ if (!fs27.existsSync(watchPath)) continue;
34085
+ fs27.watchFile(watchPath, { interval: 3e3 }, () => {
33403
34086
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
33404
34087
  });
33405
34088
  }
@@ -33521,7 +34204,7 @@ ${prompt}`;
33521
34204
  const sessionId = req.params.id;
33522
34205
  this.store.deleteSession(sessionId);
33523
34206
  this.store.deleteRuntimeSession(sessionId);
33524
- const globalDbPath = path27.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
34207
+ const globalDbPath = path28.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33525
34208
  const globalStore = new MemoryStore(globalDbPath);
33526
34209
  try {
33527
34210
  globalStore.deleteRuntimeSession(sessionId);
@@ -33614,7 +34297,7 @@ ${prompt}`;
33614
34297
  });
33615
34298
  this.app.delete("/api/sessions", auth2, (req, res) => {
33616
34299
  const body = req.body;
33617
- const globalDbPath = path27.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
34300
+ const globalDbPath = path28.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33618
34301
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
33619
34302
  const globalStore = new MemoryStore(globalDbPath);
33620
34303
  try {
@@ -33637,7 +34320,7 @@ ${prompt}`;
33637
34320
  });
33638
34321
  this.app.delete("/api/runtime", auth2, (_req, res) => {
33639
34322
  this.store.deleteAllRuntimeNodes();
33640
- const globalDbPath = path27.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
34323
+ const globalDbPath = path28.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33641
34324
  const globalStore = new MemoryStore(globalDbPath);
33642
34325
  try {
33643
34326
  globalStore.deleteAllRuntimeNodes();
@@ -33722,12 +34405,12 @@ ${prompt}`;
33722
34405
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
33723
34406
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
33724
34407
  try {
33725
- const configPath = path27.join(this.workspacePath, CASCADE_CONFIG_FILE);
33726
- const existing = fs26.existsSync(configPath) ? JSON.parse(fs26.readFileSync(configPath, "utf-8")) : {};
34408
+ const configPath = path28.join(this.workspacePath, CASCADE_CONFIG_FILE);
34409
+ const existing = fs27.existsSync(configPath) ? JSON.parse(fs27.readFileSync(configPath, "utf-8")) : {};
33727
34410
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
33728
34411
  const tmp = configPath + ".tmp";
33729
- fs26.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
33730
- fs26.renameSync(tmp, configPath);
34412
+ fs27.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
34413
+ fs27.renameSync(tmp, configPath);
33731
34414
  res.json({ ok: true });
33732
34415
  } catch (err) {
33733
34416
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -33755,7 +34438,7 @@ ${prompt}`;
33755
34438
  this.app.get("/api/runtime", auth2, (req, res) => {
33756
34439
  const scope = req.query["scope"] ?? "workspace";
33757
34440
  if (scope === "global") {
33758
- const globalDbPath = path27.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
34441
+ const globalDbPath = path28.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33759
34442
  const globalStore = new MemoryStore(globalDbPath);
33760
34443
  try {
33761
34444
  res.json({
@@ -33792,6 +34475,7 @@ ${prompt}`;
33792
34475
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
33793
34476
  this.activeSessions.set(sessionId, cascade);
33794
34477
  if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
34478
+ if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
33795
34479
  cascade.on("stream:token", (e) => {
33796
34480
  this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
33797
34481
  });
@@ -33815,13 +34499,16 @@ ${prompt}`;
33815
34499
  if (ev.requestId) this.clearPendingEscalation(ev.requestId);
33816
34500
  this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
33817
34501
  });
34502
+ cascade.on("run:started", (e) => {
34503
+ const id = e.taskId;
34504
+ if (id) this.recordSessionTask(sessionId, id);
34505
+ });
33818
34506
  try {
33819
34507
  const result = await cascade.run({
33820
34508
  prompt: runPrompt,
33821
34509
  identityId: body.identityId,
33822
34510
  approvalCallback: this.makeApprovalCallback(sessionId)
33823
34511
  });
33824
- this.recordSessionTask(sessionId, result.taskId);
33825
34512
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
33826
34513
  this.captureWhy(sessionId, cascade, result);
33827
34514
  this.socket.broadcast("cost:update", {
@@ -34096,13 +34783,13 @@ ${prompt}`;
34096
34783
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
34097
34784
  }
34098
34785
  });
34099
- const prodPath = path27.resolve(__dirname$1, "../web/dist");
34100
- const devPath = path27.resolve(__dirname$1, "../../web/dist");
34101
- const webDistPath = fs26.existsSync(prodPath) ? prodPath : devPath;
34102
- if (fs26.existsSync(webDistPath)) {
34786
+ const prodPath = path28.resolve(__dirname$1, "../web/dist");
34787
+ const devPath = path28.resolve(__dirname$1, "../../web/dist");
34788
+ const webDistPath = fs27.existsSync(prodPath) ? prodPath : devPath;
34789
+ if (fs27.existsSync(webDistPath)) {
34103
34790
  this.app.use(express.static(webDistPath));
34104
34791
  this.app.get("*", (_req, res) => {
34105
- res.sendFile(path27.join(webDistPath, "index.html"));
34792
+ res.sendFile(path28.join(webDistPath, "index.html"));
34106
34793
  });
34107
34794
  } else {
34108
34795
  this.app.get("/", (_req, res) => {
@@ -34289,14 +34976,14 @@ function planRetrieval(ctx) {
34289
34976
  }
34290
34977
  var CLOUD_SESSION_FILE = "cloud-session.json";
34291
34978
  function globalDir(homeDir = os6.homedir()) {
34292
- return path27.join(homeDir, GLOBAL_CONFIG_DIR);
34979
+ return path28.join(homeDir, GLOBAL_CONFIG_DIR);
34293
34980
  }
34294
34981
  function cloudSessionPath(dir = globalDir()) {
34295
- return path27.join(dir, CLOUD_SESSION_FILE);
34982
+ return path28.join(dir, CLOUD_SESSION_FILE);
34296
34983
  }
34297
34984
  function loadCloudSession(dir = globalDir()) {
34298
34985
  try {
34299
- const raw = fs26.readFileSync(cloudSessionPath(dir), "utf-8");
34986
+ const raw = fs27.readFileSync(cloudSessionPath(dir), "utf-8");
34300
34987
  const parsed = JSON.parse(raw);
34301
34988
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
34302
34989
  return parsed;
@@ -34305,17 +34992,17 @@ function loadCloudSession(dir = globalDir()) {
34305
34992
  }
34306
34993
  }
34307
34994
  function saveCloudSession(session, dir = globalDir()) {
34308
- fs26.mkdirSync(dir, { recursive: true, mode: 448 });
34995
+ fs27.mkdirSync(dir, { recursive: true, mode: 448 });
34309
34996
  const file = cloudSessionPath(dir);
34310
- fs26.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
34997
+ fs27.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
34311
34998
  try {
34312
- fs26.chmodSync(file, 384);
34999
+ fs27.chmodSync(file, 384);
34313
35000
  } catch {
34314
35001
  }
34315
35002
  }
34316
35003
  function clearCloudSession(dir = globalDir()) {
34317
35004
  try {
34318
- fs26.rmSync(cloudSessionPath(dir));
35005
+ fs27.rmSync(cloudSessionPath(dir));
34319
35006
  } catch {
34320
35007
  }
34321
35008
  }
@@ -34487,16 +35174,16 @@ var CloudClient = class _CloudClient {
34487
35174
  return (await this.refresh()).accessToken;
34488
35175
  }
34489
35176
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
34490
- async authed(path29, init = {}) {
34491
- const call = (token) => fetch(this.url(path29), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
35177
+ async authed(path30, init = {}) {
35178
+ const call = (token) => fetch(this.url(path30), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
34492
35179
  let res = await call(await this.accessToken());
34493
35180
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
34494
35181
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
34495
35182
  const text = await res.text();
34496
35183
  return text ? JSON.parse(text) : {};
34497
35184
  }
34498
- authedGet(path29) {
34499
- return this.authed(path29);
35185
+ authedGet(path30) {
35186
+ return this.authed(path30);
34500
35187
  }
34501
35188
  async fetchMe(token) {
34502
35189
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -34861,6 +35548,6 @@ function applySyncBundle(bundle, config, cleanup) {
34861
35548
  return next;
34862
35549
  }
34863
35550
 
34864
- export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN2 as CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, DashboardServer, DependencyScheduler, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, RESUME_SCHEMA_VERSION, ResumeStore, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskGraphValidationError, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applyEndpointEdit, applyProviderApiKey, applyProviderCredential, applySettingsCredentials, applySettingsPayload, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, commitSettings, compileSectionGraph, compileSubtaskGraph, compileTaskGraph, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, credentialDispositionForEdit, credentialEndpointIdentity, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, endpointFromSettingsPayload, evaluateAcceptance, explainRefusal, extractCharts, failures, fileExt, fileOAuthProvider, gatherSyncBundle, hasDefaultEndpoint, hasProviderCredential, hasUsableProvider, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, normalizeEndpoint, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, priorAzureRow, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sameAzureEndpoint, sameCredentialEndpoint, sameEndpoint, sessionWorthRemembering, settingsSnapshot, sniffImage, splitSlides, streamCascade, stripInline, summarizeCompleted, undecided, uniqueMcpServerName };
35551
+ export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, BrowserControlTool, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN2 as CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeConfigSchema, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, DashboardServer, DependencyScheduler, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, RESUME_SCHEMA_VERSION, ResumeStore, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskGraphValidationError, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applyEndpointEdit, applyProviderApiKey, applyProviderCredential, applySettingsCredentials, applySettingsPayload, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, commitSettings, compileSectionGraph, compileSubtaskGraph, compileTaskGraph, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, credentialDispositionForEdit, credentialEndpointIdentity, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, endpointFromSettingsPayload, evaluateAcceptance, explainRefusal, extractCharts, failures, fileExt, fileOAuthProvider, gatherSyncBundle, hasDefaultEndpoint, hasProviderCredential, hasUsableProvider, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, normalizeEndpoint, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, priorAzureRow, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sameAzureEndpoint, sameCredentialEndpoint, sameEndpoint, sessionWorthRemembering, settingsSnapshot, sniffImage, splitSlides, streamCascade, stripInline, summarizeCompleted, undecided, uniqueMcpServerName };
34865
35552
  //# sourceMappingURL=index.js.map
34866
35553
  //# sourceMappingURL=index.js.map