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.cjs CHANGED
@@ -1,14 +1,14 @@
1
1
  'use strict';
2
2
 
3
3
  var Database2 = require('better-sqlite3');
4
- var path27 = require('path');
5
- var fs26 = require('fs');
4
+ var path28 = require('path');
5
+ var fs27 = require('fs');
6
6
  var os6 = require('os');
7
7
  var crypto = require('crypto');
8
8
  var EventEmitter = require('events');
9
9
  var glob = require('glob');
10
10
  var Anthropic = require('@anthropic-ai/sdk');
11
- var dns = require('dns');
11
+ var dnsCallback = require('dns');
12
12
  var http = require('http');
13
13
  var https = require('https');
14
14
  var zlib = require('zlib');
@@ -24,6 +24,7 @@ var simpleGit = require('simple-git');
24
24
  var PDFDocument = require('pdfkit');
25
25
  var dns2 = require('dns/promises');
26
26
  var net = require('net');
27
+ var undici = require('undici');
27
28
  var index_js = require('@modelcontextprotocol/sdk/client/index.js');
28
29
  var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
29
30
  var streamableHttp_js = require('@modelcontextprotocol/sdk/client/streamableHttp.js');
@@ -61,13 +62,13 @@ function _interopNamespace(e) {
61
62
  }
62
63
 
63
64
  var Database2__default = /*#__PURE__*/_interopDefault(Database2);
64
- var path27__default = /*#__PURE__*/_interopDefault(path27);
65
- var fs26__default = /*#__PURE__*/_interopDefault(fs26);
65
+ var path28__default = /*#__PURE__*/_interopDefault(path28);
66
+ var fs27__default = /*#__PURE__*/_interopDefault(fs27);
66
67
  var os6__default = /*#__PURE__*/_interopDefault(os6);
67
68
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
68
69
  var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
69
70
  var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
70
- var dns__default = /*#__PURE__*/_interopDefault(dns);
71
+ var dnsCallback__default = /*#__PURE__*/_interopDefault(dnsCallback);
71
72
  var http__default = /*#__PURE__*/_interopDefault(http);
72
73
  var https__default = /*#__PURE__*/_interopDefault(https);
73
74
  var zlib__default = /*#__PURE__*/_interopDefault(zlib);
@@ -178,16 +179,16 @@ var require_isarray = __commonJS({
178
179
  }
179
180
  });
180
181
 
181
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/stream.js
182
+ // node_modules/readable-stream/lib/internal/streams/stream.js
182
183
  var require_stream = __commonJS({
183
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/stream.js"(exports, module) {
184
+ "node_modules/readable-stream/lib/internal/streams/stream.js"(exports, module) {
184
185
  module.exports = __require("stream");
185
186
  }
186
187
  });
187
188
 
