cascade-ai 0.3.0 → 0.5.1

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
@@ -7,19 +7,19 @@ var Anthropic = require('@anthropic-ai/sdk');
7
7
  var OpenAI = require('openai');
8
8
  var genai = require('@google/genai');
9
9
  var axios2 = require('axios');
10
- var fs2 = require('fs/promises');
11
- var path13 = require('path');
10
+ var fs3 = require('fs/promises');
11
+ var path16 = require('path');
12
12
  var ignoreFactory = require('ignore');
13
13
  var child_process = require('child_process');
14
14
  var util = require('util');
15
+ var fs15 = require('fs');
15
16
  var simpleGit = require('simple-git');
16
- var fs11 = require('fs');
17
17
  var PDFDocument = require('pdfkit');
18
18
  var index_js = require('@modelcontextprotocol/sdk/client/index.js');
19
19
  var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
20
20
  var zod = require('zod');
21
+ var os3 = require('os');
21
22
  var vm = require('vm');
22
- var os2 = require('os');
23
23
  var Database = require('better-sqlite3');
24
24
  var http = require('http');
25
25
  var url = require('url');
@@ -57,12 +57,12 @@ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
57
57
  var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
58
58
  var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
59
59
  var axios2__default = /*#__PURE__*/_interopDefault(axios2);
60
- var fs2__default = /*#__PURE__*/_interopDefault(fs2);
61
- var path13__default = /*#__PURE__*/_interopDefault(path13);
60
+ var fs3__default = /*#__PURE__*/_interopDefault(fs3);
61
+ var path16__default = /*#__PURE__*/_interopDefault(path16);
62
62
  var ignoreFactory__namespace = /*#__PURE__*/_interopNamespace(ignoreFactory);
63
- var fs11__default = /*#__PURE__*/_interopDefault(fs11);
63
+ var fs15__default = /*#__PURE__*/_interopDefault(fs15);
64
64
  var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
65
- var os2__default = /*#__PURE__*/_interopDefault(os2);
65
+ var os3__default = /*#__PURE__*/_interopDefault(os3);
66
66
  var Database__default = /*#__PURE__*/_interopDefault(Database);
67
67
  var express__default = /*#__PURE__*/_interopDefault(express);
68
68
  var rateLimit__default = /*#__PURE__*/_interopDefault(rateLimit);
@@ -165,7 +165,7 @@ var require_keytar2 = __commonJS({
165
165
  });
166
166
 
167
167
  // src/constants.ts
168
- var CASCADE_VERSION = "0.3.0";
168
+ var CASCADE_VERSION = "0.5.1";
169
169
  var CASCADE_CONFIG_DIR = ".cascade";
170
170
  var CASCADE_MD_FILE = "CASCADE.md";
