cascade-ai 0.54.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -192,6 +192,33 @@ interface ToolResult {
192
192
  output: string;
193
193
  isError: boolean;
194
194
  }
195
+ /**
196
+ * What to do about a section that escalated — a worker hit something it could
197
+ * not decide alone.
198
+ *
199
+ * `timeout` is a real outcome, not an error: a cloud run holds server resources
200
+ * while it waits, so an unanswered escalation fails the section rather than
201
+ * hanging indefinitely. The section reports WHY it failed, so an unattended run
202
+ * is diagnosable rather than merely dead.
203
+ */
204
+ interface EscalationDecision {
205
+ action: 'retry' | 'skip' | 'guidance' | 'timeout';
206
+ /** Required for 'guidance' — the instruction the retry must follow. */
207
+ note?: string;
208
+ /**
209
+ * Set when the SDK produced `action: 'skip'` itself — no listener wired,
210
+ * `autonomy: 'auto'`, or the run was aborted — rather than a person
211
+ * answering the prompt. `resolveEscalation()`, the one path a human
212
+ * response reaches, never sets this.
213
+ *
214
+ * A skip decided by the system is not the same fact as a skip a user
215
+ * chose: T2's `userSkipped` marker (which tells T1's reviewer "accept this
216
+ * as satisfied, not a gap") must NOT be set for the automatic case — a
217
+ * section nobody ever looked at is exactly the case a corrective pass
218
+ * exists to catch.
219
+ */
220
+ automatic?: boolean;
221
+ }
195
222
  interface ToolExecuteOptions {
196
223
  tierId: string;
197
224
  sessionId: string;
@@ -294,6 +321,19 @@ interface T2Result {
294
321
  t3Results: T3Result[];
295
322
  sectionSummary: string;
296
323
  issues: string[];
324
+ /**
325
+ * Set when this section reached PARTIAL because the user was asked about an
326
+ * escalation and chose "skip" — not because a T2/T3 pipeline shortfall left
327
+ * it incomplete. T1's reviewer pass sees only the section summary, so
328
+ * without this flag it cannot tell "the model gave up" from "the user
329
+ * decided this was good enough" and would generate a correction plan that
330
+ * redoes exactly the work the user just chose to keep as-is.
331
+ *
332
+ * NOT set when the 'skip' came from `EscalationDecision.automatic` — no
333
+ * listener wired, `autonomy: 'auto'`, or an aborted run. Nobody reviewed
334
+ * that section, so it must stay eligible for the corrective pass.
335
+ */
336
+ userSkipped?: boolean;
297
337
  }
298
338
  interface T3ResultPayload {
299
339
  subtaskId: string;
@@ -1569,6 +1609,21 @@ declare class CascadeRouter extends EventEmitter {
1569
1609
  */
1570
1610
  private budgetState;
1571
1611
  private budgetExceededReason;
1612
+ /**
1613
+ * Whether the run/session budget is permanently tripped, and why.
1614
+ *
1615
+ * A tier or worker that catches `BudgetExceededError` internally (T3Worker
1616
+ * does, so one dead-end subtask doesn't crash the whole run) absorbs the
1617
+ * exception into a normal FAILED/PARTIAL result — the tier returns as if
1618
+ * nothing unusual happened. That silently skips `Cascade.run()`'s dedicated
1619
+ * budget-exceeded handling (the `run:budget-exceeded` event and
1620
+ * `lastInterruptedRun` for `/continue`). This lets a caller check the
1621
+ * router's own state directly after a tier returns, regardless of how that
1622
+ * tier chose to report the error internally.
1623
+ */
1624
+ budgetExceededInfo(): {
1625
+ reason: string;
1626
+ } | null;
1572
1627
  private tpmLimiter;
1573
1628
  private localQueue;
1574
1629
  private taskAnalyzer?;
@@ -1943,8 +1998,17 @@ declare class ToolRegistry extends EventEmitter {
1943
1998
  registerPlugin(plugin: ToolPlugin): void;
1944
1999
  /** Returns the names of all registered plugins */
1945
2000
  getRegisteredPlugins(): string[];
1946
- /** Registers all tools from an MCP client */
1947
- registerMcpTools(mcpClient: McpClient): void;
2001
+ /**
2002
+ * Registers all tools from an MCP client.
2003
+ *
2004
+ * `isAllowed` receives the FINAL registered name (`mcp__server__tool`) — the
2005
+ * same string the user sees and the same one that reaches the provider — so
2006
+ * a per-tool opt-out in Settings can be expressed as a plain name list.
2007
+ * Filtering here rather than at call time means a deselected tool is genuinely
2008
+ * absent from the definitions handed to the model, so it is never proposed,
2009
+ * never counted against the tool budget, and never has to be refused.
2010
+ */
2011
+ registerMcpTools(mcpClient: McpClient, isAllowed?: (registeredName: string) => boolean): void;
1948
2012
  setIgnoredPaths(patterns: string[]): void;
1949
2013
  getToolDefinitions(): ToolDefinition[];
1950
2014
  getTool(name: string): BaseTool | undefined;
@@ -2211,6 +2275,7 @@ declare class T1Administrator extends BaseTier {
2211
2275
  private escalations;
2212
2276
  private store?;
2213
2277
  private runBreaker?;
2278
+ private escalationCallback?;
2214
2279
  private t2PeerBus;
2215
2280
  private permissionEscalator?;
2216
2281
  private toolCreator?;
@@ -2220,6 +2285,13 @@ declare class T1Administrator extends BaseTier {
2220
2285
  private peerMessageSessionId;
2221
2286
  private planApprovalCallback?;
2222
2287
  constructor(router: CascadeRouter, toolRegistry: ToolRegistry, config: CascadeConfig);
2288
+ /** Ask the user what to do when any section escalates. */
2289
+ setEscalationCallback(cb: (ctx: {
2290
+ sectionId: string;
2291
+ sectionTitle: string;
2292
+ issues: string[];
2293
+ summary: string;
2294
+ }) => Promise<EscalationDecision>): void;
2223
2295
  /** Share the run-wide circuit breaker with every section manager. */
2224
2296
  setRunBreaker(breaker: RunBreaker): void;
2225
2297
  setStore(store: MemoryStore): void;
@@ -2289,6 +2361,34 @@ interface GeneratedAsset {
2289
2361
  */
2290
2362
  type AssetSink = (asset: GeneratedAsset) => Promise<string>;
2291
2363
 
2364
+ /** What the host's browser view can tell us about the page on screen. */
2365
+ interface CurrentPageSnapshot {
2366
+ url: string;
2367
+ title: string;
2368
+ /** Rendered visible text, host-truncated if enormous. */
2369
+ text: string;
2370
+ }
2371
+ /**
2372
+ * Returns the page the user is currently viewing, or null when the browser is
2373
+ * closed / on a blank tab.
2374
+ */
2375
+ type CurrentPageProvider = () => Promise<CurrentPageSnapshot | null>;
2376
+ /** Characters of page text handed to a model in one call. */
2377
+ declare const MAX_PAGE_TEXT_CHARS = 40000;
2378
+ declare class CurrentPageTool extends BaseTool {
2379
+ readonly name = "read_current_page";
2380
+ readonly description: string;
2381
+ readonly inputSchema: {
2382
+ type: string;
2383
+ properties: {};
2384
+ required: never[];
2385
+ };
2386
+ private provider;
2387
+ constructor(provider: CurrentPageProvider);
2388
+ isDangerous(): boolean;
2389
+ execute(_input: Record<string, unknown>, _options: ToolExecuteOptions): Promise<string>;
2390
+ }
2391
+
2292
2392
  /** One entry in the per-run orchestration decision trail (see /why). */
2293
2393
  interface DecisionLogEntry {
2294
2394
  at: string;
@@ -2355,6 +2455,32 @@ declare class Cascade extends EventEmitter {
2355
2455
  resolveMcpApproval(serverName: string, approved: boolean): void;
2356
2456
  private pendingPlanApproval?;
2357
2457
  private requestPlanApproval;
2458
+ private pendingEscalations;
2459
+ private requestEscalationDecision;
2460
+ /**
2461
+ * Resolve a pending escalation from a REPL / dashboard / cloud listener.
2462
+ *
2463
+ * `requestId` comes back from the `escalation:decision-required` event and is
2464
+ * what makes an answer land on the section that asked. It is optional so a
2465
+ * host with only one escalation in flight (and any caller that predates the
2466
+ * id) still works: without it the oldest outstanding request is answered,
2467
+ * which is correct whenever there is exactly one.
2468
+ *
2469
+ * `automatic` defaults to false — a call into this method is normally a real
2470
+ * person answering the prompt. Pass `true` from a host that is itself
2471
+ * settling the gate on nobody's behalf (a REST caller who never had a
2472
+ * chance to connect, a session halt with no per-section review behind it),
2473
+ * so T2 doesn't set `userSkipped` for a decision no one actually reviewed —
2474
+ * see EscalationDecision.automatic.
2475
+ */
2476
+ resolveEscalation(action: EscalationDecision['action'], note?: string, requestId?: string, automatic?: boolean): void;
2477
+ /**
2478
+ * Release every parked escalation — run teardown, so none outlive the run.
2479
+ * `automatic: true`: teardown resolving a section nobody answered is the
2480
+ * same system-decided case as the early-return skips above, not a person
2481
+ * reviewing and accepting that section's output.
2482
+ */
2483
+ private releasePendingEscalations;
2358
2484
  /**
2359
2485
  * Resolve a pending boardroom plan approval from a REPL / dashboard listener.
2360
2486
  * An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
@@ -2405,6 +2531,17 @@ declare class Cascade extends EventEmitter {
2405
2531
  * existing tools rather than duplicating them.
2406
2532
  */
2407
2533
  setMediaSink(sink: AssetSink): void;
2534
+ /**
2535
+ * Let the agent read the page the user has open in the host's browser.
2536
+ *
2537
+ * Host-supplied because only a host WITH a browser can answer it — so the
2538
+ * tool is simply absent in the CLI and in hosted runs, rather than present
2539
+ * and always failing. Registered outside `enabledTools` for the same reason
2540
+ * media tools are: that allowlist is a blast-radius control for tools that
2541
+ * touch the machine, and reading a page the user already opened touches
2542
+ * nothing. `disabledTools` still removes it.
2543
+ */
2544
+ setCurrentPageProvider(provider: CurrentPageProvider): void;
2408
2545
  private registerMediaTools;
2409
2546
  /**
2410
2547
  * Supply per-model thumbs-up/down counts so Auto routing can weigh how models
@@ -2652,6 +2789,19 @@ declare class T2Manager extends BaseTier {
2652
2789
  private t2PeerBus?;
2653
2790
  private permissionEscalator?;
2654
2791
  private toolCreator?;
2792
+ /**
2793
+ * Asked when a section ends ESCALATED — i.e. a worker hit something it could
2794
+ * not decide alone. Without this the escalation was a dead end: the status
2795
+ * said "needs a decision" and no decision was ever requested, so an MCP run
2796
+ * that legitimately needed input just stopped there.
2797
+ */
2798
+ private escalationCallback?;
2799
+ /**
2800
+ * The one allowed retry has been spent. Retrying is bounded because an
2801
+ * escalation loop that can re-ask forever burns a budget on a question that
2802
+ * is not resolving; a second escalation is terminal instead.
2803
+ */
2804
+ private escalationRetryUsed;
2655
2805
  /** Optional boardroom gate (Moderate / root-T2 runs) — pauses after decomposition. */
2656
2806
  private planApprovalCallback?;
2657
2807
  /** AbortController for the current T3 wave — aborted on cancel-and-respawn */
@@ -2673,6 +2823,13 @@ declare class T2Manager extends BaseTier {
2673
2823
  */
2674
2824
  setPermissionEscalator(escalator: PermissionEscalator): void;
2675
2825
  setToolCreator(creator: ToolCreator): void;
2826
+ /** Ask the user what to do when a section escalates. */
2827
+ setEscalationCallback(cb: (ctx: {
2828
+ sectionId: string;
2829
+ sectionTitle: string;
2830
+ issues: string[];
2831
+ summary: string;
2832
+ }) => Promise<EscalationDecision>): void;
2676
2833
  /** Boardroom gate for Moderate (root-T2) runs: pause after decomposition. */
2677
2834
  setPlanApprovalCallback(cb: (subtasks: ReadonlyArray<{
2678
2835
  subtaskId: string;
@@ -2964,6 +3121,50 @@ declare class ConfigManager {
2964
3121
  getApiKey(provider: string): string | undefined;
2965
3122
  private loadConfig;
2966
3123
  private injectEnvKeys;
3124
+ /**
3125
+ * Follow a disambiguation rename into `tools.mcpTrusted`.
3126
+ *
3127
+ * `mcpTrusted` is matched by EXACT name (`McpClient.connect()`). Two shapes
3128
+ * of rename reach here, and they need OPPOSITE treatment:
3129
+ *
3130
+ * - Distinct raw names that only collide via sanitizing (`foo bar` /
3131
+ * `foo@bar`): each had its OWN trust entry, and the old string now belongs
3132
+ * to nothing — the server that used it moved away, so the entry has to
3133
+ * move with it. A plain rewrite is correct here.
3134
+ * - Literally identical names (a hand-edited or duplicated config: two rows
3135
+ * both named `foo`): there is only ONE trust entry for both, since
3136
+ * `mcpTrusted` itself is deduplicated. `this.config.tools.mcpServers` has
3137
+ * already been updated to the disambiguated list by the time this runs
3138
+ * (see `load()`), so the untouched survivor is STILL named `foo` — a plain
3139
+ * rewrite would move the one entry entirely onto the renamed row and
3140
+ * leave the survivor untrusted. The entry must be kept for the survivor
3141
+ * AND granted to the renamed identity, not moved.
3142
+ *
3143
+ * Renames are grouped by `from` before either treatment is applied, and
3144
+ * each group is settled in one step. Two DIFFERENT rows can start out with
3145
+ * the exact same raw name and BOTH get renamed away (e.g. three servers
3146
+ * named `foo bar`, `foo@bar`, `foo@bar` — the two `foo@bar` rows collide
3147
+ * with `foo bar` and with each other, so both are renamed to distinct
3148
+ * identities while `foo bar` survives untouched). Processing renames one at
3149
+ * a time in that shape drops coverage: the first rename's REPLACE branch
3150
+ * would erase `from` from `trusted` entirely, so the guard on the second
3151
+ * rename with the same `from` sees it already gone and silently skips —
3152
+ * granting trust to only one of the two renamed identities. Grouping first
3153
+ * means every renamed identity that shares a `from` is added or substituted
3154
+ * together, in the one pass that still sees the original entry.
3155
+ *
3156
+ * `tools.disabledTools` deliberately gets no equivalent treatment. It's
3157
+ * matched by sanitized PREFIX, and a prefix collision is exactly what made
3158
+ * an existing entry ambiguous between the two servers in the first place —
3159
+ * there is no way to tell, from the stored string alone, which of the two
3160
+ * a denial was meant for. Leaving those entries untouched resolves that
3161
+ * ambiguity for free: the survivor keeps the original (now unique) prefix,
3162
+ * so an old entry keeps applying to it, while the renamed server starts
3163
+ * clean under its new prefix. Rewriting the entry to follow the rename
3164
+ * would do the opposite — move a denial that already worked for the
3165
+ * survivor onto a server it may never have meant to cover.
3166
+ */
3167
+ private renameMcpServerReferences;
2967
3168
  private ensureDefaultIdentity;
2968
3169
  }
2969
3170
 
@@ -2977,6 +3178,10 @@ declare class DashboardSocket {
2977
3178
  private authRequired;
2978
3179
  private secret;
2979
3180
  constructor(httpServer: Server, options: DashboardSocketOptions);
3181
+ /** How many sockets are currently in a room — 0 means nobody can answer. */
3182
+ roomSize(room: string): number;
3183
+ /** Is this exact connection still live? A reconnect issues a NEW socket id. */
3184
+ hasSocket(socketId: string): boolean;
2980
3185
  broadcastToRoom(room: string, event: string, data: unknown): void;
2981
3186
  broadcast(event: string, data: unknown): void;
2982
3187
  emitCascadeEvent(ev: CascadeEvent): void;
@@ -2986,6 +3191,15 @@ declare class DashboardSocket {
2986
3191
  emitApprovalRequest(request: PermissionRequest): void;
2987
3192
  onApprovalResponse(callback: (data: PermissionDecisionPayload) => void): void;
2988
3193
  private setupHandlers;
3194
+ /**
3195
+ * A client subscribed to a session's room.
3196
+ *
3197
+ * The host uses this to replay a gate the run is already parked on. Socket.IO
3198
+ * does not buffer room emissions, so a prompt broadcast while the renderer was
3199
+ * reconnecting is simply gone; without a replay the section waits out its full
3200
+ * timeout with the user sitting in front of a screen that never asked.
3201
+ */
3202
+ onJoinSession(callback: (sessionId: string, socketId: string) => void): void;
2989
3203
  onSessionRate(callback: (sessionId: string, rating: 'good' | 'bad') => void): void;
2990
3204
  onSessionHalt(callback: (sessionId: string) => void): void;
2991
3205
  /**
@@ -2999,6 +3213,20 @@ declare class DashboardSocket {
2999
3213
  note?: string;
3000
3214
  editedPlan?: unknown;
3001
3215
  }) => void): void;
3216
+ /**
3217
+ * A section escalated and the run is parked waiting for an answer. The
3218
+ * desktop shows the escalation modal on `escalation:decision-required` and
3219
+ * answers here.
3220
+ *
3221
+ * Unlike a plan decision, silence is NOT taken as consent — the SDK fails
3222
+ * the section on timeout — so this handler only ever forwards a real choice.
3223
+ */
3224
+ onEscalationDecision(callback: (data: {
3225
+ sessionId: string;
3226
+ requestId?: string;
3227
+ action: 'retry' | 'skip' | 'guidance';
3228
+ note?: string;
3229
+ }) => void): void;
3002
3230
  onSessionSteer(callback: (message: string, sessionId?: string, nodeId?: string) => void): void;
3003
3231
  onConfigUpdate(callback: (data: {
3004
3232
  keys?: Record<string, string>;
@@ -3024,6 +3252,13 @@ declare class DashboardServer {
3024
3252
  private globalStore;
3025
3253
  private broadcastTimer;
3026
3254
  private activeSessions;
3255
+ /**
3256
+ * Supplied by the desktop shell so a run can read the page the user has
3257
+ * open in the built-in browser. Absent in the CLI, where there is no
3258
+ * browser — so the tool is simply not registered rather than present and
3259
+ * always failing.
3260
+ */
3261
+ private currentPageProvider?;
3027
3262
  private activeControllers;
3028
3263
  /**
3029
3264
  * Run taskIds per chat session — file snapshots are keyed by the run's
@@ -3038,6 +3273,17 @@ declare class DashboardServer {
3038
3273
  * map is how that answer reaches the run that's blocked on it.
3039
3274
  */
3040
3275
  private pendingApprovals;
3276
+ /**
3277
+ * The escalation prompt a session is currently parked on, kept so it can be
3278
+ * delivered to a client that was not reachable when it was raised.
3279
+ *
3280
+ * Socket.IO buffers nothing: a prompt emitted while the renderer is mid
3281
+ * reconnect is gone, and the section then waits out its full five minutes
3282
+ * with the user in front of a screen that never asked them anything. Holding
3283
+ * the payload lets `join:session` replay it, and lets a run with no reachable
3284
+ * client be settled instead of parked (see ORPHANED_ESCALATION_GRACE_MS).
3285
+ */
3286
+ private pendingEscalations;
3041
3287
  /**
3042
3288
  * The orchestration decision trail ("why") of each session's most recent
3043
3289
  * run — captured when the run ends so the desktop's Why panel can show it
@@ -3050,6 +3296,8 @@ declare class DashboardServer {
3050
3296
  private host;
3051
3297
  private workspacePath;
3052
3298
  constructor(config: CascadeConfig, store: MemoryStore, workspacePath?: string);
3299
+ /** Wire the host's browser view in (desktop only). */
3300
+ setCurrentPageProvider(provider: CurrentPageProvider): void;
3053
3301
  start(): Promise<void>;
3054
3302
  stop(): Promise<void>;
3055
3303
  getSocket(): DashboardSocket;
@@ -3116,6 +3364,66 @@ declare class DashboardServer {
3116
3364
  * client so an open desktop sees the run appear live.
3117
3365
  */
3118
3366
  private runScheduledTask;
3367
+ /**
3368
+ * Is anyone able to answer for this session right now?
3369
+ *
3370
+ * The originating socket first — that connection is definitely watching this
3371
+ * run — then the session room, which is where a client lands after a
3372
+ * reconnect issues it a new socket id.
3373
+ */
3374
+ private hasResponder;
3375
+ /**
3376
+ * Send a run event to whoever is watching this session.
3377
+ *
3378
+ * A captured socket id goes stale the moment Socket.IO reconnects — the
3379
+ * client is back, but under a new id, and `emitToSocket` on the old one is a
3380
+ * silent no-op. Falling back to the session room keeps the run addressable
3381
+ * across a reconnect; emitting to one OR the other (never both) keeps a
3382
+ * client that is both the originator and a room member from seeing every
3383
+ * token twice.
3384
+ */
3385
+ private emitToSessionClients;
3386
+ /**
3387
+ * Raise an escalation prompt, and make sure it can still be answered.
3388
+ *
3389
+ * Held in `pendingEscalations` — keyed by requestId, because a Complex wave
3390
+ * dispatches sections concurrently and two can be parked at once — so a
3391
+ * client that joins late (or rejoins after a reconnect) gets every waiting
3392
+ * prompt replayed. Keying by session would drop the first of a pair: its
3393
+ * grace timer would be cleared by the second, leaving it parked for the full
3394
+ * timeout with nobody able to answer it. If nobody is reachable at all, the gate is
3395
+ * settled as `skip` after a short grace period rather than parked for the
3396
+ * full timeout: that matches the SDK's policy when no listener exists, and
3397
+ * five minutes of a held run is a real cost for a REST caller who has already
3398
+ * hung up. The grace period exists so an ordinary two-second reconnect is not
3399
+ * mistaken for an absent user.
3400
+ */
3401
+ private raiseEscalation;
3402
+ /**
3403
+ * Replay EVERY parked prompt to a client that just subscribed to the session.
3404
+ *
3405
+ * Plural because a Complex run dispatches sections concurrently, so a wave can
3406
+ * leave more than one waiting at the same time — the same reason the SDK keys
3407
+ * its gates and the clients queue theirs.
3408
+ *
3409
+ * `timeoutMs` is recomputed as the time actually LEFT, because the SDK's timer
3410
+ * has been running since the prompt was raised. Sent verbatim, a client
3411
+ * reconnecting four minutes in would show a fresh five-minute countdown and
3412
+ * then watch the section fail with four of those minutes still on the clock.
3413
+ * A remaining duration is used rather than an absolute deadline so the two
3414
+ * sides never have to agree on a wall clock.
3415
+ */
3416
+ private replayEscalations;
3417
+ /** Forget ONE parked prompt — the one that was answered or timed out. */
3418
+ private clearPendingEscalation;
3419
+ /**
3420
+ * Forget the session's OLDEST prompt — for a client that answered without a
3421
+ * requestId, which is exactly the gate `resolveEscalation` will settle.
3422
+ * Insertion order makes `values()` oldest-first.
3423
+ */
3424
+ private clearOldestSessionEscalation;
3425
+ /** Forget every prompt a session was parked on (run ended, or halted). */
3426
+ private clearSessionEscalations;
3119
3427
  /** Deny + clear any approvals still pending for a session (run end / abort). */
3120
3428
  private denyPendingApprovals;
3121
3429
  private persistRuntimeRow;
@@ -3262,6 +3570,104 @@ declare function connectMcpWithLoopbackOAuth(opts: {
3262
3570
  timeoutMs?: number;
3263
3571
  }): Promise<LoopbackOAuthResult>;
3264
3572
 
3573
+ /** One tool a server advertises, named exactly as it is registered at run time. */
3574
+ interface DiscoveredMcpTool {
3575
+ /** The server that advertises it. */
3576
+ server: string;
3577
+ /** The server-side tool name (what the vendor calls it). */
3578
+ tool: string;
3579
+ /** The registered name — `mcp__server__tool`. This is the selection key. */
3580
+ name: string;
3581
+ description: string;
3582
+ }
3583
+ interface McpDiscoveryResult {
3584
+ server: string;
3585
+ tools: DiscoveredMcpTool[];
3586
+ /** Set when the server could not be reached or refused the connection. */
3587
+ error?: string;
3588
+ }
3589
+ /**
3590
+ * Connect to each server, list its tools, disconnect.
3591
+ *
3592
+ * Servers are pre-trusted here because the caller has already added them —
3593
+ * discovery reads a list, it never invokes anything. A server that fails is
3594
+ * reported with its error rather than throwing, so one dead connector cannot
3595
+ * blank out the Settings panel for the working ones.
3596
+ */
3597
+ declare function discoverMcpTools(servers: McpServerConfig$1[]): Promise<McpDiscoveryResult[]>;
3598
+
3599
+ /** Prefix marking a tool that came from an MCP server. */
3600
+ declare const MCP_TOOL_PREFIX = "mcp__";
3601
+ /** Provider-safe name for a tool exposed by an MCP server. Bounded — see MAX_MCP_TOOL_NAME_LENGTH. */
3602
+ declare function mcpToolName(serverName: string, toolName: string): string;
3603
+ declare function isMcpToolName(name: string): boolean;
3604
+ /**
3605
+ * A name for a NEW MCP server connection that won't collide, on its sanitized
3606
+ * tool prefix, with any name already in use.
3607
+ *
3608
+ * Compared by prefix, not by the raw name: `foo bar` and `foo@bar` are
3609
+ * different strings but both register as `mcp__foo_bar__…`, so a check against
3610
+ * raw names would accept a pair that still collides downstream — the exact gap
3611
+ * that let two connectors' tools overwrite each other in the registry.
3612
+ *
3613
+ * Mirrored on the cloud side (`cloud/server/src/db.ts` `uniqueMcpServerName`,
3614
+ * DB-backed instead of array-backed) — kept in sync by hand since the two sides
3615
+ * don't share a runtime, but the algorithm and suffix format must match or a
3616
+ * name that's unique on one side could still collide on the other after a sync
3617
+ * pull (see `mergeMcpServers` in `src/cloud/keysync.ts`, which performs the
3618
+ * same comparison for that reason).
3619
+ */
3620
+ declare function uniqueMcpServerName(desired: string, existingNames: string[]): string;
3621
+ /** One entry renamed by `disambiguateMcpServerNames`, so callers can follow it. */
3622
+ interface McpServerRename {
3623
+ from: string;
3624
+ to: string;
3625
+ }
3626
+ /**
3627
+ * Rename pre-existing entries whose sanitized prefixes collide.
3628
+ *
3629
+ * For config loaded from disk (desktop and CLI share one config file), rather
3630
+ * than a fresh connection — `uniqueMcpServerName` only guards a NEW entry, so a
3631
+ * file that already contains a colliding pair (hand-edited, or written by a
3632
+ * CLI version that predates the check) stays broken until something touches it.
3633
+ * The FIRST entry of each colliding group keeps its name, since that connection
3634
+ * is presumably already working; later ones are suffixed exactly as
3635
+ * `uniqueMcpServerName` would have produced had they been added after this
3636
+ * existed. A no-op when nothing collides, so callers can run it unconditionally
3637
+ * on every load without needing a one-time migration flag.
3638
+ *
3639
+ * A server's name is referenced elsewhere in config — `tools.mcpTrusted`
3640
+ * (`McpClient.connect()` matches it exactly) and `tools.disabledTools`
3641
+ * (matched by sanitized prefix). This function only owns `mcpServers`, so it
3642
+ * returns the rename list rather than reaching into config fields it doesn't
3643
+ * know about; the caller applies it to whatever else keys off the old name.
3644
+ */
3645
+ declare function disambiguateMcpServerNames<T extends {
3646
+ name: string;
3647
+ }>(servers: T[]): {
3648
+ servers: T[];
3649
+ renames: McpServerRename[];
3650
+ };
3651
+ /**
3652
+ * Prefix matching every tool from one server, for filtering.
3653
+ *
3654
+ * Also lets a host drop a server's per-tool selections when the server itself
3655
+ * is removed — otherwise a tool switched off before removal stays off when the
3656
+ * same connector is added back, with nothing on screen explaining why.
3657
+ */
3658
+ declare function mcpServerPrefix(serverName: string): string;
3659
+ /**
3660
+ * Drop one removed server's entries from a deny list.
3661
+ *
3662
+ * The deny list lives as one flat array, not scoped per server, so removing a
3663
+ * connector without this left its denials behind — reconnecting the SAME name
3664
+ * later silently re-disabled tools the user had switched off in a previous
3665
+ * life of that connection, with nothing on screen explaining why. Shared by
3666
+ * every removal path (desktop IPC, `cascade mcp remove`) so the behavior can't
3667
+ * drift between them the way the duplicated inline version risked.
3668
+ */
3669
+ declare function removeMcpServerDenials(disabledTools: string[] | undefined, serverName: string): string[] | undefined;
3670
+
3265
3671
  /** A unit of indexed text. `sourceId` groups chunks from one document/file. */
3266
3672
  interface Chunk {
3267
3673
  id: string;
@@ -4049,14 +4455,39 @@ declare class CloudClient {
4049
4455
  logout(): Promise<void>;
4050
4456
  }
4051
4457
 
4458
+ /**
4459
+ * Bundle format version.
4460
+ *
4461
+ * 1 — the original shape. Knows nothing about per-tool selections.
4462
+ * 2 — carries `disabledTools`.
4463
+ *
4464
+ * The distinction is load-bearing, not bookkeeping. In a v2 bundle an absent
4465
+ * `disabledTools` is a real statement — "nothing is switched off on the device
4466
+ * I pushed from" — and pulling it must be able to switch tools back ON. In a v1
4467
+ * bundle the field is absent because the format had no way to express it, and
4468
+ * reading that as "nothing is switched off" would silently re-enable the
4469
+ * destructive connector tool a user had deliberately turned off. Absent and
4470
+ * empty are the same JSON; only the version tells them apart.
4471
+ */
4472
+ type SyncBundleVersion = 1 | 2;
4052
4473
  interface SyncBundle {
4053
- v: 1;
4474
+ v: SyncBundleVersion;
4054
4475
  /** LLM provider keys / endpoints. */
4055
4476
  providers?: ProviderConfig[];
4056
4477
  /** Web-search backend keys (Brave / Tavily) + SearXNG URL. */
4057
4478
  webSearch?: WebSearchConfig;
4058
4479
  /** MCP servers / connectors (headers carry auth). */
4059
4480
  mcpServers?: McpServerConfig$1[];
4481
+ /**
4482
+ * Per-tool selections for the servers above — the tools switched OFF.
4483
+ * Present from v2 on.
4484
+ *
4485
+ * Travels WITH the servers, and must: syncing a connector without its
4486
+ * selections hands the other device the connector, its credentials, and every
4487
+ * tool switched back on — including the destructive one the user had
4488
+ * deliberately turned off, with nothing on screen saying so.
4489
+ */
4490
+ disabledTools?: string[];
4060
4491
  /** Per-tier model overrides (non-secret). */
4061
4492
  models?: ModelOverrides;
4062
4493
  /** Budget caps (non-secret). */
@@ -4074,4 +4505,4 @@ declare function gatherSyncBundle(config: CascadeConfig): SyncBundle;
4074
4505
  /** Merge a pulled bundle into a config, returning the updated config to persist. */
4075
4506
  declare function applySyncBundle(bundle: SyncBundle, config: CascadeConfig): CascadeConfig;
4076
4507
 
4077
- export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, type ApprovalRequest, type ApprovalResponse, type AuditEntry, AuditLogger, type BenchmarksConfig, type BudgetConfig, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, type CascadeConfig, type CascadeEvent, type CascadeEventType, CascadeIgnore, type CascadeMessage, CascadeRouter, type CascadeRunOptions, type CascadeRunResult, type CascadeThemeName, CascadeToolError, type Chunk, type ChunkOptions, CloudClient, type CloudConversation, type CloudMessage, type CloudSession, type CloudSessionStore, type CloudTurnInput, type CloudUser, type CodeChunkOptions, type CodeChunker, CodeSearchTool, type CompleteFn, ConfigManager, type ConversationMessage, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, type DashboardConfig, DashboardServer, type DeviceStart, type DistilledFact, type Embedder, type EncryptedBlob, type EscalationPayload, type ExtendedContextConfig, type FileManifest, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, type GenerateOptions, type GenerateResult, type GraphFactSource, GraphRetriever, type GraphSearchOptions, GraphSearchTool, type HookDefinition, type HooksConfig, HooksRunner, type Identity, type ImageAttachment, Keystore, LLMReranker, LM_STUDIO_BASE_URL, type LegacyThemeName, MODELS, type ManifestDiff, McpClient, McpOAuthProvider, type McpOAuthState, type McpOAuthStore, type McpServerConfig$1 as McpServerConfig, type MemoryConfig, MemoryStore, type Message, type MessageContent, type MessagePayload, type MessageStatus, type MessageType, type ModelInfo, type ModelOverrides, type NativeProvider, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, type OpenAIEmbedderOptions, PROVIDER_DISPLAY_NAMES, type PeerMessage, type PeerMessageEvent, type PeerSyncPayload, type PeerSyncType, type PermissionDecision, type PermissionDecisionPayload, type PermissionRequest, type PermissionTrailEntry, type PlanReviewConfig, type ProviderConfig, type ProviderType, type RankedFact, type RefreshResult, type Reranker, type RetrievalContext, type RetrievalMode, type RetrievalPlan, Retriever, type RetrieverSearchOptions, type RuntimeNode, type RuntimeNodeLog, type RuntimeRefreshPayload, type RuntimeScope, type RuntimeSession, type RuntimeSnapshotPayload, type ScheduledTask, type ScoredChunk, type SearchOptions, type Session, type SessionCheckpoint, type SessionMetadata, type SessionSubscriptionPayload, SqliteVectorStore, type StatusUpdate, type StoredMessage, type StreamChunk, type SyncBundle, T1Administrator, type T1ToT2Assignment, T1_MODEL_PRIORITY, T2Manager, type T2Result, type T2ToT3Assignment, T2_MODEL_PRIORITY, type T3Result, type T3ResultPayload, type T3SubtaskSpec, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, type TaskComplexity, TaskScheduler, Telemetry, type TelemetryConfig, type Theme, type ThemeColors, type ThemeName, type TierConfig, type TierLimits, type TierRole, type TierStatus, type TokenUsage, type ToolCall, type ToolDefinition, type ToolExecuteOptions, ToolRegistry, type ToolResult, type ToolsConfig, VISION_MODEL_PRIORITY, type VectorStore, type WebSearchConfig, type WebhookConfig, type WorkspaceConfig, WorkspaceIndex, type WorkspaceIndexOptions, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildManifest, buildSessionTranscript, cagCharBudget, chatCompleterFromProviders, chunkCode, chunkText, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, discoverMcpAuthServer, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, nodeHttpFetch, parseRankOrder, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, runCascade, sessionWorthRemembering, streamCascade };
4508
+ export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, type ApprovalRequest, type ApprovalResponse, type AuditEntry, AuditLogger, type BenchmarksConfig, type BudgetConfig, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, type CascadeConfig, type CascadeEvent, type CascadeEventType, CascadeIgnore, type CascadeMessage, CascadeRouter, type CascadeRunOptions, type CascadeRunResult, type CascadeThemeName, CascadeToolError, type Chunk, type ChunkOptions, CloudClient, type CloudConversation, type CloudMessage, type CloudSession, type CloudSessionStore, type CloudTurnInput, type CloudUser, type CodeChunkOptions, type CodeChunker, CodeSearchTool, type CompleteFn, ConfigManager, type ConversationMessage, type CurrentPageProvider, type CurrentPageSnapshot, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, type DashboardConfig, DashboardServer, type DeviceStart, type DiscoveredMcpTool, type DistilledFact, type Embedder, type EncryptedBlob, type EscalationDecision, type EscalationPayload, type ExtendedContextConfig, type FileManifest, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, type GenerateOptions, type GenerateResult, type GraphFactSource, GraphRetriever, type GraphSearchOptions, GraphSearchTool, type HookDefinition, type HooksConfig, HooksRunner, type Identity, type ImageAttachment, Keystore, LLMReranker, LM_STUDIO_BASE_URL, type LegacyThemeName, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, type ManifestDiff, McpClient, type McpDiscoveryResult, McpOAuthProvider, type McpOAuthState, type McpOAuthStore, type McpServerConfig$1 as McpServerConfig, type McpServerRename, type MemoryConfig, MemoryStore, type Message, type MessageContent, type MessagePayload, type MessageStatus, type MessageType, type ModelInfo, type ModelOverrides, type NativeProvider, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, type OpenAIEmbedderOptions, PROVIDER_DISPLAY_NAMES, type PeerMessage, type PeerMessageEvent, type PeerSyncPayload, type PeerSyncType, type PermissionDecision, type PermissionDecisionPayload, type PermissionRequest, type PermissionTrailEntry, type PlanReviewConfig, type ProviderConfig, type ProviderType, type RankedFact, type RefreshResult, type Reranker, type RetrievalContext, type RetrievalMode, type RetrievalPlan, Retriever, type RetrieverSearchOptions, type RuntimeNode, type RuntimeNodeLog, type RuntimeRefreshPayload, type RuntimeScope, type RuntimeSession, type RuntimeSnapshotPayload, type ScheduledTask, type ScoredChunk, type SearchOptions, type Session, type SessionCheckpoint, type SessionMetadata, type SessionSubscriptionPayload, SqliteVectorStore, type StatusUpdate, type StoredMessage, type StreamChunk, type SyncBundle, T1Administrator, type T1ToT2Assignment, T1_MODEL_PRIORITY, T2Manager, type T2Result, type T2ToT3Assignment, T2_MODEL_PRIORITY, type T3Result, type T3ResultPayload, type T3SubtaskSpec, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, type TaskComplexity, TaskScheduler, Telemetry, type TelemetryConfig, type Theme, type ThemeColors, type ThemeName, type TierConfig, type TierLimits, type TierRole, type TierStatus, type TokenUsage, type ToolCall, type ToolDefinition, type ToolExecuteOptions, ToolRegistry, type ToolResult, type ToolsConfig, VISION_MODEL_PRIORITY, type VectorStore, type WebSearchConfig, type WebhookConfig, type WorkspaceConfig, WorkspaceIndex, type WorkspaceIndexOptions, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildManifest, buildSessionTranscript, cagCharBudget, chatCompleterFromProviders, chunkCode, chunkText, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, isMcpToolName, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseRankOrder, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, runCascade, sessionWorthRemembering, streamCascade, uniqueMcpServerName };