188
- // node_modules/jszip/node_modules/safe-buffer/index.js
189
+ // node_modules/readable-stream/node_modules/safe-buffer/index.js
189
190
  var require_safe_buffer = __commonJS({
190
- "node_modules/jszip/node_modules/safe-buffer/index.js"(exports, module) {
191
+ "node_modules/readable-stream/node_modules/safe-buffer/index.js"(exports, module) {
191
192
  var buffer = __require("buffer");
192
193
  var Buffer2 = buffer.Buffer;
193
194
  function copyProps(src, dst) {
@@ -305,7 +306,7 @@ var require_util = __commonJS({
305
306
  typeof arg === "undefined";
306
307
  }
307
308
  exports.isPrimitive = isPrimitive;
308
- exports.isBuffer = Buffer.isBuffer;
309
+ exports.isBuffer = __require("buffer").Buffer.isBuffer;
309
310
  function objectToString(o) {
310
311
  return Object.prototype.toString.call(o);
311
312
  }
@@ -358,9 +359,9 @@ var require_inherits = __commonJS({
358
359
  }
359
360
  });
360
361
 
361
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/BufferList.js
362
+ // node_modules/readable-stream/lib/internal/streams/BufferList.js
362
363
  var require_BufferList = __commonJS({
363
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/BufferList.js"(exports, module) {
364
+ "node_modules/readable-stream/lib/internal/streams/BufferList.js"(exports, module) {
364
365
  function _classCallCheck(instance, Constructor) {
365
366
  if (!(instance instanceof Constructor)) {
366
367
  throw new TypeError("Cannot call a class as a function");
@@ -435,9 +436,9 @@ var require_BufferList = __commonJS({
435
436
  }
436
437
  });
437
438
 
438
- // node_modules/jszip/node_modules/readable-stream/lib/internal/streams/destroy.js
439
+ // node_modules/readable-stream/lib/internal/streams/destroy.js
439
440
  var require_destroy = __commonJS({
440
- "node_modules/jszip/node_modules/readable-stream/lib/internal/streams/destroy.js"(exports, module) {
441
+ "node_modules/readable-stream/lib/internal/streams/destroy.js"(exports, module) {
441
442
  var pna = require_process_nextick_args();
442
443
  function destroy(err, cb) {
443
444
  var _this = this;
@@ -510,9 +511,9 @@ var require_node = __commonJS({
510
511
  }
511
512
  });
512
513
 
513
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_writable.js
514
+ // node_modules/readable-stream/lib/_stream_writable.js
514
515
  var require_stream_writable = __commonJS({
515
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_writable.js"(exports, module) {
516
+ "node_modules/readable-stream/lib/_stream_writable.js"(exports, module) {
516
517
  var pna = require_process_nextick_args();
517
518
  module.exports = Writable;
518
519
  function CorkedRequest(state) {
@@ -949,9 +950,9 @@ var require_stream_writable = __commonJS({
949
950
  }
950
951
  });
951
952
 
952
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_duplex.js
953
+ // node_modules/readable-stream/lib/_stream_duplex.js
953
954
  var require_stream_duplex = __commonJS({
954
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_duplex.js"(exports, module) {
955
+ "node_modules/readable-stream/lib/_stream_duplex.js"(exports, module) {
955
956
  var pna = require_process_nextick_args();
956
957
  var objectKeys = Object.keys || function(obj) {
957
958
  var keys2 = [];
@@ -1025,10 +1026,67 @@ var require_stream_duplex = __commonJS({
1025
1026
  }
1026
1027
  });
1027
1028
 
1028
- // node_modules/jszip/node_modules/string_decoder/lib/string_decoder.js
1029
+ // node_modules/string_decoder/node_modules/safe-buffer/index.js
1030
+ var require_safe_buffer2 = __commonJS({
1031
+ "node_modules/string_decoder/node_modules/safe-buffer/index.js"(exports, module) {
1032
+ var buffer = __require("buffer");
1033
+ var Buffer2 = buffer.Buffer;
1034
+ function copyProps(src, dst) {
1035
+ for (var key in src) {
1036
+ dst[key] = src[key];
1037
+ }
1038
+ }
1039
+ if (Buffer2.from && Buffer2.alloc && Buffer2.allocUnsafe && Buffer2.allocUnsafeSlow) {
1040
+ module.exports = buffer;
1041
+ } else {
1042
+ copyProps(buffer, exports);
1043
+ exports.Buffer = SafeBuffer;
1044
+ }
1045
+ function SafeBuffer(arg, encodingOrOffset, length) {
1046
+ return Buffer2(arg, encodingOrOffset, length);
1047
+ }
1048
+ copyProps(Buffer2, SafeBuffer);
1049
+ SafeBuffer.from = function(arg, encodingOrOffset, length) {
1050
+ if (typeof arg === "number") {
1051
+ throw new TypeError("Argument must not be a number");
1052
+ }
1053
+ return Buffer2(arg, encodingOrOffset, length);
1054
+ };
1055
+ SafeBuffer.alloc = function(size, fill, encoding) {
1056
+ if (typeof size !== "number") {
1057
+ throw new TypeError("Argument must be a number");
1058
+ }
1059
+ var buf = Buffer2(size);
1060
+ if (fill !== void 0) {
1061
+ if (typeof encoding === "string") {
1062
+ buf.fill(fill, encoding);
1063
+ } else {
1064
+ buf.fill(fill);
1065
+ }
1066
+ } else {
1067
+ buf.fill(0);
1068
+ }
1069
+ return buf;
1070
+ };
1071
+ SafeBuffer.allocUnsafe = function(size) {
1072
+ if (typeof size !== "number") {
1073
+ throw new TypeError("Argument must be a number");
1074
+ }
1075
+ return Buffer2(size);
1076
+ };
1077
+ SafeBuffer.allocUnsafeSlow = function(size) {
1078
+ if (typeof size !== "number") {
1079
+ throw new TypeError("Argument must be a number");
1080
+ }
1081
+ return buffer.SlowBuffer(size);
1082
+ };
1083
+ }
1084
+ });
1085
+
1086
+ // node_modules/string_decoder/lib/string_decoder.js
1029
1087
  var require_string_decoder = __commonJS({
1030
- "node_modules/jszip/node_modules/string_decoder/lib/string_decoder.js"(exports) {
1031
- var Buffer2 = require_safe_buffer().Buffer;
1088
+ "node_modules/string_decoder/lib/string_decoder.js"(exports) {
1089
+ var Buffer2 = require_safe_buffer2().Buffer;
1032
1090
  var isEncoding = Buffer2.isEncoding || function(encoding) {
1033
1091
  encoding = "" + encoding;
1034
1092
  switch (encoding && encoding.toLowerCase()) {
@@ -1262,9 +1320,9 @@ var require_string_decoder = __commonJS({
1262
1320
  }
1263
1321
  });
1264
1322
 
1265
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_readable.js
1323
+ // node_modules/readable-stream/lib/_stream_readable.js
1266
1324
  var require_stream_readable = __commonJS({
1267
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_readable.js"(exports, module) {
1325
+ "node_modules/readable-stream/lib/_stream_readable.js"(exports, module) {
1268
1326
  var pna = require_process_nextick_args();
1269
1327
  module.exports = Readable2;
1270
1328
  var isArray = require_isarray();
@@ -1947,9 +2005,9 @@ var require_stream_readable = __commonJS({
1947
2005
  }
1948
2006
  });
1949
2007
 
1950
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_transform.js
2008
+ // node_modules/readable-stream/lib/_stream_transform.js
1951
2009
  var require_stream_transform = __commonJS({
1952
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_transform.js"(exports, module) {
2010
+ "node_modules/readable-stream/lib/_stream_transform.js"(exports, module) {
1953
2011
  module.exports = Transform;
1954
2012
  var Duplex = require_stream_duplex();
1955
2013
  var util = Object.create(require_util());
@@ -2046,9 +2104,9 @@ var require_stream_transform = __commonJS({
2046
2104
  }
2047
2105
  });
2048
2106
 
2049
- // node_modules/jszip/node_modules/readable-stream/lib/_stream_passthrough.js
2107
+ // node_modules/readable-stream/lib/_stream_passthrough.js
2050
2108
  var require_stream_passthrough = __commonJS({
2051
- "node_modules/jszip/node_modules/readable-stream/lib/_stream_passthrough.js"(exports, module) {
2109
+ "node_modules/readable-stream/lib/_stream_passthrough.js"(exports, module) {
2052
2110
  module.exports = PassThrough;
2053
2111
  var Transform = require_stream_transform();
2054
2112
  var util = Object.create(require_util());
@@ -2064,9 +2122,9 @@ var require_stream_passthrough = __commonJS({
2064
2122
  }
2065
2123
  });
2066
2124
 
2067
- // node_modules/jszip/node_modules/readable-stream/readable.js
2125
+ // node_modules/readable-stream/readable.js
2068
2126
  var require_readable = __commonJS({
2069
- "node_modules/jszip/node_modules/readable-stream/readable.js"(exports, module) {
2127
+ "node_modules/readable-stream/readable.js"(exports, module) {
2070
2128
  var Stream = __require("stream");
2071
2129
  if (process.env.READABLE_STREAM === "disable" && Stream) {
2072
2130
  module.exports = Stream;
@@ -2975,8 +3033,8 @@ var require_utils = __commonJS({
2975
3033
  var result = transform[inputType][outputType](input);
2976
3034
  return result;
2977
3035
  };
2978
- exports.resolve = function(path29) {
2979
- var parts = path29.split("/");
3036
+ exports.resolve = function(path30) {
3037
+ var parts = path30.split("/");
2980
3038
  var result = [];
2981
3039
  for (var index = 0; index < parts.length; index++) {
2982
3040
  var part = parts[index];
@@ -8791,18 +8849,18 @@ var require_object = __commonJS({
8791
8849
  var object = new ZipObject(name, zipObjectContent, o);
8792
8850
  this.files[name] = object;
8793
8851
  };
8794
- var parentFolder = function(path29) {
8795
- if (path29.slice(-1) === "/") {
8796
- path29 = path29.substring(0, path29.length - 1);
8852
+ var parentFolder = function(path30) {
8853
+ if (path30.slice(-1) === "/") {
8854
+ path30 = path30.substring(0, path30.length - 1);
8797
8855
  }
8798
- var lastSlash = path29.lastIndexOf("/");
8799
- return lastSlash > 0 ? path29.substring(0, lastSlash) : "";
8856
+ var lastSlash = path30.lastIndexOf("/");
8857
+ return lastSlash > 0 ? path30.substring(0, lastSlash) : "";
8800
8858
  };
8801
- var forceTrailingSlash = function(path29) {
8802
- if (path29.slice(-1) !== "/") {
8803
- path29 += "/";
8859
+ var forceTrailingSlash = function(path30) {
8860
+ if (path30.slice(-1) !== "/") {
8861
+ path30 += "/";
8804
8862
  }
8805
- return path29;
8863
+ return path30;
8806
8864
  };
8807
8865
  var folderAdd = function(name, createFolders) {
8808
8866
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -9801,12 +9859,12 @@ var init_audit_logger = __esm({
9801
9859
  constructor(workspacePath, debugMode = false) {
9802
9860
  this.workspacePath = workspacePath;
9803
9861
  this.debugMode = debugMode;
9804
- const cascadeDir = path27__default.default.join(workspacePath, ".cascade");
9805
- if (!fs26__default.default.existsSync(cascadeDir)) {
9806
- fs26__default.default.mkdirSync(cascadeDir, { recursive: true });
9862
+ const cascadeDir = path28__default.default.join(workspacePath, ".cascade");
9863
+ if (!fs27__default.default.existsSync(cascadeDir)) {
9864
+ fs27__default.default.mkdirSync(cascadeDir, { recursive: true });
9807
9865
  }
9808
- this.keyPath = path27__default.default.join(cascadeDir, "audit_log.key");
9809
- this.dbPath = path27__default.default.join(cascadeDir, "audit_log.db");
9866
+ this.keyPath = path28__default.default.join(cascadeDir, "audit_log.key");
9867
+ this.dbPath = path28__default.default.join(cascadeDir, "audit_log.db");
9810
9868
  this.initEncryptionKey();
9811
9869
  this.db = new Database2__default.default(this.dbPath);
9812
9870
  this.db.pragma("journal_mode = WAL");
@@ -9836,11 +9894,11 @@ var init_audit_logger = __esm({
9836
9894
  return crypto__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
9837
9895
  }
9838
9896
  initEncryptionKey() {
9839
- if (fs26__default.default.existsSync(this.keyPath)) {
9840
- this.encryptionKey = fs26__default.default.readFileSync(this.keyPath);
9897
+ if (fs27__default.default.existsSync(this.keyPath)) {
9898
+ this.encryptionKey = fs27__default.default.readFileSync(this.keyPath);
9841
9899
  } else {
9842
9900
  this.encryptionKey = crypto__default.default.randomBytes(32);
9843
- fs26__default.default.writeFileSync(this.keyPath, this.encryptionKey);
9901
+ fs27__default.default.writeFileSync(this.keyPath, this.encryptionKey);
9844
9902
  }
9845
9903
  }
9846
9904
  encrypt(text) {
@@ -9918,9 +9976,9 @@ var init_audit_logger = __esm({
9918
9976
  dumpDebugIfNeeded() {
9919
9977
  if (!this.debugMode) return;
9920
9978
  try {
9921
- const dumpPath = path27__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
9979
+ const dumpPath = path28__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
9922
9980
  const entries = this.getAllLogs();
9923
- fs26__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9981
+ fs27__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9924
9982
  } catch (err) {
9925
9983
  console.error("Failed to dump debug audit logs", err);
9926
9984
  }
@@ -11611,7 +11669,7 @@ function reconcilePrice(model, live) {
11611
11669
  }
11612
11670
 
11613
11671
  // src/constants.ts
11614
- var CASCADE_VERSION = "0.76.0";
11672
+ var CASCADE_VERSION = "0.78.0";
11615
11673
  var CASCADE_CONFIG_DIR = ".cascade";
11616
11674
  var CASCADE_MD_FILE = "CASCADE.md";
11617
11675
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -11632,64 +11690,88 @@ var DEFAULT_AUTO_SUMMARIZE_AT = 15e4;
11632
11690
  var DEFAULT_MAX_SESSION_MESSAGES = 1e3;
11633
11691
  var DEFAULT_RETENTION_DAYS = 90;
11634
11692
  var MODELS = {
11635
- // Anthropic
11636
- "claude-opus-4": {
11637
- id: "claude-opus-4-5",
11638
- name: "Claude Opus 4",
11693
+ // Anthropic — active Claude API models (2026-08-27).
11694
+ "claude-fable-5": {
11695
+ id: "claude-fable-5",
11696
+ name: "Claude Fable 5",
11639
11697
  provider: "anthropic",
11640
- contextWindow: 2e5,
11698
+ contextWindow: 1e6,
11641
11699
  isVisionCapable: true,
11642
- inputCostPer1kTokens: 0.015,
11643
- outputCostPer1kTokens: 0.075,
11644
- maxOutputTokens: 32e3,
11700
+ inputCostPer1kTokens: 0.01,
11701
+ outputCostPer1kTokens: 0.05,
11702
+ maxOutputTokens: 128e3,
11645
11703
  supportsStreaming: true,
11646
11704
  isLocal: false
11647
11705
  },
11648
- "claude-sonnet-4": {
11649
- id: "claude-sonnet-4-5",
11650
- name: "Claude Sonnet 4",
11706
+ "claude-opus-5": {
11707
+ id: "claude-opus-5",
11708
+ name: "Claude Opus 5",
11651
11709
  provider: "anthropic",
11652
- contextWindow: 2e5,
11710
+ contextWindow: 1e6,
11653
11711
  isVisionCapable: true,
11654
- inputCostPer1kTokens: 3e-3,
11655
- outputCostPer1kTokens: 0.015,
11656
- maxOutputTokens: 16e3,
11712
+ inputCostPer1kTokens: 5e-3,
11713
+ outputCostPer1kTokens: 0.025,
11714
+ maxOutputTokens: 128e3,
11657
11715
  supportsStreaming: true,
11658
11716
  isLocal: false
11659
11717
  },
11660
- "claude-haiku-3-5": {
11661
- id: "claude-haiku-3-5-20251001",
11662
- name: "Claude Haiku 3.5",
11718
+ "claude-opus-4-8": {
11719
+ id: "claude-opus-4-8",
11720
+ name: "Claude Opus 4.8",
11663
11721
  provider: "anthropic",
11664
- contextWindow: 2e5,
11722
+ contextWindow: 1e6,
11665
11723
  isVisionCapable: true,
11666
- inputCostPer1kTokens: 8e-4,
11667
- outputCostPer1kTokens: 4e-3,
11668
- maxOutputTokens: 8e3,
11724
+ inputCostPer1kTokens: 5e-3,
11725
+ outputCostPer1kTokens: 0.025,
11726
+ maxOutputTokens: 128e3,
11669
11727
  supportsStreaming: true,
11670
11728
  isLocal: false
11671
11729
  },
11672
- "claude-haiku-4-5": {
11673
- id: "claude-haiku-4-5-20251001",
11674
- name: "Claude Haiku 4.5",
11730
+ "claude-opus-4-7": {
11731
+ id: "claude-opus-4-7",
11732
+ name: "Claude Opus 4.7",
11675
11733
  provider: "anthropic",
11676
- contextWindow: 2e5,
11734
+ contextWindow: 1e6,
11677
11735
  isVisionCapable: true,
11678
- inputCostPer1kTokens: 8e-4,
11679
- outputCostPer1kTokens: 4e-3,
11680
- maxOutputTokens: 8e3,
11736
+ inputCostPer1kTokens: 5e-3,
11737
+ outputCostPer1kTokens: 0.025,
11738
+ maxOutputTokens: 128e3,
11681
11739
  supportsStreaming: true,
11682
11740
  isLocal: false
11683
11741
  },
11684
- "claude-opus-4-8": {
11685
- id: "claude-opus-4-8",
11686
- name: "Claude Opus 4.8",
11742
+ "claude-opus-4-6": {
11743
+ id: "claude-opus-4-6",
11744
+ name: "Claude Opus 4.6",
11745
+ provider: "anthropic",
11746
+ contextWindow: 1e6,
11747
+ isVisionCapable: true,
11748
+ inputCostPer1kTokens: 5e-3,
11749
+ outputCostPer1kTokens: 0.025,
11750
+ maxOutputTokens: 128e3,
11751
+ supportsStreaming: true,
11752
+ isLocal: false
11753
+ },
11754
+ "claude-opus-4-5": {
11755
+ id: "claude-opus-4-5-20251101",
11756
+ name: "Claude Opus 4.5",
11687
11757
  provider: "anthropic",
11688
11758
  contextWindow: 2e5,
11689
11759
  isVisionCapable: true,
11690
- inputCostPer1kTokens: 0.015,
11691
- outputCostPer1kTokens: 0.075,
11692
- maxOutputTokens: 32e3,
11760
+ inputCostPer1kTokens: 5e-3,
11761
+ outputCostPer1kTokens: 0.025,
11762
+ maxOutputTokens: 64e3,
11763
+ supportsStreaming: true,
11764
+ isLocal: false
11765
+ },
11766
+ "claude-sonnet-5": {
11767
+ id: "claude-sonnet-5",
11768
+ name: "Claude Sonnet 5",
11769
+ provider: "anthropic",
11770
+ contextWindow: 1e6,
11771
+ isVisionCapable: true,
11772
+ inputCostPer1kTokens: 2e-3,
11773
+ outputCostPer1kTokens: 0.01,
11774
+ maxOutputTokens: 128e3,
11693
11775
  supportsStreaming: true,
11694
11776
  isLocal: false
11695
11777
  },
@@ -11697,76 +11779,135 @@ var MODELS = {
11697
11779
  id: "claude-sonnet-4-6",
11698
11780
  name: "Claude Sonnet 4.6",
11699
11781
  provider: "anthropic",
11782
+ contextWindow: 1e6,
11783
+ isVisionCapable: true,
11784
+ inputCostPer1kTokens: 3e-3,
11785
+ outputCostPer1kTokens: 0.015,
11786
+ maxOutputTokens: 64e3,
11787
+ supportsStreaming: true,
11788
+ isLocal: false
11789
+ },
11790
+ "claude-sonnet-4-5": {
11791
+ id: "claude-sonnet-4-5-20250929",
11792
+ name: "Claude Sonnet 4.5",
11793
+ provider: "anthropic",
11700
11794
  contextWindow: 2e5,
11701
11795
  isVisionCapable: true,
11702
11796
  inputCostPer1kTokens: 3e-3,
11703
11797
  outputCostPer1kTokens: 0.015,
11704
- maxOutputTokens: 16e3,
11798
+ maxOutputTokens: 64e3,
11705
11799
  supportsStreaming: true,
11706
11800
  isLocal: false
11707
11801
  },
11708
- // OpenAI
11709
- "gpt-4o": {
11710
- id: "gpt-4o",
11711
- name: "GPT-4o",
11802
+ "claude-haiku-4-5": {
11803
+ id: "claude-haiku-4-5-20251001",
11804
+ name: "Claude Haiku 4.5",
11805
+ provider: "anthropic",
11806
+ contextWindow: 2e5,
11807
+ isVisionCapable: true,
11808
+ inputCostPer1kTokens: 1e-3,
11809
+ outputCostPer1kTokens: 5e-3,
11810
+ maxOutputTokens: 64e3,
11811
+ supportsStreaming: true,
11812
+ isLocal: false
11813
+ },
11814
+ // OpenAI — active chat-capable models used by Cascade's Chat Completions path.
11815
+ "gpt-5.6-sol": {
11816
+ id: "gpt-5.6-sol",
11817
+ name: "GPT-5.6 Sol",
11712
11818
  provider: "openai",
11713
- contextWindow: 128e3,
11819
+ contextWindow: 105e4,
11820
+ isVisionCapable: true,
11821
+ inputCostPer1kTokens: 4e-3,
11822
+ outputCostPer1kTokens: 0.02,
11823
+ maxOutputTokens: 128e3,
11824
+ supportsStreaming: true,
11825
+ isLocal: false
11826
+ },
11827
+ "gpt-5.6-terra": {
11828
+ id: "gpt-5.6-terra",
11829
+ name: "GPT-5.6 Terra",
11830
+ provider: "openai",
11831
+ contextWindow: 105e4,
11832
+ isVisionCapable: true,
11833
+ inputCostPer1kTokens: 2e-3,
11834
+ outputCostPer1kTokens: 0.012,
11835
+ maxOutputTokens: 128e3,
11836
+ supportsStreaming: true,
11837
+ isLocal: false
11838
+ },
11839
+ "gpt-5.6-luna": {
11840
+ id: "gpt-5.6-luna",
11841
+ name: "GPT-5.6 Luna",
11842
+ provider: "openai",
11843
+ contextWindow: 105e4,
11844
+ isVisionCapable: true,
11845
+ inputCostPer1kTokens: 2e-4,
11846
+ outputCostPer1kTokens: 12e-4,
11847
+ maxOutputTokens: 128e3,
11848
+ supportsStreaming: true,
11849
+ isLocal: false
11850
+ },
11851
+ "gpt-5.5": {
11852
+ id: "gpt-5.5",
11853
+ name: "GPT-5.5",
11854
+ provider: "openai",
11855
+ contextWindow: 105e4,
11714
11856
  isVisionCapable: true,
11715
11857
  inputCostPer1kTokens: 5e-3,
11716
- outputCostPer1kTokens: 0.015,
11717
- maxOutputTokens: 16e3,
11858
+ outputCostPer1kTokens: 0.03,
11859
+ maxOutputTokens: 128e3,
11718
11860
  supportsStreaming: true,
11719
11861
  isLocal: false
11720
11862
  },
11721
- "gpt-4o-mini": {
11722
- id: "gpt-4o-mini",
11723
- name: "GPT-4o Mini",
11863
+ "gpt-5.4": {
11864
+ id: "gpt-5.4",
11865
+ name: "GPT-5.4",
11724
11866
  provider: "openai",
11725
- contextWindow: 128e3,
11867
+ contextWindow: 105e4,
11726
11868
  isVisionCapable: true,
11727
- inputCostPer1kTokens: 15e-5,
11728
- outputCostPer1kTokens: 6e-4,
11729
- maxOutputTokens: 16e3,
11869
+ inputCostPer1kTokens: 25e-4,
11870
+ outputCostPer1kTokens: 0.015,
11871
+ maxOutputTokens: 128e3,
11730
11872
  supportsStreaming: true,
11731
11873
  isLocal: false
11732
11874
  },
11733
- "gpt-4.1": {
11734
- id: "gpt-4.1",
11735
- name: "GPT-4.1",
11875
+ "gpt-5.4-mini": {
11876
+ id: "gpt-5.4-mini",
11877
+ name: "GPT-5.4 Mini",
11736
11878
  provider: "openai",
11737
- contextWindow: 1047576,
11879
+ contextWindow: 4e5,
11738
11880
  isVisionCapable: true,
11739
- inputCostPer1kTokens: 2e-3,
11740
- outputCostPer1kTokens: 8e-3,
11741
- maxOutputTokens: 32768,
11881
+ inputCostPer1kTokens: 75e-5,
11882
+ outputCostPer1kTokens: 45e-4,
11883
+ maxOutputTokens: 128e3,
11742
11884
  supportsStreaming: true,
11743
11885
  isLocal: false
11744
11886
  },
11745
- "gpt-4.1-mini": {
11746
- id: "gpt-4.1-mini",
11747
- name: "GPT-4.1 Mini",
11887
+ "gpt-5.4-nano": {
11888
+ id: "gpt-5.4-nano",
11889
+ name: "GPT-5.4 Nano",
11748
11890
  provider: "openai",
11749
- contextWindow: 1047576,
11891
+ contextWindow: 4e5,
11750
11892
  isVisionCapable: true,
11751
- inputCostPer1kTokens: 4e-4,
11752
- outputCostPer1kTokens: 16e-4,
11753
- maxOutputTokens: 32768,
11893
+ inputCostPer1kTokens: 2e-4,
11894
+ outputCostPer1kTokens: 125e-5,
11895
+ maxOutputTokens: 128e3,
11754
11896
  supportsStreaming: true,
11755
11897
  isLocal: false
11756
11898
  },
11757
- "gpt-4.1-nano": {
11758
- id: "gpt-4.1-nano",
11759
- name: "GPT-4.1 Nano",
11899
+ "gpt-5.2": {
11900
+ id: "gpt-5.2",
11901
+ name: "GPT-5.2",
11760
11902
  provider: "openai",
11761
- contextWindow: 1047576,
11903
+ contextWindow: 4e5,
11762
11904
  isVisionCapable: true,
11763
- inputCostPer1kTokens: 1e-4,
11764
- outputCostPer1kTokens: 4e-4,
11765
- maxOutputTokens: 32768,
11905
+ inputCostPer1kTokens: 175e-5,
11906
+ outputCostPer1kTokens: 0.014,
11907
+ maxOutputTokens: 128e3,
11766
11908
  supportsStreaming: true,
11767
11909
  isLocal: false
11768
11910
  },
11769
- // GPT-5 family (offline baseline pricing; the live fetch keeps it current).
11770
11911
  "gpt-5": {
11771
11912
  id: "gpt-5",
11772
11913
  name: "GPT-5",
@@ -11803,79 +11944,149 @@ var MODELS = {
11803
11944
  supportsStreaming: true,
11804
11945
  isLocal: false
11805
11946
  },
11806
- // GPT-5 point releases — genuinely distinct capability, so they route as their
11807
- // own families (not folded into gpt-5). Offline baseline pricing; live fetch
11808
- // keeps it current. gpt-5.5 "Spud" (Apr 2026) is the current SWE-bench leader.
11809
- "gpt-5.5": {
11810
- id: "gpt-5.5",
11811
- name: "GPT-5.5",
11947
+ "gpt-4.1": {
11948
+ id: "gpt-4.1",
11949
+ name: "GPT-4.1",
11812
11950
  provider: "openai",
11813
- contextWindow: 4e5,
11951
+ contextWindow: 1047576,
11814
11952
  isVisionCapable: true,
11815
- inputCostPer1kTokens: 25e-4,
11816
- outputCostPer1kTokens: 0.02,
11817
- maxOutputTokens: 128e3,
11953
+ inputCostPer1kTokens: 2e-3,
11954
+ outputCostPer1kTokens: 8e-3,
11955
+ maxOutputTokens: 32768,
11818
11956
  supportsStreaming: true,
11819
11957
  isLocal: false
11820
11958
  },
11821
- "gpt-5.4": {
11822
- id: "gpt-5.4",
11823
- name: "GPT-5.4",
11959
+ "gpt-4.1-mini": {
11960
+ id: "gpt-4.1-mini",
11961
+ name: "GPT-4.1 Mini",
11824
11962
  provider: "openai",
11825
- contextWindow: 4e5,
11963
+ contextWindow: 1047576,
11826
11964
  isVisionCapable: true,
11827
- inputCostPer1kTokens: 125e-5,
11965
+ inputCostPer1kTokens: 4e-4,
11966
+ outputCostPer1kTokens: 16e-4,
11967
+ maxOutputTokens: 32768,
11968
+ supportsStreaming: true,
11969
+ isLocal: false
11970
+ },
11971
+ "gpt-4.1-nano": {
11972
+ id: "gpt-4.1-nano",
11973
+ name: "GPT-4.1 Nano",
11974
+ provider: "openai",
11975
+ contextWindow: 1047576,
11976
+ isVisionCapable: true,
11977
+ inputCostPer1kTokens: 1e-4,
11978
+ outputCostPer1kTokens: 4e-4,
11979
+ maxOutputTokens: 32768,
11980
+ supportsStreaming: true,
11981
+ isLocal: false
11982
+ },
11983
+ "gpt-4o": {
11984
+ id: "gpt-4o",
11985
+ name: "GPT-4o",
11986
+ provider: "openai",
11987
+ contextWindow: 128e3,
11988
+ isVisionCapable: true,
11989
+ inputCostPer1kTokens: 25e-4,
11828
11990
  outputCostPer1kTokens: 0.01,
11829
- maxOutputTokens: 128e3,
11991
+ maxOutputTokens: 16e3,
11830
11992
  supportsStreaming: true,
11831
11993
  isLocal: false
11832
11994
  },
11833
- "gpt-5.4-mini": {
11834
- id: "gpt-5.4-mini",
11835
- name: "GPT-5.4 Mini",
11995
+ "gpt-4o-mini": {
11996
+ id: "gpt-4o-mini",
11997
+ name: "GPT-4o Mini",
11836
11998
  provider: "openai",
11837
- contextWindow: 4e5,
11999
+ contextWindow: 128e3,
11838
12000
  isVisionCapable: true,
11839
- inputCostPer1kTokens: 25e-5,
11840
- outputCostPer1kTokens: 2e-3,
11841
- maxOutputTokens: 128e3,
12001
+ inputCostPer1kTokens: 15e-5,
12002
+ outputCostPer1kTokens: 6e-4,
12003
+ maxOutputTokens: 16e3,
11842
12004
  supportsStreaming: true,
11843
12005
  isLocal: false
11844
12006
  },
11845
- // Google
11846
- "gemini-2.0-flash": {
11847
- id: "gemini-2.0-flash",
11848
- name: "Gemini 2.0 Flash",
12007
+ // Google Gemini — active GA + preview chat models. Gemini 2.0/1.5 are no
12008
+ // longer advertised because the Gemini API shut those generations down.
12009
+ "gemini-3.7-flash": {
12010
+ id: "gemini-3.7-flash",
12011
+ name: "Gemini 3.7 Flash",
11849
12012
  provider: "gemini",
11850
- contextWindow: 1e6,
12013
+ contextWindow: 1048576,
11851
12014
  isVisionCapable: true,
11852
- inputCostPer1kTokens: 1e-4,
11853
- outputCostPer1kTokens: 4e-4,
11854
- maxOutputTokens: 8e3,
12015
+ inputCostPer1kTokens: 75e-5,
12016
+ outputCostPer1kTokens: 375e-5,
12017
+ maxOutputTokens: 65536,
11855
12018
  supportsStreaming: true,
11856
12019
  isLocal: false
11857
12020
  },
11858
- "gemini-1.5-pro": {
11859
- id: "gemini-1.5-pro",
11860
- name: "Gemini 1.5 Pro",
12021
+ "gemini-3.6-flash": {
12022
+ id: "gemini-3.6-flash",
12023
+ name: "Gemini 3.6 Flash",
11861
12024
  provider: "gemini",
11862
- contextWindow: 1e6,
12025
+ contextWindow: 1048576,
11863
12026
  isVisionCapable: true,
11864
- inputCostPer1kTokens: 125e-5,
11865
- outputCostPer1kTokens: 5e-3,
11866
- maxOutputTokens: 8e3,
12027
+ inputCostPer1kTokens: 75e-5,
12028
+ outputCostPer1kTokens: 375e-5,
12029
+ maxOutputTokens: 65536,
11867
12030
  supportsStreaming: true,
11868
12031
  isLocal: false
11869
12032
  },
11870
- "gemini-2.0-flash-lite": {
11871
- id: "gemini-2.0-flash-lite",
11872
- name: "Gemini 2.0 Flash-Lite",
12033
+ "gemini-3.5-flash": {
12034
+ id: "gemini-3.5-flash",
12035
+ name: "Gemini 3.5 Flash",
11873
12036
  provider: "gemini",
11874
- contextWindow: 1e6,
12037
+ contextWindow: 1048576,
11875
12038
  isVisionCapable: true,
11876
- inputCostPer1kTokens: 5e-5,
11877
- outputCostPer1kTokens: 2e-4,
11878
- maxOutputTokens: 8e3,
12039
+ inputCostPer1kTokens: 15e-4,
12040
+ outputCostPer1kTokens: 9e-3,
12041
+ maxOutputTokens: 65536,
12042
+ supportsStreaming: true,
12043
+ isLocal: false
12044
+ },
12045
+ "gemini-3.5-flash-lite": {
12046
+ id: "gemini-3.5-flash-lite",
12047
+ name: "Gemini 3.5 Flash-Lite",
12048
+ provider: "gemini",
12049
+ contextWindow: 1048576,
12050
+ isVisionCapable: true,
12051
+ inputCostPer1kTokens: 3e-4,
12052
+ outputCostPer1kTokens: 25e-4,
12053
+ maxOutputTokens: 65536,
12054
+ supportsStreaming: true,
12055
+ isLocal: false
12056
+ },
12057
+ "gemini-3.1-flash-lite": {
12058
+ id: "gemini-3.1-flash-lite",
12059
+ name: "Gemini 3.1 Flash-Lite",
12060
+ provider: "gemini",
12061
+ contextWindow: 1048576,
12062
+ isVisionCapable: true,
12063
+ inputCostPer1kTokens: 25e-5,
12064
+ outputCostPer1kTokens: 15e-4,
12065
+ maxOutputTokens: 65536,
12066
+ supportsStreaming: true,
12067
+ isLocal: false
12068
+ },
12069
+ "gemini-3.1-pro-preview": {
12070
+ id: "gemini-3.1-pro-preview",
12071
+ name: "Gemini 3.1 Pro Preview",
12072
+ provider: "gemini",
12073
+ contextWindow: 1048576,
12074
+ isVisionCapable: true,
12075
+ inputCostPer1kTokens: 2e-3,
12076
+ outputCostPer1kTokens: 0.012,
12077
+ maxOutputTokens: 65536,
12078
+ supportsStreaming: true,
12079
+ isLocal: false
12080
+ },
12081
+ "gemini-3-flash-preview": {
12082
+ id: "gemini-3-flash-preview",
12083
+ name: "Gemini 3 Flash Preview",
12084
+ provider: "gemini",
12085
+ contextWindow: 1048576,
12086
+ isVisionCapable: true,
12087
+ inputCostPer1kTokens: 5e-4,
12088
+ outputCostPer1kTokens: 3e-3,
12089
+ maxOutputTokens: 65536,
11879
12090
  supportsStreaming: true,
11880
12091
  isLocal: false
11881
12092
  },
@@ -11883,7 +12094,7 @@ var MODELS = {
11883
12094
  id: "gemini-2.5-pro",
11884
12095
  name: "Gemini 2.5 Pro",
11885
12096
  provider: "gemini",
11886
- contextWindow: 1e6,
12097
+ contextWindow: 1048576,
11887
12098
  isVisionCapable: true,
11888
12099
  inputCostPer1kTokens: 125e-5,
11889
12100
  outputCostPer1kTokens: 0.01,
@@ -11895,10 +12106,22 @@ var MODELS = {
11895
12106
  id: "gemini-2.5-flash",
11896
12107
  name: "Gemini 2.5 Flash",
11897
12108
  provider: "gemini",
11898
- contextWindow: 1e6,
12109
+ contextWindow: 1048576,
11899
12110
  isVisionCapable: true,
11900
- inputCostPer1kTokens: 15e-5,
11901
- outputCostPer1kTokens: 6e-4,
12111
+ inputCostPer1kTokens: 3e-4,
12112
+ outputCostPer1kTokens: 25e-4,
12113
+ maxOutputTokens: 65536,
12114
+ supportsStreaming: true,
12115
+ isLocal: false
12116
+ },
12117
+ "gemini-2.5-flash-lite": {
12118
+ id: "gemini-2.5-flash-lite",
12119
+ name: "Gemini 2.5 Flash-Lite",
12120
+ provider: "gemini",
12121
+ contextWindow: 1048576,
12122
+ isVisionCapable: true,
12123
+ inputCostPer1kTokens: 1e-4,
12124
+ outputCostPer1kTokens: 4e-4,
11902
12125
  maxOutputTokens: 65536,
11903
12126
  supportsStreaming: true,
11904
12127
  isLocal: false
@@ -11968,43 +12191,57 @@ for (const _m of Object.values(MODELS)) {
11968
12191
  _m.pricingUnknown = false;
11969
12192
  }
11970
12193
  var T1_MODEL_PRIORITY = [
12194
+ "gpt-5.6-sol",
12195
+ "claude-fable-5",
12196
+ "claude-opus-5",
11971
12197
  "gpt-5.5",
11972
12198
  "gpt-5.4",
12199
+ "gemini-3.1-pro-preview",
12200
+ "claude-opus-4-8",
12201
+ "claude-sonnet-5",
12202
+ "gemini-3.7-flash",
12203
+ "gpt-5.2",
11973
12204
  "gpt-5",
11974
- "claude-opus-4",
11975
- "claude-sonnet-4",
11976
12205
  "gemini-2.5-pro",
11977
12206
  "gpt-4.1",
11978
- "gpt-4o",
11979
- "gemini-1.5-pro"
12207
+ "gpt-4o"
11980
12208
  ];
11981
12209
  var T2_MODEL_PRIORITY = [
12210
+ "gpt-5.6-terra",
12211
+ "claude-sonnet-5",
12212
+ "gemini-3.7-flash",
12213
+ "gemini-3.6-flash",
11982
12214
  "gpt-5.4-mini",
11983
12215
  "gpt-5-mini",
11984
- "claude-sonnet-4",
12216
+ "claude-sonnet-4-6",
11985
12217
  "claude-haiku-4-5",
11986
- "claude-haiku-3-5",
12218
+ "gemini-3.5-flash",
11987
12219
  "gemini-2.5-flash",
11988
12220
  "gpt-4.1-mini",
11989
12221
  "gpt-4o-mini",
11990
- "gemini-2.0-flash",
11991
12222
  "llama3:70b"
11992
12223
  ];
11993
12224
  var T3_MODEL_PRIORITY = [
12225
+ "gpt-5.6-luna",
12226
+ "gpt-5.4-nano",
12227
+ "gemini-3.5-flash-lite",
12228
+ "gemini-3.1-flash-lite",
12229
+ "claude-haiku-4-5",
11994
12230
  "gpt-5-nano",
12231
+ "gemini-2.5-flash-lite",
11995
12232
  "llama3.2:3b",
11996
12233
  "mistral:7b",
11997
- "claude-haiku-4-5",
11998
- "claude-haiku-3-5",
11999
12234
  "gpt-4.1-nano",
12000
12235
  "gpt-4o-mini",
12001
- "gemini-2.5-flash",
12002
- "gemini-2.0-flash"
12236
+ "gemini-2.5-flash"
12003
12237
  ];
12004
12238
  var VISION_MODEL_PRIORITY = [
12005
- "claude-sonnet-4",
12239
+ "gpt-5.6-sol",
12240
+ "claude-fable-5",
12241
+ "claude-sonnet-5",
12242
+ "gemini-3.7-flash",
12006
12243
  "gpt-4o",
12007
- "gemini-2.0-flash",
12244
+ "gemini-2.5-flash",
12008
12245
  "llava"
12009
12246
  ];
12010
12247
  var COMPLEXITY_T2_COUNT = {
@@ -12185,7 +12422,7 @@ function isChatModel(id, methods) {
12185
12422
  return !NON_CHAT.test(id);
12186
12423
  }
12187
12424
  try {
12188
- dns__default.default.setDefaultResultOrder("ipv4first");
12425
+ dnsCallback__default.default.setDefaultResultOrder("ipv4first");
12189
12426
  } catch {
12190
12427
  }
12191
12428
  function preferIpv4Host(url) {
@@ -12201,8 +12438,8 @@ function normalizeEndpoint(url) {
12201
12438
  if (!raw) return "";
12202
12439
  try {
12203
12440
  const u = new URL(raw);
12204
- const path29 = stripTrailingSlashes(`${u.pathname}${u.search}`);
12205
- return `${u.protocol}//${u.host.toLowerCase()}${path29}`;
12441
+ const path30 = stripTrailingSlashes(`${u.pathname}${u.search}`);
12442
+ return `${u.protocol}//${u.host.toLowerCase()}${path30}`;
12206
12443
  } catch {
12207
12444
  return raw.toLowerCase();
12208
12445
  }
@@ -12869,52 +13106,127 @@ var OpenAIProvider = class extends BaseProvider {
12869
13106
  }
12870
13107
  };
12871
13108
 
13109
+ // src/providers/cloud-model-catalog.json
13110
+ var cloud_model_catalog_default = {
13111
+ models: [
13112
+ { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
13113
+ { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
13114
+ { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
13115
+ { id: "gpt-5.5", name: "GPT-5.5", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
13116
+ { id: "gpt-5.4", name: "GPT-5.4", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 105e4, maxOutputTokens: 128e3, vision: true, toolUse: true },
13117
+ { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
13118
+ { id: "gpt-5.4-nano", name: "GPT-5.4 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
13119
+ { id: "gpt-5", name: "GPT-5", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
13120
+ { id: "gpt-5-mini", name: "GPT-5 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
13121
+ { id: "gpt-5-nano", name: "GPT-5 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 4e5, maxOutputTokens: 128e3, vision: true, toolUse: true },
13122
+ { id: "gpt-4.1", name: "GPT-4.1", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
13123
+ { id: "gpt-4.1-mini", name: "GPT-4.1 Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
13124
+ { id: "gpt-4.1-nano", name: "GPT-4.1 Nano", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 1047576, maxOutputTokens: 32768, vision: true, toolUse: true },
13125
+ { id: "gpt-4o", name: "GPT-4o", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 128e3, maxOutputTokens: 16384, vision: true, toolUse: true },
13126
+ { id: "gpt-4o-mini", name: "GPT-4o Mini", providers: ["openai", "azure"], lifecycle: "ga", contextWindow: 128e3, maxOutputTokens: 16384, vision: true, toolUse: true },
13127
+ { id: "claude-fable-5", name: "Claude Fable 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13128
+ { id: "claude-opus-5", name: "Claude Opus 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13129
+ { id: "claude-opus-4-8", name: "Claude Opus 4.8", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13130
+ { id: "claude-opus-4-7", name: "Claude Opus 4.7", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13131
+ { id: "claude-opus-4-6", name: "Claude Opus 4.6", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, vision: true, toolUse: true },
13132
+ { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, vision: true, toolUse: true },
13133
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13134
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", providers: ["anthropic"], lifecycle: "ga", contextWindow: 1e6, maxOutputTokens: 128e3, vision: true, toolUse: true },
13135
+ { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, vision: true, toolUse: true },
13136
+ { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", providers: ["anthropic"], lifecycle: "ga", contextWindow: 2e5, maxOutputTokens: 64e3, vision: true, toolUse: true },
13137
+ { id: "gemini-3.7-flash", name: "Gemini 3.7 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13138
+ { id: "gemini-3.6-flash", name: "Gemini 3.6 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13139
+ { id: "gemini-3.5-flash", name: "Gemini 3.5 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13140
+ { id: "gemini-3.5-flash-lite", name: "Gemini 3.5 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13141
+ { id: "gemini-3.1-flash-lite", name: "Gemini 3.1 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13142
+ { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro", providers: ["gemini"], lifecycle: "preview", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13143
+ { id: "gemini-3-flash-preview", name: "Gemini 3 Flash", providers: ["gemini"], lifecycle: "preview", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13144
+ { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13145
+ { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true },
13146
+ { id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash-Lite", providers: ["gemini"], lifecycle: "ga", contextWindow: 1048576, maxOutputTokens: 65536, vision: true, toolUse: true }
13147
+ ]
13148
+ };
13149
+
12872
13150
  // src/providers/azure.ts
12873
13151
  var DEFAULT_AZURE_API_VERSION = "2024-12-01-preview";
12874
- var AZURE_BASE_MODEL_RULES = [
12875
- [/gpt-?5\.5/, "gpt-5.5"],
12876
- [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
12877
- [/gpt-?5\.4/, "gpt-5.4"],
12878
- [/gpt-?5.*nano/, "gpt-5-nano"],
12879
- [/gpt-?5.*mini/, "gpt-5-mini"],
12880
- [/gpt-?5/, "gpt-5"],
12881
- [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
12882
- [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
12883
- [/gpt-?4\.1/, "gpt-4.1"],
12884
- [/gpt-?4o-mini/, "gpt-4o-mini"],
12885
- [/gpt-?4o/, "gpt-4o"]
12886
- ];
12887
- var AZURE_BASE_MODELS = AZURE_BASE_MODEL_RULES.map(([, id]) => id);
13152
+ var CLOUD_MODELS = cloud_model_catalog_default.models;
13153
+ 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));
13154
+ var AZURE_BASE_MODELS = AZURE_CATALOG.map((m) => m.id);
13155
+ function compactModelId(id) {
13156
+ return id.toLowerCase().replace(/[^a-z0-9.]/g, "");
13157
+ }
13158
+ function catalogEntry(id) {
13159
+ const n = id.toLowerCase();
13160
+ return CLOUD_MODELS.find((m) => m.id.toLowerCase() === n);
13161
+ }
12888
13162
  function inferAzureBaseModel(deploymentName) {
12889
13163
  const n = deploymentName.toLowerCase();
12890
- for (const [re, base] of AZURE_BASE_MODEL_RULES) if (re.test(n)) return base;
13164
+ const compact = compactModelId(n);
13165
+ const hasBareGpt56Alias = compact.includes("gpt5.6");
13166
+ for (const model of AZURE_CATALOG) {
13167
+ if (hasBareGpt56Alias && model.id === "gpt-5") continue;
13168
+ if (n.includes(model.id.toLowerCase()) || compact.includes(compactModelId(model.id))) {
13169
+ return model.id;
13170
+ }
13171
+ }
13172
+ if (hasBareGpt56Alias) return "gpt-5.6-sol";
13173
+ const gpt5At = compact.indexOf("gpt5");
13174
+ if (gpt5At !== -1) {
13175
+ const suffix = compact.slice(gpt5At + "gpt5".length);
13176
+ if (suffix.includes("nano")) return "gpt-5-nano";
13177
+ if (suffix.includes("mini")) return "gpt-5-mini";
13178
+ return "gpt-5";
13179
+ }
13180
+ if (compact.includes("gpt4.1nano")) return "gpt-4.1-nano";
13181
+ if (compact.includes("gpt4.1mini")) return "gpt-4.1-mini";
13182
+ if (compact.includes("gpt4.1")) return "gpt-4.1";
13183
+ if (compact.includes("gpt4omini")) return "gpt-4o-mini";
13184
+ if (compact.includes("gpt4o")) return "gpt-4o";
12891
13185
  return null;
12892
13186
  }
13187
+ function catalogModelInfo(baseModelId) {
13188
+ const entry = catalogEntry(baseModelId);
13189
+ if (!entry?.contextWindow) return void 0;
13190
+ return {
13191
+ id: entry.id,
13192
+ name: entry.name,
13193
+ provider: "openai",
13194
+ contextWindow: entry.contextWindow,
13195
+ isVisionCapable: entry.vision ?? false,
13196
+ inputCostPer1kTokens: 0,
13197
+ outputCostPer1kTokens: 0,
13198
+ maxOutputTokens: entry.maxOutputTokens ?? 16e3,
13199
+ supportsStreaming: true,
13200
+ isLocal: false,
13201
+ supportsToolUse: entry.toolUse ?? true,
13202
+ pricingUnknown: true
13203
+ };
13204
+ }
12893
13205
  function azureModelForDeployment(cfg) {
12894
13206
  if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
12895
13207
  const id = cfg.deploymentName.trim();
12896
13208
  const name = cfg.label?.trim() || id;
12897
13209
  const baseModelId = cfg.model?.trim() || inferAzureBaseModel(id) || void 0;
12898
- const base = baseModelId ? MODELS[baseModelId] : void 0;
12899
- if (base) {
13210
+ const base = baseModelId ? MODELS[baseModelId] ?? catalogModelInfo(baseModelId) : void 0;
13211
+ if (base && baseModelId) {
12900
13212
  const azurePrice = resolvePricing(
12901
13213
  { id, provider: "azure", isLocal: false, baseModelId },
12902
13214
  { region: cfg.region }
12903
13215
  );
13216
+ const exactAzurePrice = !azurePrice.unknown && !azurePrice.estimatedFromProvider;
13217
+ const nativeToolsOnThisTransport = /^gpt-5\.6-(?:sol|terra|luna)$/i.test(baseModelId) ? false : base.supportsToolUse ?? true;
12904
13218
  return {
12905
13219
  ...base,
12906
13220
  id,
12907
- // callable deployment name
12908
13221
  name,
12909
13222
  provider: "azure",
12910
13223
  baseModelId,
12911
- // real identity for benchmark + live pricing
12912
- supportsToolUse: base.supportsToolUse ?? true,
12913
- ...azurePrice.unknown ? {} : {
13224
+ supportsToolUse: nativeToolsOnThisTransport,
13225
+ ...!azurePrice.unknown ? {
12914
13226
  inputCostPer1kTokens: azurePrice.input,
12915
13227
  outputCostPer1kTokens: azurePrice.output,
12916
- pricingUnknown: false
12917
- }
13228
+ pricingUnknown: !exactAzurePrice
13229
+ } : { pricingUnknown: true }
12918
13230
  };
12919
13231
  }
12920
13232
  return {
@@ -12929,18 +13241,23 @@ function azureModelForDeployment(cfg) {
12929
13241
  maxOutputTokens: 16e3,
12930
13242
  supportsStreaming: true,
12931
13243
  isLocal: false,
12932
- supportsToolUse: true
13244
+ supportsToolUse: true,
13245
+ pricingUnknown: true
12933
13246
  };
12934
13247
  }
13248
+ function stripTrailingSlashes3(value) {
13249
+ let end = value.length;
13250
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
13251
+ return value.slice(0, end);
13252
+ }
12935
13253
  var AzureOpenAIProvider = class extends OpenAIProvider {
12936
13254
  constructor(config, model) {
12937
13255
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
12938
- const endpoint = rawUrl.replace(/\/+$/, "");
13256
+ const endpoint = stripTrailingSlashes3(rawUrl);
12939
13257
  super(
12940
13258
  {
12941
13259
  ...config,
12942
13260
  baseUrl: endpoint
12943
- // Kept for superclass compatibility if it reads it
12944
13261
  },
12945
13262
  model
12946
13263
  );
@@ -14632,13 +14949,13 @@ var DeadModelStore = class {
14632
14949
  function fileDeadModelPersistence(filePath) {
14633
14950
  return {
14634
14951
  load() {
14635
- if (!fs26.existsSync(filePath)) return [];
14636
- const raw = JSON.parse(fs26.readFileSync(filePath, "utf-8"));
14952
+ if (!fs27.existsSync(filePath)) return [];
14953
+ const raw = JSON.parse(fs27.readFileSync(filePath, "utf-8"));
14637
14954
  return Array.isArray(raw) ? raw : [];
14638
14955
  },
14639
14956
  save(records) {
14640
- fs26.mkdirSync(path27.dirname(filePath), { recursive: true });
14641
- fs26.writeFileSync(filePath, JSON.stringify(records, null, 2));
14957
+ fs27.mkdirSync(path28.dirname(filePath), { recursive: true });
14958
+ fs27.writeFileSync(filePath, JSON.stringify(records, null, 2));
14642
14959
  }
14643
14960
  };
14644
14961
  }
@@ -14683,7 +15000,7 @@ var Keystore = class {
14683
15000
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
14684
15001
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
14685
15002
  this.backend = "keytar";
14686
- if (password && fs26__default.default.existsSync(this.storePath)) {
15003
+ if (password && fs27__default.default.existsSync(this.storePath)) {
14687
15004
  try {
14688
15005
  const fileEntries = this.decryptFile(password);
14689
15006
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -14702,7 +15019,7 @@ var Keystore = class {
14702
15019
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
14703
15020
  );
14704
15021
  }
14705
- if (!fs26__default.default.existsSync(this.storePath)) {
15022
+ if (!fs27__default.default.existsSync(this.storePath)) {
14706
15023
  const salt = crypto__default.default.randomBytes(SALT_LEN);
14707
15024
  this.masterKey = this.deriveKey(password, salt);
14708
15025
  this.writeWithSalt({}, salt);
@@ -14716,7 +15033,7 @@ var Keystore = class {
14716
15033
  }
14717
15034
  /** Synchronous legacy unlock kept for AES-only environments. */
14718
15035
  unlockSync(password) {
14719
- if (!fs26__default.default.existsSync(this.storePath)) {
15036
+ if (!fs27__default.default.existsSync(this.storePath)) {
14720
15037
  const salt = crypto__default.default.randomBytes(SALT_LEN);
14721
15038
  this.masterKey = this.deriveKey(password, salt);
14722
15039
  this.writeWithSalt({}, salt);
@@ -14774,7 +15091,7 @@ var Keystore = class {
14774
15091
  }
14775
15092
  }
14776
15093
  decryptFile(password, knownSalt) {
14777
- if (!fs26__default.default.existsSync(this.storePath)) return {};
15094
+ if (!fs27__default.default.existsSync(this.storePath)) return {};
14778
15095
  try {
14779
15096
  const { salt, ciphertext, iv, tag } = this.readRaw();
14780
15097
  const useSalt = knownSalt ?? salt;
@@ -14796,8 +15113,8 @@ var Keystore = class {
14796
15113
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
14797
15114
  const tag = cipher.getAuthTag();
14798
15115
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
14799
- fs26__default.default.mkdirSync(path27__default.default.dirname(this.storePath), { recursive: true });
14800
- fs26__default.default.writeFileSync(this.storePath, out, { mode: 384 });
15116
+ fs27__default.default.mkdirSync(path28__default.default.dirname(this.storePath), { recursive: true });
15117
+ fs27__default.default.writeFileSync(this.storePath, out, { mode: 384 });
14801
15118
  }
14802
15119
  writeWithSalt(data, salt) {
14803
15120
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -14807,11 +15124,11 @@ var Keystore = class {
14807
15124
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
14808
15125
  const tag = cipher.getAuthTag();
14809
15126
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
14810
- fs26__default.default.mkdirSync(path27__default.default.dirname(this.storePath), { recursive: true });
14811
- fs26__default.default.writeFileSync(this.storePath, out, { mode: 384 });
15127
+ fs27__default.default.mkdirSync(path28__default.default.dirname(this.storePath), { recursive: true });
15128
+ fs27__default.default.writeFileSync(this.storePath, out, { mode: 384 });
14812
15129
  }
14813
15130
  readRaw() {
14814
- const buf = fs26__default.default.readFileSync(this.storePath);
15131
+ const buf = fs27__default.default.readFileSync(this.storePath);
14815
15132
  let offset = 0;
14816
15133
  const salt = buf.subarray(offset, offset + SALT_LEN);
14817
15134
  offset += SALT_LEN;
@@ -14844,7 +15161,7 @@ var CascadeIgnore = class {
14844
15161
  ]);
14845
15162
  }
14846
15163
  async load(workspacePath) {
14847
- const filePath = path27__default.default.join(workspacePath, ".cascadeignore");
15164
+ const filePath = path28__default.default.join(workspacePath, ".cascadeignore");
14848
15165
  try {
14849
15166
  const content = await fs11__default.default.readFile(filePath, "utf-8");
14850
15167
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -14855,7 +15172,7 @@ var CascadeIgnore = class {
14855
15172
  }
14856
15173
  isIgnored(filePath, workspacePath) {
14857
15174
  try {
14858
- const relative = workspacePath ? path27__default.default.relative(workspacePath, filePath) : filePath;
15175
+ const relative = workspacePath ? path28__default.default.relative(workspacePath, filePath) : filePath;
14859
15176
  return this.ig.ignores(relative);
14860
15177
  } catch {
14861
15178
  return false;
@@ -14866,7 +15183,7 @@ var CascadeIgnore = class {
14866
15183
  }
14867
15184
  };
14868
15185
  async function loadCascadeMd(workspacePath) {
14869
- const filePath = path27__default.default.join(workspacePath, "CASCADE.md");
15186
+ const filePath = path28__default.default.join(workspacePath, "CASCADE.md");
14870
15187
  try {
14871
15188
  const raw = await fs11__default.default.readFile(filePath, "utf-8");
14872
15189
  return parseCascadeMd(raw);
@@ -14897,7 +15214,7 @@ ${raw.trim()}`;
14897
15214
  var MemoryStore = class _MemoryStore {
14898
15215
  db;
14899
15216
  constructor(dbPath) {
14900
- fs26__default.default.mkdirSync(path27__default.default.dirname(dbPath), { recursive: true });
15217
+ fs27__default.default.mkdirSync(path28__default.default.dirname(dbPath), { recursive: true });
14901
15218
  try {
14902
15219
  this.db = new Database2__default.default(dbPath, { timeout: 5e3 });
14903
15220
  this.db.pragma("journal_mode = WAL");
@@ -15795,13 +16112,38 @@ var WebSearchConfigSchema = zod.z.object({
15795
16112
  /** Tavily API key — get one at https://tavily.com */
15796
16113
  tavilyApiKey: zod.z.string().optional(),
15797
16114
  /** Max results per search (default 5) */
15798
- maxResults: zod.z.number().default(5)
16115
+ maxResults: zod.z.number().default(5),
16116
+ /**
16117
+ * Send `searxngUrl` through the SSRF guard. Off for a local run, where the
16118
+ * URL is the machine owner's own config and a self-hosted SearXNG is
16119
+ * routinely on a private address; on for a host that accepts it from a
16120
+ * request. See `src/tools/web-search.ts`.
16121
+ *
16122
+ * It has to be named HERE, not just on the TypeScript interfaces: the config
16123
+ * passes through `CascadeConfigSchema.parse()` on its way into a run, and
16124
+ * zod drops every key this object does not declare.
16125
+ */
16126
+ guardSearxngUrl: zod.z.boolean().optional()
15799
16127
  });
15800
16128
  var ToolsConfigSchema = zod.z.object({
15801
16129
  shellAllowlist: zod.z.array(zod.z.string()).default([]),
15802
16130
  shellBlocklist: zod.z.array(zod.z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
15803
16131
  requireApprovalFor: zod.z.array(zod.z.string()).default([]),
15804
16132
  browserEnabled: zod.z.boolean().default(false),
16133
+ /**
16134
+ * Let a run ACT on the page open in the desktop's built-in browser — click,
16135
+ * type, submit — not just read it.
16136
+ *
16137
+ * Distinct from `browserEnabled`, which turns on the headless Playwright
16138
+ * tool. That one drives a throwaway browser with an empty cookie jar; this
16139
+ * one drives the session the user is signed into, which is a different order
16140
+ * of consequence and so gets its own switch rather than riding on that one.
16141
+ *
16142
+ * Off by default. Turning it on does not hand anything over silently: the
16143
+ * tool still goes through the approval chain, and the browser panel carries a
16144
+ * stop control for the run that is acting.
16145
+ */
16146
+ agentBrowserControl: zod.z.boolean().default(false),
15805
16147
  mcpServers: zod.z.array(McpServerConfigSchema).optional(),
15806
16148
  mcpTrusted: zod.z.array(zod.z.string()).optional(),
15807
16149
  /** Web search backends — at least one should be configured for best results */
@@ -16199,7 +16541,7 @@ function credentialEndpointsConflict(type, rowEndpoint, credentialEndpoint) {
16199
16541
 
16200
16542
  // src/config/global-credentials.ts
16201
16543
  function credentialsPath(globalDir2) {
16202
- return path27__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
16544
+ return path28__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
16203
16545
  }
16204
16546
  function providerKey(p) {
16205
16547
  return p.type;
@@ -16229,7 +16571,7 @@ function isPersistable(p) {
16229
16571
  }
16230
16572
  function loadGlobalCredentials(globalDir2) {
16231
16573
  try {
16232
- const raw = fs26__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
16574
+ const raw = fs27__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
16233
16575
  const parsed = JSON.parse(raw);
16234
16576
  if (!Array.isArray(parsed.providers)) return [];
16235
16577
  return parsed.providers.filter(
@@ -16241,13 +16583,13 @@ function loadGlobalCredentials(globalDir2) {
16241
16583
  }
16242
16584
  function saveGlobalCredentials(globalDir2, providers) {
16243
16585
  const filePath = credentialsPath(globalDir2);
16244
- fs26__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
16586
+ fs27__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
16245
16587
  const body = { version: 1, providers: providers.filter(isPersistable) };
16246
16588
  const tmp = `${filePath}.tmp`;
16247
- fs26__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
16248
- fs26__default.default.renameSync(tmp, filePath);
16589
+ fs27__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
16590
+ fs27__default.default.renameSync(tmp, filePath);
16249
16591
  try {
16250
- fs26__default.default.chmodSync(filePath, 384);
16592
+ fs27__default.default.chmodSync(filePath, 384);
16251
16593
  } catch {
16252
16594
  }
16253
16595
  }
@@ -16648,7 +16990,7 @@ var ConfigManager = class {
16648
16990
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
16649
16991
  constructor(workspacePath = process.cwd(), globalDirOverride) {
16650
16992
  this.workspacePath = workspacePath;
16651
- this.globalDir = globalDirOverride ?? path27__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
16993
+ this.globalDir = globalDirOverride ?? path28__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
16652
16994
  }
16653
16995
  async load() {
16654
16996
  this.config = await this.loadConfig();
@@ -16662,8 +17004,8 @@ var ConfigManager = class {
16662
17004
  this.ignore = new CascadeIgnore();
16663
17005
  await this.ignore.load(this.workspacePath);
16664
17006
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
16665
- this.keystore = new Keystore(path27__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
16666
- this.store = new MemoryStore(path27__default.default.join(this.workspacePath, CASCADE_DB_FILE));
17007
+ this.keystore = new Keystore(path28__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
17008
+ this.store = new MemoryStore(path28__default.default.join(this.workspacePath, CASCADE_DB_FILE));
16667
17009
  const globalRevoked = stripRevokedCredentials(loadGlobalCredentials(this.globalDir));
16668
17010
  const globalCreds = filterRetiredCredentials(globalRevoked.kept);
16669
17011
  if (globalRevoked.removed > 0) this.revokedCredentials += globalRevoked.removed;
@@ -16736,8 +17078,8 @@ var ConfigManager = class {
16736
17078
  * never reached the disk (see `commitSettings`).
16737
17079
  */
16738
17080
  async save(config = this.config) {
16739
- const configPath = path27__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16740
- await fs11__default.default.mkdir(path27__default.default.dirname(configPath), { recursive: true });
17081
+ const configPath = path28__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
17082
+ await fs11__default.default.mkdir(path28__default.default.dirname(configPath), { recursive: true });
16741
17083
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
16742
17084
  try {
16743
17085
  saveGlobalCredentials(this.globalDir, config.providers);
@@ -16787,7 +17129,7 @@ var ConfigManager = class {
16787
17129
  this.revokedCredentials = 0;
16788
17130
  this.revokedPins = [];
16789
17131
  this.revokedNotice = void 0;
16790
- const configPath = path27__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
17132
+ const configPath = path28__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16791
17133
  try {
16792
17134
  const raw = await fs11__default.default.readFile(configPath, "utf-8");
16793
17135
  const parsed = JSON.parse(raw);
@@ -16881,7 +17223,7 @@ var ConfigManager = class {
16881
17223
  return onResource;
16882
17224
  }
16883
17225
  async persistClearedPins(tiers) {
16884
- const configPath = path27__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
17226
+ const configPath = path28__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16885
17227
  try {
16886
17228
  const raw = JSON.parse(await fs11__default.default.readFile(configPath, "utf-8"));
16887
17229
  const models = raw["models"];
@@ -17475,7 +17817,7 @@ function computeDelegationSavings(stats, t1Model) {
17475
17817
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
17476
17818
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
17477
17819
  var FETCH_TIMEOUT_MS = 8e3;
17478
- var DEFAULT_CACHE_FILE = path27__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
17820
+ var DEFAULT_CACHE_FILE = path28__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
17479
17821
  function normalizeModelId2(id) {
17480
17822
  let s = id.toLowerCase();
17481
17823
  const slash = s.lastIndexOf("/");
@@ -17622,7 +17964,7 @@ var LiveDataProvider = class {
17622
17964
  }
17623
17965
  async saveCache() {
17624
17966
  try {
17625
- await fs11__default.default.mkdir(path27__default.default.dirname(this.opts.cacheFile), { recursive: true });
17967
+ await fs11__default.default.mkdir(path28__default.default.dirname(this.opts.cacheFile), { recursive: true });
17626
17968
  const cache = {
17627
17969
  fetchedAt: this.fetchedAt,
17628
17970
  snapshot: this.snapshot ?? void 0,
@@ -17729,9 +18071,15 @@ var FAMILY_BENCHMARKS = {
17729
18071
  "claude-opus": { code: 95, analysis: 92, creative: 90, data: 88 },
17730
18072
  "claude-sonnet": { code: 93, analysis: 88, creative: 87, data: 85 },
17731
18073
  "claude-haiku": { code: 80, analysis: 75, creative: 76, data: 72 },
17732
- // OpenAI GPT-5 family — current flagship; strongest all-round. Offline
17733
- // baseline (the live fetch refreshes these). Distinct point releases carry
17734
- // their own scores: gpt-5.5 "Spud" leads SWE-bench (88.7%); gpt-5.4 ~80%.
18074
+ // OpenAI GPT-5 family. GPT-5.6 is split by durable capability tier instead
18075
+ // of being folded into generic gpt-5: Sol, Terra and Luna have materially
18076
+ // different benchmark/cost envelopes and should compete independently.
18077
+ // Scores are the committed normalized snapshot; provenance is in
18078
+ // scripts/benchmarks/sources/openai-gpt-5.6-2026-08.json.
18079
+ "gpt-5.6-sol": { code: 100, analysis: 98, creative: 88, data: 88 },
18080
+ "gpt-5.6-terra": { code: 97, analysis: 92, creative: 82, data: 85 },
18081
+ "gpt-5.6-luna": { code: 93, analysis: 85, creative: 74, data: 60 },
18082
+ // Older GPT-5 point releases remain distinct where public data supports it.
17735
18083
  "gpt-5.5": { code: 97, analysis: 96, creative: 94, data: 95 },
17736
18084
  "gpt-5.4": { code: 94, analysis: 94, creative: 92, data: 92 },
17737
18085
  "gpt-5.4-mini": { code: 86, analysis: 85, creative: 85, data: 83 },
@@ -17767,13 +18115,19 @@ var FAMILY_BENCHMARKS = {
17767
18115
  "llama-small": { code: 55, analysis: 56, creative: 60, data: 54 }
17768
18116
  };
17769
18117
  var FAMILY_MATCHERS = [
18118
+ [/fable/i, "claude-opus"],
17770
18119
  [/opus/i, "claude-opus"],
17771
18120
  [/sonnet/i, "claude-sonnet"],
17772
18121
  [/haiku/i, "claude-haiku"],
17773
- // GPT-5 family — ordered most-specific first. Distinct point releases
17774
- // (gpt-5.5, gpt-5.4, gpt-5.4-mini) match their own family; nano/mini before
17775
- // the base; unrecognised gpt-5.x still fold into the gpt-5 base.
18122
+ // GPT-5 family — ordered most-specific first. GPT-5.6 durable tiers carry
18123
+ // their own profiles. 5.4 Nano intentionally maps to the nano class rather
18124
+ // than being swallowed by the generic 5.4 rule.
18125
+ [/gpt-?5\.6.*sol/i, "gpt-5.6-sol"],
18126
+ [/gpt-?5\.6.*terra/i, "gpt-5.6-terra"],
18127
+ [/gpt-?5\.6.*luna/i, "gpt-5.6-luna"],
18128
+ [/gpt-?5\.6(?:\s|$)/i, "gpt-5.6-sol"],
17776
18129
  [/gpt-?5\.5/i, "gpt-5.5"],
18130
+ [/gpt-?5\.4.*nano/i, "gpt-5-nano"],
17777
18131
  [/gpt-?5\.4.*mini/i, "gpt-5.4-mini"],
17778
18132
  [/gpt-?5\.4/i, "gpt-5.4"],
17779
18133
  [/gpt-?5.*nano/i, "gpt-5-nano"],
@@ -20574,6 +20928,23 @@ Now execute your subtask using this context where relevant.`
20574
20928
  this.setStatus("FAILED", finalOutput);
20575
20929
  this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput, "FAILED");
20576
20930
  return this.buildResult("ESCALATED", finalOutput, { checksRun, passed, failed }, issues, correctionAttempts);
20931
+ } finally {
20932
+ this.releaseBrowserControl();
20933
+ }
20934
+ }
20935
+ /**
20936
+ * End this worker's hold on the shared browser.
20937
+ *
20938
+ * Duck-typed rather than imported: `browser_control` is registered only by a
20939
+ * host that HAS a browser (the desktop), so in the CLI and hosted runs there
20940
+ * is no such tool and this is a no-op. Importing the class to instanceof-check
20941
+ * it would couple the worker to a tool that usually is not there.
20942
+ */
20943
+ releaseBrowserControl() {
20944
+ try {
20945
+ const tool = this.toolRegistry.getTool?.("browser_control");
20946
+ tool?.releaseActor?.(this.id);
20947
+ } catch {
20577
20948
  }
20578
20949
  }
20579
20950
  sendToPeer(toId, content) {
@@ -20832,8 +21203,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
20832
21203
  // Media generation can run for a minute; without this a cancelled run
20833
21204
  // still pays for an image nobody will see.
20834
21205
  ...this.signal ? { signal: this.signal } : {},
20835
- saveSnapshot: async (path29, content) => {
20836
- this.store?.addFileSnapshot(this.taskId, path29, content);
21206
+ saveSnapshot: async (path30, content) => {
21207
+ this.store?.addFileSnapshot(this.taskId, path30, content);
20837
21208
  },
20838
21209
  sendPeerSync: (to, syncType, content) => {
20839
21210
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -21036,7 +21407,7 @@ ${assignment.expectedOutput}`;
21036
21407
  const { promisify: promisify4 } = await import('util');
21037
21408
  const execFileAsync3 = promisify4(execFile4);
21038
21409
  for (const artifactPath of artifactPaths) {
21039
- const absolutePath = path27__default.default.resolve(this.artifactRoot(), artifactPath);
21410
+ const absolutePath = path28__default.default.resolve(this.artifactRoot(), artifactPath);
21040
21411
  try {
21041
21412
  const stat = await fs11__default.default.stat(absolutePath);
21042
21413
  if (!stat.isFile()) {
@@ -21071,7 +21442,7 @@ ${assignment.expectedOutput}`;
21071
21442
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
21072
21443
  continue;
21073
21444
  }
21074
- const ext = path27__default.default.extname(absolutePath).toLowerCase();
21445
+ const ext = path28__default.default.extname(absolutePath).toLowerCase();
21075
21446
  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;
21076
21447
  if (check) {
21077
21448
  try {
@@ -21163,7 +21534,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
21163
21534
  async checkAcceptance(assignment) {
21164
21535
  const criteria = assignment.acceptance ?? [];
21165
21536
  if (!criteria.length) return [];
21166
- const resolve2 = (target) => path27__default.default.resolve(this.artifactRoot(), target);
21537
+ const resolve2 = (target) => path28__default.default.resolve(this.artifactRoot(), target);
21167
21538
  const workerCanWriteFiles = canProduceFiles(this.tools.map((t) => t.name));
21168
21539
  return evaluateAcceptance(criteria, assignment.files ?? [], {
21169
21540
  stat: async (target) => {
@@ -24467,16 +24838,16 @@ function resolveInWorkspace(workspaceRoot, input) {
24467
24838
  if (typeof input !== "string" || input.length === 0) {
24468
24839
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
24469
24840
  }
24470
- const root = path27__default.default.resolve(workspaceRoot);
24471
- const abs = path27__default.default.isAbsolute(input) ? path27__default.default.resolve(input) : path27__default.default.resolve(root, input);
24472
- const rel = path27__default.default.relative(root, abs);
24473
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path27__default.default.isAbsolute(rel)) {
24841
+ const root = path28__default.default.resolve(workspaceRoot);
24842
+ const abs = path28__default.default.isAbsolute(input) ? path28__default.default.resolve(input) : path28__default.default.resolve(root, input);
24843
+ const rel = path28__default.default.relative(root, abs);
24844
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path28__default.default.isAbsolute(rel)) {
24474
24845
  throw new WorkspaceSandboxError(input, root);
24475
24846
  }
24476
24847
  try {
24477
- const real = fs26__default.default.realpathSync(abs);
24478
- const realRel = path27__default.default.relative(root, real);
24479
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path27__default.default.isAbsolute(realRel))) {
24848
+ const real = fs27__default.default.realpathSync(abs);
24849
+ const realRel = path28__default.default.relative(root, real);
24850
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path28__default.default.isAbsolute(realRel))) {
24480
24851
  throw new WorkspaceSandboxError(input, root);
24481
24852
  }
24482
24853
  } catch (e) {
@@ -25025,9 +25396,9 @@ async function animatePptx(bytes, scheme) {
25025
25396
  const zip = await import_jszip.default.loadAsync(bytes);
25026
25397
  const slidePaths = Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n));
25027
25398
  if (slidePaths.length === 0) return bytes;
25028
- for (const path29 of slidePaths) {
25029
- const xml = await zip.file(path29).async("string");
25030
- zip.file(path29, animateSlideXml(xml, scheme));
25399
+ for (const path30 of slidePaths) {
25400
+ const xml = await zip.file(path30).async("string");
25401
+ zip.file(path30, animateSlideXml(xml, scheme));
25031
25402
  }
25032
25403
  return await zip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
25033
25404
  } catch {
@@ -25413,7 +25784,7 @@ var GenerateDocumentTool = class extends BaseTool {
25413
25784
  }
25414
25785
  }
25415
25786
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
25416
- await fs11__default.default.mkdir(path27__default.default.dirname(absPath), { recursive: true });
25787
+ await fs11__default.default.mkdir(path28__default.default.dirname(absPath), { recursive: true });
25417
25788
  await fs11__default.default.writeFile(absPath, bytes);
25418
25789
  const charts = countCharts(content);
25419
25790
  return [
@@ -25631,7 +26002,7 @@ var FileWriteTool = class extends BaseTool {
25631
26002
  } catch {
25632
26003
  }
25633
26004
  }
25634
- await fs11__default.default.mkdir(path27__default.default.dirname(absPath), { recursive: true });
26005
+ await fs11__default.default.mkdir(path28__default.default.dirname(absPath), { recursive: true });
25635
26006
  await fs11__default.default.writeFile(absPath, content, "utf-8");
25636
26007
  return `Written ${content.length} characters to ${filePath}`;
25637
26008
  }
@@ -25989,12 +26360,10 @@ ${data.description}`;
25989
26360
  }
25990
26361
  }
25991
26362
  };
25992
-
25993
- // src/tools/browser.ts
25994
26363
  var BROWSER_LAUNCH_TIMEOUT_MS = 15e3;
25995
26364
  var BrowserTool = class extends BaseTool {
25996
26365
  name = "browser";
25997
- description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Only available with multimodal models.";
26366
+ description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Screenshots are saved to a file for image_analyze to read.";
25998
26367
  inputSchema = {
25999
26368
  type: "object",
26000
26369
  properties: {
@@ -26061,7 +26430,14 @@ var BrowserTool = class extends BaseTool {
26061
26430
  }
26062
26431
  case "screenshot": {
26063
26432
  const buf = await page2.screenshot({ type: "png" });
26064
- return `data:image/png;base64,${buf.toString("base64")}`;
26433
+ const rel = path28__default.default.join(".cascade", "screenshots", `screenshot-${Date.now()}-${crypto.randomUUID().slice(0, 8)}.png`);
26434
+ const abs = resolveInWorkspace(this.workspaceRoot, rel);
26435
+ await fs11__default.default.mkdir(path28__default.default.dirname(abs), { recursive: true });
26436
+ await fs11__default.default.writeFile(abs, buf);
26437
+ return [
26438
+ `Screenshot saved to ${abs} (${Math.round(buf.byteLength / 1024)} KB).`,
26439
+ `Call image_analyze with that path to see what is on it \u2014 that needs a vision-capable model.`
26440
+ ].join(" ");
26065
26441
  }
26066
26442
  case "evaluate": {
26067
26443
  const result = await page2.evaluate(input["script"]);
@@ -26129,7 +26505,7 @@ var ImageAnalyzeTool = class extends BaseTool {
26129
26505
  };
26130
26506
  async function fileToImageAttachment(filePath) {
26131
26507
  const data = await fs11__default.default.readFile(filePath);
26132
- const ext = path27__default.default.extname(filePath).toLowerCase();
26508
+ const ext = path28__default.default.extname(filePath).toLowerCase();
26133
26509
  const mimeMap = {
26134
26510
  ".jpg": "image/jpeg",
26135
26511
  ".jpeg": "image/jpeg",
@@ -26163,14 +26539,14 @@ var PDFCreateTool = class extends BaseTool {
26163
26539
  const filePath = input["path"];
26164
26540
  const content = input["content"];
26165
26541
  const title = input["title"];
26166
- const dir = path27__default.default.dirname(filePath);
26167
- if (!fs26__default.default.existsSync(dir)) {
26168
- fs26__default.default.mkdirSync(dir, { recursive: true });
26542
+ const dir = path28__default.default.dirname(filePath);
26543
+ if (!fs27__default.default.existsSync(dir)) {
26544
+ fs27__default.default.mkdirSync(dir, { recursive: true });
26169
26545
  }
26170
26546
  return new Promise((resolve2, reject) => {
26171
26547
  try {
26172
26548
  const doc = new PDFDocument__default.default({ margin: 50 });
26173
- const stream = fs26__default.default.createWriteStream(filePath);
26549
+ const stream = fs27__default.default.createWriteStream(filePath);
26174
26550
  doc.pipe(stream);
26175
26551
  if (title) {
26176
26552
  doc.info["Title"] = title;
@@ -26248,22 +26624,22 @@ var CodeInterpreterTool = class extends BaseTool {
26248
26624
  }
26249
26625
  cmdPrefix = NODE_CMD;
26250
26626
  }
26251
- const tmpDir = path27__default.default.join(this.workspaceRoot, ".cascade", "tmp");
26252
- if (!fs26__default.default.existsSync(tmpDir)) {
26253
- fs26__default.default.mkdirSync(tmpDir, { recursive: true });
26627
+ const tmpDir = path28__default.default.join(this.workspaceRoot, ".cascade", "tmp");
26628
+ if (!fs27__default.default.existsSync(tmpDir)) {
26629
+ fs27__default.default.mkdirSync(tmpDir, { recursive: true });
26254
26630
  }
26255
26631
  const extension = language === "python" ? "py" : "js";
26256
26632
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
26257
- const filePath = path27__default.default.join(tmpDir, fileName);
26258
- fs26__default.default.writeFileSync(filePath, code, "utf-8");
26633
+ const filePath = path28__default.default.join(tmpDir, fileName);
26634
+ fs27__default.default.writeFileSync(filePath, code, "utf-8");
26259
26635
  const execArgs = [filePath, ...args];
26260
26636
  return new Promise((resolve2) => {
26261
26637
  const startMs = Date.now();
26262
26638
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
26263
26639
  const duration = Date.now() - startMs;
26264
26640
  try {
26265
- if (fs26__default.default.existsSync(filePath)) {
26266
- fs26__default.default.unlinkSync(filePath);
26641
+ if (fs27__default.default.existsSync(filePath)) {
26642
+ fs27__default.default.unlinkSync(filePath);
26267
26643
  }
26268
26644
  } catch (cleanupErr) {
26269
26645
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -26343,18 +26719,178 @@ ${formatted}`;
26343
26719
  return `Unknown action: ${action}`;
26344
26720
  }
26345
26721
  };
26722
+ var SsrfBlockedError = class extends Error {
26723
+ constructor(message) {
26724
+ super(message);
26725
+ this.name = "SsrfBlockedError";
26726
+ }
26727
+ };
26728
+ var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
26729
+ var MAX_REDIRECTS2 = 5;
26730
+ function allowLocal() {
26731
+ return process.env["CASCADE_ALLOW_LOCAL_FETCH"] === "1";
26732
+ }
26733
+ function isPrivateAddress(ip) {
26734
+ const type = net__default.default.isIP(ip);
26735
+ if (type === 4) return isPrivateIPv4(ip);
26736
+ if (type === 6) return isPrivateIPv6(ip);
26737
+ return true;
26738
+ }
26739
+ function isPrivateIPv4(ip) {
26740
+ const parts = ip.split(".").map((p) => Number(p));
26741
+ if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return true;
26742
+ const [a, b] = parts;
26743
+ if (a === 0) return true;
26744
+ if (a === 10) return true;
26745
+ if (a === 127) return true;
26746
+ if (a === 169 && b === 254) return true;
26747
+ if (a === 172 && b >= 16 && b <= 31) return true;
26748
+ if (a === 192 && b === 168) return true;
26749
+ if (a === 100 && b >= 64 && b <= 127) return true;
26750
+ if (a >= 224) return true;
26751
+ return false;
26752
+ }
26753
+ function ipv6Groups(ip) {
26754
+ let s = ip.toLowerCase().replace(/^\[|\]$/g, "");
26755
+ const zone = s.indexOf("%");
26756
+ if (zone !== -1) s = s.slice(0, zone);
26757
+ const dotted = /^(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
26758
+ if (dotted) {
26759
+ const o = [dotted[2], dotted[3], dotted[4], dotted[5]].map((p) => Number(p));
26760
+ if (o.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
26761
+ s = `${dotted[1]}${(o[0] << 8 | o[1]).toString(16)}:${(o[2] << 8 | o[3]).toString(16)}`;
26762
+ }
26763
+ const halves = s.split("::");
26764
+ if (halves.length > 2) return null;
26765
+ const split = (part) => part ? part.split(":") : [];
26766
+ const head = split(halves[0] ?? "");
26767
+ const tail = halves.length === 2 ? split(halves[1] ?? "") : [];
26768
+ const zeros = halves.length === 2 ? 8 - head.length - tail.length : 0;
26769
+ if (zeros < 0) return null;
26770
+ const groups = [...head, ...Array(zeros).fill("0"), ...tail];
26771
+ if (groups.length !== 8) return null;
26772
+ const out = [];
26773
+ for (const g of groups) {
26774
+ if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
26775
+ out.push(parseInt(g, 16));
26776
+ }
26777
+ return out;
26778
+ }
26779
+ function embeddedIPv4(g) {
26780
+ const quad = (hi, lo) => `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
26781
+ const leadingZero = (n) => g.slice(0, n).every((x) => x === 0);
26782
+ if (leadingZero(5) && g[5] === 65535) return quad(g[6], g[7]);
26783
+ if (leadingZero(6)) return quad(g[6], g[7]);
26784
+ if (g[0] === 100 && g[1] === 65435) {
26785
+ if (g[2] === 1) {
26786
+ return quad(g[3], (g[4] & 255) << 8 | g[5] >> 8);
26787
+ }
26788
+ if (g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0) return quad(g[6], g[7]);
26789
+ return null;
26790
+ }
26791
+ if (g[0] === 8194) return quad(g[1], g[2]);
26792
+ return null;
26793
+ }
26794
+ function isPrivateIPv6(ip) {
26795
+ const g = ipv6Groups(ip);
26796
+ if (!g) return true;
26797
+ const v4 = embeddedIPv4(g);
26798
+ if (v4) return isPrivateIPv4(v4);
26799
+ if (g[0] === 100 && g[1] === 65435) return true;
26800
+ if (g.every((x) => x === 0)) return true;
26801
+ if (leading7Zero(g) && g[7] === 1) return true;
26802
+ if ((g[0] & 65472) === 65152) return true;
26803
+ if ((g[0] & 65024) === 64512) return true;
26804
+ if ((g[0] & 65280) === 65280) return true;
26805
+ return false;
26806
+ }
26807
+ function leading7Zero(g) {
26808
+ return g.slice(0, 7).every((x) => x === 0);
26809
+ }
26810
+ async function assertPublicUrl(rawUrl) {
26811
+ let url;
26812
+ try {
26813
+ url = new URL(rawUrl);
26814
+ } catch {
26815
+ throw new SsrfBlockedError(`Invalid URL: ${rawUrl}`);
26816
+ }
26817
+ if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
26818
+ throw new SsrfBlockedError(`Blocked URL scheme "${url.protocol}" \u2014 only http and https are allowed.`);
26819
+ }
26820
+ if (allowLocal()) return url;
26821
+ const host = url.hostname.replace(/^\[|\]$/g, "");
26822
+ if (net__default.default.isIP(host)) {
26823
+ if (isPrivateAddress(host)) {
26824
+ throw new SsrfBlockedError(`Blocked request to non-public address ${host}.`);
26825
+ }
26826
+ return url;
26827
+ }
26828
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
26829
+ throw new SsrfBlockedError(`Blocked request to local hostname "${host}".`);
26830
+ }
26831
+ let addresses;
26832
+ try {
26833
+ const records = await dns2__default.default.lookup(host, { all: true });
26834
+ addresses = records.map((r) => r.address);
26835
+ } catch {
26836
+ throw new SsrfBlockedError(`Could not resolve host "${host}".`);
26837
+ }
26838
+ if (addresses.length === 0 || addresses.some((addr) => isPrivateAddress(addr))) {
26839
+ throw new SsrfBlockedError(`Blocked request to "${host}" \u2014 resolves to a non-public address.`);
26840
+ }
26841
+ return url;
26842
+ }
26843
+ var ssrfAgent = new undici.Agent({
26844
+ connect: {
26845
+ lookup(hostname, options, callback) {
26846
+ if (allowLocal()) {
26847
+ dnsCallback__default.default.lookup(hostname, options, callback);
26848
+ return;
26849
+ }
26850
+ dnsCallback__default.default.lookup(hostname, { ...options, all: true }, (err, addresses) => {
26851
+ if (err) return callback(err, "", 0);
26852
+ const list = Array.isArray(addresses) ? addresses : [addresses];
26853
+ const bad = list.find((a) => isPrivateAddress(a.address));
26854
+ if (bad) {
26855
+ return callback(
26856
+ new SsrfBlockedError(`Blocked connection to "${hostname}" \u2014 resolved to non-public address ${bad.address}.`),
26857
+ "",
26858
+ 0
26859
+ );
26860
+ }
26861
+ if (options.all) return callback(null, list);
26862
+ const first = list[0];
26863
+ return callback(null, first.address, first.family);
26864
+ });
26865
+ }
26866
+ }
26867
+ });
26868
+ async function safeFetch(rawUrl, init = {}) {
26869
+ let currentUrl = (await assertPublicUrl(rawUrl)).toString();
26870
+ for (let i = 0; i <= MAX_REDIRECTS2; i++) {
26871
+ const resp = await fetch(currentUrl, { ...init, redirect: "manual", dispatcher: ssrfAgent });
26872
+ if (resp.status < 300 || resp.status >= 400) return resp;
26873
+ const location = resp.headers.get("location");
26874
+ if (!location) return resp;
26875
+ const next = new URL(location, currentUrl);
26876
+ await assertPublicUrl(next.toString());
26877
+ currentUrl = next.toString();
26878
+ }
26879
+ throw new SsrfBlockedError(`Too many redirects (>${MAX_REDIRECTS2}).`);
26880
+ }
26346
26881
 
26347
26882
  // src/tools/web-search.ts
26348
- async function searchSearXNG(query, baseUrl, maxResults) {
26883
+ async function searchSearXNG(query, baseUrl, maxResults, guardUrl = false) {
26349
26884
  const url = new URL("/search", baseUrl);
26350
26885
  url.searchParams.set("q", query);
26351
26886
  url.searchParams.set("format", "json");
26352
26887
  url.searchParams.set("categories", "general");
26353
26888
  url.searchParams.set("engines", "google,bing,duckduckgo");
26354
- const resp = await fetch(url.toString(), {
26889
+ const init = {
26355
26890
  headers: { "User-Agent": "Cascade-AI/1.0 WebSearchTool" },
26356
26891
  signal: AbortSignal.timeout(1e4)
26357
- });
26892
+ };
26893
+ const resp = guardUrl ? await safeFetch(url.toString(), init) : await fetch(url.toString(), init);
26358
26894
  if (!resp.ok) {
26359
26895
  throw new Error(`SearXNG returned HTTP ${resp.status}`);
26360
26896
  }
@@ -26483,7 +27019,11 @@ var WebSearchTool = class extends BaseTool {
26483
27019
  searxngUrl: config.searxngUrl ?? process.env["SEARXNG_URL"],
26484
27020
  braveApiKey: config.braveApiKey ?? process.env["BRAVE_SEARCH_API_KEY"],
26485
27021
  tavilyApiKey: config.tavilyApiKey ?? process.env["TAVILY_API_KEY"],
26486
- maxResults: config.maxResults ?? 5
27022
+ maxResults: config.maxResults ?? 5,
27023
+ // Note this reads only from `config`, never from the environment: the
27024
+ // guard tracks the trustworthiness of the URL the host passed in, and an
27025
+ // env var says nothing about that.
27026
+ guardSearxngUrl: config.guardSearxngUrl ?? false
26487
27027
  };
26488
27028
  }
26489
27029
  async execute(input, _options) {
@@ -26498,7 +27038,7 @@ var WebSearchTool = class extends BaseTool {
26498
27038
  let anyBackendReachable = false;
26499
27039
  if (this.config.searxngUrl) {
26500
27040
  try {
26501
- results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
27041
+ results = await searchSearXNG(query, this.config.searxngUrl, maxResults, this.config.guardSearxngUrl);
26502
27042
  anyBackendReachable = true;
26503
27043
  if (results.length > 0) return this.formatResults(query, results);
26504
27044
  errors.push("SearXNG: returned 0 results");
@@ -26575,7 +27115,7 @@ var GlobTool = class extends BaseTool {
26575
27115
  };
26576
27116
  async execute(input, _options) {
26577
27117
  const pattern = input["pattern"];
26578
- const searchPath = input["path"] ? path27__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27118
+ const searchPath = input["path"] ? path28__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
26579
27119
  const matches = await glob.glob(pattern, {
26580
27120
  cwd: searchPath,
26581
27121
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -26588,7 +27128,7 @@ var GlobTool = class extends BaseTool {
26588
27128
  const withMtime = await Promise.all(
26589
27129
  matches.map(async (rel) => {
26590
27130
  try {
26591
- const stat = await fs11__default.default.stat(path27__default.default.join(searchPath, rel));
27131
+ const stat = await fs11__default.default.stat(path28__default.default.join(searchPath, rel));
26592
27132
  return { rel, mtime: stat.mtimeMs };
26593
27133
  } catch {
26594
27134
  return { rel, mtime: 0 };
@@ -26637,7 +27177,7 @@ var GrepTool = class extends BaseTool {
26637
27177
  };
26638
27178
  async execute(input, _options) {
26639
27179
  const pattern = input["pattern"];
26640
- const searchPath = input["path"] ? path27__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
27180
+ const searchPath = input["path"] ? path28__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
26641
27181
  const globPattern = input["glob"];
26642
27182
  const outputMode = input["output_mode"] ?? "content";
26643
27183
  const context = input["context"] ?? 0;
@@ -26691,12 +27231,12 @@ var GrepTool = class extends BaseTool {
26691
27231
  nodir: true
26692
27232
  });
26693
27233
  } catch {
26694
- files = [path27__default.default.relative(searchPath, searchPath) || "."];
27234
+ files = [path28__default.default.relative(searchPath, searchPath) || "."];
26695
27235
  }
26696
27236
  const results = [];
26697
27237
  let totalCount = 0;
26698
27238
  for (const rel of files) {
26699
- const abs = path27__default.default.join(searchPath, rel);
27239
+ const abs = path28__default.default.join(searchPath, rel);
26700
27240
  let content;
26701
27241
  try {
26702
27242
  content = await fs11__default.default.readFile(abs, "utf-8");
@@ -26737,92 +27277,6 @@ Total: ${totalCount} matches`);
26737
27277
  return results.join("\n");
26738
27278
  }
26739
27279
  };
26740
- var SsrfBlockedError = class extends Error {
26741
- constructor(message) {
26742
- super(message);
26743
- this.name = "SsrfBlockedError";
26744
- }
26745
- };
26746
- var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
26747
- var MAX_REDIRECTS2 = 5;
26748
- function allowLocal() {
26749
- return process.env["CASCADE_ALLOW_LOCAL_FETCH"] === "1";
26750
- }
26751
- function isPrivateAddress(ip) {
26752
- const type = net__default.default.isIP(ip);
26753
- if (type === 4) return isPrivateIPv4(ip);
26754
- if (type === 6) return isPrivateIPv6(ip);
26755
- return true;
26756
- }
26757
- function isPrivateIPv4(ip) {
26758
- const parts = ip.split(".").map((p) => Number(p));
26759
- if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return true;
26760
- const [a, b] = parts;
26761
- if (a === 0) return true;
26762
- if (a === 10) return true;
26763
- if (a === 127) return true;
26764
- if (a === 169 && b === 254) return true;
26765
- if (a === 172 && b >= 16 && b <= 31) return true;
26766
- if (a === 192 && b === 168) return true;
26767
- if (a === 100 && b >= 64 && b <= 127) return true;
26768
- if (a >= 224) return true;
26769
- return false;
26770
- }
26771
- function isPrivateIPv6(ip) {
26772
- const lower = ip.toLowerCase().replace(/^\[|\]$/g, "");
26773
- if (lower === "::1" || lower === "::") return true;
26774
- if (lower.startsWith("fe80")) return true;
26775
- if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
26776
- const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower);
26777
- if (mapped) return isPrivateIPv4(mapped[1]);
26778
- return false;
26779
- }
26780
- async function assertPublicUrl(rawUrl) {
26781
- let url;
26782
- try {
26783
- url = new URL(rawUrl);
26784
- } catch {
26785
- throw new SsrfBlockedError(`Invalid URL: ${rawUrl}`);
26786
- }
26787
- if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
26788
- throw new SsrfBlockedError(`Blocked URL scheme "${url.protocol}" \u2014 only http and https are allowed.`);
26789
- }
26790
- if (allowLocal()) return url;
26791
- const host = url.hostname.replace(/^\[|\]$/g, "");
26792
- if (net__default.default.isIP(host)) {
26793
- if (isPrivateAddress(host)) {
26794
- throw new SsrfBlockedError(`Blocked request to non-public address ${host}.`);
26795
- }
26796
- return url;
26797
- }
26798
- if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
26799
- throw new SsrfBlockedError(`Blocked request to local hostname "${host}".`);
26800
- }
26801
- let addresses;
26802
- try {
26803
- const records = await dns2__default.default.lookup(host, { all: true });
26804
- addresses = records.map((r) => r.address);
26805
- } catch {
26806
- throw new SsrfBlockedError(`Could not resolve host "${host}".`);
26807
- }
26808
- if (addresses.length === 0 || addresses.some((addr) => isPrivateAddress(addr))) {
26809
- throw new SsrfBlockedError(`Blocked request to "${host}" \u2014 resolves to a non-public address.`);
26810
- }
26811
- return url;
26812
- }
26813
- async function safeFetch(rawUrl, init = {}) {
26814
- let currentUrl = (await assertPublicUrl(rawUrl)).toString();
26815
- for (let i = 0; i <= MAX_REDIRECTS2; i++) {
26816
- const resp = await fetch(currentUrl, { ...init, redirect: "manual" });
26817
- if (resp.status < 300 || resp.status >= 400) return resp;
26818
- const location = resp.headers.get("location");
26819
- if (!location) return resp;
26820
- const next = new URL(location, currentUrl);
26821
- await assertPublicUrl(next.toString());
26822
- currentUrl = next.toString();
26823
- }
26824
- throw new SsrfBlockedError(`Too many redirects (>${MAX_REDIRECTS2}).`);
26825
- }
26826
27280
 
26827
27281
  // src/tools/web-fetch.ts
26828
27282
  var MAX_CHARS = 5e4;
@@ -27095,10 +27549,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
27095
27549
  }
27096
27550
  isIgnored(filePath) {
27097
27551
  if (!filePath) return false;
27098
- const abs = path27__default.default.resolve(this.workspaceRoot, filePath);
27099
- const rel = path27__default.default.relative(this.workspaceRoot, abs);
27100
- if (!rel || rel.startsWith("..") || path27__default.default.isAbsolute(rel)) return true;
27101
- const posixRel = rel.split(path27__default.default.sep).join("/");
27552
+ const abs = path28__default.default.resolve(this.workspaceRoot, filePath);
27553
+ const rel = path28__default.default.relative(this.workspaceRoot, abs);
27554
+ if (!rel || rel.startsWith("..") || path28__default.default.isAbsolute(rel)) return true;
27555
+ const posixRel = rel.split(path28__default.default.sep).join("/");
27102
27556
  return this.ignoreMatcher.ignores(posixRel);
27103
27557
  }
27104
27558
  };
@@ -27332,22 +27786,22 @@ var FileMcpOAuthStore = class {
27332
27786
  filePath;
27333
27787
  load() {
27334
27788
  try {
27335
- return JSON.parse(fs26__default.default.readFileSync(this.filePath, "utf-8"));
27789
+ return JSON.parse(fs27__default.default.readFileSync(this.filePath, "utf-8"));
27336
27790
  } catch {
27337
27791
  return void 0;
27338
27792
  }
27339
27793
  }
27340
27794
  save(state) {
27341
- fs26__default.default.mkdirSync(path27__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
27342
- fs26__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
27795
+ fs27__default.default.mkdirSync(path28__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
27796
+ fs27__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
27343
27797
  try {
27344
- fs26__default.default.chmodSync(this.filePath, 384);
27798
+ fs27__default.default.chmodSync(this.filePath, 384);
27345
27799
  } catch {
27346
27800
  }
27347
27801
  }
27348
27802
  clear() {
27349
27803
  try {
27350
- fs26__default.default.rmSync(this.filePath);
27804
+ fs27__default.default.rmSync(this.filePath);
27351
27805
  } catch {
27352
27806
  }
27353
27807
  }
@@ -28185,7 +28639,7 @@ var TaskAnalyzer = class {
28185
28639
  analysisCache.clear();
28186
28640
  }
28187
28641
  };
28188
- var DEFAULT_STATS_FILE = path27__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
28642
+ var DEFAULT_STATS_FILE = path28__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
28189
28643
  var ModelPerformanceTracker = class _ModelPerformanceTracker {
28190
28644
  stats = /* @__PURE__ */ new Map();
28191
28645
  featureStats = /* @__PURE__ */ new Map();
@@ -28225,7 +28679,7 @@ var ModelPerformanceTracker = class _ModelPerformanceTracker {
28225
28679
  async save() {
28226
28680
  if (this.readOnly) return;
28227
28681
  try {
28228
- await fs11__default.default.mkdir(path27__default.default.dirname(this.statsFile), { recursive: true });
28682
+ await fs11__default.default.mkdir(path28__default.default.dirname(this.statsFile), { recursive: true });
28229
28683
  const modelsObj = {};
28230
28684
  const featuresObj = {};
28231
28685
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -28858,7 +29312,7 @@ Required capability: ${description.slice(0, 300)}`;
28858
29312
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
28859
29313
  async loadPersistedTools() {
28860
29314
  if (!this.workspacePath || !this.persistEnabled) return;
28861
- const file = path27__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
29315
+ const file = path28__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
28862
29316
  try {
28863
29317
  const raw = await fs11__default.default.readFile(file, "utf-8");
28864
29318
  const specs = JSON.parse(raw);
@@ -28882,8 +29336,8 @@ Required capability: ${description.slice(0, 300)}`;
28882
29336
  }
28883
29337
  async persist() {
28884
29338
  if (!this.workspacePath || !this.persistEnabled) return;
28885
- const dir = path27__default.default.join(this.workspacePath, ".cascade");
28886
- const file = path27__default.default.join(dir, DYNAMIC_TOOLS_FILE);
29339
+ const dir = path28__default.default.join(this.workspacePath, ".cascade");
29340
+ const file = path28__default.default.join(dir, DYNAMIC_TOOLS_FILE);
28887
29341
  try {
28888
29342
  await fs11__default.default.mkdir(dir, { recursive: true });
28889
29343
  await fs11__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -28903,12 +29357,12 @@ var WorldStateDB = class {
28903
29357
  constructor(workspacePath, debugMode = false) {
28904
29358
  this.workspacePath = workspacePath;
28905
29359
  this.debugMode = debugMode;
28906
- const cascadeDir = path27__default.default.join(workspacePath, ".cascade");
28907
- if (!fs26__default.default.existsSync(cascadeDir)) {
28908
- fs26__default.default.mkdirSync(cascadeDir, { recursive: true });
29360
+ const cascadeDir = path28__default.default.join(workspacePath, ".cascade");
29361
+ if (!fs27__default.default.existsSync(cascadeDir)) {
29362
+ fs27__default.default.mkdirSync(cascadeDir, { recursive: true });
28909
29363
  }
28910
- this.keyPath = path27__default.default.join(cascadeDir, "world_state.key");
28911
- this.dbPath = path27__default.default.join(cascadeDir, "world_state.db");
29364
+ this.keyPath = path28__default.default.join(cascadeDir, "world_state.key");
29365
+ this.dbPath = path28__default.default.join(cascadeDir, "world_state.db");
28912
29366
  this.initEncryptionKey();
28913
29367
  this.db = new Database2__default.default(this.dbPath);
28914
29368
  this.db.pragma("journal_mode = WAL");
@@ -28965,11 +29419,11 @@ var WorldStateDB = class {
28965
29419
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
28966
29420
  }
28967
29421
  initEncryptionKey() {
28968
- if (fs26__default.default.existsSync(this.keyPath)) {
28969
- this.encryptionKey = fs26__default.default.readFileSync(this.keyPath);
29422
+ if (fs27__default.default.existsSync(this.keyPath)) {
29423
+ this.encryptionKey = fs27__default.default.readFileSync(this.keyPath);
28970
29424
  } else {
28971
29425
  this.encryptionKey = crypto__default.default.randomBytes(32);
28972
- fs26__default.default.writeFileSync(this.keyPath, this.encryptionKey);
29426
+ fs27__default.default.writeFileSync(this.keyPath, this.encryptionKey);
28973
29427
  }
28974
29428
  }
28975
29429
  encrypt(text) {
@@ -29255,9 +29709,9 @@ var WorldStateDB = class {
29255
29709
  dumpDebugIfNeeded() {
29256
29710
  if (!this.debugMode) return;
29257
29711
  try {
29258
- const dumpPath = path27__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
29712
+ const dumpPath = path28__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
29259
29713
  const entries = this.getAllEntries();
29260
- fs26__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
29714
+ fs27__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
29261
29715
  } catch (err) {
29262
29716
  console.error("Failed to dump debug world state", err);
29263
29717
  }
@@ -29313,11 +29767,11 @@ var ResumeStore = class {
29313
29767
  version: RESUME_SCHEMA_VERSION,
29314
29768
  createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
29315
29769
  };
29316
- await fs26.promises.mkdir(this.dir, { recursive: true });
29317
- const finalPath = path27__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
29770
+ await fs27.promises.mkdir(this.dir, { recursive: true });
29771
+ const finalPath = path28__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
29318
29772
  const tempPath = `${finalPath}.${process.pid}.tmp`;
29319
- await fs26.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
29320
- await fs26.promises.rename(tempPath, finalPath);
29773
+ await fs27.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
29774
+ await fs27.promises.rename(tempPath, finalPath);
29321
29775
  await this.prune();
29322
29776
  return true;
29323
29777
  } catch {
@@ -29328,7 +29782,7 @@ var ResumeStore = class {
29328
29782
  async list() {
29329
29783
  let entries;
29330
29784
  try {
29331
- entries = await fs26.promises.readdir(this.dir);
29785
+ entries = await fs27.promises.readdir(this.dir);
29332
29786
  } catch {
29333
29787
  return [];
29334
29788
  }
@@ -29337,7 +29791,7 @@ var ResumeStore = class {
29337
29791
  const checkpoints = [];
29338
29792
  for (const candidate of candidates) {
29339
29793
  try {
29340
- const raw = await fs26.promises.readFile(path27__default.default.join(this.dir, candidate.name), "utf-8");
29794
+ const raw = await fs27.promises.readFile(path28__default.default.join(this.dir, candidate.name), "utf-8");
29341
29795
  const parsed = JSON.parse(raw);
29342
29796
  if (isCheckpoint(parsed)) checkpoints.push(parsed);
29343
29797
  } catch {
@@ -29371,12 +29825,12 @@ var ResumeStore = class {
29371
29825
  const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
29372
29826
  if (!target) return null;
29373
29827
  try {
29374
- const entries = await fs26.promises.readdir(this.dir);
29828
+ const entries = await fs27.promises.readdir(this.dir);
29375
29829
  const wanted = safeId(target.taskId);
29376
29830
  const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
29377
29831
  if (!source) return null;
29378
29832
  const claimId = `${this.now()}-${safeId(target.taskId)}`;
29379
- await fs26.promises.rename(path27__default.default.join(this.dir, source), path27__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
29833
+ await fs27.promises.rename(path28__default.default.join(this.dir, source), path28__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
29380
29834
  return { checkpoint: target, claimId };
29381
29835
  } catch {
29382
29836
  return null;
@@ -29385,9 +29839,9 @@ var ResumeStore = class {
29385
29839
  /** Give a claim back, so the work stays recoverable after a failed start. */
29386
29840
  async release(claimId) {
29387
29841
  try {
29388
- await fs26.promises.rename(
29389
- path27__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
29390
- path27__default.default.join(this.dir, `${claimId}.json`)
29842
+ await fs27.promises.rename(
29843
+ path28__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
29844
+ path28__default.default.join(this.dir, `${claimId}.json`)
29391
29845
  );
29392
29846
  } catch {
29393
29847
  }
@@ -29395,7 +29849,7 @@ var ResumeStore = class {
29395
29849
  /** Discard a claim for good — the resumed run has taken ownership. */
29396
29850
  async settle(claimId) {
29397
29851
  try {
29398
- await fs26.promises.rm(path27__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
29852
+ await fs27.promises.rm(path28__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
29399
29853
  } catch {
29400
29854
  }
29401
29855
  }
@@ -29417,15 +29871,15 @@ var ResumeStore = class {
29417
29871
  async reclaimStale() {
29418
29872
  let reclaimed = 0;
29419
29873
  try {
29420
- const entries = await fs26.promises.readdir(this.dir);
29874
+ const entries = await fs27.promises.readdir(this.dir);
29421
29875
  const cutoff = this.now() - this.claimTimeoutMs;
29422
29876
  for (const name of entries) {
29423
29877
  if (!name.endsWith(CLAIM_SUFFIX)) continue;
29424
29878
  const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
29425
29879
  if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
29426
- await fs26.promises.rename(
29427
- path27__default.default.join(this.dir, name),
29428
- path27__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
29880
+ await fs27.promises.rename(
29881
+ path28__default.default.join(this.dir, name),
29882
+ path28__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
29429
29883
  );
29430
29884
  reclaimed++;
29431
29885
  }
@@ -29436,12 +29890,12 @@ var ResumeStore = class {
29436
29890
  /** Remove every checkpoint for a task id. */
29437
29891
  async delete(taskId) {
29438
29892
  try {
29439
- const entries = await fs26.promises.readdir(this.dir);
29893
+ const entries = await fs27.promises.readdir(this.dir);
29440
29894
  const wanted = safeId(taskId);
29441
29895
  await Promise.all(entries.map(async (name) => {
29442
29896
  const match = FILE_PATTERN.exec(name);
29443
29897
  if (match?.[2] === wanted) {
29444
- await fs26.promises.rm(path27__default.default.join(this.dir, name), { force: true });
29898
+ await fs27.promises.rm(path28__default.default.join(this.dir, name), { force: true });
29445
29899
  }
29446
29900
  }));
29447
29901
  } catch {
@@ -29450,13 +29904,13 @@ var ResumeStore = class {
29450
29904
  /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
29451
29905
  async prune() {
29452
29906
  try {
29453
- const entries = await fs26.promises.readdir(this.dir);
29907
+ const entries = await fs27.promises.readdir(this.dir);
29454
29908
  const cutoff = this.now() - this.maxAgeMs;
29455
29909
  const files = entries.map((name) => ({ name, match: FILE_PATTERN.exec(name) })).filter((e) => e.match !== null).map((e) => ({ name: e.name, timestamp: Number(e.match[1]) })).sort((a, b) => b.timestamp - a.timestamp);
29456
29910
  const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
29457
29911
  const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
29458
29912
  await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
29459
- (name) => fs26.promises.rm(path27__default.default.join(this.dir, name), { force: true })
29913
+ (name) => fs27.promises.rm(path28__default.default.join(this.dir, name), { force: true })
29460
29914
  ));
29461
29915
  } catch {
29462
29916
  }
@@ -29833,7 +30287,7 @@ var WorkspaceIndex = class {
29833
30287
  isIgnored;
29834
30288
  chunker;
29835
30289
  constructor(opts) {
29836
- this.root = path27__default.default.resolve(opts.root);
30290
+ this.root = path28__default.default.resolve(opts.root);
29837
30291
  this.db = opts.db;
29838
30292
  this.namespace = opts.namespace ?? "code";
29839
30293
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -29865,14 +30319,14 @@ var WorkspaceIndex = class {
29865
30319
  return;
29866
30320
  }
29867
30321
  for (const entry of entries) {
29868
- const abs = path27__default.default.join(dir, entry.name);
30322
+ const abs = path28__default.default.join(dir, entry.name);
29869
30323
  if (entry.isDirectory()) {
29870
30324
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
29871
30325
  await walk(abs);
29872
30326
  } else if (entry.isFile()) {
29873
- const ext = path27__default.default.extname(entry.name).slice(1).toLowerCase();
30327
+ const ext = path28__default.default.extname(entry.name).slice(1).toLowerCase();
29874
30328
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
29875
- out.push(path27__default.default.relative(this.root, abs));
30329
+ out.push(path28__default.default.relative(this.root, abs));
29876
30330
  }
29877
30331
  }
29878
30332
  };
@@ -29902,7 +30356,7 @@ var WorkspaceIndex = class {
29902
30356
  const entries = [];
29903
30357
  for (const rel of rels) {
29904
30358
  try {
29905
- const abs = path27__default.default.join(this.root, rel);
30359
+ const abs = path28__default.default.join(this.root, rel);
29906
30360
  const stat = await fs11__default.default.stat(abs);
29907
30361
  if (stat.size > this.maxFileBytes) continue;
29908
30362
  const buf = await fs11__default.default.readFile(abs);
@@ -30307,6 +30761,127 @@ var CurrentPageTool = class extends BaseTool {
30307
30761
  }
30308
30762
  };
30309
30763
 
30764
+ // src/tools/browser-control.ts
30765
+ var BrowserControlTool = class extends BaseTool {
30766
+ name = "browser_control";
30767
+ 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.";
30768
+ inputSchema = {
30769
+ type: "object",
30770
+ properties: {
30771
+ action: {
30772
+ type: "string",
30773
+ enum: ["navigate", "click", "fill", "press", "wait_for", "extract_text"],
30774
+ description: "What to do."
30775
+ },
30776
+ url: { type: "string", description: "For navigate: the http(s) address to open." },
30777
+ selector: {
30778
+ type: "string",
30779
+ description: "CSS selector for click, fill and wait_for. Optional for extract_text (defaults to the whole page)."
30780
+ },
30781
+ value: { type: "string", description: "For fill: the text to type." },
30782
+ key: { type: "string", description: 'For press: a key name such as "Enter", "Tab" or "Escape".' },
30783
+ timeoutMs: { type: "number", description: "For wait_for: how long to wait before giving up (default 10000, max 30000)." }
30784
+ },
30785
+ required: ["action"]
30786
+ };
30787
+ controller;
30788
+ release;
30789
+ /** One-way. See `revoke`. */
30790
+ revoked = false;
30791
+ constructor(controller, release) {
30792
+ super();
30793
+ this.controller = controller;
30794
+ this.release = release;
30795
+ }
30796
+ /**
30797
+ * A worker has finished. Ends its hold on the browser.
30798
+ *
30799
+ * Called from the tier's terminal path rather than by the model, because a
30800
+ * model cannot be relied on to announce that it is done — which is why the
30801
+ * host cannot use idle time as the boundary instead.
30802
+ */
30803
+ releaseActor(actorId) {
30804
+ this.release?.(actorId);
30805
+ }
30806
+ /**
30807
+ * Permanently disable this tool, whatever the registry still holds.
30808
+ *
30809
+ * Registration-time gates cannot express "this run turned out to be
30810
+ * unattended": a host that wires the controller and only then declares the
30811
+ * run unattended has already registered the tool, and a check that ran at
30812
+ * registration has no second chance. Enforcing at EXECUTION time makes the
30813
+ * guarantee independent of call order, which is the whole point of stating
30814
+ * the invariant rather than relying on the scheduler's current sequence.
30815
+ *
30816
+ * One-way on purpose. A safety gate that can be re-opened by a later call is
30817
+ * a gate that a refactor re-opens by accident.
30818
+ */
30819
+ revoke() {
30820
+ this.revoked = true;
30821
+ }
30822
+ // Acting on a session the user is signed into is the highest-consequence
30823
+ // thing in the tool set — higher than shell, arguably, because the blast
30824
+ // radius is someone else's server rather than this machine.
30825
+ isDangerous() {
30826
+ return true;
30827
+ }
30828
+ async execute(input, options) {
30829
+ if (this.revoked) {
30830
+ return "Error: browser control is not available in this run. It requires a person watching who can stop it, and nobody is.";
30831
+ }
30832
+ const kind = input["action"];
30833
+ if (!kind) return "Error: action is required.";
30834
+ if (options?.signal?.aborted) return "Error: the run was cancelled before this action ran.";
30835
+ const missing = requiredFieldFor(kind, input);
30836
+ if (missing) return `Error: "${kind}" needs ${missing}.`;
30837
+ const action = {
30838
+ kind,
30839
+ ...typeof input["url"] === "string" ? { url: input["url"] } : {},
30840
+ ...typeof input["selector"] === "string" ? { selector: input["selector"] } : {},
30841
+ ...typeof input["value"] === "string" ? { value: input["value"] } : {},
30842
+ ...typeof input["key"] === "string" ? { key: input["key"] } : {},
30843
+ ...typeof input["timeoutMs"] === "number" ? { timeoutMs: Math.min(Math.max(input["timeoutMs"], 0), 3e4) } : {}
30844
+ };
30845
+ let outcome;
30846
+ try {
30847
+ outcome = await this.controller(action, {
30848
+ // `sessionId` is the run. The host scopes both revocation and its
30849
+ // single-owner lease on it, so passing it is not telemetry — drop it
30850
+ // and a Stop in one run silently stops every later one.
30851
+ sessionId: options?.sessionId ?? "",
30852
+ actorId: options?.tierId ?? options?.sessionId ?? "",
30853
+ ...options?.signal ? { signal: options.signal } : {}
30854
+ });
30855
+ } catch (err) {
30856
+ return `Error: the browser could not perform "${kind}" \u2014 ${err instanceof Error ? err.message : String(err)}`;
30857
+ }
30858
+ const where = outcome.url ? `
30859
+ Page: ${outcome.title ? `${outcome.title} \u2014 ` : ""}${outcome.url}` : "";
30860
+ return `${outcome.ok ? "" : "Failed: "}${outcome.detail}${where}`;
30861
+ }
30862
+ };
30863
+ function requiredFieldFor(kind, input) {
30864
+ const has = (k) => typeof input[k] === "string" && input[k].length > 0;
30865
+ const given = (k) => typeof input[k] === "string";
30866
+ switch (kind) {
30867
+ case "navigate":
30868
+ return has("url") ? null : "a url";
30869
+ case "click":
30870
+ return has("selector") ? null : "a selector";
30871
+ case "fill":
30872
+ return has("selector") ? given("value") ? null : "a value" : "a selector";
30873
+ case "press":
30874
+ return has("key") ? null : "a key";
30875
+ case "wait_for":
30876
+ return has("selector") ? null : "a selector";
30877
+ // extract_text works with or without a selector.
30878
+ case "extract_text":
30879
+ return null;
30880
+ default:
30881
+ return null;
30882
+ }
30883
+ }
30884
+
30310
30885
  // src/core/cascade.ts
30311
30886
  var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
30312
30887
  function buildContextualPrompt(prompt, history = []) {
@@ -30392,13 +30967,13 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
30392
30967
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
30393
30968
  try {
30394
30969
  this.router.setDeadModelStore(new DeadModelStore(
30395
- fileDeadModelPersistence(path27__default.default.join(workspacePath, ".cascade", "dead-models.json"))
30970
+ fileDeadModelPersistence(path28__default.default.join(workspacePath, ".cascade", "dead-models.json"))
30396
30971
  ));
30397
30972
  } catch {
30398
30973
  }
30399
30974
  try {
30400
30975
  this.resumeStore = new ResumeStore({
30401
- dir: path27__default.default.join(workspacePath, ".cascade", "resume")
30976
+ dir: path28__default.default.join(workspacePath, ".cascade", "resume")
30402
30977
  });
30403
30978
  } catch {
30404
30979
  }
@@ -30486,8 +31061,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
30486
31061
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
30487
31062
  return;
30488
31063
  }
30489
- const dbPath = ci.dbPath || path27__default.default.join(this.workspacePath, ".cascade", "code-index.db");
30490
- fs26__default.default.mkdirSync(path27__default.default.dirname(dbPath), { recursive: true });
31064
+ const dbPath = ci.dbPath || path28__default.default.join(this.workspacePath, ".cascade", "code-index.db");
31065
+ fs27__default.default.mkdirSync(path28__default.default.dirname(dbPath), { recursive: true });
30491
31066
  const db = new Database2__default.default(dbPath);
30492
31067
  db.pragma("journal_mode = WAL");
30493
31068
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -30799,6 +31374,49 @@ ${input}` }],
30799
31374
  if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
30800
31375
  this.toolRegistry.register(new CurrentPageTool(provider));
30801
31376
  }
31377
+ /**
31378
+ * True when nobody is watching this run — a scheduled/cron task, which
31379
+ * auto-approves every tool because there is no one to ask.
31380
+ *
31381
+ * That auto-approval is fine for ordinary tools and disqualifying for this
31382
+ * one: `browser_control` drives the signed-in browser, and "every run asks
31383
+ * first" is the promise the Settings copy makes. The property used to hold
31384
+ * only because `runScheduledTask` happens not to call `setBrowserController`
31385
+ * — a guarantee resting on an ABSENT call, which is exactly what a later
31386
+ * refactor adds without noticing. Stated and enforced instead.
31387
+ */
31388
+ unattended = false;
31389
+ /**
31390
+ * Declare that no human is watching this run, so capabilities that depend on
31391
+ * a person being able to see and stop them are refused outright.
31392
+ */
31393
+ setUnattended(on) {
31394
+ this.unattended = on;
31395
+ if (!on) return;
31396
+ const tool = this.toolRegistry.getTool("browser_control");
31397
+ tool?.revoke?.();
31398
+ }
31399
+ /**
31400
+ * Wire the host's browser so a run can ACT on the open page, not just read it.
31401
+ *
31402
+ * Two gates before this registers anything, and both matter:
31403
+ *
31404
+ * - `tools.agentBrowserControl` must be on. Unlike read_current_page, which
31405
+ * reaches nothing the user has not already opened themselves, this drives
31406
+ * the session they are signed into. That is opt-in, not a default.
31407
+ * - `disabledTools` still removes it, the same as every other tool.
31408
+ * - The run must not be UNATTENDED. See `setUnattended`.
31409
+ *
31410
+ * Host-supplied like the page provider: the tool exists only where there is a
31411
+ * real browser to drive, so it never has to be refused in the CLI or a hosted
31412
+ * run — it is simply not there.
31413
+ */
31414
+ setBrowserController(controller, release) {
31415
+ if (this.unattended) return;
31416
+ if (!this.config.tools?.agentBrowserControl) return;
31417
+ if ((this.config.tools?.disabledTools ?? []).includes("browser_control")) return;
31418
+ this.toolRegistry.register(new BrowserControlTool(controller, release));
31419
+ }
30802
31420
  registerMediaTools(workspacePath) {
30803
31421
  const denied = new Set(this.config.tools?.disabledTools ?? []);
30804
31422
  const permitted = (name) => !denied.has(name);
@@ -31404,6 +32022,7 @@ ${prompt}` : prompt;
31404
32022
  this.router.setRunSignal(options.signal);
31405
32023
  const startMs = Date.now();
31406
32024
  const taskId = crypto.randomUUID();
32025
+ this.emit("run:started", { taskId });
31407
32026
  this.decisionLog = [];
31408
32027
  this.currentRunCompleted = [];
31409
32028
  this.wroteCheckpointThisRun = false;
@@ -32169,6 +32788,7 @@ function applySettingsPayload(config, data) {
32169
32788
  if (["auto", "T1", "T2", "T3"].includes(a["forceTier"])) config.routing = { ...config.routing ?? {}, forceTier: a["forceTier"] };
32170
32789
  if (typeof a["benchmarksLive"] === "boolean") config.benchmarks = { ...config.benchmarks ?? {}, live: a["benchmarksLive"] };
32171
32790
  if (["isolate", "worker", "auto"].includes(a["dynamicToolSandbox"])) config.tools = { ...config.tools ?? {}, dynamicToolSandbox: a["dynamicToolSandbox"] };
32791
+ if (typeof a["agentBrowserControl"] === "boolean") config.tools = { ...config.tools ?? {}, agentBrowserControl: a["agentBrowserControl"] };
32172
32792
  if (typeof a["factsExtraction"] === "boolean") config.knowledge = { ...config.knowledge ?? {}, factsExtraction: a["factsExtraction"] };
32173
32793
  if (typeof a["rememberSessions"] === "boolean") config.memory = { ...config.memory ?? {}, rememberSessions: a["rememberSessions"] };
32174
32794
  if (typeof a["enableToolCreation"] === "boolean") config.enableToolCreation = a["enableToolCreation"];
@@ -32226,6 +32846,7 @@ function settingsSnapshot(config) {
32226
32846
  forceTier: config.routing?.forceTier,
32227
32847
  benchmarksLive: config.benchmarks?.live,
32228
32848
  dynamicToolSandbox: config.tools?.dynamicToolSandbox,
32849
+ agentBrowserControl: config.tools?.agentBrowserControl,
32229
32850
  factsExtraction: config.knowledge?.factsExtraction,
32230
32851
  rememberSessions: config.memory?.rememberSessions,
32231
32852
  enableToolCreation: config.enableToolCreation,
@@ -32261,8 +32882,8 @@ function replaceInPlace(target, source) {
32261
32882
  }
32262
32883
  function writeConfigFile(configPath, config) {
32263
32884
  try {
32264
- fs26__default.default.mkdirSync(path27__default.default.dirname(configPath), { recursive: true });
32265
- fs26__default.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
32885
+ fs27__default.default.mkdirSync(path28__default.default.dirname(configPath), { recursive: true });
32886
+ fs27__default.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
32266
32887
  return { ok: true };
32267
32888
  } catch (err) {
32268
32889
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
@@ -32672,7 +33293,7 @@ function aggregateCostStats(sessions, opts = {}) {
32672
33293
  }
32673
33294
 
32674
33295
  // src/dashboard/server.ts
32675
- var __dirname$1 = path27__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
33296
+ var __dirname$1 = path28__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
32676
33297
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
32677
33298
  var MASK = "***";
32678
33299
  function masked(key, value) {
@@ -32730,6 +33351,14 @@ var DashboardServer = class {
32730
33351
  * always failing.
32731
33352
  */
32732
33353
  currentPageProvider;
33354
+ /**
33355
+ * Supplied by the desktop shell so a run can ACT on that page — click, type,
33356
+ * submit — not just read it. Same reasoning as the provider above: absent in
33357
+ * the CLI, so the tool is not registered rather than present and failing.
33358
+ * Cascade applies the `tools.agentBrowserControl` gate on top of this.
33359
+ */
33360
+ browserController;
33361
+ browserActorRelease;
32733
33362
  activeControllers = /* @__PURE__ */ new Map();
32734
33363
  /**
32735
33364
  * Run taskIds per chat session — file snapshots are keyed by the run's
@@ -32791,6 +33420,7 @@ var DashboardServer = class {
32791
33420
  });
32792
33421
  this.socket.onConfigUpdate(async (data) => {
32793
33422
  const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
33423
+ if (result.ok) this.settingsChangeHook?.(this.config);
32794
33424
  const refused = result.refused.map((r) => ({
32795
33425
  type: r.type,
32796
33426
  reason: r.reason,
@@ -32814,6 +33444,7 @@ var DashboardServer = class {
32814
33444
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
32815
33445
  this.activeSessions.set(sessionId, cascade);
32816
33446
  if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
33447
+ if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
32817
33448
  cascade.on("stream:token", (e) => {
32818
33449
  this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
32819
33450
  });
@@ -32837,13 +33468,16 @@ var DashboardServer = class {
32837
33468
  if (ev.requestId) this.clearPendingEscalation(ev.requestId);
32838
33469
  this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
32839
33470
  });
33471
+ cascade.on("run:started", (e) => {
33472
+ const id = e.taskId;
33473
+ if (id) this.recordSessionTask(sessionId, id);
33474
+ });
32840
33475
  try {
32841
33476
  const result = await cascade.run({
32842
33477
  prompt: runPrompt,
32843
33478
  signal: abortController.signal,
32844
33479
  approvalCallback: this.makeApprovalCallback(sessionId)
32845
33480
  });
32846
- this.recordSessionTask(sessionId, result.taskId);
32847
33481
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
32848
33482
  this.captureWhy(sessionId, cascade, result);
32849
33483
  this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
@@ -32909,6 +33543,49 @@ var DashboardServer = class {
32909
33543
  setCurrentPageProvider(provider) {
32910
33544
  this.currentPageProvider = provider;
32911
33545
  }
33546
+ /** Let runs act on that view, not just read it (desktop only). */
33547
+ setBrowserController(controller, release) {
33548
+ this.browserController = controller;
33549
+ this.browserActorRelease = release;
33550
+ }
33551
+ /**
33552
+ * Told whenever settings are committed through THIS server.
33553
+ *
33554
+ * Settings reach the live config by two routes — the desktop's Electron IPC
33555
+ * handler and this server's `config:update` socket — and both run the same
33556
+ * `commitSettings`. The desktop mirrors `tools.agentBrowserControl` into its
33557
+ * browser module after its own route, but had no way to learn about the
33558
+ * other, so a socket write left the module's copy stale: an enable the module
33559
+ * still refused, or worse a disable the module still honoured for a run
33560
+ * already holding the tool. This is the same sibling-writer drift
33561
+ * `settings-payload.ts` exists to prevent, one layer up.
33562
+ */
33563
+ settingsChangeHook;
33564
+ onSettingsChanged(hook) {
33565
+ this.settingsChangeHook = hook;
33566
+ }
33567
+ /**
33568
+ * Told when a run ends, so a host holding run-scoped state can release it.
33569
+ *
33570
+ * The desktop uses this to retire a run's browser Stop control: that has to
33571
+ * outlive each individual action (the gap between two actions is when the
33572
+ * user most wants it) but must not outlive the run.
33573
+ */
33574
+ /**
33575
+ * Fired when a run ends, carrying BOTH identifiers explicitly.
33576
+ *
33577
+ * They are different things and conflating them was a live bug: a chat
33578
+ * `sessionId` can contain many runs (hence `sessionTaskIds`), while
33579
+ * `Cascade.run()` mints a fresh random `taskId` per run — and it is the
33580
+ * taskId that tools see as `ToolExecuteOptions.sessionId`. Passing only the
33581
+ * chat id meant browser cleanup keyed on the task id matched nothing, so a
33582
+ * finished run kept its Stop banner armed and held its browser lease until
33583
+ * the deadlock ceiling.
33584
+ */
33585
+ runEndedHook;
33586
+ onRunEnded(hook) {
33587
+ this.runEndedHook = hook;
33588
+ }
32912
33589
  async start() {
32913
33590
  const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
32914
33591
  if (!isLoopback) {
@@ -32991,7 +33668,7 @@ var DashboardServer = class {
32991
33668
  * silently and still typecheck.
32992
33669
  */
32993
33670
  persistConfig(config) {
32994
- const result = writeConfigFile(path27__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE), config);
33671
+ const result = writeConfigFile(path28__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE), config);
32995
33672
  if (!result.ok) console.warn(`[dashboard] Failed to persist config: ${result.error}`);
32996
33673
  return result;
32997
33674
  }
@@ -33010,7 +33687,7 @@ var DashboardServer = class {
33010
33687
  */
33011
33688
  syncGlobalCredentials() {
33012
33689
  try {
33013
- saveGlobalCredentials(path27__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
33690
+ saveGlobalCredentials(path28__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
33014
33691
  } catch (err) {
33015
33692
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
33016
33693
  }
@@ -33025,15 +33702,15 @@ var DashboardServer = class {
33025
33702
  resolveDashboardSecret() {
33026
33703
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
33027
33704
  if (fromConfig) return fromConfig;
33028
- const secretPath = path27__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
33705
+ const secretPath = path28__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
33029
33706
  try {
33030
- if (fs26__default.default.existsSync(secretPath)) {
33031
- const existing = fs26__default.default.readFileSync(secretPath, "utf-8").trim();
33707
+ if (fs27__default.default.existsSync(secretPath)) {
33708
+ const existing = fs27__default.default.readFileSync(secretPath, "utf-8").trim();
33032
33709
  if (existing.length >= 16) return existing;
33033
33710
  }
33034
33711
  const generated = crypto.randomUUID();
33035
- fs26__default.default.mkdirSync(path27__default.default.dirname(secretPath), { recursive: true });
33036
- fs26__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
33712
+ fs27__default.default.mkdirSync(path28__default.default.dirname(secretPath), { recursive: true });
33713
+ fs27__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
33037
33714
  if (this.config.dashboard.auth) {
33038
33715
  console.warn(
33039
33716
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -33060,7 +33737,7 @@ var DashboardServer = class {
33060
33737
  // ── Setup ─────────────────────────────────────
33061
33738
  getGlobalStore() {
33062
33739
  if (!this.globalStore) {
33063
- const globalDbPath = path27__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33740
+ const globalDbPath = path28__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33064
33741
  this.globalStore = new MemoryStore(globalDbPath);
33065
33742
  }
33066
33743
  return this.globalStore;
@@ -33099,6 +33776,8 @@ var DashboardServer = class {
33099
33776
  }
33100
33777
  /** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
33101
33778
  persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
33779
+ const endedTaskId = result?.taskId ?? this.sessionTaskIds.get(sessionId)?.at(-1);
33780
+ this.runEndedHook?.({ sessionId, ...endedTaskId ? { taskId: endedTaskId } : {} });
33102
33781
  try {
33103
33782
  if (reply && reply.trim()) {
33104
33783
  this.store.addMessage({ id: crypto.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -33192,6 +33871,7 @@ var DashboardServer = class {
33192
33871
  const prompt = task.prompt;
33193
33872
  const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
33194
33873
  const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
33874
+ cascade.setUnattended(true);
33195
33875
  this.activeSessions.set(sessionId, cascade);
33196
33876
  cascade.on("tier:status", (e) => {
33197
33877
  this.socket.broadcast("tier:status", { sessionId, ...e });
@@ -33199,13 +33879,16 @@ var DashboardServer = class {
33199
33879
  cascade.on("peer:message", (e) => {
33200
33880
  this.socket.emitPeerMessage(e);
33201
33881
  });
33882
+ cascade.on("run:started", (e) => {
33883
+ const id = e.taskId;
33884
+ if (id) this.recordSessionTask(sessionId, id);
33885
+ });
33202
33886
  try {
33203
33887
  const result = await cascade.run({
33204
33888
  prompt,
33205
33889
  identityId: task.identityId,
33206
33890
  approvalCallback: async () => ({ approved: true, always: false })
33207
33891
  });
33208
- this.recordSessionTask(sessionId, result.taskId);
33209
33892
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
33210
33893
  this.captureWhy(sessionId, cascade, result);
33211
33894
  this.socket.broadcast("session:complete", { sessionId, result });
@@ -33440,12 +34123,12 @@ ${prompt}`;
33440
34123
  }
33441
34124
  }
33442
34125
  watchRuntimeChanges() {
33443
- const workspaceDbPath = path27__default.default.join(this.workspacePath, CASCADE_DB_FILE);
33444
- const globalDbPath = path27__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
34126
+ const workspaceDbPath = path28__default.default.join(this.workspacePath, CASCADE_DB_FILE);
34127
+ const globalDbPath = path28__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33445
34128
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
33446
34129
  for (const watchPath of watchPaths) {
33447
- if (!fs26__default.default.existsSync(watchPath)) continue;
33448
- fs26__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
34130
+ if (!fs27__default.default.existsSync(watchPath)) continue;
34131
+ fs27__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
33449
34132
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
33450
34133
  });
33451
34134
  }
@@ -33567,7 +34250,7 @@ ${prompt}`;
33567
34250
  const sessionId = req.params.id;
33568
34251
  this.store.deleteSession(sessionId);
33569
34252
  this.store.deleteRuntimeSession(sessionId);
33570
- const globalDbPath = path27__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
34253
+ const globalDbPath = path28__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33571
34254
  const globalStore = new MemoryStore(globalDbPath);
33572
34255
  try {
33573
34256
  globalStore.deleteRuntimeSession(sessionId);
@@ -33660,7 +34343,7 @@ ${prompt}`;
33660
34343
  });
33661
34344
  this.app.delete("/api/sessions", auth2, (req, res) => {
33662
34345
  const body = req.body;
33663
- const globalDbPath = path27__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
34346
+ const globalDbPath = path28__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33664
34347
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
33665
34348
  const globalStore = new MemoryStore(globalDbPath);
33666
34349
  try {
@@ -33683,7 +34366,7 @@ ${prompt}`;
33683
34366
  });
33684
34367
  this.app.delete("/api/runtime", auth2, (_req, res) => {
33685
34368
  this.store.deleteAllRuntimeNodes();
33686
- const globalDbPath = path27__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
34369
+ const globalDbPath = path28__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33687
34370
  const globalStore = new MemoryStore(globalDbPath);
33688
34371
  try {
33689
34372
  globalStore.deleteAllRuntimeNodes();
@@ -33768,12 +34451,12 @@ ${prompt}`;
33768
34451
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
33769
34452
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
33770
34453
  try {
33771
- const configPath = path27__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
33772
- const existing = fs26__default.default.existsSync(configPath) ? JSON.parse(fs26__default.default.readFileSync(configPath, "utf-8")) : {};
34454
+ const configPath = path28__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
34455
+ const existing = fs27__default.default.existsSync(configPath) ? JSON.parse(fs27__default.default.readFileSync(configPath, "utf-8")) : {};
33773
34456
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
33774
34457
  const tmp = configPath + ".tmp";
33775
- fs26__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
33776
- fs26__default.default.renameSync(tmp, configPath);
34458
+ fs27__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
34459
+ fs27__default.default.renameSync(tmp, configPath);
33777
34460
  res.json({ ok: true });
33778
34461
  } catch (err) {
33779
34462
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -33801,7 +34484,7 @@ ${prompt}`;
33801
34484
  this.app.get("/api/runtime", auth2, (req, res) => {
33802
34485
  const scope = req.query["scope"] ?? "workspace";
33803
34486
  if (scope === "global") {
33804
- const globalDbPath = path27__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
34487
+ const globalDbPath = path28__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
33805
34488
  const globalStore = new MemoryStore(globalDbPath);
33806
34489
  try {
33807
34490
  res.json({
@@ -33838,6 +34521,7 @@ ${prompt}`;
33838
34521
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
33839
34522
  this.activeSessions.set(sessionId, cascade);
33840
34523
  if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
34524
+ if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
33841
34525
  cascade.on("stream:token", (e) => {
33842
34526
  this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
33843
34527
  });
@@ -33861,13 +34545,16 @@ ${prompt}`;
33861
34545
  if (ev.requestId) this.clearPendingEscalation(ev.requestId);
33862
34546
  this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
33863
34547
  });
34548
+ cascade.on("run:started", (e) => {
34549
+ const id = e.taskId;
34550
+ if (id) this.recordSessionTask(sessionId, id);
34551
+ });
33864
34552
  try {
33865
34553
  const result = await cascade.run({
33866
34554
  prompt: runPrompt,
33867
34555
  identityId: body.identityId,
33868
34556
  approvalCallback: this.makeApprovalCallback(sessionId)
33869
34557
  });
33870
- this.recordSessionTask(sessionId, result.taskId);
33871
34558
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
33872
34559
  this.captureWhy(sessionId, cascade, result);
33873
34560
  this.socket.broadcast("cost:update", {
@@ -34142,13 +34829,13 @@ ${prompt}`;
34142
34829
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
34143
34830
  }
34144
34831
  });
34145
- const prodPath = path27__default.default.resolve(__dirname$1, "../web/dist");
34146
- const devPath = path27__default.default.resolve(__dirname$1, "../../web/dist");
34147
- const webDistPath = fs26__default.default.existsSync(prodPath) ? prodPath : devPath;
34148
- if (fs26__default.default.existsSync(webDistPath)) {
34832
+ const prodPath = path28__default.default.resolve(__dirname$1, "../web/dist");
34833
+ const devPath = path28__default.default.resolve(__dirname$1, "../../web/dist");
34834
+ const webDistPath = fs27__default.default.existsSync(prodPath) ? prodPath : devPath;
34835
+ if (fs27__default.default.existsSync(webDistPath)) {
34149
34836
  this.app.use(express__default.default.static(webDistPath));
34150
34837
  this.app.get("*", (_req, res) => {
34151
- res.sendFile(path27__default.default.join(webDistPath, "index.html"));
34838
+ res.sendFile(path28__default.default.join(webDistPath, "index.html"));
34152
34839
  });
34153
34840
  } else {
34154
34841
  this.app.get("/", (_req, res) => {
@@ -34335,14 +35022,14 @@ function planRetrieval(ctx) {
34335
35022
  }
34336
35023
  var CLOUD_SESSION_FILE = "cloud-session.json";
34337
35024
  function globalDir(homeDir = os6__default.default.homedir()) {
34338
- return path27__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
35025
+ return path28__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
34339
35026
  }
34340
35027
  function cloudSessionPath(dir = globalDir()) {
34341
- return path27__default.default.join(dir, CLOUD_SESSION_FILE);
35028
+ return path28__default.default.join(dir, CLOUD_SESSION_FILE);
34342
35029
  }
34343
35030
  function loadCloudSession(dir = globalDir()) {
34344
35031
  try {
34345
- const raw = fs26__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
35032
+ const raw = fs27__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
34346
35033
  const parsed = JSON.parse(raw);
34347
35034
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
34348
35035
  return parsed;
@@ -34351,17 +35038,17 @@ function loadCloudSession(dir = globalDir()) {
34351
35038
  }
34352
35039
  }
34353
35040
  function saveCloudSession(session, dir = globalDir()) {
34354
- fs26__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
35041
+ fs27__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
34355
35042
  const file = cloudSessionPath(dir);
34356
- fs26__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
35043
+ fs27__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
34357
35044
  try {
34358
- fs26__default.default.chmodSync(file, 384);
35045
+ fs27__default.default.chmodSync(file, 384);
34359
35046
  } catch {
34360
35047
  }
34361
35048
  }
34362
35049
  function clearCloudSession(dir = globalDir()) {
34363
35050
  try {
34364
- fs26__default.default.rmSync(cloudSessionPath(dir));
35051
+ fs27__default.default.rmSync(cloudSessionPath(dir));
34365
35052
  } catch {
34366
35053
  }
34367
35054
  }
@@ -34533,16 +35220,16 @@ var CloudClient = class _CloudClient {
34533
35220
  return (await this.refresh()).accessToken;
34534
35221
  }
34535
35222
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
34536
- async authed(path29, init = {}) {
34537
- const call = (token) => fetch(this.url(path29), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
35223
+ async authed(path30, init = {}) {
35224
+ const call = (token) => fetch(this.url(path30), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
34538
35225
  let res = await call(await this.accessToken());
34539
35226
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
34540
35227
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
34541
35228
  const text = await res.text();
34542
35229
  return text ? JSON.parse(text) : {};
34543
35230
  }
34544
- authedGet(path29) {
34545
- return this.authed(path29);
35231
+ authedGet(path30) {
35232
+ return this.authed(path30);
34546
35233
  }
34547
35234
  async fetchMe(token) {
34548
35235
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -34910,6 +35597,7 @@ function applySyncBundle(bundle, config, cleanup) {
34910
35597
  exports.AZURE_BASE_MODELS = AZURE_BASE_MODELS;
34911
35598
  exports.AZURE_BASE_URL_TEMPLATE = AZURE_BASE_URL_TEMPLATE;
34912
35599
  exports.AuditLogger = AuditLogger;
35600
+ exports.BrowserControlTool = BrowserControlTool;
34913
35601
  exports.CASCADE_AUDIT_FILE = CASCADE_AUDIT_FILE;
34914
35602
  exports.CASCADE_CONFIG_DIR = CASCADE_CONFIG_DIR;
34915
35603
  exports.CASCADE_CONFIG_FILE = CASCADE_CONFIG_FILE;
@@ -34923,6 +35611,7 @@ exports.CHARS_PER_TOKEN = CHARS_PER_TOKEN2;
34923
35611
  exports.COMPLEXITY_T2_COUNT = COMPLEXITY_T2_COUNT;
34924
35612
  exports.Cascade = Cascade;
34925
35613
  exports.CascadeCancelledError = CascadeCancelledError;
35614
+ exports.CascadeConfigSchema = CascadeConfigSchema;
34926
35615
  exports.CascadeIgnore = CascadeIgnore;
34927
35616
  exports.CascadeRouter = CascadeRouter;
34928
35617
  exports.CascadeToolError = CascadeToolError;