171
171
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -903,19 +903,21 @@ var OpenAIProvider = class extends BaseProvider {
903
903
  // src/providers/azure.ts
904
904
  var AzureOpenAIProvider = class extends OpenAIProvider {
905
905
  constructor(config, model) {
906
- const baseUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
906
+ const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
907
+ const endpoint = rawUrl.replace(/\/+$/, "");
907
908
  super(
908
909
  {
909
910
  ...config,
910
- baseUrl: `${baseUrl}/openai/deployments/${config.deploymentName ?? model.id}`
911
+ baseUrl: endpoint
912
+ // Kept for superclass compatibility if it reads it
911
913
  },
912
914
  model
913
915
  );
914
- this.client = new OpenAI__default.default({
916
+ this.client = new OpenAI.AzureOpenAI({
915
917
  apiKey: config.apiKey,
916
- baseURL: `${baseUrl}/openai/deployments/${config.deploymentName ?? model.id}`,
917
- defaultQuery: { "api-version": config.apiVersion ?? "2024-08-01-preview" },
918
- defaultHeaders: { "api-key": config.apiKey ?? "" }
918
+ endpoint,
919
+ deployment: config.deploymentName ?? model.id,
920
+ apiVersion: config.apiVersion ?? "2024-08-01-preview"
919
921
  });
920
922
  }
921
923
  async listModels() {
@@ -1157,6 +1159,22 @@ var GeminiProvider = class extends BaseProvider {
1157
1159
  };
1158
1160
  }
1159
1161
  };
1162
+ var TOOL_CAPABLE_FAMILIES = [
1163
+ "llama3.1",
1164
+ "llama3.2",
1165
+ "llama3.3",
1166
+ "qwen2",
1167
+ "qwen2.5",
1168
+ "qwen3",
1169
+ "mistral-nemo",
1170
+ "mistral-small",
1171
+ "command-r",
1172
+ "firefunction"
1173
+ ];
1174
+ function isToolCapable(modelName) {
1175
+ const name = modelName.toLowerCase();
1176
+ return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
1177
+ }
1160
1178
  var OllamaProvider = class extends BaseProvider {
1161
1179
  baseUrl;
1162
1180
  constructor(config, model) {
@@ -1169,12 +1187,21 @@ var OllamaProvider = class extends BaseProvider {
1169
1187
  }
1170
1188
  async generateStream(options, onChunk) {
1171
1189
  const messages = this.convertMessages(options.messages, options.systemPrompt);
1190
+ const ollamaTools = options.tools?.map((t) => ({
1191
+ type: "function",
1192
+ function: {
1193
+ name: t.name,
1194
+ description: t.description,
1195
+ parameters: t.inputSchema
1196
+ }
1197
+ }));
1172
1198
  const response = await axios2__default.default.post(
1173
1199
  `${this.baseUrl}/api/chat`,
1174
1200
  {
1175
1201
  model: this.model.id,
1176
1202
  messages,
1177
1203
  stream: true,
1204
+ tools: ollamaTools?.length ? ollamaTools : void 0,
1178
1205
  options: {
1179
1206
  num_predict: options.maxTokens ?? this.model.maxOutputTokens,
1180
1207
  temperature: options.temperature ?? 0.7
@@ -1185,6 +1212,7 @@ var OllamaProvider = class extends BaseProvider {
1185
1212
  let fullContent = "";
1186
1213
  let inputTokens = 0;
1187
1214
  let outputTokens = 0;
1215
+ const pendingToolCalls = [];
1188
1216
  await new Promise((resolve, reject) => {
1189
1217
  let buffer = "";
1190
1218
  response.data.on("data", (chunk) => {
@@ -1199,6 +1227,9 @@ var OllamaProvider = class extends BaseProvider {
1199
1227
  fullContent += parsed.message.content;
1200
1228
  onChunk({ text: parsed.message.content, finishReason: null });
1201
1229
  }
1230
+ if (parsed.message?.tool_calls?.length) {
1231
+ pendingToolCalls.push(...parsed.message.tool_calls);
1232
+ }
1202
1233
  if (parsed.done) {
1203
1234
  inputTokens = parsed.prompt_eval_count ?? 0;
1204
1235
  outputTokens = parsed.eval_count ?? 0;
@@ -1216,6 +1247,9 @@ var OllamaProvider = class extends BaseProvider {
1216
1247
  fullContent += parsed.message.content;
1217
1248
  onChunk({ text: parsed.message.content, finishReason: null });
1218
1249
  }
1250
+ if (parsed.message?.tool_calls?.length) {
1251
+ pendingToolCalls.push(...parsed.message.tool_calls);
1252
+ }
1219
1253
  if (parsed.done) {
1220
1254
  inputTokens = parsed.prompt_eval_count ?? inputTokens;
1221
1255
  outputTokens = parsed.eval_count ?? outputTokens;
@@ -1227,11 +1261,30 @@ var OllamaProvider = class extends BaseProvider {
1227
1261
  });
1228
1262
  response.data.on("error", reject);
1229
1263
  });
1230
- onChunk({ text: "", finishReason: "stop" });
1264
+ const toolCalls = pendingToolCalls.map((tc, i) => {
1265
+ let input;
1266
+ if (typeof tc.function.arguments === "string") {
1267
+ try {
1268
+ input = JSON.parse(tc.function.arguments);
1269
+ } catch {
1270
+ input = { __rawArguments: tc.function.arguments };
1271
+ }
1272
+ } else {
1273
+ input = tc.function.arguments;
1274
+ }
1275
+ return {
1276
+ id: `ollama-tool-${Date.now()}-${i}`,
1277
+ name: tc.function.name,
1278
+ input
1279
+ };
1280
+ });
1281
+ const finishReason = toolCalls.length ? "tool_use" : "stop";
1282
+ onChunk({ text: "", finishReason });
1231
1283
  return {
1232
1284
  content: fullContent,
1233
1285
  usage: this.makeUsage(inputTokens, outputTokens),
1234
- finishReason: "stop"
1286
+ toolCalls: toolCalls.length ? toolCalls : void 0,
1287
+ finishReason
1235
1288
  };
1236
1289
  }
1237
1290
  async countTokens(text) {
@@ -1255,6 +1308,7 @@ var OllamaProvider = class extends BaseProvider {
1255
1308
  maxOutputTokens: 4e3,
1256
1309
  supportsStreaming: true,
1257
1310
  isLocal: true,
1311
+ supportsToolUse: isToolCapable(m.name),
1258
1312
  minSizeB: this.parseSizeB(m.details?.parameter_size)
1259
1313
  }));
1260
1314
  } catch {
@@ -1277,6 +1331,26 @@ var OllamaProvider = class extends BaseProvider {
1277
1331
  result.push({ role: "system", content: typeof m.content === "string" ? m.content : "" });
1278
1332
  continue;
1279
1333
  }
1334
+ if (m.role === "tool") {
1335
+ result.push({
1336
+ role: "tool",
1337
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
1338
+ });
1339
+ continue;
1340
+ }
1341
+ if (m.role === "assistant" && m.toolCalls?.length) {
1342
+ result.push({
1343
+ role: "assistant",
1344
+ content: typeof m.content === "string" ? m.content : "",
1345
+ tool_calls: m.toolCalls.map((tc) => ({
1346
+ function: {
1347
+ name: tc.name,
1348
+ arguments: tc.input
1349
+ }
1350
+ }))
1351
+ });
1352
+ continue;
1353
+ }
1280
1354
  if (typeof m.content === "string") {
1281
1355
  result.push({ role: m.role, content: m.content });
1282
1356
  continue;
@@ -1409,6 +1483,26 @@ var ModelSelector = class {
1409
1483
  return T3_MODEL_PRIORITY;
1410
1484
  }
1411
1485
  }
1486
+ getAllAvailableModels() {
1487
+ return Array.from(this.availableModels.values()).filter(
1488
+ (m) => this.availableProviders.has(m.provider)
1489
+ );
1490
+ }
1491
+ /**
1492
+ * Returns all available models eligible for the given tier, ordered by the
1493
+ * tier's priority chain. Use this as the candidate set for scored selection.
1494
+ */
1495
+ getCandidatesForTier(tier) {
1496
+ const priority = this.getPriorityList(tier);
1497
+ const candidates = [];
1498
+ for (const key of priority) {
1499
+ const model = this.availableModels.get(key);
1500
+ if (model && this.availableProviders.has(model.provider)) {
1501
+ candidates.push(model);
1502
+ }
1503
+ }
1504
+ return candidates;
1505
+ }
1412
1506
  isProviderAvailable(provider) {
1413
1507
  return this.availableProviders.has(provider);
1414
1508
  }
@@ -1614,11 +1708,203 @@ var TpmLimiter = class {
1614
1708
  }
1615
1709
  };
1616
1710
 
1711
+ // src/core/router/local-queue.ts
1712
+ var LocalRequestQueue = class {
1713
+ maxConcurrent;
1714
+ active = 0;
1715
+ queue = [];
1716
+ constructor(maxConcurrent = 1) {
1717
+ this.maxConcurrent = Math.max(1, maxConcurrent);
1718
+ }
1719
+ /**
1720
+ * Acquire a queue slot. Returns a `release` function that MUST be called
1721
+ * when the inference call is done (even on error). Rejects if the slot
1722
+ * cannot be acquired within `timeoutMs`.
1723
+ */
1724
+ async acquire(timeoutMs) {
1725
+ if (this.active < this.maxConcurrent) {
1726
+ this.active++;
1727
+ return this.makeRelease();
1728
+ }
1729
+ return new Promise((resolve, reject) => {
1730
+ let settled = false;
1731
+ let timer;
1732
+ const resolver = (release) => {
1733
+ if (settled) return;
1734
+ settled = true;
1735
+ if (timer !== void 0) clearTimeout(timer);
1736
+ resolve(release);
1737
+ };
1738
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
1739
+ timer = setTimeout(() => {
1740
+ if (settled) return;
1741
+ settled = true;
1742
+ const idx = this.queue.indexOf(resolver);
1743
+ if (idx !== -1) this.queue.splice(idx, 1);
1744
+ reject(new Error(
1745
+ `Local model queue: timed out waiting for a free slot after ${timeoutMs}ms. Active: ${this.active}, Queued: ${this.queue.length}. Consider increasing localConcurrency or localInferenceTimeoutMs in your config.`
1746
+ ));
1747
+ }, timeoutMs);
1748
+ }
1749
+ this.queue.push(resolver);
1750
+ });
1751
+ }
1752
+ /** Number of in-flight requests. */
1753
+ get activeCount() {
1754
+ return this.active;
1755
+ }
1756
+ /** Number of requests waiting for a slot. */
1757
+ get queueDepth() {
1758
+ return this.queue.length;
1759
+ }
1760
+ makeRelease() {
1761
+ let called = false;
1762
+ return () => {
1763
+ if (called) return;
1764
+ called = true;
1765
+ this.active--;
1766
+ const next = this.queue.shift();
1767
+ if (next) {
1768
+ this.active++;
1769
+ next(this.makeRelease());
1770
+ }
1771
+ };
1772
+ }
1773
+ };
1774
+
1617
1775
  // src/utils/cost.ts
1618
1776
  function calculateCost(inputTokens, outputTokens, model) {
1619
1777
  return inputTokens / 1e3 * model.inputCostPer1kTokens + outputTokens / 1e3 * model.outputCostPer1kTokens;
1620
1778
  }
1621
1779
 
1780
+ // src/utils/retry.ts
1781
+ var CascadeCancelledError = class extends Error {
1782
+ constructor(reason) {
1783
+ super(reason ?? "Run was cancelled via AbortSignal");
1784
+ this.name = "CascadeCancelledError";
1785
+ }
1786
+ };
1787
+ var CascadeToolError = class extends Error {
1788
+ /** A friendly message to show the user / T3 */
1789
+ userMessage;
1790
+ /** Whether this error class is retryable by default */
1791
+ retryable;
1792
+ constructor(userMessage, cause, retryable = false) {
1793
+ const causeMsg = cause instanceof Error ? cause.message : String(cause);
1794
+ super(`${userMessage}: ${causeMsg}`);
1795
+ this.name = "CascadeToolError";
1796
+ this.userMessage = userMessage;
1797
+ this.retryable = retryable;
1798
+ }
1799
+ };
1800
+ async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed out") {
1801
+ let timer;
1802
+ const timeoutPromise = new Promise((_, reject) => {
1803
+ timer = setTimeout(
1804
+ () => reject(new Error(errorMessage)),
1805
+ timeoutMs
1806
+ );
1807
+ });
1808
+ try {
1809
+ return await Promise.race([promise, timeoutPromise]);
1810
+ } finally {
1811
+ if (timer !== void 0) clearTimeout(timer);
1812
+ }
1813
+ }
1814
+
1815
+ // src/core/router/model-profiler.ts
1816
+ var SKIP_PATTERN = /embed|dall-e|whisper|tts|vision|instruct-vision|rerank/i;
1817
+ var SPECIALIZATION_KEYWORDS = {
1818
+ code: ["code", "coding", "programming", "developer", "software", "function", "debug", "typescript", "python", "javascript"],
1819
+ analysis: ["analysis", "analytical", "reasoning", "logic", "research", "evaluate", "assess", "explain"],
1820
+ creative: ["creative", "writing", "story", "poetry", "content", "blog", "essay", "narrative"],
1821
+ data: ["data", "sql", "statistics", "chart", "csv", "json", "excel", "spreadsheet", "math", "mathematical"],
1822
+ instruction: ["instruction", "instruction-following", "accurate", "precise", "factual"],
1823
+ multilingual: ["multilingual", "language", "translation", "linguistic"],
1824
+ long_context: ["long", "context", "document", "book", "summarize", "large"]
1825
+ };
1826
+ function extractSpecializations(description) {
1827
+ const lower = description.toLowerCase();
1828
+ const found = [];
1829
+ for (const [key, terms] of Object.entries(SPECIALIZATION_KEYWORDS)) {
1830
+ if (terms.some((t) => lower.includes(t))) {
1831
+ found.push(key);
1832
+ }
1833
+ }
1834
+ return found;
1835
+ }
1836
+ async function fetchOpenRouterModels() {
1837
+ try {
1838
+ const resp = await fetch("https://openrouter.ai/api/v1/models", {
1839
+ headers: { "User-Agent": "Cascade-AI/0.4.0" },
1840
+ signal: AbortSignal.timeout(8e3)
1841
+ });
1842
+ if (!resp.ok) return [];
1843
+ const data = await resp.json();
1844
+ return data.data ?? [];
1845
+ } catch {
1846
+ return [];
1847
+ }
1848
+ }
1849
+ async function queryModelDirectly(router, model) {
1850
+ try {
1851
+ const result = await router.generate("T3", {
1852
+ messages: [{
1853
+ role: "user",
1854
+ content: 'What are your top 3 task specializations? Reply with valid JSON only: {"specializations": ["<area1>", "<area2>", "<area3>"]}'
1855
+ }],
1856
+ maxTokens: 60
1857
+ });
1858
+ const match = /\{[\s\S]*?\}/.exec(result.content);
1859
+ if (!match) return [];
1860
+ const parsed = JSON.parse(match[0]);
1861
+ const specs = parsed.specializations;
1862
+ if (!Array.isArray(specs)) return [];
1863
+ return specs.filter((s) => typeof s === "string").slice(0, 5);
1864
+ } catch {
1865
+ return [];
1866
+ }
1867
+ }
1868
+ var ModelProfiler = class {
1869
+ store;
1870
+ router;
1871
+ constructor(store, router) {
1872
+ this.store = store;
1873
+ this.router = router;
1874
+ }
1875
+ /**
1876
+ * Profile all models that haven't been profiled yet.
1877
+ * Safe to call concurrently — SQLite upsert handles races.
1878
+ */
1879
+ async profileAll(models) {
1880
+ const alreadyProfiled = new Set(this.store.getProfiledModelIds());
1881
+ const toProfile = models.filter(
1882
+ (m) => !alreadyProfiled.has(m.id) && !SKIP_PATTERN.test(m.id) && !SKIP_PATTERN.test(m.name)
1883
+ );
1884
+ if (toProfile.length === 0) return;
1885
+ const openRouterModels = await fetchOpenRouterModels();
1886
+ const orByNormalizedId = /* @__PURE__ */ new Map();
1887
+ for (const m of openRouterModels) {
1888
+ orByNormalizedId.set(m.id.toLowerCase(), m);
1889
+ const short = m.id.split("/").pop();
1890
+ if (short) orByNormalizedId.set(short.toLowerCase(), m);
1891
+ }
1892
+ await Promise.allSettled(
1893
+ toProfile.map(async (model) => {
1894
+ let specializations = [];
1895
+ const orMatch = orByNormalizedId.get(model.id.toLowerCase()) ?? orByNormalizedId.get(model.id.split("/").pop()?.toLowerCase() ?? "");
1896
+ if (orMatch?.description) {
1897
+ specializations = extractSpecializations(orMatch.description);
1898
+ }
1899
+ if (specializations.length === 0 && this.router) {
1900
+ specializations = await queryModelDirectly(this.router);
1901
+ }
1902
+ this.store.saveModelProfile(model.id, model.provider, specializations);
1903
+ })
1904
+ );
1905
+ }
1906
+ };
1907
+
1622
1908
  // src/core/router/index.ts
1623
1909
  var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
1624
1910
  selector;
@@ -1646,6 +1932,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
1646
1932
  budgetState = "ok";
1647
1933
  budgetExceededReason;
1648
1934
  tpmLimiter;
1935
+ localQueue;
1649
1936
  /** Thrown when the configured budget is exceeded. */
1650
1937
  static BudgetExceededError = class extends Error {
1651
1938
  constructor(msg) {
@@ -1662,6 +1949,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
1662
1949
  this.selector = new ModelSelector(availableProviders);
1663
1950
  this.failover = new FailoverManager(this.selector);
1664
1951
  this.tpmLimiter = new TpmLimiter(config.rateLimits?.providerTpm ?? {});
1952
+ this.localQueue = new LocalRequestQueue(config.localConcurrency ?? 1);
1665
1953
  const ollamaCfg = config.providers.find((p) => p.type === "ollama");
1666
1954
  if (availableProviders.has("ollama")) {
1667
1955
  await this.discoverOllamaModels(ollamaCfg);
@@ -1673,7 +1961,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
1673
1961
  if (!model) {
1674
1962
  throw new Error(`Configured model "${override}" for ${tier} could not be loaded. Check provider availability and exact model name.`);
1675
1963
  }
1676
- if (model.id !== override) {
1964
+ if (model.id !== override && `${model.provider}:${model.id}` !== override) {
1677
1965
  throw new Error(`Configured model "${override}" for ${tier} resolved to "${model.id}". Use the exact provider model ID or prefix the provider (e.g. gemini:${override}).`);
1678
1966
  }
1679
1967
  this.tierModels.set(tier, model);
@@ -1688,6 +1976,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
1688
1976
  }
1689
1977
  }
1690
1978
  }
1979
+ /**
1980
+ * Run model specialization profiling in the background.
1981
+ * Only profiles models that haven't been profiled yet (cache-first).
1982
+ * No-op if store is not provided.
1983
+ */
1984
+ async profileModels(store) {
1985
+ const allModels = this.selector.getAllAvailableModels();
1986
+ const profiler = new ModelProfiler(store, this);
1987
+ profiler.profileAll(allModels).catch(() => {
1988
+ });
1989
+ }
1691
1990
  async generate(tier, options, onChunk, requireVision = false) {
1692
1991
  if (this.budgetState === "exceeded") {
1693
1992
  throw new _CascadeRouter.BudgetExceededError(
@@ -1709,9 +2008,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
1709
2008
  await this.tpmLimiter.acquire(model.provider, estimatedTokens);
1710
2009
  }
1711
2010
  const useStream = Boolean(onChunk) && model.supportsStreaming && typeof provider.generateStream === "function";
2011
+ let releaseLocalSlot;
2012
+ if (model.isLocal) {
2013
+ const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
2014
+ const queueWaitMs = Math.round(inferenceTimeoutMs / 2);
2015
+ releaseLocalSlot = await this.localQueue.acquire(queueWaitMs);
2016
+ }
1712
2017
  try {
1713
2018
  let result;
1714
- if (useStream && onChunk) {
2019
+ if (model.isLocal) {
2020
+ const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
2021
+ const inferencePromise = useStream && onChunk ? provider.generateStream(options, (chunk) => {
2022
+ const text = typeof chunk?.text === "string" ? chunk.text : "";
2023
+ if (text) onChunk({ ...chunk, text });
2024
+ }) : provider.generate(options);
2025
+ result = await withTimeout(
2026
+ inferencePromise,
2027
+ inferenceTimeoutMs,
2028
+ `Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`
2029
+ );
2030
+ } else if (useStream && onChunk) {
1715
2031
  try {
1716
2032
  result = await provider.generateStream(options, (chunk) => {
1717
2033
  const text = typeof chunk?.text === "string" ? chunk.text : "";
@@ -1749,10 +2065,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
1749
2065
  if (fallback) {
1750
2066
  this.tierModels.set(tier, fallback);
1751
2067
  this.ensureProvider(fallback, this.config.providers);
2068
+ releaseLocalSlot?.();
2069
+ releaseLocalSlot = void 0;
1752
2070
  return this.generate(tier, options, onChunk, requireVision);
1753
2071
  }
1754
2072
  }
1755
2073
  throw err;
2074
+ } finally {
2075
+ releaseLocalSlot?.();
1756
2076
  }
1757
2077
  }
1758
2078
  getModelForTier(tier) {
@@ -1992,29 +2312,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
1992
2312
  return /rate.?limit|429|too.?many.?requests|quota/i.test(msg);
1993
2313
  }
1994
2314
  };
1995
-
1996
- // src/utils/retry.ts
1997
- var CascadeCancelledError = class extends Error {
1998
- constructor(reason) {
1999
- super(reason ?? "Run was cancelled via AbortSignal");
2000
- this.name = "CascadeCancelledError";
2001
- }
2002
- };
2003
- var CascadeToolError = class extends Error {
2004
- /** A friendly message to show the user / T3 */
2005
- userMessage;
2006
- /** Whether this error class is retryable by default */
2007
- retryable;
2008
- constructor(userMessage, cause, retryable = false) {
2009
- const causeMsg = cause instanceof Error ? cause.message : String(cause);
2010
- super(`${userMessage}: ${causeMsg}`);
2011
- this.name = "CascadeToolError";
2012
- this.userMessage = userMessage;
2013
- this.retryable = retryable;
2014
- }
2015
- };
2016
-
2017
- // src/core/tiers/base.ts
2018
2315
  var BaseTier = class extends EventEmitter__default.default {
2019
2316
  id;
2020
2317
  role;
@@ -2295,6 +2592,97 @@ var AuditLogger = class {
2295
2592
  }
2296
2593
  };
2297
2594
 
2595
+ // src/tools/text-tool-parser.ts
2596
+ var TOOL_CALL_RE = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
2597
+ var JSON_BLOCK_RE = /```json\s*([\s\S]*?)\s*```/g;
2598
+ var FUNCTION_OBJ_RE = /\{\s*"function"\s*:\s*\{[^}]*"name"\s*:[^}]*\}\s*\}/g;
2599
+ function parseTextToolCalls(text) {
2600
+ const results = tryXmlBlocks(text);
2601
+ if (results.length > 0) return results;
2602
+ const jsonBlockResults = tryJsonCodeBlocks(text);
2603
+ if (jsonBlockResults.length > 0) return jsonBlockResults;
2604
+ return tryFunctionCallObjects(text);
2605
+ }
2606
+ function tryXmlBlocks(text) {
2607
+ const results = [];
2608
+ let match;
2609
+ TOOL_CALL_RE.lastIndex = 0;
2610
+ while ((match = TOOL_CALL_RE.exec(text)) !== null) {
2611
+ try {
2612
+ const raw = JSON.parse(match[1]);
2613
+ if (typeof raw.name !== "string") continue;
2614
+ const input = typeof raw.input === "object" && raw.input !== null ? raw.input : {};
2615
+ results.push({ name: raw.name, input });
2616
+ } catch {
2617
+ }
2618
+ }
2619
+ return results;
2620
+ }
2621
+ function tryJsonCodeBlocks(text) {
2622
+ const results = [];
2623
+ let match;
2624
+ JSON_BLOCK_RE.lastIndex = 0;
2625
+ while ((match = JSON_BLOCK_RE.exec(text)) !== null) {
2626
+ try {
2627
+ const raw = JSON.parse(match[1]);
2628
+ if (typeof raw.name !== "string") continue;
2629
+ const input = typeof raw.input === "object" && raw.input !== null ? raw.input : {};
2630
+ results.push({ name: raw.name, input });
2631
+ } catch {
2632
+ }
2633
+ }
2634
+ return results;
2635
+ }
2636
+ function tryFunctionCallObjects(text) {
2637
+ const results = [];
2638
+ let match;
2639
+ FUNCTION_OBJ_RE.lastIndex = 0;
2640
+ while ((match = FUNCTION_OBJ_RE.exec(text)) !== null) {
2641
+ try {
2642
+ const raw = JSON.parse(match[0]);
2643
+ const fn = raw.function;
2644
+ if (!fn || typeof fn.name !== "string") continue;
2645
+ const input = typeof fn.arguments === "object" && fn.arguments !== null ? fn.arguments : {};
2646
+ results.push({ name: fn.name, input });
2647
+ } catch {
2648
+ }
2649
+ }
2650
+ return results;
2651
+ }
2652
+ function toToolCall(parsed, index) {
2653
+ return {
2654
+ id: `text-tool-${Date.now()}-${index}`,
2655
+ name: parsed.name,
2656
+ input: parsed.input
2657
+ };
2658
+ }
2659
+ function buildTextToolSystemPrompt(tools) {
2660
+ const toolDefs = tools.map((t) => {
2661
+ const props = t.inputSchema?.properties ?? {};
2662
+ const paramLines = Object.entries(props).map(([k, v]) => ` "${k}": "<${v.description ?? k}>"`);
2663
+ return `\u2022 ${t.name}: ${t.description}
2664
+ Input: {${paramLines.length ? "\n" + paramLines.join(",\n") + "\n " : ""}}`;
2665
+ }).join("\n");
2666
+ return `
2667
+ TOOL USE INSTRUCTIONS:
2668
+ You do not have native tool-use capability. To call a tool, write a <tool_call> block:
2669
+
2670
+ <tool_call>
2671
+ {"name": "<tool_name>", "input": {<parameters>}}
2672
+ </tool_call>
2673
+
2674
+ Available tools:
2675
+ ${toolDefs}
2676
+
2677
+ EXAMPLE \u2014 calling the "shell" tool to list files:
2678
+ <tool_call>
2679
+ {"name": "shell", "input": {"command": "ls -la /workspace"}}
2680
+ </tool_call>
2681
+
2682
+ You will then receive a user message with the result, then continue your work.
2683
+ Only call one tool at a time. When you have enough information, provide your final answer.`;
2684
+ }
2685
+
2298
2686
  // src/core/tiers/t3-worker.ts
2299
2687
  var T3_SYSTEM_PROMPT = `You are a T3 Worker agent in the Cascade AI system. Your job is to execute a specific subtask completely and accurately.
2300
2688
 
@@ -2496,6 +2884,9 @@ Now execute your subtask using this context where relevant.`
2496
2884
  const MAX_ITERATIONS = 15;
2497
2885
  const requiresArtifact = this.requiresArtifact();
2498
2886
  tools = [...tools];
2887
+ const t3Model = this.router.getModelForTier("T3");
2888
+ const useTextTools = t3Model?.supportsToolUse === false && tools.length > 0;
2889
+ const textToolSuffix = useTextTools ? buildTextToolSystemPrompt(tools) : "";
2499
2890
  while (iterations < MAX_ITERATIONS) {
2500
2891
  iterations++;
2501
2892
  this.throwIfCancelled();
@@ -2503,8 +2894,9 @@ Now execute your subtask using this context where relevant.`
2503
2894
  messages: this.context.getMessages(),
2504
2895
  systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
2505
2896
 
2506
- HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
2507
- tools: tools.length ? tools : void 0,
2897
+ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
2898
+ // Don't pass tools array when model can't use them natively
2899
+ tools: useTextTools ? void 0 : tools.length ? tools : void 0,
2508
2900
  maxTokens: 4096
2509
2901
  };
2510
2902
  const result = await this.router.generate(
@@ -2514,9 +2906,19 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
2514
2906
  this.emit("stream:token", { tierId: this.id, text: chunk.text });
2515
2907
  }
2516
2908
  );
2517
- await this.context.addMessage({ role: "assistant", content: result.content, toolCalls: result.toolCalls });
2518
- if (!result.toolCalls?.length) {
2909
+ let effectiveToolCalls = result.toolCalls ?? [];
2910
+ if (useTextTools && effectiveToolCalls.length === 0) {
2911
+ const textCalls = parseTextToolCalls(result.content);
2912
+ effectiveToolCalls = textCalls.map((tc, i) => toToolCall(tc, i));
2913
+ }
2914
+ const effectiveResult = { ...result, toolCalls: effectiveToolCalls };
2915
+ await this.context.addMessage({ role: "assistant", content: result.content, toolCalls: effectiveToolCalls });
2916
+ if (!effectiveResult.toolCalls?.length) {
2519
2917
  if (requiresArtifact) {
2918
+ const artifactCheck = await this.verifyArtifacts(this.assignment);
2919
+ if (artifactCheck.ok) {
2920
+ return { output: result.content, toolCalls: allToolCalls };
2921
+ }
2520
2922
  stalledArtifactIterations += 1;
2521
2923
  if (stalledArtifactIterations >= 2) {
2522
2924
  if (stalledArtifactIterations === 2) {
@@ -2526,17 +2928,24 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
2526
2928
  }
2527
2929
  await this.context.addMessage({
2528
2930
  role: "user",
2529
- content: "You have not yet created and verified the required artifact. Use tools to create the file in the workspace, verify it exists, and inspect the result before concluding."
2931
+ content: `You have not yet created and verified the required artifact. Issues: ${artifactCheck.issues.join("; ")}. Use tools to create the file in the workspace, verify it exists, and inspect the result before concluding.`
2530
2932
  });
2531
2933
  continue;
2532
2934
  }
2533
2935
  return { output: result.content, toolCalls: allToolCalls };
2534
2936
  }
2535
2937
  stalledArtifactIterations = 0;
2536
- if (result.finishReason === "stop" && !requiresArtifact) {
2537
- return { output: result.content, toolCalls: allToolCalls };
2938
+ if (effectiveResult.finishReason === "stop" && effectiveResult.toolCalls.length === 0) {
2939
+ if (requiresArtifact) {
2940
+ const artifactCheck = await this.verifyArtifacts(this.assignment);
2941
+ if (artifactCheck.ok) {
2942
+ return { output: result.content, toolCalls: allToolCalls };
2943
+ }
2944
+ } else {
2945
+ return { output: result.content, toolCalls: allToolCalls };
2946
+ }
2538
2947
  }
2539
- for (const tc of result.toolCalls) {
2948
+ for (const tc of effectiveResult.toolCalls) {
2540
2949
  allToolCalls.push(tc);
2541
2950
  const toolResult = await this.executeTool(tc);
2542
2951
  await this.context.addMessage({
@@ -2594,13 +3003,15 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
2594
3003
  currentAction: `Using tool: ${tc.name}`,
2595
3004
  status: "IN_PROGRESS"
2596
3005
  });
3006
+ this.emit("tool:call", { id: tc.id, tierId: this.id, toolName: tc.name, input: tc.input });
3007
+ const toolStartMs = Date.now();
2597
3008
  try {
2598
3009
  const result = await this.toolRegistry.execute(tc.name, tc.input, {
2599
3010
  tierId: this.id,
2600
3011
  sessionId: this.taskId,
2601
3012
  requireApproval: false,
2602
- saveSnapshot: async (path14, content) => {
2603
- this.store?.addFileSnapshot(this.taskId, path14, content);
3013
+ saveSnapshot: async (path17, content) => {
3014
+ this.store?.addFileSnapshot(this.taskId, path17, content);
2604
3015
  },
2605
3016
  sendPeerSync: (to, syncType, content) => {
2606
3017
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -2617,11 +3028,83 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
2617
3028
  this.audit.fileChange(this.id, tc.input["path"] ?? "unknown", tc.name);
2618
3029
  }
2619
3030
  }
2620
- this.emit("tool:result", { tierId: this.id, toolName: tc.name, result });
3031
+ const durationMs = Date.now() - toolStartMs;
3032
+ this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, output: typeof result === "string" ? result : JSON.stringify(result), durationMs });
2621
3033
  return typeof result === "string" ? result : JSON.stringify(result);
2622
3034
  } catch (err) {
2623
- return `Tool error: ${err instanceof Error ? err.message : String(err)}`;
3035
+ const durationMs = Date.now() - toolStartMs;
3036
+ const errMsg = err instanceof Error ? err.message : String(err);
3037
+ this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: errMsg, durationMs });
3038
+ return `Tool error: ${errMsg}`;
3039
+ }
3040
+ }
3041
+ /**
3042
+ * Adaptive fallback cascade — invoked when executeTool() fails.
3043
+ * Strategy order:
3044
+ * 1. Find a semantically similar registered tool and retry with same input
3045
+ * 2. Synthesize a new tool via ToolCreator (if available) and run it
3046
+ * 3. Return the original error so the agent loop can decide what to do next
3047
+ */
3048
+ async adaptiveFallback(tc, originalError) {
3049
+ const altTool = this.findAlternativeTool(tc.name);
3050
+ if (altTool) {
3051
+ this.log(`Adaptive fallback: trying alternative tool "${altTool}" for failed "${tc.name}"`);
3052
+ this.sendStatusUpdate({ progressPct: 50, currentAction: `Fallback: trying ${altTool}`, status: "IN_PROGRESS" });
3053
+ try {
3054
+ const result = await this.toolRegistry.execute(altTool, tc.input, {
3055
+ tierId: this.id,
3056
+ sessionId: this.taskId,
3057
+ requireApproval: false
3058
+ });
3059
+ const str = typeof result === "string" ? result : JSON.stringify(result);
3060
+ if (!str.startsWith("Tool error:") && !str.startsWith("Error:")) {
3061
+ return `[Fallback via ${altTool}]: ${str}`;
3062
+ }
3063
+ } catch {
3064
+ }
3065
+ }
3066
+ if (this.toolCreator) {
3067
+ this.log(`Adaptive fallback: requesting dynamic tool synthesis for "${tc.name}"`);
3068
+ this.sendStatusUpdate({ progressPct: 55, currentAction: `Synthesizing fallback tool for: ${tc.name}`, status: "IN_PROGRESS" });
3069
+ try {
3070
+ const newToolName = await this.toolCreator.createTool(
3071
+ `Replacement for "${tc.name}" \u2014 original failed with: ${originalError.slice(0, 150)}`,
3072
+ this.assignment?.subtaskTitle ?? tc.name
3073
+ );
3074
+ if (newToolName) {
3075
+ this.log(`Adaptive fallback: synthesized "${newToolName}", retrying`);
3076
+ const result = await this.toolRegistry.execute(newToolName, tc.input, {
3077
+ tierId: this.id,
3078
+ sessionId: this.taskId,
3079
+ requireApproval: false
3080
+ });
3081
+ const str = typeof result === "string" ? result : JSON.stringify(result);
3082
+ if (!str.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str}`;
3083
+ }
3084
+ } catch {
3085
+ }
2624
3086
  }
3087
+ return originalError;
3088
+ }
3089
+ /**
3090
+ * Find a registered tool whose name/description semantically overlaps with
3091
+ * the failing tool. Returns the best candidate name, or null if none found.
3092
+ */
3093
+ findAlternativeTool(failedToolName) {
3094
+ const failedKeywords = failedToolName.toLowerCase().split(/[_\-\s]+/);
3095
+ const allTools = this.toolRegistry.getToolDefinitions();
3096
+ let bestScore = 0;
3097
+ let bestName = null;
3098
+ for (const tool of allTools) {
3099
+ if (tool.name === failedToolName) continue;
3100
+ const toolWords = tool.name.toLowerCase().split(/[_\-\s]+/);
3101
+ const score = failedKeywords.filter((k) => toolWords.includes(k)).length;
3102
+ if (score > bestScore && score >= 1) {
3103
+ bestScore = score;
3104
+ bestName = tool.name;
3105
+ }
3106
+ }
3107
+ return bestName;
2625
3108
  }
2626
3109
  /**
2627
3110
  * Announce which files this T3 plans to edit, then acquire locks on them
@@ -2681,12 +3164,12 @@ ${assignment.expectedOutput}`;
2681
3164
  if (!artifactPaths.length) return { ok: true, issues: [] };
2682
3165
  const issues = [];
2683
3166
  const { exec: exec3 } = await import('child_process');
2684
- const { promisify: promisify3 } = await import('util');
2685
- const execAsync2 = promisify3(exec3);
3167
+ const { promisify: promisify4 } = await import('util');
3168
+ const execAsync2 = promisify4(exec3);
2686
3169
  for (const artifactPath of artifactPaths) {
2687
- const absolutePath = path13__default.default.resolve(process.cwd(), artifactPath);
3170
+ const absolutePath = path16__default.default.resolve(process.cwd(), artifactPath);
2688
3171
  try {
2689
- const stat = await fs2__default.default.stat(absolutePath);
3172
+ const stat = await fs3__default.default.stat(absolutePath);
2690
3173
  if (!stat.isFile()) {
2691
3174
  issues.push(`Expected artifact is not a file: ${artifactPath}`);
2692
3175
  continue;
@@ -2696,7 +3179,7 @@ ${assignment.expectedOutput}`;
2696
3179
  continue;
2697
3180
  }
2698
3181
  if (!/\.pdf$/i.test(artifactPath)) {
2699
- const content = await fs2__default.default.readFile(absolutePath, "utf-8");
3182
+ const content = await fs3__default.default.readFile(absolutePath, "utf-8");
2700
3183
  if (!content.trim()) {
2701
3184
  issues.push(`Artifact content is empty: ${artifactPath}`);
2702
3185
  continue;
@@ -2705,7 +3188,7 @@ ${assignment.expectedOutput}`;
2705
3188
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
2706
3189
  continue;
2707
3190
  }
2708
- const ext = path13__default.default.extname(absolutePath).toLowerCase();
3191
+ const ext = path16__default.default.extname(absolutePath).toLowerCase();
2709
3192
  try {
2710
3193
  if (ext === ".ts" || ext === ".tsx") {
2711
3194
  await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -2823,6 +3306,11 @@ var PeerBus = class extends EventEmitter__default.default {
2823
3306
  barriers = /* @__PURE__ */ new Map();
2824
3307
  broadcastLog = [];
2825
3308
  fileLocks = /* @__PURE__ */ new Map();
3309
+ /** subtaskIds whose T3 is being retried by T2 — dependents should re-wait rather than fail fast */
3310
+ retryPending = /* @__PURE__ */ new Set();
3311
+ /** Called when any peer message or broadcast is sent — used for dashboard visibility. */
3312
+ onPeerMessage;
3313
+ sessionId = "";
2826
3314
  register(peerId) {
2827
3315
  this.members.add(peerId);
2828
3316
  }
@@ -2844,11 +3332,33 @@ var PeerBus = class extends EventEmitter__default.default {
2844
3332
  this.waiters.delete(subtaskId);
2845
3333
  }
2846
3334
  /**
2847
- * Wait for a specific subtask's output resolves immediately if already available
3335
+ * Mark a subtask as retry-pending so dependents re-wait instead of failing fast
3336
+ * when they see an ESCALATED status.
3337
+ */
3338
+ markRetryPending(subtaskId) {
3339
+ this.retryPending.add(subtaskId);
3340
+ this.outputs.delete(subtaskId);
3341
+ }
3342
+ /** Called by T2 after retry resolves (success or final failure). */
3343
+ clearRetryPending(subtaskId) {
3344
+ this.retryPending.delete(subtaskId);
3345
+ }
3346
+ /** Remove a single output entry so a respawned worker can republish without clearing prior-wave outputs. */
3347
+ clearOutput(subtaskId) {
3348
+ this.outputs.delete(subtaskId);
3349
+ this.waiters.delete(subtaskId);
3350
+ this.retryPending.delete(subtaskId);
3351
+ }
3352
+ isRetryPending(subtaskId) {
3353
+ return this.retryPending.has(subtaskId);
3354
+ }
3355
+ /**
3356
+ * Wait for a specific subtask's output — resolves immediately if already available.
3357
+ * If the output is ESCALATED but a retry is pending, waits for the retry result.
2848
3358
  */
2849
3359
  waitFor(subtaskId, timeoutMs = 12e4) {
2850
3360
  const existing = this.outputs.get(subtaskId);
2851
- if (existing) return Promise.resolve(existing);
3361
+ if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
2852
3362
  return new Promise((resolve, reject) => {
2853
3363
  const resolver = (output) => {
2854
3364
  clearTimeout(timer);
@@ -2879,6 +3389,7 @@ var PeerBus = class extends EventEmitter__default.default {
2879
3389
  * Also logs to broadcastLog so collect() can retrieve recent broadcasts.
2880
3390
  */
2881
3391
  broadcast(fromId, payload) {
3392
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2882
3393
  const msg = {
2883
3394
  fromId,
2884
3395
  toId: "*",
@@ -2886,10 +3397,18 @@ var PeerBus = class extends EventEmitter__default.default {
2886
3397
  subtaskId: "",
2887
3398
  syncType: "SHARE_OUTPUT",
2888
3399
  payload,
2889
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3400
+ timestamp
2890
3401
  };
2891
- this.broadcastLog.push({ fromId, payload, timestamp: msg.timestamp });
3402
+ this.broadcastLog.push({ fromId, payload, timestamp });
2892
3403
  this.emit("broadcast", msg);
3404
+ this.onPeerMessage?.({
3405
+ fromId,
3406
+ toId: void 0,
3407
+ syncType: "SHARE_OUTPUT",
3408
+ payload: typeof payload === "string" ? payload : JSON.stringify(payload),
3409
+ timestamp,
3410
+ sessionId: this.sessionId
3411
+ });
2893
3412
  }
2894
3413
  /**
2895
3414
  * Collect all broadcast messages received within a time window.
@@ -2975,6 +3494,16 @@ var PeerBus = class extends EventEmitter__default.default {
2975
3494
  isFileLocked(filePath) {
2976
3495
  return this.fileLocks.has(filePath);
2977
3496
  }
3497
+ /**
3498
+ * Reset all runtime output/waiter state for a fresh T3 respawn wave.
3499
+ * Preserves member registrations and barrier definitions.
3500
+ */
3501
+ reset() {
3502
+ this.outputs.clear();
3503
+ this.waiters.clear();
3504
+ this.retryPending.clear();
3505
+ this.broadcastLog = [];
3506
+ }
2978
3507
  /**
2979
3508
  * Clear broadcast log — call between phases to avoid stale announcements.
2980
3509
  */
@@ -2985,6 +3514,7 @@ var PeerBus = class extends EventEmitter__default.default {
2985
3514
  * Send a targeted message to a specific peer
2986
3515
  */
2987
3516
  send(fromId, toId, syncType, subtaskId, payload) {
3517
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2988
3518
  const msg = {
2989
3519
  fromId,
2990
3520
  toId,
@@ -2992,10 +3522,18 @@ var PeerBus = class extends EventEmitter__default.default {
2992
3522
  subtaskId,
2993
3523
  syncType,
2994
3524
  payload,
2995
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3525
+ timestamp
2996
3526
  };
2997
3527
  this.emit(`message:${toId}`, msg);
2998
3528
  this.emit("message", msg);
3529
+ this.onPeerMessage?.({
3530
+ fromId,
3531
+ toId,
3532
+ syncType: syncType ?? "SHARE_OUTPUT",
3533
+ payload: typeof payload === "string" ? payload : JSON.stringify(payload),
3534
+ timestamp,
3535
+ sessionId: this.sessionId
3536
+ });
2999
3537
  }
3000
3538
  /**
3001
3539
  * Barrier — wait until N peers have all reached this point
@@ -3048,6 +3586,8 @@ var T2Manager = class extends BaseTier {
3048
3586
  t2PeerBus;
3049
3587
  permissionEscalator;
3050
3588
  toolCreator;
3589
+ /** AbortController for the current T3 wave — aborted on cancel-and-respawn */
3590
+ waveAbortController = null;
3051
3591
  setPeerBus(bus) {
3052
3592
  this.t2PeerBus = bus;
3053
3593
  this.t2PeerBus.register(this.id);
@@ -3056,6 +3596,14 @@ var T2Manager = class extends BaseTier {
3056
3596
  this.receivePeerSync(msg.fromId, msg.payload);
3057
3597
  });
3058
3598
  }
3599
+ setPeerMessageCallback(cb, sessionId) {
3600
+ this.t3PeerBus.onPeerMessage = cb;
3601
+ this.t3PeerBus.sessionId = sessionId;
3602
+ if (this.t2PeerBus) {
3603
+ this.t2PeerBus.onPeerMessage = cb;
3604
+ this.t2PeerBus.sessionId = sessionId;
3605
+ }
3606
+ }
3059
3607
  constructor(router, toolRegistry, parentId) {
3060
3608
  super("T2", void 0, parentId);
3061
3609
  this.router = router;
@@ -3223,6 +3771,26 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3223
3771
  }];
3224
3772
  }
3225
3773
  }
3774
+ buildWorkerMap(assignments, taskId) {
3775
+ const workerMap = /* @__PURE__ */ new Map();
3776
+ for (const a of assignments) {
3777
+ const worker = new T3Worker(this.router, this.toolRegistry, this.id);
3778
+ if (this.store) worker.setStore(this.store, taskId);
3779
+ worker.setPeerBus(this.t3PeerBus);
3780
+ if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
3781
+ if (this.toolCreator) worker.setToolCreator(this.toolCreator);
3782
+ workerMap.set(a.subtaskId, worker);
3783
+ this.t3Workers.set(a.subtaskId, worker);
3784
+ worker.on("stream:token", (e) => this.emit("stream:token", e));
3785
+ worker.on("log", (e) => this.emit("log", e));
3786
+ worker.on("tier:status", (e) => this.emit("tier:status", e));
3787
+ worker.on("tool:approval-request", (e) => this.emit("tool:approval-request", {
3788
+ ...e,
3789
+ __cascadeResponder: (decision) => worker.emit(`tool:approval-response:${e.id}`, decision)
3790
+ }));
3791
+ }
3792
+ return workerMap;
3793
+ }
3226
3794
  async executeSubtasks(subtasks, taskId) {
3227
3795
  const assignments = subtasks.map((s) => ({
3228
3796
  ...s,
@@ -3249,6 +3817,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3249
3817
  worker.on("stream:token", (e) => this.emit("stream:token", e));
3250
3818
  worker.on("log", (e) => this.emit("log", e));
3251
3819
  worker.on("tier:status", (e) => this.emit("tier:status", e));
3820
+ worker.on("tool:call", (e) => this.emit("tool:call", e));
3821
+ worker.on("tool:result", (e) => this.emit("tool:result", e));
3252
3822
  worker.on("tool:approval-request", (e) => this.emit("tool:approval-request", {
3253
3823
  ...e,
3254
3824
  __cascadeResponder: (decision) => worker.emit(`tool:approval-response:${e.id}`, decision)
@@ -3285,6 +3855,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3285
3855
  const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
3286
3856
  let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
3287
3857
  let wave = 0;
3858
+ let respawnBudget = 1;
3288
3859
  while (remaining.size > 0) {
3289
3860
  const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
3290
3861
  if (runnableIds.length === 0) {
@@ -3305,15 +3876,62 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3305
3876
  status: "IN_PROGRESS"
3306
3877
  });
3307
3878
  this.throwIfCancelled();
3879
+ this.waveAbortController = new AbortController();
3880
+ const waveSignal = AbortSignal.any(
3881
+ [this.signal, this.waveAbortController.signal].filter(Boolean)
3882
+ );
3308
3883
  const waveResults = await Promise.allSettled(
3309
3884
  runnableIds.map(async (id) => {
3310
3885
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
3311
3886
  const worker = workerMap.get(id);
3312
- const result = await worker.execute(assignment, taskId, this.signal);
3887
+ const result = await worker.execute(assignment, taskId, waveSignal);
3313
3888
  resultMap.set(id, result);
3314
3889
  return result;
3315
3890
  })
3316
3891
  );
3892
+ const escalatedToolIdx = respawnBudget > 0 ? waveResults.findIndex(
3893
+ (r) => r.status === "fulfilled" && r.value.status === "ESCALATED" && r.value.issues.some((iss) => iss.includes("dynamic tool generation"))
3894
+ ) : -1;
3895
+ if (escalatedToolIdx !== -1 && this.toolCreator) {
3896
+ respawnBudget--;
3897
+ this.waveAbortController.abort();
3898
+ const escalatedId = runnableIds[escalatedToolIdx];
3899
+ const escalatedAssignment = sanitizedAssignments.find((a) => a.subtaskId === escalatedId);
3900
+ this.log(`Wave ${wave}: tool escalation detected \u2014 synthesizing tool then respawning all ${runnableIds.length} worker(s)`);
3901
+ this.sendStatusUpdate({
3902
+ progressPct: 50,
3903
+ currentAction: `Synthesizing dynamic tool for: ${escalatedAssignment.subtaskTitle}`,
3904
+ status: "IN_PROGRESS"
3905
+ });
3906
+ const toolName = await this.toolCreator.createTool(
3907
+ `Help complete: ${escalatedAssignment.subtaskTitle}`,
3908
+ escalatedAssignment.description
3909
+ );
3910
+ if (toolName) {
3911
+ this.log(`Tool "${toolName}" created \u2014 respawning wave ${wave} workers`);
3912
+ for (const a of sanitizedAssignments) {
3913
+ if (runnableIds.includes(a.subtaskId)) {
3914
+ a.description += `
3915
+
3916
+ [SYSTEM]: Dynamic tool "${toolName}" is now available \u2014 use it to complete your task.`;
3917
+ }
3918
+ }
3919
+ }
3920
+ for (const id of runnableIds) {
3921
+ this.t3PeerBus.clearOutput(id);
3922
+ }
3923
+ const freshMap = this.buildWorkerMap(
3924
+ sanitizedAssignments.filter((a) => runnableIds.includes(a.subtaskId)),
3925
+ taskId
3926
+ );
3927
+ for (const [k, v] of freshMap) workerMap.set(k, v);
3928
+ for (const id of runnableIds) {
3929
+ remaining.add(id);
3930
+ inDegree.set(id, 0);
3931
+ }
3932
+ wave--;
3933
+ continue;
3934
+ }
3317
3935
  for (let i = 0; i < runnableIds.length; i++) {
3318
3936
  const id = runnableIds[i];
3319
3937
  remaining.delete(id);
@@ -3321,61 +3939,22 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3321
3939
  if (r.status === "rejected") {
3322
3940
  this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
3323
3941
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
3324
- const retried = await this.retryT3(assignment, taskId);
3325
- resultMap.set(id, retried);
3326
- } else if (r.status === "fulfilled" && r.value.status === "ESCALATED" && r.value.issues.some((i2) => i2.includes("dynamic tool generation"))) {
3327
- const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
3328
- if (this.toolCreator) {
3329
- this.log(`T3 escalated for tool. T2 spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`);
3330
- this.sendStatusUpdate({
3331
- progressPct: 50,
3332
- currentAction: `Spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`,
3333
- status: "IN_PROGRESS"
3942
+ try {
3943
+ const retried = await this.retryT3(assignment, taskId);
3944
+ resultMap.set(id, retried);
3945
+ } catch (retryErr) {
3946
+ const msg = retryErr instanceof Error ? retryErr.message : String(retryErr);
3947
+ this.log(`T3 retry for ${id} threw before publishing \u2014 unblocking dependents with FAILED`);
3948
+ this.t3PeerBus.publish(this.id, id, `Retry failed: ${msg}`, "FAILED");
3949
+ resultMap.set(id, {
3950
+ subtaskId: id,
3951
+ status: "FAILED",
3952
+ output: `Retry threw: ${msg}`,
3953
+ testResults: { checksRun: [], passed: [], failed: [] },
3954
+ issues: [msg],
3955
+ peerSyncsUsed: [],
3956
+ correctionAttempts: 1
3334
3957
  });
3335
- const toolName = await this.toolCreator.createTool(
3336
- `Help complete: ${assignment.subtaskTitle}`,
3337
- assignment.description
3338
- );
3339
- if (toolName) {
3340
- this.log(`T2 verifying new tool: ${toolName}`);
3341
- this.sendStatusUpdate({
3342
- progressPct: 60,
3343
- currentAction: `T2 Verifying new tool: ${toolName}`,
3344
- status: "IN_PROGRESS"
3345
- });
3346
- try {
3347
- const verifyResult = await this.router.generate("T2", {
3348
- messages: [{ role: "user", content: `A new tool named "${toolName}" was just created dynamically to help with: ${assignment.description}. Based on its name and purpose, does this seem like a valid addition? Reply "VERIFIED" or "REJECTED".` }],
3349
- systemPrompt: this.systemPromptOverride + "You are T2 Manager verifying a dynamic tool.",
3350
- maxTokens: 50
3351
- });
3352
- if (!verifyResult.content.toUpperCase().includes("REJECTED")) {
3353
- this.log(`T2 verification passed for ${toolName}. Restarting original T3.`);
3354
- const retried = await this.retryT3({
3355
- ...assignment,
3356
- description: `${assignment.description}
3357
-
3358
- [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built and verified for you. Use it to complete your task.`
3359
- }, taskId);
3360
- resultMap.set(id, retried);
3361
- } else {
3362
- this.log(`T2 rejected the dynamic tool: ${toolName}`);
3363
- resultMap.set(id, r.value);
3364
- }
3365
- } catch {
3366
- const retried = await this.retryT3({
3367
- ...assignment,
3368
- description: `${assignment.description}
3369
-
3370
- [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built for you. Use it to complete your task.`
3371
- }, taskId);
3372
- resultMap.set(id, retried);
3373
- }
3374
- } else {
3375
- resultMap.set(id, r.value);
3376
- }
3377
- } else {
3378
- resultMap.set(id, r.value);
3379
3958
  }
3380
3959
  }
3381
3960
  for (const dependent of adj.get(id) ?? []) {
@@ -3646,6 +4225,8 @@ var T1Administrator = class extends BaseTier {
3646
4225
  toolCreator;
3647
4226
  /** Stored overall task goal — used when evaluating escalated permissions */
3648
4227
  taskGoal = "";
4228
+ peerMessageCallback;
4229
+ peerMessageSessionId = "";
3649
4230
  constructor(router, toolRegistry, config) {
3650
4231
  super("T1", "T1");
3651
4232
  this.router = router;
@@ -3666,6 +4247,12 @@ var T1Administrator = class extends BaseTier {
3666
4247
  setToolCreator(creator) {
3667
4248
  this.toolCreator = creator;
3668
4249
  }
4250
+ setPeerMessageCallback(cb, sessionId) {
4251
+ this.peerMessageCallback = cb;
4252
+ this.peerMessageSessionId = sessionId;
4253
+ this.t2PeerBus.onPeerMessage = cb;
4254
+ this.t2PeerBus.sessionId = sessionId;
4255
+ }
3669
4256
  async execute(userPrompt, images, systemContext, signal) {
3670
4257
  this.signal = signal;
3671
4258
  this.taskId = crypto.randomUUID();
@@ -3885,6 +4472,9 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
3885
4472
  manager.setStore(this.store);
3886
4473
  }
3887
4474
  manager.setPeerBus(this.t2PeerBus);
4475
+ if (this.peerMessageCallback) {
4476
+ manager.setPeerMessageCallback(this.peerMessageCallback, this.peerMessageSessionId);
4477
+ }
3888
4478
  if (this.permissionEscalator) {
3889
4479
  manager.setPermissionEscalator(this.permissionEscalator);
3890
4480
  }
@@ -3895,6 +4485,8 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
3895
4485
  bind(manager, "stream:token", (e) => this.emit("stream:token", e));
3896
4486
  bind(manager, "log", (e) => this.emit("log", e));
3897
4487
  bind(manager, "tier:status", (e) => this.emit("tier:status", e));
4488
+ bind(manager, "tool:call", (e) => this.emit("tool:call", e));
4489
+ bind(manager, "tool:result", (e) => this.emit("tool:result", e));
3898
4490
  bind(manager, "tool:approval-request", (e) => this.emit("tool:approval-request", e));
3899
4491
  bind(manager, "message", (msg) => {
3900
4492
  if (msg.type === "PEER_SYNC") {
@@ -4254,13 +4846,21 @@ function resolveInWorkspace(workspaceRoot, input) {
4254
4846
  if (typeof input !== "string" || input.length === 0) {
4255
4847
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
4256
4848
  }
4257
- const root = path13__default.default.resolve(workspaceRoot);
4258
- const abs = path13__default.default.isAbsolute(input) ? path13__default.default.resolve(input) : path13__default.default.resolve(root, input);
4259
- const rel = path13__default.default.relative(root, abs);
4260
- if (rel === "" || rel === ".") return abs;
4261
- if (rel.startsWith("..") || path13__default.default.isAbsolute(rel)) {
4849
+ const root = path16__default.default.resolve(workspaceRoot);
4850
+ const abs = path16__default.default.isAbsolute(input) ? path16__default.default.resolve(input) : path16__default.default.resolve(root, input);
4851
+ const rel = path16__default.default.relative(root, abs);
4852
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path16__default.default.isAbsolute(rel)) {
4262
4853
  throw new WorkspaceSandboxError(input, root);
4263
4854
  }
4855
+ try {
4856
+ const real = fs15__default.default.realpathSync(abs);
4857
+ const realRel = path16__default.default.relative(root, real);
4858
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path16__default.default.isAbsolute(realRel))) {
4859
+ throw new WorkspaceSandboxError(input, root);
4860
+ }
4861
+ } catch (e) {
4862
+ if (e instanceof WorkspaceSandboxError) throw e;
4863
+ }
4264
4864
  return abs;
4265
4865
  }
4266
4866
 
@@ -4282,7 +4882,7 @@ var FileReadTool = class extends BaseTool {
4282
4882
  const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
4283
4883
  const offset = input["offset"] ?? 1;
4284
4884
  const limit = input["limit"];
4285
- const content = await fs2__default.default.readFile(absPath, "utf-8");
4885
+ const content = await fs3__default.default.readFile(absPath, "utf-8");
4286
4886
  const lines = content.split("\n");
4287
4887
  const start = Math.max(0, offset - 1);
4288
4888
  const end = limit ? start + limit : lines.length;
@@ -4311,13 +4911,13 @@ var FileWriteTool = class extends BaseTool {
4311
4911
  const content = input["content"];
4312
4912
  if (options.saveSnapshot) {
4313
4913
  try {
4314
- const oldContent = await fs2__default.default.readFile(absPath, "utf-8");
4914
+ const oldContent = await fs3__default.default.readFile(absPath, "utf-8");
4315
4915
  await options.saveSnapshot(absPath, oldContent);
4316
4916
  } catch {
4317
4917
  }
4318
4918
  }
4319
- await fs2__default.default.mkdir(path13__default.default.dirname(absPath), { recursive: true });
4320
- await fs2__default.default.writeFile(absPath, content, "utf-8");
4919
+ await fs3__default.default.mkdir(path16__default.default.dirname(absPath), { recursive: true });
4920
+ await fs3__default.default.writeFile(absPath, content, "utf-8");
4321
4921
  return `Written ${content.length} characters to ${filePath}`;
4322
4922
  }
4323
4923
  };
@@ -4343,7 +4943,7 @@ var FileEditTool = class extends BaseTool {
4343
4943
  const oldString = input["old_string"];
4344
4944
  const newString = input["new_string"];
4345
4945
  const replaceAll = input["replace_all"] ?? false;
4346
- const rawContent = await fs2__default.default.readFile(absPath, "utf-8");
4946
+ const rawContent = await fs3__default.default.readFile(absPath, "utf-8");
4347
4947
  if (options.saveSnapshot) {
4348
4948
  await options.saveSnapshot(absPath, rawContent);
4349
4949
  }
@@ -4355,7 +4955,7 @@ var FileEditTool = class extends BaseTool {
4355
4955
  );
4356
4956
  }
4357
4957
  const updated = replaceAll ? content.split(normalizedOld).join(newString) : content.replace(normalizedOld, newString);
4358
- await fs2__default.default.writeFile(absPath, updated, "utf-8");
4958
+ await fs3__default.default.writeFile(absPath, updated, "utf-8");
4359
4959
  const count = replaceAll ? content.split(normalizedOld).length - 1 : 1;
4360
4960
  return `Replaced ${count} occurrence(s) in ${filePath}`;
4361
4961
  }
@@ -4378,12 +4978,12 @@ var FileDeleteTool = class extends BaseTool {
4378
4978
  const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
4379
4979
  if (options.saveSnapshot) {
4380
4980
  try {
4381
- const oldContent = await fs2__default.default.readFile(absPath, "utf-8");
4981
+ const oldContent = await fs3__default.default.readFile(absPath, "utf-8");
4382
4982
  await options.saveSnapshot(absPath, oldContent);
4383
4983
  } catch {
4384
4984
  }
4385
4985
  }
4386
- await fs2__default.default.rm(absPath, { recursive: false });
4986
+ await fs3__default.default.rm(absPath, { recursive: false });
4387
4987
  return `Deleted ${filePath}`;
4388
4988
  }
4389
4989
  };
@@ -4400,7 +5000,7 @@ var FileListTool = class extends BaseTool {
4400
5000
  async execute(input, _options) {
4401
5001
  const inputPath = input["path"] || ".";
4402
5002
  const absPath = resolveInWorkspace(this.workspaceRoot, inputPath);
4403
- const entries = await fs2__default.default.readdir(absPath, { withFileTypes: true });
5003
+ const entries = await fs3__default.default.readdir(absPath, { withFileTypes: true });
4404
5004
  return entries.map((e) => `${e.isDirectory() ? "[DIR] " : " "}${e.name}`).join("\n") || "(empty directory)";
4405
5005
  }
4406
5006
  };
@@ -4783,8 +5383,8 @@ var ImageAnalyzeTool = class extends BaseTool {
4783
5383
  }
4784
5384
  };
4785
5385
  async function fileToImageAttachment(filePath) {
4786
- const data = await fs2__default.default.readFile(filePath);
4787
- const ext = path13__default.default.extname(filePath).toLowerCase();
5386
+ const data = await fs3__default.default.readFile(filePath);
5387
+ const ext = path16__default.default.extname(filePath).toLowerCase();
4788
5388
  const mimeMap = {
4789
5389
  ".jpg": "image/jpeg",
4790
5390
  ".jpeg": "image/jpeg",
@@ -4818,14 +5418,14 @@ var PDFCreateTool = class extends BaseTool {
4818
5418
  const filePath = input["path"];
4819
5419
  const content = input["content"];
4820
5420
  const title = input["title"];
4821
- const dir = path13__default.default.dirname(filePath);
4822
- if (!fs11__default.default.existsSync(dir)) {
4823
- fs11__default.default.mkdirSync(dir, { recursive: true });
5421
+ const dir = path16__default.default.dirname(filePath);
5422
+ if (!fs15__default.default.existsSync(dir)) {
5423
+ fs15__default.default.mkdirSync(dir, { recursive: true });
4824
5424
  }
4825
5425
  return new Promise((resolve, reject) => {
4826
5426
  try {
4827
5427
  const doc = new PDFDocument__default.default({ margin: 50 });
4828
- const stream = fs11__default.default.createWriteStream(filePath);
5428
+ const stream = fs15__default.default.createWriteStream(filePath);
4829
5429
  doc.pipe(stream);
4830
5430
  if (title) {
4831
5431
  doc.info["Title"] = title;
@@ -4903,14 +5503,14 @@ var CodeInterpreterTool = class extends BaseTool {
4903
5503
  }
4904
5504
  cmdPrefix = NODE_CMD;
4905
5505
  }
4906
- const tmpDir = path13__default.default.join(process.cwd(), ".cascade", "tmp");
4907
- if (!fs11__default.default.existsSync(tmpDir)) {
4908
- fs11__default.default.mkdirSync(tmpDir, { recursive: true });
5506
+ const tmpDir = path16__default.default.join(process.cwd(), ".cascade", "tmp");
5507
+ if (!fs15__default.default.existsSync(tmpDir)) {
5508
+ fs15__default.default.mkdirSync(tmpDir, { recursive: true });
4909
5509
  }
4910
5510
  const extension = language === "python" ? "py" : "js";
4911
5511
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
4912
- const filePath = path13__default.default.join(tmpDir, fileName);
4913
- fs11__default.default.writeFileSync(filePath, code, "utf-8");
5512
+ const filePath = path16__default.default.join(tmpDir, fileName);
5513
+ fs15__default.default.writeFileSync(filePath, code, "utf-8");
4914
5514
  const quotedPath = `"${filePath}"`;
4915
5515
  const quotedArgs = args.map((a) => `"${a}"`).join(" ");
4916
5516
  const fullCmd = `${cmdPrefix} ${quotedPath}${quotedArgs ? " " + quotedArgs : ""}`;
@@ -4919,8 +5519,8 @@ var CodeInterpreterTool = class extends BaseTool {
4919
5519
  child_process.exec(fullCmd, { cwd: process.cwd(), timeout: 3e4 }, (error, stdout, stderr) => {
4920
5520
  const duration = Date.now() - startMs;
4921
5521
  try {
4922
- if (fs11__default.default.existsSync(filePath)) {
4923
- fs11__default.default.unlinkSync(filePath);
5522
+ if (fs15__default.default.existsSync(filePath)) {
5523
+ fs15__default.default.unlinkSync(filePath);
4924
5524
  }
4925
5525
  } catch (cleanupErr) {
4926
5526
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -5180,6 +5780,253 @@ var WebSearchTool = class extends BaseTool {
5180
5780
  return lines.join("\n");
5181
5781
  }
5182
5782
  };
5783
+ var GlobTool = class extends BaseTool {
5784
+ name = "glob";
5785
+ description = "Fast file pattern matching. Returns file paths matching a glob pattern, sorted by modification time. Use this to find files by name patterns.";
5786
+ inputSchema = {
5787
+ type: "object",
5788
+ properties: {
5789
+ pattern: {
5790
+ type: "string",
5791
+ description: 'Glob pattern to match files against, e.g. "**/*.ts", "src/**/*.tsx"'
5792
+ },
5793
+ path: {
5794
+ type: "string",
5795
+ description: "Directory to search in. Defaults to the workspace root."
5796
+ }
5797
+ },
5798
+ required: ["pattern"]
5799
+ };
5800
+ async execute(input, _options) {
5801
+ const pattern = input["pattern"];
5802
+ const searchPath = input["path"] ? path16__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
5803
+ const matches = await glob.glob(pattern, {
5804
+ cwd: searchPath,
5805
+ ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
5806
+ nodir: true,
5807
+ dot: false
5808
+ });
5809
+ if (matches.length === 0) {
5810
+ return `No files matched pattern: ${pattern}`;
5811
+ }
5812
+ const withMtime = await Promise.all(
5813
+ matches.map(async (rel) => {
5814
+ try {
5815
+ const stat = await fs3__default.default.stat(path16__default.default.join(searchPath, rel));
5816
+ return { rel, mtime: stat.mtimeMs };
5817
+ } catch {
5818
+ return { rel, mtime: 0 };
5819
+ }
5820
+ })
5821
+ );
5822
+ withMtime.sort((a, b) => b.mtime - a.mtime);
5823
+ const lines = withMtime.map((f) => f.rel);
5824
+ return lines.join("\n");
5825
+ }
5826
+ };
5827
+ var execFileAsync = util.promisify(child_process.execFile);
5828
+ var GrepTool = class extends BaseTool {
5829
+ name = "grep";
5830
+ description = "Search file contents using a regex pattern. Returns matching lines with file paths and line numbers. Tries ripgrep (rg) first, falls back to Node.js regex scan.";
5831
+ inputSchema = {
5832
+ type: "object",
5833
+ properties: {
5834
+ pattern: {
5835
+ type: "string",
5836
+ description: "Regular expression pattern to search for in file contents"
5837
+ },
5838
+ path: {
5839
+ type: "string",
5840
+ description: "File or directory to search in. Defaults to workspace root."
5841
+ },
5842
+ glob: {
5843
+ type: "string",
5844
+ description: 'Glob pattern to filter files, e.g. "*.ts", "**/*.tsx"'
5845
+ },
5846
+ output_mode: {
5847
+ type: "string",
5848
+ enum: ["content", "files_with_matches", "count"],
5849
+ description: '"content" shows matching lines (default), "files_with_matches" shows file paths only, "count" shows match counts'
5850
+ },
5851
+ context: {
5852
+ type: "number",
5853
+ description: "Lines of context around each match (content mode only). Default: 0."
5854
+ },
5855
+ case_insensitive: {
5856
+ type: "boolean",
5857
+ description: "Case-insensitive search. Default: false."
5858
+ }
5859
+ },
5860
+ required: ["pattern"]
5861
+ };
5862
+ async execute(input, _options) {
5863
+ const pattern = input["pattern"];
5864
+ const searchPath = input["path"] ? path16__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
5865
+ const globPattern = input["glob"];
5866
+ const outputMode = input["output_mode"] ?? "content";
5867
+ const context = input["context"] ?? 0;
5868
+ const caseInsensitive = input["case_insensitive"] ?? false;
5869
+ try {
5870
+ const result = await this.runRipgrep(
5871
+ pattern,
5872
+ searchPath,
5873
+ globPattern,
5874
+ outputMode,
5875
+ context,
5876
+ caseInsensitive
5877
+ );
5878
+ return result;
5879
+ } catch {
5880
+ }
5881
+ return this.nodeScan(pattern, searchPath, globPattern, outputMode, context, caseInsensitive);
5882
+ }
5883
+ async runRipgrep(pattern, searchPath, globPattern, outputMode, context, caseInsensitive) {
5884
+ const args = ["--no-heading"];
5885
+ if (caseInsensitive) args.push("-i");
5886
+ if (outputMode === "files_with_matches") args.push("-l");
5887
+ else if (outputMode === "count") args.push("-c");
5888
+ else {
5889
+ args.push("-n");
5890
+ if (context > 0) args.push(`-C${context}`);
5891
+ }
5892
+ if (globPattern) args.push("--glob", globPattern);
5893
+ args.push("--", pattern, searchPath);
5894
+ const { stdout } = await execFileAsync("rg", args, {
5895
+ timeout: 15e3,
5896
+ maxBuffer: 2 * 1024 * 1024
5897
+ });
5898
+ const trimmed = stdout.trim();
5899
+ return trimmed || `No matches found for: ${pattern}`;
5900
+ }
5901
+ async nodeScan(pattern, searchPath, globPattern, outputMode, context, caseInsensitive) {
5902
+ const flags = caseInsensitive ? "gi" : "g";
5903
+ let regex;
5904
+ try {
5905
+ regex = new RegExp(pattern, flags);
5906
+ } catch {
5907
+ return `Invalid regex pattern: ${pattern}`;
5908
+ }
5909
+ const fileGlob = globPattern ?? "**/*";
5910
+ let files;
5911
+ try {
5912
+ files = await glob.glob(fileGlob, {
5913
+ cwd: searchPath,
5914
+ ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
5915
+ nodir: true
5916
+ });
5917
+ } catch {
5918
+ files = [path16__default.default.relative(searchPath, searchPath) || "."];
5919
+ }
5920
+ const results = [];
5921
+ let totalCount = 0;
5922
+ for (const rel of files) {
5923
+ const abs = path16__default.default.join(searchPath, rel);
5924
+ let content;
5925
+ try {
5926
+ content = await fs3__default.default.readFile(abs, "utf-8");
5927
+ } catch {
5928
+ continue;
5929
+ }
5930
+ const lines = content.split("\n");
5931
+ const matchingLines = [];
5932
+ for (let i = 0; i < lines.length; i++) {
5933
+ if (regex.test(lines[i])) matchingLines.push(i);
5934
+ regex.lastIndex = 0;
5935
+ }
5936
+ if (matchingLines.length === 0) continue;
5937
+ totalCount += matchingLines.length;
5938
+ if (outputMode === "files_with_matches") {
5939
+ results.push(rel);
5940
+ } else if (outputMode === "count") {
5941
+ results.push(`${rel}: ${matchingLines.length}`);
5942
+ } else {
5943
+ const shown = /* @__PURE__ */ new Set();
5944
+ for (const lineIdx of matchingLines) {
5945
+ const start = Math.max(0, lineIdx - context);
5946
+ const end = Math.min(lines.length - 1, lineIdx + context);
5947
+ for (let i = start; i <= end; i++) shown.add(i);
5948
+ }
5949
+ const sortedIdxs = [...shown].sort((a, b) => a - b);
5950
+ for (const i of sortedIdxs) {
5951
+ const marker = matchingLines.includes(i) ? ":" : "-";
5952
+ results.push(`${rel}:${i + 1}${marker}${lines[i]}`);
5953
+ }
5954
+ }
5955
+ }
5956
+ if (results.length === 0) return `No matches found for: ${pattern}`;
5957
+ if (outputMode === "count") {
5958
+ results.push(`
5959
+ Total: ${totalCount} matches`);
5960
+ }
5961
+ return results.join("\n");
5962
+ }
5963
+ };
5964
+
5965
+ // src/tools/web-fetch.ts
5966
+ var MAX_CHARS = 5e4;
5967
+ var TIMEOUT_MS = 15e3;
5968
+ function stripHtml(html) {
5969
+ let text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "");
5970
+ text = text.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n").replace(/<\/h[1-6]>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<\/tr>/gi, "\n").replace(/<\/td>/gi, " ");
5971
+ text = text.replace(/<[^>]+>/g, "");
5972
+ text = text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
5973
+ text = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).join("\n");
5974
+ return text;
5975
+ }
5976
+ var WebFetchTool = class extends BaseTool {
5977
+ name = "web_fetch";
5978
+ description = "Fetch a URL and return its content as plain text (HTML stripped). Use for reading documentation, web pages, or any URL. Limit: 50,000 characters.";
5979
+ inputSchema = {
5980
+ type: "object",
5981
+ properties: {
5982
+ url: {
5983
+ type: "string",
5984
+ description: "The URL to fetch"
5985
+ },
5986
+ prompt: {
5987
+ type: "string",
5988
+ description: "Optional hint for what information to extract from the page (not used for filtering, just context)"
5989
+ }
5990
+ },
5991
+ required: ["url"]
5992
+ };
5993
+ async execute(input, _options) {
5994
+ const url = input["url"];
5995
+ let resp;
5996
+ try {
5997
+ resp = await fetch(url, {
5998
+ headers: {
5999
+ "User-Agent": "Cascade-AI/1.0 WebFetchTool",
6000
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.5"
6001
+ },
6002
+ signal: AbortSignal.timeout(TIMEOUT_MS),
6003
+ redirect: "follow"
6004
+ });
6005
+ } catch (err) {
6006
+ return `Failed to fetch ${url}: ${err instanceof Error ? err.message : String(err)}`;
6007
+ }
6008
+ if (!resp.ok) {
6009
+ return `HTTP ${resp.status} ${resp.statusText} from ${url}`;
6010
+ }
6011
+ const contentType = resp.headers.get("content-type") ?? "";
6012
+ let text;
6013
+ try {
6014
+ const raw = await resp.text();
6015
+ text = contentType.includes("html") ? stripHtml(raw) : raw;
6016
+ } catch (err) {
6017
+ return `Failed to read response body: ${err instanceof Error ? err.message : String(err)}`;
6018
+ }
6019
+ if (text.length > MAX_CHARS) {
6020
+ text = text.slice(0, MAX_CHARS) + `
6021
+
6022
+ [Content truncated at ${MAX_CHARS} characters]`;
6023
+ }
6024
+ return `URL: ${url}
6025
+ Content-Type: ${contentType}
6026
+
6027
+ ${text}`;
6028
+ }
6029
+ };
5183
6030
 
5184
6031
  // src/tools/mcp.ts
5185
6032
  var McpToolWrapper = class extends BaseTool {
@@ -5205,7 +6052,7 @@ var McpToolWrapper = class extends BaseTool {
5205
6052
 
5206
6053
  // src/tools/registry.ts
5207
6054
  var ignore = ignoreFactory__namespace.default.default ?? ignoreFactory__namespace.default;
5208
- var ToolRegistry = class {
6055
+ var ToolRegistry = class extends EventEmitter__default.default {
5209
6056
  tools = /* @__PURE__ */ new Map();
5210
6057
  config;
5211
6058
  ignoreMatcher = ignore();
@@ -5213,12 +6060,36 @@ var ToolRegistry = class {
5213
6060
  /** Loaded plugins, keyed by plugin name */
5214
6061
  plugins = /* @__PURE__ */ new Map();
5215
6062
  constructor(config, workspaceRoot = process.cwd()) {
6063
+ super();
5216
6064
  this.config = config;
5217
6065
  this.workspaceRoot = workspaceRoot;
5218
6066
  this.registerDefaults();
5219
6067
  }
5220
6068
  register(tool) {
5221
6069
  this.tools.set(tool.name, tool);
6070
+ this.emit("tool:added", tool.name);
6071
+ }
6072
+ /**
6073
+ * Wait until a named tool is registered, resolving immediately if it already exists.
6074
+ * T3 workers can call this after encountering a missing-tool error to resume
6075
+ * automatically once T2 synthesizes the tool.
6076
+ */
6077
+ waitForTool(toolName, timeoutMs = 6e4) {
6078
+ if (this.tools.has(toolName)) return Promise.resolve();
6079
+ return new Promise((resolve, reject) => {
6080
+ const timer = setTimeout(() => {
6081
+ this.off("tool:added", handler);
6082
+ reject(new Error(`Timeout waiting for tool: ${toolName}`));
6083
+ }, timeoutMs);
6084
+ const handler = (name) => {
6085
+ if (name === toolName) {
6086
+ clearTimeout(timer);
6087
+ this.off("tool:added", handler);
6088
+ resolve();
6089
+ }
6090
+ };
6091
+ this.on("tool:added", handler);
6092
+ });
5222
6093
  }
5223
6094
  /**
5224
6095
  * Register a ToolPlugin, loading all its tools into the registry.
@@ -5303,7 +6174,10 @@ var ToolRegistry = class {
5303
6174
  new PDFCreateTool(),
5304
6175
  new CodeInterpreterTool(),
5305
6176
  new PeerCommunicationTool(),
5306
- new WebSearchTool(this.config.webSearch)
6177
+ new WebSearchTool(this.config.webSearch),
6178
+ new GlobTool(),
6179
+ new GrepTool(),
6180
+ new WebFetchTool()
5307
6181
  ];
5308
6182
  for (const tool of tools) {
5309
6183
  tool.setWorkspaceRoot(this.workspaceRoot);
@@ -5320,10 +6194,10 @@ var ToolRegistry = class {
5320
6194
  }
5321
6195
  isIgnored(filePath) {
5322
6196
  if (!filePath) return false;
5323
- const abs = path13__default.default.resolve(this.workspaceRoot, filePath);
5324
- const rel = path13__default.default.relative(this.workspaceRoot, abs);
5325
- if (!rel || rel.startsWith("..") || path13__default.default.isAbsolute(rel)) return true;
5326
- const posixRel = rel.split(path13__default.default.sep).join("/");
6197
+ const abs = path16__default.default.resolve(this.workspaceRoot, filePath);
6198
+ const rel = path16__default.default.relative(this.workspaceRoot, abs);
6199
+ if (!rel || rel.startsWith("..") || path16__default.default.isAbsolute(rel)) return true;
6200
+ const posixRel = rel.split(path16__default.default.sep).join("/");
5327
6201
  return this.ignoreMatcher.ignores(posixRel);
5328
6202
  }
5329
6203
  };
@@ -5660,7 +6534,24 @@ var CascadeConfigSchema = zod.z.object({
5660
6534
  * Generated tools are session-scoped and sandboxed in node:vm.
5661
6535
  * HTTP calls from generated tools require approval.
5662
6536
  */
5663
- enableToolCreation: zod.z.boolean().default(false)
6537
+ enableToolCreation: zod.z.boolean().default(true),
6538
+ /**
6539
+ * External plugin paths or npm package names to load at startup.
6540
+ * Each entry must export a default ToolPlugin object.
6541
+ * Example: ["./plugins/my-tool.js", "cascade-plugin-slack"]
6542
+ */
6543
+ plugins: zod.z.array(zod.z.string()).default([]),
6544
+ /**
6545
+ * Maximum number of concurrent inference requests to any local model provider
6546
+ * (e.g. Ollama). Defaults to 1 to prevent GPU memory pressure when multiple
6547
+ * T3 workers run in parallel on a single-GPU machine.
6548
+ */
6549
+ localConcurrency: zod.z.number().int().min(1).default(1),
6550
+ /**
6551
+ * Timeout in milliseconds for a single local model inference call.
6552
+ * Local models can take minutes for large parameter counts. Default: 5 minutes.
6553
+ */
6554
+ localInferenceTimeoutMs: zod.z.number().int().min(1e3).default(3e5)
5664
6555
  });
5665
6556
 
5666
6557
  // src/config/validate.ts
@@ -5788,139 +6679,237 @@ function heuristicAnalyze(prompt) {
5788
6679
  const estimatedTokens = wordCount * 5;
5789
6680
  return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
5790
6681
  }
5791
- function selectModelFromProfile(profile, tier, selector) {
5792
- if (profile.requiresVision) {
5793
- return selector.selectVisionModel();
5794
- }
5795
- if (tier === "T1") {
5796
- if (profile.complexity >= 4) {
5797
- return selector.selectForTier("T1");
5798
- } else {
5799
- return selector.selectForTier("T2");
5800
- }
5801
- }
5802
- if (tier === "T2") {
5803
- if (profile.type === "code" || profile.type === "data") {
5804
- return selector.selectForTier("T2");
5805
- } else if (profile.complexity <= 2) {
5806
- return selector.selectForTier("T3");
5807
- }
5808
- return selector.selectForTier("T2");
5809
- }
5810
- if (tier === "T3") {
5811
- if (profile.complexity >= 4 || profile.requiresReasoning) {
5812
- return selector.selectForTier("T2");
5813
- } else if (profile.type === "creative") {
5814
- return selector.selectForTier("T2");
5815
- } else {
5816
- return selector.selectForTier("T3");
5817
- }
5818
- }
5819
- return selector.selectForTier(tier);
5820
- }
5821
6682
  var analysisCache = /* @__PURE__ */ new Map();
6683
+ var TASK_TYPE_TAGS = {
6684
+ code: ["code", "instruction"],
6685
+ analysis: ["analysis", "instruction"],
6686
+ creative: ["creative", "multilingual"],
6687
+ data: ["data", "code"],
6688
+ mixed: []
6689
+ };
5822
6690
  var TaskAnalyzer = class {
5823
- router;
5824
- constructor(router) {
5825
- this.router = router;
6691
+ tracker;
6692
+ lastProfile = null;
6693
+ lastSelectedModels = /* @__PURE__ */ new Map();
6694
+ constructor(tracker) {
6695
+ this.tracker = tracker;
6696
+ }
6697
+ setTracker(tracker) {
6698
+ this.tracker = tracker;
6699
+ }
6700
+ /** Returns the TaskProfile from the most recent analyze() call — used for outcome recording. */
6701
+ getLastProfile() {
6702
+ return this.lastProfile;
5826
6703
  }
5827
6704
  /**
5828
- * Analyze a prompt and return a TaskProfile.
5829
- * Uses heuristics first; falls back to AI inference if confidence is low.
6705
+ * Analyze a prompt and return a TaskProfile using pure heuristics.
6706
+ * Low confidence prompts fall back to a conservative mixed/moderate profile.
5830
6707
  */
5831
6708
  async analyze(prompt) {
5832
6709
  const cacheKey = prompt.slice(0, 200);
5833
6710
  const cached = analysisCache.get(cacheKey);
5834
- if (cached) return cached;
5835
- const heuristic = heuristicAnalyze(prompt);
5836
- if (heuristic.confidence < 0.7 && this.router) {
5837
- try {
5838
- const aiProfile = await this.aiInference(prompt);
5839
- const merged = {
5840
- type: aiProfile.type,
5841
- complexity: aiProfile.complexity,
5842
- requiresReasoning: aiProfile.requiresReasoning,
5843
- requiresVision: heuristic.requiresVision || aiProfile.requiresVision,
5844
- estimatedTokens: heuristic.estimatedTokens,
5845
- confidence: 0.9
5846
- // AI-backed
5847
- };
5848
- analysisCache.set(cacheKey, merged);
5849
- return merged;
5850
- } catch {
5851
- }
6711
+ if (cached) {
6712
+ this.lastProfile = cached;
6713
+ return cached;
5852
6714
  }
5853
- analysisCache.set(cacheKey, heuristic);
5854
- return heuristic;
6715
+ const profile = heuristicAnalyze(prompt);
6716
+ analysisCache.set(cacheKey, profile);
6717
+ this.lastProfile = profile;
6718
+ return profile;
5855
6719
  }
5856
6720
  /**
5857
- * Select the optimal model for a given tier based on task analysis.
6721
+ * Select the optimal model for a given tier.
6722
+ * Scores tier-eligible models using cost efficiency + historical performance.
6723
+ * Falls back to the priority-list default when no candidates have history.
5858
6724
  */
5859
6725
  async selectModel(prompt, tier, selector) {
5860
6726
  const profile = await this.analyze(prompt);
5861
- return selectModelFromProfile(profile, tier, selector);
6727
+ if (profile.requiresVision) {
6728
+ return selector.selectVisionModel();
6729
+ }
6730
+ const candidates = selector.getCandidatesForTier(tier);
6731
+ if (candidates.length === 0) return selector.selectForTier(tier);
6732
+ const scored = candidates.map((m) => ({
6733
+ model: m,
6734
+ score: this.scoreModel(m, profile)
6735
+ }));
6736
+ scored.sort((a, b) => b.score - a.score);
6737
+ const best = scored[0]?.model ?? selector.selectForTier(tier);
6738
+ if (best) this.lastSelectedModels.set(tier, best);
6739
+ return best;
5862
6740
  }
5863
- async aiInference(prompt) {
5864
- if (!this.router) throw new Error("No router for AI inference");
5865
- const inferencePrompt = `Analyze this task and return ONLY a JSON object \u2014 no other text.
5866
-
5867
- Task: "${prompt.slice(0, 300)}"
5868
-
5869
- Return: { "type": "code"|"analysis"|"creative"|"data"|"mixed", "complexity": 1-5, "requiresReasoning": true|false, "requiresVision": true|false }
5870
-
5871
- Where complexity: 1=trivial, 2=simple, 3=moderate, 4=complex, 5=research-grade.`;
5872
- const result = await this.router.generate("T3", {
5873
- messages: [{ role: "user", content: inferencePrompt }],
5874
- maxTokens: 80
5875
- });
5876
- const jsonMatch = /\{[\s\S]*?\}/.exec(result.content);
5877
- if (!jsonMatch) throw new Error("No JSON in AI inference response");
5878
- const parsed = JSON.parse(jsonMatch[0]);
5879
- const validTypes = ["code", "analysis", "creative", "data", "mixed"];
5880
- const type = validTypes.includes(parsed.type) ? parsed.type : "mixed";
5881
- const complexity = Math.max(1, Math.min(5, Math.round(parsed.complexity)));
5882
- return {
5883
- type,
5884
- complexity,
5885
- requiresReasoning: Boolean(parsed.requiresReasoning),
5886
- requiresVision: Boolean(parsed.requiresVision),
5887
- estimatedTokens: 0,
5888
- confidence: 0.9
5889
- };
6741
+ /**
6742
+ * Record the outcome of a completed run across all tiers that were selected
6743
+ * during this session and persist stats to disk.
6744
+ */
6745
+ recordRunOutcome(outcome, costByTier) {
6746
+ if (!this.tracker || !this.lastProfile) return;
6747
+ const taskType = this.lastProfile.type;
6748
+ for (const [tier, model] of this.lastSelectedModels) {
6749
+ const cost = costByTier[tier] ?? 0;
6750
+ this.tracker.record(model.id, taskType, outcome, 0, cost);
6751
+ }
6752
+ this.lastSelectedModels.clear();
6753
+ void this.tracker.save();
6754
+ }
6755
+ scoreModel(model, profile) {
6756
+ const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
6757
+ const costEff = this.costEfficiency(model, profile.complexity);
6758
+ const match = this.taskMatchScore(model, profile);
6759
+ return perf * costEff * match;
6760
+ }
6761
+ costEfficiency(model, complexity) {
6762
+ if (this.tracker) return this.tracker.costEfficiencyScore(model, complexity);
6763
+ const blended = model.inputCostPer1kTokens + model.outputCostPer1kTokens * 2;
6764
+ const normalised = Math.min(1, blended / 0.05);
6765
+ const complexityWeight = (6 - complexity) / 5;
6766
+ return Math.max(0.1, 1 - normalised * complexityWeight);
6767
+ }
6768
+ taskMatchScore(model, profile) {
6769
+ const expected = TASK_TYPE_TAGS[profile.type];
6770
+ if (!model.specializations?.length || expected.length === 0) return 1;
6771
+ const matches = expected.filter((tag) => model.specializations.includes(tag)).length;
6772
+ return matches > 0 ? 1 + matches / expected.length * 0.3 : 0.8;
5890
6773
  }
5891
6774
  /** Clear the analysis cache (call between sessions). */
5892
6775
  static clearCache() {
5893
6776
  analysisCache.clear();
5894
6777
  }
5895
6778
  };
6779
+ var DEFAULT_STATS_FILE = path16__default.default.join(os3__default.default.homedir(), ".cascade", "model-perf.json");
6780
+ var ModelPerformanceTracker = class {
6781
+ stats = /* @__PURE__ */ new Map();
6782
+ statsFile;
6783
+ loaded = false;
6784
+ constructor(statsFile = DEFAULT_STATS_FILE) {
6785
+ this.statsFile = statsFile;
6786
+ }
6787
+ async load() {
6788
+ if (this.loaded) return;
6789
+ this.loaded = true;
6790
+ try {
6791
+ const raw = await fs3__default.default.readFile(this.statsFile, "utf-8");
6792
+ const parsed = JSON.parse(raw);
6793
+ for (const [key, stat] of Object.entries(parsed)) {
6794
+ this.stats.set(key, stat);
6795
+ }
6796
+ } catch {
6797
+ }
6798
+ }
6799
+ async save() {
6800
+ try {
6801
+ await fs3__default.default.mkdir(path16__default.default.dirname(this.statsFile), { recursive: true });
6802
+ const obj = {};
6803
+ for (const [key, stat] of this.stats) obj[key] = stat;
6804
+ await fs3__default.default.writeFile(this.statsFile, JSON.stringify(obj, null, 2), "utf-8");
6805
+ } catch {
6806
+ }
6807
+ }
6808
+ record(modelId, taskType, outcome, retries = 0, costUsd = 0) {
6809
+ const key = `${modelId}:${taskType}`;
6810
+ const s = this.stats.get(key) ?? {
6811
+ successCount: 0,
6812
+ failureCount: 0,
6813
+ totalRetries: 0,
6814
+ totalCostUsd: 0,
6815
+ sampleCount: 0
6816
+ };
6817
+ this.stats.set(key, {
6818
+ successCount: s.successCount + (outcome === "success" ? 1 : 0),
6819
+ failureCount: s.failureCount + (outcome === "failure" ? 1 : 0),
6820
+ totalRetries: s.totalRetries + retries,
6821
+ totalCostUsd: s.totalCostUsd + costUsd,
6822
+ sampleCount: s.sampleCount + 1
6823
+ });
6824
+ }
6825
+ /**
6826
+ * Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
6827
+ * High retry counts penalise the score.
6828
+ */
6829
+ performanceScore(modelId, taskType) {
6830
+ const key = `${modelId}:${taskType}`;
6831
+ const s = this.stats.get(key);
6832
+ if (!s || s.sampleCount === 0) return 0.5;
6833
+ const successRate = s.successCount / s.sampleCount;
6834
+ const avgRetries = s.totalRetries / s.sampleCount;
6835
+ const retryPenalty = Math.min(0.4, avgRetries / 3);
6836
+ return Math.max(0.05, successRate * (1 - retryPenalty));
6837
+ }
6838
+ /**
6839
+ * Returns 0.1–1.0. Cheaper models score higher, with the penalty scaled
6840
+ * down for complex tasks (where capability matters more than cost).
6841
+ *
6842
+ * blended cost = input + 2 × output (output tokens are typically pricier).
6843
+ * normalised over $0.05 blended as the "expensive" ceiling.
6844
+ */
6845
+ costEfficiencyScore(model, complexity) {
6846
+ const blended = model.inputCostPer1kTokens + model.outputCostPer1kTokens * 2;
6847
+ const normalised = Math.min(1, blended / 0.05);
6848
+ const complexityWeight = (6 - complexity) / 5;
6849
+ return Math.max(0.1, 1 - normalised * complexityWeight);
6850
+ }
6851
+ };
5896
6852
  var DynamicTool = class extends BaseTool {
5897
6853
  name;
5898
6854
  description;
5899
6855
  inputSchema;
5900
6856
  executeCode;
5901
6857
  _isDangerous;
5902
- constructor(spec) {
6858
+ registry;
6859
+ escalator;
6860
+ constructor(spec, registry, escalator) {
5903
6861
  super();
5904
6862
  this.name = spec.name;
5905
6863
  this.description = spec.description;
5906
6864
  this.inputSchema = spec.inputSchema;
5907
6865
  this.executeCode = spec.executeCode;
5908
6866
  this._isDangerous = spec.isDangerous;
6867
+ this.registry = registry;
6868
+ this.escalator = escalator;
5909
6869
  }
5910
6870
  isDangerous() {
5911
6871
  return this._isDangerous;
5912
6872
  }
5913
- async execute(input, _options) {
6873
+ async execute(input, options) {
6874
+ const registry = this.registry;
6875
+ const escalator = this.escalator;
6876
+ const callTool = async (toolName, toolInput) => {
6877
+ if (!registry.hasTool(toolName)) return `Tool not found: ${toolName}`;
6878
+ if (registry.isDangerous(toolName)) {
6879
+ if (escalator) {
6880
+ const req = {
6881
+ id: `dynamic-${this.name}-${toolName}-${Date.now()}`,
6882
+ requestedBy: `dynamic_tool:${this.name}`,
6883
+ parentT2Id: options.tierId,
6884
+ toolName,
6885
+ input: toolInput,
6886
+ isDangerous: true,
6887
+ subtaskContext: `Dynamic tool "${this.name}" requesting access to "${toolName}"`,
6888
+ sectionContext: `Dynamic tool "${this.name}"`
6889
+ };
6890
+ const decision = await escalator.requestPermission(req);
6891
+ if (!decision.approved) {
6892
+ return `Permission denied for ${toolName} (decided by ${decision.decidedBy}).`;
6893
+ }
6894
+ }
6895
+ }
6896
+ try {
6897
+ const result = await registry.execute(toolName, toolInput, options);
6898
+ return typeof result === "string" ? result : JSON.stringify(result);
6899
+ } catch (err) {
6900
+ return `Error calling ${toolName}: ${err instanceof Error ? err.message : String(err)}`;
6901
+ }
6902
+ };
5914
6903
  const sandbox = {
5915
6904
  input,
5916
6905
  fetch: globalThis.fetch,
6906
+ callTool,
5917
6907
  JSON,
5918
6908
  Math,
5919
6909
  Date,
5920
6910
  console: { log: () => {
5921
6911
  }, error: () => {
5922
6912
  } },
5923
- // Silenced
5924
6913
  setTimeout,
5925
6914
  clearTimeout,
5926
6915
  Promise,
@@ -5953,29 +6942,42 @@ Generate a minimal, safe JavaScript tool function for the described operation.
5953
6942
 
5954
6943
  Rules:
5955
6944
  - Return ONLY a JSON object with these fields: name, description, inputSchema, executeCode, isDangerous
5956
- - executeCode is a self-contained JavaScript function body that:
5957
- - Receives: input (object), fetch (if HTTP needed)
6945
+ - executeCode is a self-contained JavaScript async function body that:
6946
+ - Receives: input (object), fetch (for HTTP), callTool(toolName, input) (to call any registered cascade tool)
5958
6947
  - Returns: a string result
5959
- - Uses no require(), no fs, no process \u2014 only fetch, JSON, Math, Date, String, Number, Array, Object
6948
+ - For file operations, prefer: await callTool('file_read', { path: input.path })
6949
+ - For shell commands, prefer: await callTool('shell', { command: 'ls -la' })
6950
+ - For pure computation / HTTP: use fetch or built-ins (JSON, Math, Date, String, Number, Array, Object)
5960
6951
  - Must complete in under 15 seconds
5961
- - isDangerous should be true only if the tool makes write operations or external HTTP calls
6952
+ - isDangerous: true if the tool calls dangerous cascade tools (shell, file_write, file_delete, git) or makes HTTP calls that write data
5962
6953
  - name must be snake_case, start with "dynamic_", max 40 chars
5963
6954
  - description must be \u2264 120 chars
5964
6955
 
5965
- Example executeCode for an HTTP tool:
5966
- "const res = await fetch(input.url); const text = await res.text(); return text.slice(0, 2000);"
6956
+ Example for a file-summary tool:
6957
+ {
6958
+ "name": "dynamic_summarize_file",
6959
+ "description": "Read a file and return a one-paragraph summary",
6960
+ "inputSchema": { "path": { "type": "string", "description": "File path to summarize" } },
6961
+ "executeCode": "const content = await callTool('file_read', { path: input.path }); return content.slice(0, 500);",
6962
+ "isDangerous": false
6963
+ }
5967
6964
 
5968
6965
  Return ONLY valid JSON \u2014 no other text.`;
5969
6966
  var ToolCreator = class {
5970
6967
  router;
5971
6968
  registry;
6969
+ escalator;
5972
6970
  createdTools = /* @__PURE__ */ new Set();
5973
6971
  constructor(router, registry) {
5974
6972
  this.router = router;
5975
6973
  this.registry = registry;
5976
6974
  }
6975
+ setPermissionEscalator(escalator) {
6976
+ this.escalator = escalator;
6977
+ }
5977
6978
  /**
5978
6979
  * Generate a new tool from a description and register it with the ToolRegistry.
6980
+ * The generated tool has access to all registered cascade tools via callTool().
5979
6981
  * Returns the tool name if successful, null if generation failed.
5980
6982
  */
5981
6983
  async createTool(description, context) {
@@ -5986,26 +6988,21 @@ Required capability: ${description.slice(0, 300)}`;
5986
6988
  try {
5987
6989
  const result = await this.router.generate("T3", {
5988
6990
  messages: [{ role: "user", content: prompt }],
5989
- maxTokens: 600
6991
+ maxTokens: 800
5990
6992
  });
5991
6993
  const jsonMatch = /\{[\s\S]*\}/.exec(result.content);
5992
- if (!jsonMatch) {
5993
- return null;
5994
- }
6994
+ if (!jsonMatch) return null;
5995
6995
  const spec = JSON.parse(jsonMatch[0]);
5996
- if (!spec.name || !spec.description || !spec.executeCode || !spec.inputSchema) {
5997
- return null;
5998
- }
6996
+ if (!spec.name || !spec.description || !spec.executeCode || !spec.inputSchema) return null;
5999
6997
  if (this.createdTools.has(spec.name) || this.registry.hasTool(spec.name)) {
6000
6998
  spec.name = `${spec.name}_${Date.now() % 1e4}`;
6001
6999
  }
6002
7000
  try {
6003
- vm.createContext({ input: {}, fetch: globalThis.fetch });
6004
- new Function("input", "fetch", spec.executeCode);
6005
- } catch (err) {
7001
+ new Function("input", "fetch", "callTool", spec.executeCode);
7002
+ } catch {
6006
7003
  return null;
6007
7004
  }
6008
- const tool = new DynamicTool(spec);
7005
+ const tool = new DynamicTool(spec, this.registry, this.escalator);
6009
7006
  this.registry.register(tool);
6010
7007
  this.createdTools.add(spec.name);
6011
7008
  return spec.name;
@@ -6013,16 +7010,14 @@ Required capability: ${description.slice(0, 300)}`;
6013
7010
  return null;
6014
7011
  }
6015
7012
  }
6016
- /**
6017
- * Returns the names of all tools created in this session.
6018
- */
7013
+ /** Returns the names of all tools created in this session. */
6019
7014
  getCreatedTools() {
6020
7015
  return Array.from(this.createdTools);
6021
7016
  }
6022
7017
  };
6023
7018
 
6024
7019
  // src/core/cascade.ts
6025
- var Cascade = class extends EventEmitter__default.default {
7020
+ var Cascade = class _Cascade extends EventEmitter__default.default {
6026
7021
  router;
6027
7022
  toolRegistry;
6028
7023
  mcpClient;
@@ -6033,6 +7028,7 @@ var Cascade = class extends EventEmitter__default.default {
6033
7028
  audit;
6034
7029
  telemetry;
6035
7030
  taskAnalyzer;
7031
+ perfTracker;
6036
7032
  toolCreator;
6037
7033
  constructor(config, workspacePath, store) {
6038
7034
  super();
@@ -6049,10 +7045,12 @@ var Cascade = class extends EventEmitter__default.default {
6049
7045
  this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
6050
7046
  }
6051
7047
  initOptionalFeatures() {
6052
- const cfg = this.config;
6053
- if (cfg["cascadeAuto"] === true) {
6054
- this.taskAnalyzer = new TaskAnalyzer(this.router);
7048
+ if (this.config.cascadeAuto === true) {
7049
+ this.perfTracker = new ModelPerformanceTracker();
7050
+ void this.perfTracker.load();
7051
+ this.taskAnalyzer = new TaskAnalyzer(this.perfTracker);
6055
7052
  }
7053
+ const cfg = this.config;
6056
7054
  if (cfg["enableToolCreation"] === true) {
6057
7055
  this.toolCreator = new ToolCreator(this.router, this.toolRegistry);
6058
7056
  }
@@ -6118,6 +7116,26 @@ var Cascade = class extends EventEmitter__default.default {
6118
7116
  }
6119
7117
  }
6120
7118
  }
7119
+ const pluginPaths = this.config["plugins"];
7120
+ if (pluginPaths?.length) {
7121
+ for (const pluginPath of pluginPaths) {
7122
+ try {
7123
+ const mod = await import(pluginPath);
7124
+ const plugin = mod.default ?? mod;
7125
+ if (plugin && Array.isArray(plugin.tools)) {
7126
+ this.toolRegistry.registerPlugin(plugin);
7127
+ } else {
7128
+ console.warn(`[cascade] Plugin "${pluginPath}" does not export a valid ToolPlugin.`);
7129
+ }
7130
+ } catch (err) {
7131
+ console.warn(`[cascade] Failed to load plugin "${pluginPath}":`, err);
7132
+ }
7133
+ }
7134
+ }
7135
+ if (this.config.cascadeAuto && this.store) {
7136
+ this.router.profileModels(this.store).catch(() => {
7137
+ });
7138
+ }
6121
7139
  this.initOptionalFeatures();
6122
7140
  this.initialized = true;
6123
7141
  })();
@@ -6128,21 +7146,48 @@ var Cascade = class extends EventEmitter__default.default {
6128
7146
  throw err;
6129
7147
  }
6130
7148
  }
7149
+ isCasualGreeting(prompt) {
7150
+ const casual = /^(hi|hello|hey|greetings|thanks|thank you|thx|bye|goodbye|cya)$/i.test(prompt.trim().replace(/[!?.]+$/, ""));
7151
+ return casual;
7152
+ }
6131
7153
  looksLikeSimpleArtifactTask(prompt) {
6132
7154
  return /create .*\.(txt|md|json|csv)\b/i.test(prompt) && !/(research|compare|thorough|pdf|report|analy[sz]e|architecture|multi-agent)/i.test(prompt);
6133
7155
  }
6134
- async determineComplexity(prompt, workspacePath, conversationHistory = []) {
6135
- if (this.looksLikeSimpleArtifactTask(prompt)) {
6136
- return "Simple";
6137
- }
6138
- let workspaceContext = "";
7156
+ looksLikeConversational(prompt) {
7157
+ const LOW_COMPLEXITY = [
7158
+ /^(?:hi|hello|hey|thanks|thank you|ok|okay|yes|no|sure|got it|sounds good)\b/i,
7159
+ /^(?:what is|what are|list|show me|tell me|who is|where is|when is|how do i)\b/i,
7160
+ /\b(?:simple|quick|brief|small|single|one-line|typo|rename)\b/i
7161
+ ];
7162
+ const wordCount = prompt.trim().split(/\s+/).length;
7163
+ return wordCount <= 12 && LOW_COMPLEXITY.some((re) => re.test(prompt.trim()));
7164
+ }
7165
+ // Cache glob scan results per workspace path to avoid repeated I/O.
7166
+ static globCache = /* @__PURE__ */ new Map();
7167
+ async countWorkspaceFiles(workspacePath) {
7168
+ const now = Date.now();
7169
+ const cached = _Cascade.globCache.get(workspacePath);
7170
+ if (cached && cached.expiresAt > now) return cached.count;
6139
7171
  try {
6140
7172
  const files = await glob.glob("**/*.*", {
6141
7173
  cwd: workspacePath,
6142
7174
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
6143
7175
  nodir: true
6144
7176
  });
6145
- workspaceContext = `Workspace Scout: Found ~${files.length} source files in the project.`;
7177
+ _Cascade.globCache.set(workspacePath, { count: files.length, expiresAt: now + 3e4 });
7178
+ return files.length;
7179
+ } catch {
7180
+ return 0;
7181
+ }
7182
+ }
7183
+ async determineComplexity(prompt, workspacePath, conversationHistory = []) {
7184
+ if (this.isCasualGreeting(prompt)) return "Simple";
7185
+ if (this.looksLikeSimpleArtifactTask(prompt)) return "Simple";
7186
+ if (this.looksLikeConversational(prompt)) return "Simple";
7187
+ let workspaceContext = "";
7188
+ try {
7189
+ const count = await this.countWorkspaceFiles(workspacePath);
7190
+ workspaceContext = `Workspace Scout: Found ~${count} source files in the project.`;
6146
7191
  } catch {
6147
7192
  workspaceContext = "Workspace Scout: Could not scan workspace.";
6148
7193
  }
@@ -6228,7 +7273,7 @@ ${prompt}` : prompt;
6228
7273
  this.telemetry.capture("cascade:session_start", {
6229
7274
  complexity,
6230
7275
  providerCount: this.config.providers.length,
6231
- cascadeAutoEnabled: this.config["cascadeAuto"] === true,
7276
+ cascadeAutoEnabled: this.config.cascadeAuto === true,
6232
7277
  toolCreationEnabled: this.config["enableToolCreation"] === true
6233
7278
  });
6234
7279
  this.emit("tier:root", { role: complexity === "Simple" ? "T3" : complexity === "Moderate" ? "T2" : "T1" });
@@ -6243,6 +7288,7 @@ ${prompt}` : prompt;
6243
7288
  }));
6244
7289
  }
6245
7290
  const toolCreator = this.toolCreator;
7291
+ if (toolCreator) toolCreator.setPermissionEscalator(escalator);
6246
7292
  let finalOutput = "";
6247
7293
  let t2Results = [];
6248
7294
  let runError = null;
@@ -6264,6 +7310,8 @@ ${prompt}` : prompt;
6264
7310
  });
6265
7311
  tier.on("log", (e) => this.emit("log", e));
6266
7312
  tier.on("tier:status", (e) => this.emit("tier:status", e));
7313
+ tier.on("tool:call", (e) => this.emit("tool:call", e));
7314
+ tier.on("tool:result", (e) => this.emit("tool:result", e));
6267
7315
  tier.on("tool:approval-request", async (request) => {
6268
7316
  this.emit("tool:approval-request", request);
6269
7317
  let decision = { approved: false };
@@ -6318,6 +7366,7 @@ ${prompt}` : prompt;
6318
7366
  }
6319
7367
  t2.setPermissionEscalator(escalator);
6320
7368
  if (toolCreator) t2.setToolCreator(toolCreator);
7369
+ t2.setPeerMessageCallback((e) => this.emit("peer:message", e), options.sessionId ?? "");
6321
7370
  bindTierEvents(t2);
6322
7371
  const assignment = {
6323
7372
  sectionId: taskId,
@@ -6347,6 +7396,7 @@ ${prompt}` : prompt;
6347
7396
  }
6348
7397
  t1.setPermissionEscalator(escalator);
6349
7398
  if (toolCreator) t1.setToolCreator(toolCreator);
7399
+ t1.setPeerMessageCallback((e) => this.emit("peer:message", e), options.sessionId ?? "");
6350
7400
  bindTierEvents(t1);
6351
7401
  t1.on("plan", (e) => this.emit("plan", e));
6352
7402
  const result = await t1.execute(options.prompt, options.images, void 0, options.signal);
@@ -6370,6 +7420,13 @@ ${prompt}` : prompt;
6370
7420
  escalator.cancelAllPending();
6371
7421
  } catch {
6372
7422
  }
7423
+ if (this.taskAnalyzer) {
7424
+ try {
7425
+ const stats2 = this.router.getStats();
7426
+ this.taskAnalyzer.recordRunOutcome(runError ? "failure" : "success", stats2.costByTier);
7427
+ } catch {
7428
+ }
7429
+ }
6373
7430
  try {
6374
7431
  const stats2 = this.router.getStats();
6375
7432
  const durationMs2 = Date.now() - startMs;
@@ -6470,7 +7527,7 @@ var Keystore = class {
6470
7527
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
6471
7528
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
6472
7529
  this.backend = "keytar";
6473
- if (password && fs11__default.default.existsSync(this.storePath)) {
7530
+ if (password && fs15__default.default.existsSync(this.storePath)) {
6474
7531
  try {
6475
7532
  const fileEntries = this.decryptFile(password);
6476
7533
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -6489,7 +7546,7 @@ var Keystore = class {
6489
7546
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
6490
7547
  );
6491
7548
  }
6492
- if (!fs11__default.default.existsSync(this.storePath)) {
7549
+ if (!fs15__default.default.existsSync(this.storePath)) {
6493
7550
  const salt = crypto__default.default.randomBytes(SALT_LEN);
6494
7551
  this.masterKey = this.deriveKey(password, salt);
6495
7552
  this.writeWithSalt({}, salt);
@@ -6503,7 +7560,7 @@ var Keystore = class {
6503
7560
  }
6504
7561
  /** Synchronous legacy unlock kept for AES-only environments. */
6505
7562
  unlockSync(password) {
6506
- if (!fs11__default.default.existsSync(this.storePath)) {
7563
+ if (!fs15__default.default.existsSync(this.storePath)) {
6507
7564
  const salt = crypto__default.default.randomBytes(SALT_LEN);
6508
7565
  this.masterKey = this.deriveKey(password, salt);
6509
7566
  this.writeWithSalt({}, salt);
@@ -6561,7 +7618,7 @@ var Keystore = class {
6561
7618
  }
6562
7619
  }
6563
7620
  decryptFile(password, knownSalt) {
6564
- if (!fs11__default.default.existsSync(this.storePath)) return {};
7621
+ if (!fs15__default.default.existsSync(this.storePath)) return {};
6565
7622
  try {
6566
7623
  const { salt, ciphertext, iv, tag } = this.readRaw();
6567
7624
  const useSalt = knownSalt ?? salt;
@@ -6583,8 +7640,8 @@ var Keystore = class {
6583
7640
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
6584
7641
  const tag = cipher.getAuthTag();
6585
7642
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
6586
- fs11__default.default.mkdirSync(path13__default.default.dirname(this.storePath), { recursive: true });
6587
- fs11__default.default.writeFileSync(this.storePath, out, { mode: 384 });
7643
+ fs15__default.default.mkdirSync(path16__default.default.dirname(this.storePath), { recursive: true });
7644
+ fs15__default.default.writeFileSync(this.storePath, out, { mode: 384 });
6588
7645
  }
6589
7646
  writeWithSalt(data, salt) {
6590
7647
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -6594,11 +7651,11 @@ var Keystore = class {
6594
7651
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
6595
7652
  const tag = cipher.getAuthTag();
6596
7653
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
6597
- fs11__default.default.mkdirSync(path13__default.default.dirname(this.storePath), { recursive: true });
6598
- fs11__default.default.writeFileSync(this.storePath, out, { mode: 384 });
7654
+ fs15__default.default.mkdirSync(path16__default.default.dirname(this.storePath), { recursive: true });
7655
+ fs15__default.default.writeFileSync(this.storePath, out, { mode: 384 });
6599
7656
  }
6600
7657
  readRaw() {
6601
- const buf = fs11__default.default.readFileSync(this.storePath);
7658
+ const buf = fs15__default.default.readFileSync(this.storePath);
6602
7659
  let offset = 0;
6603
7660
  const salt = buf.subarray(offset, offset + SALT_LEN);
6604
7661
  offset += SALT_LEN;
@@ -6631,9 +7688,9 @@ var CascadeIgnore = class {
6631
7688
  ]);
6632
7689
  }
6633
7690
  async load(workspacePath) {
6634
- const filePath = path13__default.default.join(workspacePath, ".cascadeignore");
7691
+ const filePath = path16__default.default.join(workspacePath, ".cascadeignore");
6635
7692
  try {
6636
- const content = await fs2__default.default.readFile(filePath, "utf-8");
7693
+ const content = await fs3__default.default.readFile(filePath, "utf-8");
6637
7694
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
6638
7695
  this.ig.add(lines);
6639
7696
  this.loaded = true;
@@ -6642,7 +7699,7 @@ var CascadeIgnore = class {
6642
7699
  }
6643
7700
  isIgnored(filePath, workspacePath) {
6644
7701
  try {
6645
- const relative = workspacePath ? path13__default.default.relative(workspacePath, filePath) : filePath;
7702
+ const relative = workspacePath ? path16__default.default.relative(workspacePath, filePath) : filePath;
6646
7703
  return this.ig.ignores(relative);
6647
7704
  } catch {
6648
7705
  return false;
@@ -6653,9 +7710,9 @@ var CascadeIgnore = class {
6653
7710
  }
6654
7711
  };
6655
7712
  async function loadCascadeMd(workspacePath) {
6656
- const filePath = path13__default.default.join(workspacePath, "CASCADE.md");
7713
+ const filePath = path16__default.default.join(workspacePath, "CASCADE.md");
6657
7714
  try {
6658
- const raw = await fs2__default.default.readFile(filePath, "utf-8");
7715
+ const raw = await fs3__default.default.readFile(filePath, "utf-8");
6659
7716
  return parseCascadeMd(raw);
6660
7717
  } catch {
6661
7718
  return null;
@@ -6684,7 +7741,7 @@ ${raw.trim()}`;
6684
7741
  var MemoryStore = class _MemoryStore {
6685
7742
  db;
6686
7743
  constructor(dbPath) {
6687
- fs11__default.default.mkdirSync(path13__default.default.dirname(dbPath), { recursive: true });
7744
+ fs15__default.default.mkdirSync(path16__default.default.dirname(dbPath), { recursive: true });
6688
7745
  try {
6689
7746
  this.db = new Database__default.default(dbPath, { timeout: 5e3 });
6690
7747
  this.db.pragma("journal_mode = WAL");
@@ -7167,6 +8224,27 @@ Original error: ${err.message}`
7167
8224
  if (!row.oldest) return Infinity;
7168
8225
  return Date.now() - new Date(row.oldest).getTime();
7169
8226
  }
8227
+ saveModelProfile(modelId, provider, specializations) {
8228
+ const cacheKey = `${provider}:${modelId}`;
8229
+ const existing = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(cacheKey);
8230
+ const meta = existing ? JSON.parse(existing.metadata) : { id: modelId, provider, name: modelId, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
8231
+ meta.specializations = specializations;
8232
+ this.db.prepare(`
8233
+ INSERT INTO model_cache (id, provider, model_id, name, metadata, updated_at)
8234
+ VALUES (?, ?, ?, ?, ?, ?)
8235
+ ON CONFLICT(id) DO UPDATE SET metadata = excluded.metadata, updated_at = excluded.updated_at
8236
+ `).run(cacheKey, provider, modelId, meta.name ?? modelId, JSON.stringify(meta), (/* @__PURE__ */ new Date()).toISOString());
8237
+ }
8238
+ getModelProfile(modelId, provider) {
8239
+ const row = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(`${provider}:${modelId}`);
8240
+ return row ? JSON.parse(row.metadata) : void 0;
8241
+ }
8242
+ getProfiledModelIds() {
8243
+ const rows = this.db.prepare(
8244
+ "SELECT model_id FROM model_cache WHERE json_extract(metadata, '$.specializations') IS NOT NULL"
8245
+ ).all();
8246
+ return rows.map((r) => r.model_id);
8247
+ }
7170
8248
  // ── Tool Result Cache (in-memory, TTL-based) ──────────────────────────
7171
8249
  // Avoids redundant calls for read-only tools within a short window.
7172
8250
  // Not persisted to DB — cleared on process restart.
@@ -7421,15 +8499,15 @@ var ConfigManager = class {
7421
8499
  globalDir;
7422
8500
  constructor(workspacePath = process.cwd()) {
7423
8501
  this.workspacePath = workspacePath;
7424
- this.globalDir = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR);
8502
+ this.globalDir = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR);
7425
8503
  }
7426
8504
  async load() {
7427
8505
  this.config = await this.loadConfig();
7428
8506
  this.ignore = new CascadeIgnore();
7429
8507
  await this.ignore.load(this.workspacePath);
7430
8508
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
7431
- this.keystore = new Keystore(path13__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
7432
- this.store = new MemoryStore(path13__default.default.join(this.workspacePath, CASCADE_DB_FILE));
8509
+ this.keystore = new Keystore(path16__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
8510
+ this.store = new MemoryStore(path16__default.default.join(this.workspacePath, CASCADE_DB_FILE));
7433
8511
  await this.injectEnvKeys();
7434
8512
  await this.ensureDefaultIdentity();
7435
8513
  }
@@ -7452,9 +8530,9 @@ var ConfigManager = class {
7452
8530
  return this.workspacePath;
7453
8531
  }
7454
8532
  async save() {
7455
- const configPath = path13__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
7456
- await fs2__default.default.mkdir(path13__default.default.dirname(configPath), { recursive: true });
7457
- await fs2__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
8533
+ const configPath = path16__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
8534
+ await fs3__default.default.mkdir(path16__default.default.dirname(configPath), { recursive: true });
8535
+ await fs3__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
7458
8536
  }
7459
8537
  async updateConfig(updates) {
7460
8538
  this.config = validateConfig({ ...this.config, ...updates });
@@ -7477,9 +8555,9 @@ var ConfigManager = class {
7477
8555
  return configProvider?.apiKey;
7478
8556
  }
7479
8557
  async loadConfig() {
7480
- const configPath = path13__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
8558
+ const configPath = path16__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
7481
8559
  try {
7482
- const raw = await fs2__default.default.readFile(configPath, "utf-8");
8560
+ const raw = await fs3__default.default.readFile(configPath, "utf-8");
7483
8561
  return validateConfig(JSON.parse(raw));
7484
8562
  } catch (err) {
7485
8563
  if (err.code === "ENOENT") {
@@ -7643,6 +8721,9 @@ var DashboardSocket = class {
7643
8721
  emitStreamToken(tierId, text, sessionId) {
7644
8722
  this.io.to(`session:${sessionId}`).emit("stream:token", { tierId, text, sessionId });
7645
8723
  }
8724
+ emitPeerMessage(event) {
8725
+ this.io.to(`session:${event.sessionId}`).emit("peer:message", event);
8726
+ }
7646
8727
  emitApprovalRequest(request) {
7647
8728
  this.io.emit("permission:user-required", request);
7648
8729
  }
@@ -7685,16 +8766,13 @@ var DashboardSocket = class {
7685
8766
  const { sessionId } = normalizeSessionSubscriptionPayload(payload);
7686
8767
  socket.leave(`session:${sessionId}`);
7687
8768
  });
7688
- socket.on("join:tenant", (tenantId) => {
7689
- socket.join(`tenant:${tenantId}`);
7690
- });
7691
8769
  });
7692
8770
  }
7693
8771
  close() {
7694
8772
  this.io.close();
7695
8773
  }
7696
8774
  };
7697
- var __dirname$1 = path13__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))));
8775
+ var __dirname$1 = path16__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))));
7698
8776
  var DashboardServer = class {
7699
8777
  app;
7700
8778
  httpServer;
@@ -7760,15 +8838,15 @@ var DashboardServer = class {
7760
8838
  resolveDashboardSecret() {
7761
8839
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
7762
8840
  if (fromConfig) return fromConfig;
7763
- const secretPath = path13__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
8841
+ const secretPath = path16__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
7764
8842
  try {
7765
- if (fs11__default.default.existsSync(secretPath)) {
7766
- const existing = fs11__default.default.readFileSync(secretPath, "utf-8").trim();
8843
+ if (fs15__default.default.existsSync(secretPath)) {
8844
+ const existing = fs15__default.default.readFileSync(secretPath, "utf-8").trim();
7767
8845
  if (existing.length >= 16) return existing;
7768
8846
  }
7769
8847
  const generated = crypto.randomUUID();
7770
- fs11__default.default.mkdirSync(path13__default.default.dirname(secretPath), { recursive: true });
7771
- fs11__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
8848
+ fs15__default.default.mkdirSync(path16__default.default.dirname(secretPath), { recursive: true });
8849
+ fs15__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
7772
8850
  if (this.config.dashboard.auth) {
7773
8851
  console.warn(
7774
8852
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -7795,7 +8873,7 @@ var DashboardServer = class {
7795
8873
  // ── Setup ─────────────────────────────────────
7796
8874
  getGlobalStore() {
7797
8875
  if (!this.globalStore) {
7798
- const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
8876
+ const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7799
8877
  this.globalStore = new MemoryStore(globalDbPath);
7800
8878
  }
7801
8879
  return this.globalStore;
@@ -7856,12 +8934,12 @@ var DashboardServer = class {
7856
8934
  }
7857
8935
  }
7858
8936
  watchRuntimeChanges() {
7859
- const workspaceDbPath = path13__default.default.join(this.workspacePath, CASCADE_DB_FILE);
7860
- const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
8937
+ const workspaceDbPath = path16__default.default.join(this.workspacePath, CASCADE_DB_FILE);
8938
+ const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7861
8939
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
7862
8940
  for (const watchPath of watchPaths) {
7863
- if (!fs11__default.default.existsSync(watchPath)) continue;
7864
- fs11__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
8941
+ if (!fs15__default.default.existsSync(watchPath)) continue;
8942
+ fs15__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
7865
8943
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
7866
8944
  });
7867
8945
  }
@@ -7892,6 +8970,21 @@ var DashboardServer = class {
7892
8970
  legacyHeaders: false,
7893
8971
  message: { error: "Too many login attempts. Try again in 15 minutes." }
7894
8972
  });
8973
+ const apiLimiter = rateLimit__default.default({
8974
+ windowMs: 60 * 1e3,
8975
+ limit: 60,
8976
+ standardHeaders: "draft-7",
8977
+ legacyHeaders: false,
8978
+ message: { error: "Too many requests. Slow down." }
8979
+ });
8980
+ this.app.use("/api", apiLimiter);
8981
+ const mutationLimiter = rateLimit__default.default({
8982
+ windowMs: 60 * 1e3,
8983
+ limit: 10,
8984
+ standardHeaders: "draft-7",
8985
+ legacyHeaders: false,
8986
+ message: { error: "Too many requests on this endpoint." }
8987
+ });
7895
8988
  this.app.post("/api/auth/login", loginLimiter, (req, res) => {
7896
8989
  const { username, password } = req.body ?? {};
7897
8990
  if (!authRequired) {
@@ -7927,22 +9020,33 @@ var DashboardServer = class {
7927
9020
  res.status(401).json({ error: "Invalid credentials" });
7928
9021
  }
7929
9022
  });
7930
- this.app.post("/api/force-halt", auth, (req, res) => {
7931
- const { sessionId, nodeId } = req.body;
9023
+ this.app.post("/api/force-halt", auth, mutationLimiter, (req, res) => {
9024
+ const body = req.body;
9025
+ const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
9026
+ const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
7932
9027
  const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
7933
9028
  this.socket.broadcast("session:halt", payload);
7934
9029
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
7935
9030
  res.json({ success: true, ...payload });
7936
9031
  });
7937
- this.app.post("/api/approve", auth, (req, res) => {
7938
- const { nodeId, sessionId } = req.body;
9032
+ this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
9033
+ const body = req.body;
9034
+ const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
9035
+ const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
7939
9036
  const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
7940
9037
  this.socket.broadcast("session:approve", payload);
7941
9038
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
7942
9039
  res.json({ success: true, ...payload });
7943
9040
  });
7944
- this.app.post("/api/inject", auth, (req, res) => {
7945
- const { message, sessionId, nodeId } = req.body;
9041
+ this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
9042
+ const body = req.body;
9043
+ const message = typeof body["message"] === "string" ? body["message"] : void 0;
9044
+ const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
9045
+ const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
9046
+ if (!message) {
9047
+ res.status(400).json({ error: "message is required and must be a string" });
9048
+ return;
9049
+ }
7946
9050
  const payload = { sessionId, nodeId, message, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
7947
9051
  this.socket.broadcast("session:message-injected", payload);
7948
9052
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
@@ -7965,7 +9069,7 @@ var DashboardServer = class {
7965
9069
  const sessionId = req.params.id;
7966
9070
  this.store.deleteSession(sessionId);
7967
9071
  this.store.deleteRuntimeSession(sessionId);
7968
- const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
9072
+ const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7969
9073
  const globalStore = new MemoryStore(globalDbPath);
7970
9074
  try {
7971
9075
  globalStore.deleteRuntimeSession(sessionId);
@@ -7979,7 +9083,7 @@ var DashboardServer = class {
7979
9083
  });
7980
9084
  this.app.delete("/api/sessions", auth, (req, res) => {
7981
9085
  const body = req.body;
7982
- const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
9086
+ const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7983
9087
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
7984
9088
  const globalStore = new MemoryStore(globalDbPath);
7985
9089
  try {
@@ -8002,7 +9106,7 @@ var DashboardServer = class {
8002
9106
  });
8003
9107
  this.app.delete("/api/runtime", auth, (_req, res) => {
8004
9108
  this.store.deleteAllRuntimeNodes();
8005
- const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
9109
+ const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
8006
9110
  const globalStore = new MemoryStore(globalDbPath);
8007
9111
  try {
8008
9112
  globalStore.deleteAllRuntimeNodes();
@@ -8064,16 +9168,26 @@ var DashboardServer = class {
8064
9168
  });
8065
9169
  this.app.put("/api/config", auth, async (req, res) => {
8066
9170
  const body = req.body;
8067
- if (body.tierLimits) this.config.tierLimits = { ...this.config.tierLimits, ...body.tierLimits };
8068
- if (body.budget) this.config.budget = { ...this.config.budget, ...body.budget };
9171
+ if (body["tierLimits"] !== void 0 && (typeof body["tierLimits"] !== "object" || Array.isArray(body["tierLimits"]))) {
9172
+ res.status(400).json({ error: "tierLimits must be an object" });
9173
+ return;
9174
+ }
9175
+ if (body["budget"] !== void 0 && (typeof body["budget"] !== "object" || Array.isArray(body["budget"]))) {
9176
+ res.status(400).json({ error: "budget must be an object" });
9177
+ return;
9178
+ }
9179
+ if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
9180
+ if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
8069
9181
  try {
8070
- const configPath = path13__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
8071
- const existing = fs11__default.default.existsSync(configPath) ? JSON.parse(fs11__default.default.readFileSync(configPath, "utf-8")) : {};
9182
+ const configPath = path16__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
9183
+ const existing = fs15__default.default.existsSync(configPath) ? JSON.parse(fs15__default.default.readFileSync(configPath, "utf-8")) : {};
8072
9184
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
8073
- fs11__default.default.writeFileSync(configPath, JSON.stringify(updated, null, 2), "utf-8");
9185
+ const tmp = configPath + ".tmp";
9186
+ fs15__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
9187
+ fs15__default.default.renameSync(tmp, configPath);
8074
9188
  res.json({ ok: true });
8075
9189
  } catch (err) {
8076
- res.status(500).json({ error: `Failed to save config: ${String(err)}` });
9190
+ res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
8077
9191
  }
8078
9192
  });
8079
9193
  this.app.get("/api/runtime/logs/:sessionId", auth, (req, res) => {
@@ -8098,7 +9212,7 @@ var DashboardServer = class {
8098
9212
  this.app.get("/api/runtime", auth, (req, res) => {
8099
9213
  const scope = req.query["scope"] ?? "workspace";
8100
9214
  if (scope === "global") {
8101
- const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
9215
+ const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
8102
9216
  const globalStore = new MemoryStore(globalDbPath);
8103
9217
  try {
8104
9218
  res.json({
@@ -8119,7 +9233,7 @@ var DashboardServer = class {
8119
9233
  logs: this.store.listRuntimeNodeLogs(void 0, void 0, 500)
8120
9234
  });
8121
9235
  });
8122
- this.app.post("/api/run", auth, (req, res) => {
9236
+ this.app.post("/api/run", auth, mutationLimiter, (req, res) => {
8123
9237
  const body = req.body;
8124
9238
  if (!body.prompt || typeof body.prompt !== "string") {
8125
9239
  res.status(400).json({ error: "prompt is required" });
@@ -8140,12 +9254,15 @@ var DashboardServer = class {
8140
9254
  cascade.on("permission:user-required", (e) => {
8141
9255
  this.socket.broadcastToRoom(`session:${sessionId}`, "permission:user-required", { sessionId, ...e });
8142
9256
  });
9257
+ cascade.on("peer:message", (e) => {
9258
+ this.socket.emitPeerMessage(e);
9259
+ });
8143
9260
  try {
8144
9261
  const result = await cascade.run({ prompt: body.prompt, identityId: body.identityId });
8145
9262
  this.socket.broadcast("cost:update", {
8146
9263
  sessionId,
8147
- tokens: result.usage.totalTokens,
8148
- costUsd: result.usage.estimatedCostUsd
9264
+ totalTokens: result.usage.totalTokens,
9265
+ totalCostUsd: result.usage.estimatedCostUsd
8149
9266
  });
8150
9267
  this.socket.broadcastToRoom(`session:${sessionId}`, "session:complete", { sessionId, result });
8151
9268
  this.throttledBroadcast("workspace");
@@ -8168,13 +9285,13 @@ var DashboardServer = class {
8168
9285
  }))
8169
9286
  });
8170
9287
  });
8171
- const prodPath = path13__default.default.resolve(__dirname$1, "../web/dist");
8172
- const devPath = path13__default.default.resolve(__dirname$1, "../../web/dist");
8173
- const webDistPath = fs11__default.default.existsSync(prodPath) ? prodPath : devPath;
8174
- if (fs11__default.default.existsSync(webDistPath)) {
9288
+ const prodPath = path16__default.default.resolve(__dirname$1, "../web/dist");
9289
+ const devPath = path16__default.default.resolve(__dirname$1, "../../web/dist");
9290
+ const webDistPath = fs15__default.default.existsSync(prodPath) ? prodPath : devPath;
9291
+ if (fs15__default.default.existsSync(webDistPath)) {
8175
9292
  this.app.use(express__default.default.static(webDistPath));
8176
9293
  this.app.get("*", (_req, res) => {
8177
- res.sendFile(path13__default.default.join(webDistPath, "index.html"));
9294
+ res.sendFile(path16__default.default.join(webDistPath, "index.html"));
8178
9295
  });
8179
9296
  } else {
8180
9297
  this.app.get("/", (_req, res) => {
@@ -8251,7 +9368,7 @@ var TaskScheduler = class {
8251
9368
  return cron__default.default.validate(expression);
8252
9369
  }
8253
9370
  };
8254
- var execFileAsync = util.promisify(child_process.execFile);
9371
+ var execFileAsync2 = util.promisify(child_process.execFile);
8255
9372
  var SAFE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
8256
9373
  function sanitizeEnvValue(v) {
8257
9374
  const raw = typeof v === "string" ? v : JSON.stringify(v);
@@ -8290,7 +9407,7 @@ var HooksRunner = class {
8290
9407
  const isWin = process.platform === "win32";
8291
9408
  const shell = isWin ? "cmd.exe" : "/bin/sh";
8292
9409
  const shellArgs = isWin ? ["/d", "/s", "/c", hook.command] : ["-c", hook.command];
8293
- const { stdout } = await execFileAsync(shell, shellArgs, {
9410
+ const { stdout } = await execFileAsync2(shell, shellArgs, {
8294
9411
  timeout: hook.timeout ?? 1e4,
8295
9412
  env: { ...process.env, ...envVars },
8296
9413
  windowsHide: true