refactor: 移除 context-mode,Chrome 快照改为直接返回

context-mode 的 ctx_execute 系工具以真实 shell 执行且权限为 allow,绕过了
bash 策略与 protectedWrites;其每轮注入、独立 MCP 子进程与 skills 也与组合包
提示词单一所有权冲突。大输出职责已由 pi-overflow(入口截断)与
pi-compaction(压力遮蔽)承担:
- 删除 context-mode 依赖、wrapper/过滤器、skills,以及 ctx_* 的权限、
  tools.jsonc、路由规则、tool-search 常驻与 4 个分组
- better-sqlite3@12 仅为 context-mode 传递依赖,随之移除根 allowScripts
- pi-chrome:按用户决定移除快照隐私规则与 .pi/chrome-context 临时文件机制,
  快照 JSON 直接作为工具结果返回、details 只留小型元数据,超长由 pi-overflow
  截断并用 overflow_query 提取;授权生命周期不变

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
云服务部-叶林立
2026-10-21 20:45:33 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 2a6bf8c929
commit 56cbc83296
36 changed files with 178 additions and 2142 deletions
@@ -4,11 +4,6 @@ import { existsSync, readFileSync, statSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import { dirname, join, resolve } from "node:path";
import {
SessionSnapshotStore,
type SnapshotCaptureMetadata,
type SnapshotTarget,
} from "../../src/session-snapshot-store.ts";
/**
* Existing-profile Chrome bridge for pi.
@@ -89,13 +84,28 @@ function snapshotFromIncludedResult(raw: unknown): unknown {
return raw && typeof raw === "object" ? (raw as { snapshot?: unknown }).snapshot : undefined;
}
export function formatSnapshotCapture(metadata: SnapshotCaptureMetadata): string {
const kib = Math.max(1, Math.ceil(metadata.bytes / 1024));
return [
`Chrome snapshot saved to ${metadata.path} (${kib} KiB, ${metadata.elementCount} elements${metadata.matchCount ? `, ${metadata.matchCount} matches` : ""}).`,
"Analyze it immediately with ctx_execute_file; FILE_CONTENT is JSON and the page data is under .snapshot.",
"This capture is replaced by the next snapshot for the same target and becomes stale after page interaction.",
].join("\n");
/**
* Small, session-file-safe metadata describing a returned snapshot. The full
* snapshot goes to the model as text — details must stay small because Pi
* persists them in the session file.
*/
export function snapshotStats(snapshot: unknown): { elementCount: number; matchCount: number } {
const object = snapshot && typeof snapshot === "object" && !Array.isArray(snapshot)
? snapshot as { elements?: unknown; matches?: unknown }
: {};
return {
elementCount: Array.isArray(object.elements) ? object.elements.length : 0,
matchCount: Array.isArray(object.matches) ? object.matches.length : 0,
};
}
/**
* Action summary followed by the inline snapshot JSON. Oversized results are
* bounded by pi-overflow and recoverable through overflow_query, so no local
* capture file is written.
*/
export function formatActionResultText(text: string, snapshot: unknown | undefined): string {
return snapshot === undefined ? text : `${text}\n\n${safeJson(snapshot)}`;
}
export function formatChromeFindMatches(snapshot: any): string {
@@ -642,7 +652,6 @@ export default function (pi: ExtensionAPI): void {
let countdownInterval: NodeJS.Timeout | undefined;
// Remembered so bridge sends can tag tabs with this session's group even when ctx isn't handy.
let sessionCtx: ExtensionContext | undefined;
let snapshotStore: SessionSnapshotStore | undefined;
const clearAuthExpiryTimer = (): void => {
if (!authExpiryTimer) return;
@@ -716,7 +725,6 @@ export default function (pi: ExtensionAPI): void {
persistAuth();
// Revoking control ends pi-chrome's automation for this session; tidy up the target we own.
cleanupAutomationTargetBestEffort();
void snapshotStore?.clear().catch(() => undefined);
};
const authSummary = (): string => {
@@ -847,45 +855,22 @@ export default function (pi: ExtensionAPI): void {
return { ...params, foreground: !background } as T;
};
const snapshotTargetFrom = (params: Record<string, unknown>): SnapshotTarget => ({
targetId: typeof params.targetId === "string" ? params.targetId : undefined,
urlIncludes: typeof params.urlIncludes === "string" ? params.urlIncludes : undefined,
titleIncludes: typeof params.titleIncludes === "string" ? params.titleIncludes : undefined,
});
const saveSnapshotCapture = async (snapshot: unknown, params: Record<string, unknown>): Promise<SnapshotCaptureMetadata> => {
if (!snapshotStore) throw new Error("Chrome snapshot storage is unavailable before session_start.");
const stored = await snapshotStore.save(snapshot, snapshotTargetFrom(params));
return stored.metadata;
};
const invalidateSnapshotCapture = async (params?: Record<string, unknown>): Promise<void> => {
if (!snapshotStore) return;
if (!params) await snapshotStore.clear();
else await snapshotStore.invalidate(snapshotTargetFrom(params));
};
const formatActionWithCapture = async (raw: unknown, params: Record<string, unknown>, text: string): Promise<ToolTextResult> => {
const formatActionResult = (raw: unknown, params: Record<string, unknown>, text: string): ToolTextResult => {
const snapshot = snapshotFromIncludedResult(raw);
const result = params.includeSnapshot && raw && typeof raw === "object"
? (raw as { result?: unknown }).result
: raw;
if (!snapshot) {
await invalidateSnapshotCapture(params);
return { content: [{ type: "text", text }], details: { result: result as Json } };
}
const capture = await saveSnapshotCapture(snapshot, params);
return {
content: [{ type: "text", text: `${text}\n\n${formatSnapshotCapture(capture)}` }],
details: { result: result as Json, capture },
content: [{ type: "text", text: formatActionResultText(text, snapshot) }],
details: { result: result as Json, snapshot: snapshotStats(snapshot) },
};
};
pi.on("session_start", async (_event, ctx) => {
sessionCtx = ctx;
const sessionId = ctx.sessionManager.getSessionId?.() || "unknown-session";
snapshotStore = new SessionSnapshotStore(workspaceCwd(ctx), sessionId);
await snapshotStore.clear();
await bridge.start();
// Reestablish in-memory state after a /reload restored chromeAuthorizedUntil from globalThis.
if (chromeControlAuthorized()) {
@@ -903,10 +888,6 @@ export default function (pi: ExtensionAPI): void {
pi.on("session_shutdown", async (event) => {
clearAuthExpiryTimer();
clearCountdownInterval();
// Snapshot files are intentionally ephemeral. A reload must take a fresh observation too,
// because the browser page may have changed while the extension was reloading.
await snapshotStore?.clear().catch(() => undefined);
snapshotStore = undefined;
// Keep the owned automation target across /reload, but clean it up on real session end.
if (event?.reason !== "reload") cleanupAutomationTargetBestEffort();
bridge.stop();
@@ -933,12 +914,12 @@ Capability model (important):
- Interactive controls (click/type/fill/key/hover/drag/scroll/tap) use Chrome's real input layer via chrome.debugger / CDP. Events satisfy normal user-activation gates.
- Input bypasses page CSP because it is injected at browser input layer, not page JavaScript. Chrome may show the “Pi Chrome Connector started debugging this browser” banner while attached.
- \`chrome_evaluate\` and \`chrome_snapshot\` run in MAIN world via **CDP \`Runtime.evaluate\`**, which is not subject to the page's Content-Security-Policy. They work even on strict-CSP pages (e.g. github.com, many bank/SaaS apps) that block \`'unsafe-eval'\`. \`chrome_navigate initScript\` likewise injects at document_start via CDP and bypasses CSP. \`chrome_screenshot\`, \`chrome_tab\`, and Chrome input also work under any CSP.
- \`chrome_snapshot\` and snapshots requested with \`includeSnapshot=true\` are written to a private session-local JSON capture; raw page data is never returned inline. The tool result gives the path for immediate \`ctx_execute_file\` analysis.
- \`chrome_snapshot\` and snapshots requested with \`includeSnapshot=true\` return the page snapshot as JSON directly in the tool result. When the result is large it carries a \`[pi-overflow id=…]\` marker — extract only the uids, text, or state you need with \`overflow_query\` (jsonPath such as \`elements[0].uid\` or \`summary.focused\`, or a \`search\`) instead of taking another snapshot.
Usage rules:
1. If a chrome_* tool says Chrome control is locked, ask the user to run \`/chrome authorize\` before retrying.
2. Call \`chrome_snapshot\`, then immediately call \`ctx_execute_file\` on the returned path and print only the uids/state needed for the next action. Never use read/cat on the full capture.
3. Prefer \`uid\` over \`selector\`. After any page interaction, treat the old capture as stale. Use \`includeSnapshot=true\` when a fresh post-action capture is needed.
2. Call \`chrome_snapshot\` and read the returned JSON (or query a pi-overflow marker with \`overflow_query\`); keep only the uids/state needed for the next action.
3. Prefer \`uid\` over \`selector\`. After any page interaction, earlier snapshots are stale — take a fresh one (or pass \`includeSnapshot=true\` on the action) instead of reusing old uids.
4. If \`chrome_evaluate\` returns null when you expected a value, the expression evaluated to null/undefined in the page; surface the value via \`JSON.stringify\` to confirm.
5. \`chrome_navigate\` supports an optional \`initScript\` that runs at document_start in MAIN world for the next navigation (good for seeding localStorage or stubbing Date.now).
6. By default chrome_* tools run in the background without focusing Chrome; pass \`background=false\` or run /chrome background off when the user wants to watch Chrome work.
@@ -1331,7 +1312,6 @@ Usage rules:
const text = tabs.map((tab) => `${tab.id}\t${tab.active ? "*" : " "}\t${tab.group?.title ? `[${tab.group.title}] ` : ""}${tab.title || "(untitled)"}\t${tab.url}`).join("\n") || "No tabs.";
return { content: [{ type: "text", text }], details: { tabs } };
}
if (params.action !== "version") await invalidateSnapshotCapture();
return { content: [{ type: "text", text: safeJson(result) }], details: { result: result as Json } };
},
});
@@ -1340,8 +1320,8 @@ Usage rules:
name: "chrome_snapshot",
label: "Chrome Snapshot",
description:
"Capture the current Chrome page into a private, session-local JSON file instead of placing the raw DOM snapshot in model context. The result returns a path for immediate ctx_execute_file analysis. Each target keeps only its latest capture; page interaction makes it stale. Use mode/query/nearUid to control what Chrome captures. Runs in the background by default; pass background=false so the user can watch.",
promptSnippet: "Save the current Chrome page to a session-local JSON capture, then analyze that path with ctx_execute_file for only the required uids and state.",
"Capture the current Chrome page and return the snapshot as JSON directly in the result. When it is large the result carries a [pi-overflow id=…] marker — extract only the uids, text, or state you need with overflow_query (jsonPath or search). Page interaction makes a snapshot stale; take a fresh one after navigating or acting. Use mode/query/nearUid to control what Chrome captures. Runs in the background by default; pass background=false so the user can watch.",
promptSnippet: "Capture the current Chrome page and return its JSON snapshot; query large results via overflow_query.",
parameters: Type.Object({
targetId: Type.Optional(Type.String()),
urlIncludes: Type.Optional(Type.String()),
@@ -1366,8 +1346,7 @@ Usage rules:
DEFAULT_TIMEOUT_MS,
signal,
);
const capture = await saveSnapshotCapture(snapshot, params);
return { content: [{ type: "text", text: formatSnapshotCapture(capture) }], details: { capture } };
return { content: [{ type: "text", text: safeJson(snapshot) }], details: { snapshot: snapshotStats(snapshot) } };
},
});
@@ -1375,8 +1354,8 @@ Usage rules:
name: "chrome_find",
label: "Chrome Find",
description:
"Find elements, regions, or text on the live Chrome page. Returns only compact ranked matches with stable uids and coordinates; the full structured snapshot is saved to the same private session-local file used by chrome_snapshot for optional ctx_execute_file analysis.",
promptSnippet: "Find live Chrome controls/text/regions and return compact stable-uid matches while keeping the full snapshot out of model context.",
"Find elements, regions, or text on the live Chrome page. Returns only compact ranked matches with stable uids and coordinates.",
promptSnippet: "Find live Chrome controls/text/regions and return compact stable-uid matches.",
parameters: Type.Object({
query: Type.String({ description: "What to find, e.g. 'merge button', 'email error', 'approve PR', 'search box'." }),
mode: Type.Optional(StringEnum(snapshotModeValues)),
@@ -1397,10 +1376,9 @@ Usage rules:
DEFAULT_TIMEOUT_MS,
signal,
);
const capture = await saveSnapshotCapture(snapshot, params);
return {
content: [{ type: "text", text: `${formatChromeFindMatches(snapshot)}\n\n${formatSnapshotCapture(capture)}` }],
details: { capture },
content: [{ type: "text", text: formatChromeFindMatches(snapshot) }],
details: { snapshot: snapshotStats(snapshot) },
};
},
});
@@ -1446,9 +1424,8 @@ Usage rules:
DEFAULT_TIMEOUT_MS,
signal,
);
const capture = await saveSnapshotCapture(snapshot, params);
const text = `chrome_inspect fallback: loaded Chrome extension does not yet support page.inspect; reload it at chrome://extensions for deep inspect.\n\n${formatSnapshotCapture(capture)}`;
return { content: [{ type: "text", text }], details: { capture, fallback: "page.snapshot" } };
const text = `chrome_inspect fallback: loaded Chrome extension does not yet support page.inspect; reload it at chrome://extensions for deep inspect.\n\n${safeJson(snapshot)}`;
return { content: [{ type: "text", text }], details: { snapshot: snapshotStats(snapshot), fallback: "page.snapshot" } };
}
},
});
@@ -1475,7 +1452,6 @@ Usage rules:
}),
async execute(_id, params, signal): Promise<ToolTextResult> {
const result = await authorizedBridgeSend("page.navigate", withBackground(params), (params.timeoutMs ?? 15_000) + 2_000, signal);
await invalidateSnapshotCapture(params);
return { content: [{ type: "text", text: `Navigated to ${params.url}${params.initScript ? " (with initScript)" : ""}` }], details: { result: result as Json } };
},
});
@@ -1500,7 +1476,6 @@ Usage rules:
}),
async execute(_id, params, signal): Promise<ToolTextResult> {
const value = await authorizedBridgeSend("page.evaluate", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
await invalidateSnapshotCapture(params);
const text = value === undefined
? "undefined"
: typeof value === "string"
@@ -1514,7 +1489,7 @@ Usage rules:
name: "chrome_click",
label: "Chrome Click",
description:
"Click a snapshot uid, CSS selector, or viewport coordinate using Chrome's real input layer. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-click snapshot to the session-local file and return its ctx_execute_file path, never the raw snapshot.",
"Click a snapshot uid, CSS selector, or viewport coordinate using Chrome's real input layer. Earlier snapshots become stale after the click. Pass includeSnapshot=true to get a fresh post-click snapshot JSON inline.",
promptSnippet: "Click page elements in Chrome by snapshot uid, selector, or viewport coordinate.",
parameters: Type.Object({
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot. Prefer uid over selector after taking a snapshot." })),
@@ -1522,7 +1497,7 @@ Usage rules:
x: Type.Optional(Type.Number({ description: "Viewport x coordinate if uid/selector is omitted." })),
y: Type.Optional(Type.Number({ description: "Viewport y coordinate if uid/selector is omitted." })),
domFallback: Type.Optional(Type.Boolean({ description: "If true (default), fall back to DOM-dispatched click if Chrome's CDP input path is blocked by another extension overlay or debugger failure." })),
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-click snapshot to the session-local capture file and return its path." })),
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-click snapshot JSON inline after the action summary." })),
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
targetId: Type.Optional(Type.String()),
urlIncludes: Type.Optional(Type.String()),
@@ -1539,7 +1514,7 @@ Usage rules:
const summary = summarizeActionResult(result);
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
const text = summary ? `Clicked ${target} — ${summary}` : `Clicked ${target}`;
return formatActionWithCapture(raw, params, text);
return formatActionResult(raw, params, text);
},
});
@@ -1547,13 +1522,13 @@ Usage rules:
name: "chrome_type",
label: "Chrome Type",
description:
"Focus an optional snapshot uid or CSS selector, then type text using Chrome's real keyboard input. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-type snapshot and return its ctx_execute_file path.",
"Focus an optional snapshot uid or CSS selector, then type text using Chrome's real keyboard input. Earlier snapshots become stale after typing. Pass includeSnapshot=true to get a fresh post-type snapshot JSON inline.",
promptSnippet: "Type text into Chrome, optionally focusing a snapshot uid or selector first.",
parameters: Type.Object({
text: Type.String(),
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot." })),
selector: Type.Optional(Type.String({ description: "CSS selector to focus before typing." })),
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-type snapshot to the session-local capture file and return its path." })),
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-type snapshot JSON inline after the action summary." })),
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
pressEnter: Type.Optional(Type.Boolean()),
targetId: Type.Optional(Type.String()),
@@ -1572,7 +1547,7 @@ Usage rules:
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
const base = `Typed ${params.text.length} character(s)${into}.`;
const text = summary ? `${base} (${summary})` : base;
return formatActionWithCapture(raw, params, text);
return formatActionResult(raw, params, text);
},
});
@@ -1580,7 +1555,7 @@ Usage rules:
name: "chrome_fill",
label: "Chrome Fill",
description:
"Set the full value of a text input, textarea, or contenteditable element using Chrome input. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-fill snapshot and return its ctx_execute_file path.",
"Set the full value of a text input, textarea, or contenteditable element using Chrome input. Earlier snapshots become stale after filling. Pass includeSnapshot=true to get a fresh post-fill snapshot JSON inline.",
promptSnippet: "Fill a Chrome form field by snapshot uid or selector, optionally returning a fresh snapshot.",
parameters: Type.Object({
text: Type.String(),
@@ -1588,7 +1563,7 @@ Usage rules:
selector: Type.Optional(Type.String({ description: "CSS selector to fill if uid is omitted." })),
submit: Type.Optional(Type.Boolean({ description: "If true, press Enter after filling." })),
domFallback: Type.Optional(Type.Boolean({ description: "If true (default), fall back to DOM value-setting if Chrome's CDP input path is blocked by another extension overlay or debugger failure." })),
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-fill snapshot to the session-local capture file and return its path." })),
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-fill snapshot JSON inline after the action summary." })),
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
targetId: Type.Optional(Type.String()),
urlIncludes: Type.Optional(Type.String()),
@@ -1606,7 +1581,7 @@ Usage rules:
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
const base = `Filled ${params.text.length} character(s)${into}.`;
const text = summary ? `${base} (${summary})` : base;
return formatActionWithCapture(raw, params, text);
return formatActionResult(raw, params, text);
},
});
@@ -1614,7 +1589,7 @@ Usage rules:
name: "chrome_key",
label: "Chrome Key",
description:
"Send a keyboard key to an existing Chrome tab. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-key snapshot and return its ctx_execute_file path. Runs in the background by default; pass background=false so the user can watch.",
"Send a keyboard key to an existing Chrome tab. Earlier snapshots become stale after the key press. Pass includeSnapshot=true to get a fresh post-key snapshot JSON inline. Runs in the background by default; pass background=false so the user can watch.",
promptSnippet: "Press keys in Chrome through the companion extension.",
parameters: Type.Object({
key: Type.String(),
@@ -1624,7 +1599,7 @@ Usage rules:
altKey: Type.Optional(Type.Boolean()),
metaKey: Type.Optional(Type.Boolean()),
}, { description: "Modifier keys to hold while pressing the key (chord)." })),
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-key snapshot to the session-local capture file and return its path." })),
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-key snapshot JSON inline after the action summary." })),
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
targetId: Type.Optional(Type.String()),
urlIncludes: Type.Optional(Type.String()),
@@ -1641,7 +1616,7 @@ Usage rules:
const summary = summarizeActionResult(result);
const base = `Pressed ${params.key}.`;
const text = summary ? `${base} (${summary})` : base;
return formatActionWithCapture(raw, params, text);
return formatActionResult(raw, params, text);
},
});
@@ -1663,7 +1638,6 @@ Usage rules:
}),
async execute(_id, params, signal): Promise<ToolTextResult> {
const result = await authorizedBridgeSend("page.waitFor", params, (params.timeoutMs ?? 10_000) + 2_000, signal);
await invalidateSnapshotCapture(params);
return { content: [{ type: "text", text: `Observed ${params.kind}: ${params.value}` }], details: { result: result as Json } };
},
});
@@ -1807,7 +1781,6 @@ Usage rules:
}),
async execute(_id, params, signal): Promise<ToolTextResult> {
const result = await authorizedBridgeSend("page.hover", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
await invalidateSnapshotCapture(params);
return { content: [{ type: "text", text: `Hovered ${params.uid ?? params.selector ?? `${params.x},${params.y}`}` }], details: { result: result as Json } };
},
});
@@ -1834,7 +1807,6 @@ Usage rules:
}),
async execute(_id, params, signal): Promise<ToolTextResult> {
const result = await authorizedBridgeSend("page.drag", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
await invalidateSnapshotCapture(params);
return { content: [{ type: "text", text: `Dragged from ${params.fromUid ?? params.fromSelector} to ${params.toUid ?? params.toSelector}` }], details: { result: result as Json } };
},
});
@@ -1857,7 +1829,6 @@ Usage rules:
}),
async execute(_id, params, signal): Promise<ToolTextResult> {
const result = await authorizedBridgeSend("page.tap", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
await invalidateSnapshotCapture(params);
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
return { content: [{ type: "text", text: `Tapped ${target} (touch)` }], details: { result: result as Json } };
},
@@ -1881,7 +1852,6 @@ Usage rules:
}),
async execute(_id, params, signal): Promise<ToolTextResult> {
const result = await authorizedBridgeSend("page.scroll", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
await invalidateSnapshotCapture(params);
return { content: [{ type: "text", text: `Scrolled dy=${params.deltaY ?? 0} dx=${params.deltaX ?? 0}` }], details: { result: result as Json } };
},
});
@@ -1904,7 +1874,6 @@ Usage rules:
const cwd = workspaceCwd(ctx);
const paths = params.paths.map((p) => resolve(cwd, p));
const result = await authorizedBridgeSend("page.upload", withBackground({ ...params, paths }), DEFAULT_TIMEOUT_MS, signal);
await invalidateSnapshotCapture(params);
return { content: [{ type: "text", text: `Uploaded ${paths.length} file(s) to ${params.uid ?? params.selector}` }], details: { result: result as Json } };
},
});