@phi-code-admin/phi-code 0.86.0 → 0.88.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/CHANGELOG.md +104 -0
- package/docs/adr/0001-phase-contract.md +57 -0
- package/docs/adr/0002-independent-review.md +47 -0
- package/docs/fork-policy.md +18 -0
- package/extensions/phi/agents.ts +7 -4
- package/extensions/phi/benchmark.ts +129 -49
- package/extensions/phi/browser.ts +10 -37
- package/extensions/phi/btw/btw.ts +1 -7
- package/extensions/phi/chrome/index.ts +1283 -741
- package/extensions/phi/commit.ts +9 -14
- package/extensions/phi/goal/index.ts +10 -33
- package/extensions/phi/init.ts +37 -47
- package/extensions/phi/keys.ts +2 -7
- package/extensions/phi/mcp/callback-server.ts +162 -165
- package/extensions/phi/mcp/config.ts +122 -136
- package/extensions/phi/mcp/errors.ts +18 -23
- package/extensions/phi/mcp/index.ts +322 -355
- package/extensions/phi/mcp/oauth-provider.ts +289 -289
- package/extensions/phi/mcp/server-manager.ts +390 -413
- package/extensions/phi/mcp/tool-bridge.ts +381 -415
- package/extensions/phi/memory.ts +2 -2
- package/extensions/phi/models.ts +27 -26
- package/extensions/phi/orchestrator.ts +451 -237
- package/extensions/phi/productivity.ts +4 -2
- package/extensions/phi/providers/agent-def.ts +1 -1
- package/extensions/phi/providers/alibaba.ts +56 -7
- package/extensions/phi/providers/context-window.ts +4 -1
- package/extensions/phi/providers/live-models.ts +5 -20
- package/extensions/phi/providers/orchestrator-helpers.ts +31 -5
- package/extensions/phi/providers/phase-machine.ts +283 -0
- package/extensions/phi/setup.ts +196 -169
- package/extensions/phi/skill-loader.ts +14 -17
- package/extensions/phi/smart-router.ts +6 -6
- package/extensions/phi/web-search.ts +90 -50
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "phi-code";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
3
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
6
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext } from "phi-code";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Existing-profile Chrome bridge for pi.
|
|
@@ -77,7 +77,9 @@ function safeJson(value: unknown): string {
|
|
|
77
77
|
const snapshotModeValues = ["auto", "interactive", "forms", "pageMap", "text", "changes", "full"] as const;
|
|
78
78
|
|
|
79
79
|
function compactLine(value: unknown, max = 140): string {
|
|
80
|
-
const text = String(value ?? "")
|
|
80
|
+
const text = String(value ?? "")
|
|
81
|
+
.replace(/\s+/g, " ")
|
|
82
|
+
.trim();
|
|
81
83
|
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -93,18 +95,33 @@ function formatChromeSnapshot(snapshot: any): string {
|
|
|
93
95
|
lines.push(`# Chrome snapshot${snapshot.mode ? ` (${snapshot.mode})` : ""}`);
|
|
94
96
|
lines.push(`${snapshot.title || "(untitled)"}`);
|
|
95
97
|
if (snapshot.url) lines.push(`${snapshot.url}`);
|
|
96
|
-
if (snapshot.viewport)
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
if (snapshot.viewport)
|
|
99
|
+
lines.push(
|
|
100
|
+
`viewport=${snapshot.viewport.width}x${snapshot.viewport.height} scroll=${snapshot.viewport.scrollX || 0},${snapshot.viewport.scrollY || 0}`,
|
|
101
|
+
);
|
|
102
|
+
if (snapshot.summary?.modal)
|
|
103
|
+
lines.push(`modal: ${snapshot.summary.modal.uid} ${compactLine(snapshot.summary.modal.label)}`);
|
|
104
|
+
if (snapshot.summary?.focused)
|
|
105
|
+
lines.push(
|
|
106
|
+
`focused: ${snapshot.summary.focused.uid} ${snapshot.summary.focused.role || ""} ${compactLine(snapshot.summary.focused.label)}`,
|
|
107
|
+
);
|
|
99
108
|
if (Array.isArray(snapshot.summary?.hints) && snapshot.summary.hints.length) {
|
|
100
109
|
lines.push("\n## Hints");
|
|
101
110
|
for (const hint of snapshot.summary.hints.slice(0, 6)) lines.push(`- ${hint}`);
|
|
102
111
|
}
|
|
103
112
|
if (snapshot.diff && !snapshot.diff.firstSnapshot) {
|
|
104
113
|
const changed = [
|
|
105
|
-
...(snapshot.diff.changes || []).map((c: any) =>
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
...(snapshot.diff.changes || []).map((c: any) =>
|
|
115
|
+
c.kind === "textChanged"
|
|
116
|
+
? "text changed"
|
|
117
|
+
: `${c.kind}: ${compactLine(c.before, 50)} → ${compactLine(c.after, 50)}`,
|
|
118
|
+
),
|
|
119
|
+
...(snapshot.diff.added || [])
|
|
120
|
+
.slice(0, 4)
|
|
121
|
+
.map((e: any) => `added ${e.uid} ${e.role || ""} ${compactLine(e.label)}`),
|
|
122
|
+
...(snapshot.diff.updated || [])
|
|
123
|
+
.slice(0, 4)
|
|
124
|
+
.map((u: any) => `updated ${u.uid} ${compactLine(u.after?.label || u.before?.label)}`),
|
|
108
125
|
];
|
|
109
126
|
if (changed.length) {
|
|
110
127
|
lines.push("\n## Changed since last snapshot");
|
|
@@ -114,29 +131,49 @@ function formatChromeSnapshot(snapshot: any): string {
|
|
|
114
131
|
if (Array.isArray(snapshot.matches) && snapshot.matches.length) {
|
|
115
132
|
lines.push(`\n## Matches for "${snapshot.query}"`);
|
|
116
133
|
for (const match of snapshot.matches.slice(0, 12)) {
|
|
117
|
-
if (match.kind === "text")
|
|
118
|
-
|
|
119
|
-
else
|
|
134
|
+
if (match.kind === "text")
|
|
135
|
+
lines.push(`- ${match.uid} text ${compactLine(match.text)} @ ${rectText(match.rect)}`);
|
|
136
|
+
else if (match.kind === "region")
|
|
137
|
+
lines.push(
|
|
138
|
+
`- ${match.uid} region ${compactLine(match.label)} headings=${(match.headings || []).map((h: string) => compactLine(h, 50)).join(" | ")}`,
|
|
139
|
+
);
|
|
140
|
+
else
|
|
141
|
+
lines.push(
|
|
142
|
+
`- ${match.uid} ${match.role || match.tag || "element"}${match.disabled ? " disabled" : ""} ${compactLine(match.label || match.selector)} @ ${rectText(match.rect)}`,
|
|
143
|
+
);
|
|
120
144
|
}
|
|
121
145
|
}
|
|
122
146
|
if (snapshot.mode === "pageMap" && snapshot.pageMap) {
|
|
123
147
|
lines.push("\n## Page map");
|
|
124
148
|
for (const region of (snapshot.pageMap.regions || []).slice(0, 18)) {
|
|
125
149
|
lines.push(`- ${region.uid} ${region.kind}: ${compactLine(region.label)}`);
|
|
126
|
-
for (const action of (region.actions || []).slice(0, 5))
|
|
150
|
+
for (const action of (region.actions || []).slice(0, 5))
|
|
151
|
+
lines.push(
|
|
152
|
+
` - ${action.uid} ${action.role || ""}${action.disabled ? " disabled" : ""} ${compactLine(action.label)}`,
|
|
153
|
+
);
|
|
127
154
|
}
|
|
128
155
|
if (snapshot.pageMap.headings?.length) {
|
|
129
156
|
lines.push("\nHeadings:");
|
|
130
|
-
for (const h of snapshot.pageMap.headings.slice(0, 20))
|
|
157
|
+
for (const h of snapshot.pageMap.headings.slice(0, 20))
|
|
158
|
+
lines.push(`- ${h.uid} h${h.level || ""} ${compactLine(h.text)}`);
|
|
131
159
|
}
|
|
132
160
|
}
|
|
133
161
|
if (Array.isArray(snapshot.layout) && snapshot.layout.length && snapshot.mode !== "changes") {
|
|
134
162
|
lines.push("\n## Layout / context");
|
|
135
163
|
for (const section of snapshot.layout.slice(0, snapshot.mode === "pageMap" ? 18 : 8)) {
|
|
136
|
-
const bits = [
|
|
164
|
+
const bits = [
|
|
165
|
+
`${section.uid}`,
|
|
166
|
+
section.role || section.tag,
|
|
167
|
+
compactLine(section.label || section.text || "(unnamed section)", 110),
|
|
168
|
+
`@ ${rectText(section.rect)}`,
|
|
169
|
+
];
|
|
137
170
|
lines.push(`- ${bits.filter(Boolean).join(" ")}`);
|
|
138
|
-
const fieldLabels = (section.fields || [])
|
|
139
|
-
|
|
171
|
+
const fieldLabels = (section.fields || [])
|
|
172
|
+
.slice(0, 4)
|
|
173
|
+
.map((f: any) => `${f.uid} ${compactLine(f.label || f.role, 40)}`);
|
|
174
|
+
const actionLabels = (section.actions || [])
|
|
175
|
+
.slice(0, 5)
|
|
176
|
+
.map((a: any) => `${a.uid}${a.disabled ? " disabled" : ""} ${compactLine(a.label || a.role, 40)}`);
|
|
140
177
|
if (fieldLabels.length) lines.push(` fields: ${fieldLabels.join("; ")}`);
|
|
141
178
|
if (actionLabels.length) lines.push(` actions: ${actionLabels.join("; ")}`);
|
|
142
179
|
}
|
|
@@ -146,28 +183,52 @@ function formatChromeSnapshot(snapshot: any): string {
|
|
|
146
183
|
const submits = snapshot.forms?.submits || [];
|
|
147
184
|
if (fields.length || submits.length) lines.push("\n## Forms");
|
|
148
185
|
for (const field of fields.slice(0, snapshot.mode === "forms" ? 40 : 12)) {
|
|
149
|
-
const bits = [
|
|
186
|
+
const bits = [
|
|
187
|
+
field.uid,
|
|
188
|
+
field.role || field.tag,
|
|
189
|
+
field.required ? "required" : "",
|
|
190
|
+
field.invalid ? "invalid" : "",
|
|
191
|
+
field.disabled ? "disabled" : "",
|
|
192
|
+
compactLine(field.label || field.selector, 90),
|
|
193
|
+
];
|
|
150
194
|
if (field.value) bits.push(`value=${compactLine(field.value, 50)}`);
|
|
151
195
|
else if (field.valueRedacted) bits.push("value=[redacted]");
|
|
152
196
|
lines.push(`- ${bits.filter(Boolean).join(" ")} @ ${rectText(field.rect)}`);
|
|
153
197
|
}
|
|
154
|
-
for (const submit of submits.slice(0, 8))
|
|
198
|
+
for (const submit of submits.slice(0, 8))
|
|
199
|
+
lines.push(
|
|
200
|
+
`- ${submit.uid} submit/action${submit.disabled ? " disabled" : ""} ${compactLine(submit.label || submit.selector)} @ ${rectText(submit.rect)}`,
|
|
201
|
+
);
|
|
155
202
|
}
|
|
156
203
|
if (Array.isArray(snapshot.elements) && snapshot.mode !== "pageMap") {
|
|
157
204
|
lines.push("\n## Visible actions");
|
|
158
205
|
for (const el of snapshot.elements.slice(0, snapshot.mode === "interactive" ? 60 : 25)) {
|
|
159
|
-
const flags = [el.disabled ? "disabled" : "", el.occluded ? `occluded-by-${el.occluded.tag}` : ""]
|
|
206
|
+
const flags = [el.disabled ? "disabled" : "", el.occluded ? `occluded-by-${el.occluded.tag}` : ""]
|
|
207
|
+
.filter(Boolean)
|
|
208
|
+
.join(",");
|
|
160
209
|
const context = el.context?.label ? ` in ${el.context.uid} ${compactLine(el.context.label, 60)}` : "";
|
|
161
|
-
lines.push(
|
|
210
|
+
lines.push(
|
|
211
|
+
`- ${el.uid} ${el.role || el.tag}${flags ? ` [${flags}]` : ""} ${compactLine(el.label || el.selector)}${context} @ ${rectText(el.rect)}`,
|
|
212
|
+
);
|
|
162
213
|
}
|
|
163
|
-
if (snapshot.elements.length > (snapshot.mode === "interactive" ? 60 : 25))
|
|
214
|
+
if (snapshot.elements.length > (snapshot.mode === "interactive" ? 60 : 25))
|
|
215
|
+
lines.push(
|
|
216
|
+
`- … ${snapshot.elements.length - (snapshot.mode === "interactive" ? 60 : 25)} more; retry with maxElements or mode=interactive`,
|
|
217
|
+
);
|
|
164
218
|
}
|
|
165
|
-
if (
|
|
219
|
+
if (
|
|
220
|
+
(snapshot.mode === "text" || snapshot.mode === "auto") &&
|
|
221
|
+
Array.isArray(snapshot.textSnippets) &&
|
|
222
|
+
snapshot.textSnippets.length
|
|
223
|
+
) {
|
|
166
224
|
lines.push("\n## Text snippets");
|
|
167
|
-
for (const snip of snapshot.textSnippets.slice(0, snapshot.mode === "text" ? 40 : 14))
|
|
225
|
+
for (const snip of snapshot.textSnippets.slice(0, snapshot.mode === "text" ? 40 : 14))
|
|
226
|
+
lines.push(`- ${snip.uid} ${compactLine(snip.text, snapshot.mode === "text" ? 240 : 160)}`);
|
|
168
227
|
if (snapshot.textTruncated) lines.push("- … page text truncated; retry with mode=text or maxTextChars for more");
|
|
169
228
|
}
|
|
170
|
-
lines.push(
|
|
229
|
+
lines.push(
|
|
230
|
+
"\nTip: use chrome_snapshot({query:'...', mode:'interactive|forms|pageMap|text|changes|full'}) or nearUid to zoom in.",
|
|
231
|
+
);
|
|
171
232
|
return truncateText(lines.join("\n"));
|
|
172
233
|
}
|
|
173
234
|
|
|
@@ -181,26 +242,41 @@ function formatChromeInspect(inspect: any): string {
|
|
|
181
242
|
const t = inspect.target || {};
|
|
182
243
|
const lines: string[] = [];
|
|
183
244
|
lines.push(`# Chrome inspect ${t.uid || ""}`.trim());
|
|
184
|
-
lines.push(
|
|
245
|
+
lines.push(
|
|
246
|
+
`${t.role || t.tag || "element"}${t.disabled ? " disabled" : ""}${t.occluded ? ` occluded-by-${t.occluded.tag}` : ""} ${compactLine(t.label || t.selector)}`,
|
|
247
|
+
);
|
|
185
248
|
if (t.selector) lines.push(`selector: ${t.selector}`);
|
|
186
249
|
if (t.rect) lines.push(`rect: ${rectText(t.rect)}`);
|
|
187
|
-
if (inspect.clickSuggestion)
|
|
250
|
+
if (inspect.clickSuggestion)
|
|
251
|
+
lines.push(
|
|
252
|
+
`suggested click: chrome_click({ uid: "${inspect.clickSuggestion.uid}" }) or x=${inspect.clickSuggestion.x}, y=${inspect.clickSuggestion.y}`,
|
|
253
|
+
);
|
|
188
254
|
if (Array.isArray(inspect.nearbyText) && inspect.nearbyText.length) {
|
|
189
255
|
lines.push("\n## Nearby text");
|
|
190
256
|
for (const item of inspect.nearbyText.slice(0, 12)) lines.push(`- ${item.uid} ${compactLine(item.text, 180)}`);
|
|
191
257
|
}
|
|
192
258
|
if (inspect.formContext) {
|
|
193
259
|
lines.push("\n## Form context");
|
|
194
|
-
for (const field of (inspect.formContext.fields || []).slice(0, 20))
|
|
195
|
-
|
|
260
|
+
for (const field of (inspect.formContext.fields || []).slice(0, 20))
|
|
261
|
+
lines.push(
|
|
262
|
+
`- ${field.uid} ${field.role || field.tag}${field.disabled ? " disabled" : ""} ${compactLine(field.label || field.selector)}${field.value ? ` value=${compactLine(field.value, 60)}` : field.valueRedacted ? " value=[redacted]" : ""}`,
|
|
263
|
+
);
|
|
264
|
+
for (const action of (inspect.formContext.actions || []).slice(0, 10))
|
|
265
|
+
lines.push(
|
|
266
|
+
`- ${action.uid} action${action.disabled ? " disabled" : ""} ${compactLine(action.label || action.selector)}`,
|
|
267
|
+
);
|
|
196
268
|
}
|
|
197
269
|
if (Array.isArray(inspect.nearbyActions) && inspect.nearbyActions.length) {
|
|
198
270
|
lines.push("\n## Nearby actions");
|
|
199
|
-
for (const action of inspect.nearbyActions.slice(0, 18))
|
|
271
|
+
for (const action of inspect.nearbyActions.slice(0, 18))
|
|
272
|
+
lines.push(
|
|
273
|
+
`- ${action.uid} ${action.role || action.tag}${action.disabled ? " disabled" : ""} ${compactLine(action.label || action.selector)} @ ${rectText(action.rect)}`,
|
|
274
|
+
);
|
|
200
275
|
}
|
|
201
276
|
if (Array.isArray(inspect.ancestors) && inspect.ancestors.length) {
|
|
202
277
|
lines.push("\n## Ancestors");
|
|
203
|
-
for (const a of inspect.ancestors.slice(0, 6))
|
|
278
|
+
for (const a of inspect.ancestors.slice(0, 6))
|
|
279
|
+
lines.push(`- ${a.uid} ${a.role || a.tag} ${compactLine(a.label || a.selector, 120)}`);
|
|
204
280
|
}
|
|
205
281
|
return truncateText(lines.join("\n"));
|
|
206
282
|
}
|
|
@@ -230,7 +306,11 @@ function browserExtensionPath(): string {
|
|
|
230
306
|
|
|
231
307
|
function hostnameOf(url: string | undefined): string {
|
|
232
308
|
if (!url) return "";
|
|
233
|
-
try {
|
|
309
|
+
try {
|
|
310
|
+
return new URL(url).hostname;
|
|
311
|
+
} catch {
|
|
312
|
+
return "";
|
|
313
|
+
}
|
|
234
314
|
}
|
|
235
315
|
|
|
236
316
|
// Description of a click/type/fill result's significant fields so the agent doesn't have to
|
|
@@ -243,12 +323,13 @@ function summarizeActionResult(result: unknown): string | undefined {
|
|
|
243
323
|
// Many real effects — class/aria/data-state toggles, JS-held state, canvas, async updates —
|
|
244
324
|
// don't move it, so a false value is NOT proof the action did nothing. Surface it only as a
|
|
245
325
|
// soft hint, and never present it as a failure on its own.
|
|
246
|
-
if (r.pageMutated === false)
|
|
326
|
+
if (r.pageMutated === false)
|
|
327
|
+
parts.push("no coarse DOM change detected (may still have taken effect — verify with includeSnapshot)");
|
|
247
328
|
if (r.defaultPrevented === true) parts.push("defaultPrevented=true");
|
|
248
329
|
if (r.elementVisible === false) parts.push("element NOT visible");
|
|
249
330
|
if (r.occludedBy) {
|
|
250
331
|
const o = r.occludedBy as { tag?: string; id?: string };
|
|
251
|
-
parts.push(`occluded by <${o.tag ?? "?"}${o.id ?
|
|
332
|
+
parts.push(`occluded by <${o.tag ?? "?"}${o.id ? `#${o.id}` : ""}>`);
|
|
252
333
|
}
|
|
253
334
|
if (r.valueMatches === false) parts.push("input value did not stick");
|
|
254
335
|
if (r.autoplayHint) parts.push("autoplay-gated affordance");
|
|
@@ -272,7 +353,7 @@ function corsHeadersFor(request: IncomingMessage): Record<string, string> {
|
|
|
272
353
|
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
273
354
|
"access-control-allow-headers": "content-type",
|
|
274
355
|
"access-control-expose-headers": "x-pi-chrome-version",
|
|
275
|
-
|
|
356
|
+
vary: "origin",
|
|
276
357
|
};
|
|
277
358
|
}
|
|
278
359
|
|
|
@@ -287,7 +368,12 @@ function isLocalProcessRequest(request: IncomingMessage): boolean {
|
|
|
287
368
|
return !request.headers.origin && !request.headers["sec-fetch-site"];
|
|
288
369
|
}
|
|
289
370
|
|
|
290
|
-
function sendJson(
|
|
371
|
+
function sendJson(
|
|
372
|
+
response: ServerResponse,
|
|
373
|
+
status: number,
|
|
374
|
+
body: unknown,
|
|
375
|
+
extraHeaders?: Record<string, string>,
|
|
376
|
+
): void {
|
|
291
377
|
response.writeHead(status, {
|
|
292
378
|
"content-type": "application/json; charset=utf-8",
|
|
293
379
|
"cache-control": "no-store",
|
|
@@ -393,12 +479,22 @@ class ChromeProfileBridge {
|
|
|
393
479
|
this.mode = undefined;
|
|
394
480
|
}
|
|
395
481
|
|
|
396
|
-
send(
|
|
482
|
+
send(
|
|
483
|
+
action: string,
|
|
484
|
+
params: Record<string, unknown>,
|
|
485
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
486
|
+
signal?: AbortSignal,
|
|
487
|
+
): Promise<unknown> {
|
|
397
488
|
if (this.mode === "client") return this.sendViaOwner(action, params, timeoutMs, signal);
|
|
398
489
|
return this.sendLocal(action, params, timeoutMs, signal);
|
|
399
490
|
}
|
|
400
491
|
|
|
401
|
-
private sendLocal(
|
|
492
|
+
private sendLocal(
|
|
493
|
+
action: string,
|
|
494
|
+
params: Record<string, unknown>,
|
|
495
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
496
|
+
signal?: AbortSignal,
|
|
497
|
+
): Promise<unknown> {
|
|
402
498
|
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
403
499
|
const command = { id, action, params };
|
|
404
500
|
return new Promise((resolveCommand, rejectCommand) => {
|
|
@@ -425,8 +521,14 @@ class ChromeProfileBridge {
|
|
|
425
521
|
}, timeoutMs);
|
|
426
522
|
this.pending.set(id, {
|
|
427
523
|
command,
|
|
428
|
-
resolve: (value) => {
|
|
429
|
-
|
|
524
|
+
resolve: (value) => {
|
|
525
|
+
cleanupAbort();
|
|
526
|
+
resolveCommand(value);
|
|
527
|
+
},
|
|
528
|
+
reject: (err) => {
|
|
529
|
+
cleanupAbort();
|
|
530
|
+
rejectCommand(err);
|
|
531
|
+
},
|
|
430
532
|
timer,
|
|
431
533
|
});
|
|
432
534
|
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -444,12 +546,17 @@ class ChromeProfileBridge {
|
|
|
444
546
|
return `Timed out after ${timeoutMs}ms: the Chrome extension received the command but never returned a result. The action may be long-running, or the result post failed. Run /chrome doctor; if it persists, reload 'Pi Chrome Connector' at chrome://extensions.`;
|
|
445
547
|
}
|
|
446
548
|
if (pollAgeMs === undefined || pollAgeMs > 60_000) {
|
|
447
|
-
return `Timed out after ${timeoutMs}ms: the Chrome extension is not polling (last seen ${pollAgeMs === undefined ? "never" : Math.round(pollAgeMs / 1000)
|
|
549
|
+
return `Timed out after ${timeoutMs}ms: the Chrome extension is not polling (last seen ${pollAgeMs === undefined ? "never" : `${Math.round(pollAgeMs / 1000)}s ago`}). Run /chrome onboard, then load the bundled browser-extension folder in your normal Chrome profile and keep that Chrome window open.`;
|
|
448
550
|
}
|
|
449
551
|
return `Timed out after ${timeoutMs}ms: the Chrome extension is polling (last seen ${Math.round(pollAgeMs / 1000)}s ago) but did not pick up this command in time. Retry; if it persists, reload 'Pi Chrome Connector' at chrome://extensions.`;
|
|
450
552
|
}
|
|
451
553
|
|
|
452
|
-
private async sendViaOwner(
|
|
554
|
+
private async sendViaOwner(
|
|
555
|
+
action: string,
|
|
556
|
+
params: Record<string, unknown>,
|
|
557
|
+
timeoutMs: number,
|
|
558
|
+
signal?: AbortSignal,
|
|
559
|
+
): Promise<unknown> {
|
|
453
560
|
const controller = new AbortController();
|
|
454
561
|
const timer = setTimeout(() => controller.abort(), timeoutMs + 2_000);
|
|
455
562
|
const forwardAbort = () => controller.abort();
|
|
@@ -464,13 +571,18 @@ class ChromeProfileBridge {
|
|
|
464
571
|
body: JSON.stringify({ action, params, timeoutMs }),
|
|
465
572
|
signal: controller.signal,
|
|
466
573
|
});
|
|
467
|
-
const payload = (await response.json().catch(() => ({}))) as {
|
|
574
|
+
const payload = (await response.json().catch(() => ({}))) as {
|
|
575
|
+
ok?: boolean;
|
|
576
|
+
result?: unknown;
|
|
577
|
+
error?: string;
|
|
578
|
+
};
|
|
468
579
|
if (response.status === 404) {
|
|
469
580
|
throw new Error(
|
|
470
581
|
"A running Pi session owns the Chrome bridge but is using an older pi-chrome without multi-session support. Restart that Pi session after `pi update`, then retry.",
|
|
471
582
|
);
|
|
472
583
|
}
|
|
473
|
-
if (!response.ok || !payload.ok)
|
|
584
|
+
if (!response.ok || !payload.ok)
|
|
585
|
+
throw new Error(payload.error ?? `Chrome bridge owner HTTP ${response.status}`);
|
|
474
586
|
return payload.result;
|
|
475
587
|
} catch (error) {
|
|
476
588
|
if ((error as Error).name === "AbortError") {
|
|
@@ -662,7 +774,12 @@ const CHROME_TOOL_NAMES = [
|
|
|
662
774
|
const CHROME_TOOL_NAME_SET = new Set<string>(CHROME_TOOL_NAMES);
|
|
663
775
|
|
|
664
776
|
function StringEnum<T extends readonly [string, ...string[]]>(values: T) {
|
|
665
|
-
return Type.Union(
|
|
777
|
+
return Type.Union(
|
|
778
|
+
values.map((value) => Type.Literal(value)) as [
|
|
779
|
+
ReturnType<typeof Type.Literal>,
|
|
780
|
+
...ReturnType<typeof Type.Literal>[],
|
|
781
|
+
],
|
|
782
|
+
);
|
|
666
783
|
}
|
|
667
784
|
|
|
668
785
|
export default function (pi: ExtensionAPI): void {
|
|
@@ -711,7 +828,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
711
828
|
authExpiryTimer = undefined;
|
|
712
829
|
};
|
|
713
830
|
|
|
714
|
-
const activeToolNamesWithoutChrome = (): string[] =>
|
|
831
|
+
const activeToolNamesWithoutChrome = (): string[] =>
|
|
832
|
+
pi.getActiveTools().filter((name) => !CHROME_TOOL_NAME_SET.has(name));
|
|
715
833
|
|
|
716
834
|
const activateChromeTools = (): void => {
|
|
717
835
|
registerChromeTools(pi);
|
|
@@ -761,7 +879,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
761
879
|
|
|
762
880
|
const updateChromeStatus = (ctx: ExtensionContext): void => {
|
|
763
881
|
if (chromeControlAuthorized()) {
|
|
764
|
-
ctx.ui.setStatus("chrome", ctx.ui.theme.fg("success", "●")
|
|
882
|
+
ctx.ui.setStatus("chrome", `${ctx.ui.theme.fg("success", "●")} Chrome Bridge`);
|
|
765
883
|
} else {
|
|
766
884
|
ctx.ui.setStatus("chrome", undefined);
|
|
767
885
|
}
|
|
@@ -770,25 +888,37 @@ export default function (pi: ExtensionAPI): void {
|
|
|
770
888
|
const scheduleAuthExpiry = (ctx: ExtensionContext, until: number | "indefinite"): void => {
|
|
771
889
|
clearAuthExpiryTimer();
|
|
772
890
|
if (until === "indefinite") return;
|
|
773
|
-
authExpiryTimer = setTimeout(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
891
|
+
authExpiryTimer = setTimeout(
|
|
892
|
+
() => {
|
|
893
|
+
if (chromeAuthorizedUntil !== until) return;
|
|
894
|
+
try {
|
|
895
|
+
lockChromeControl();
|
|
896
|
+
ctx.ui.notify(
|
|
897
|
+
"Chrome control authorization expired. Run /chrome authorize to allow chrome_* tools again.",
|
|
898
|
+
"info",
|
|
899
|
+
);
|
|
900
|
+
updateChromeStatus(ctx);
|
|
901
|
+
} catch (error) {
|
|
902
|
+
console.warn(`Failed to expire pi-chrome authorization cleanly: ${(error as Error).message}`);
|
|
903
|
+
}
|
|
904
|
+
},
|
|
905
|
+
Math.max(0, until - Date.now()),
|
|
906
|
+
);
|
|
783
907
|
};
|
|
784
908
|
|
|
785
|
-
const authorizedBridgeSend = (
|
|
909
|
+
const authorizedBridgeSend = (
|
|
910
|
+
action: string,
|
|
911
|
+
params: Record<string, unknown>,
|
|
912
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
913
|
+
signal?: AbortSignal,
|
|
914
|
+
): Promise<unknown> => {
|
|
786
915
|
requireChromeControlAuthorized();
|
|
787
916
|
// Any tab Pi *uses* (page.* interactions) should join this session's group, mirroring the
|
|
788
917
|
// auto-grouping that tab.new already does. Tagging the wire params lets getTabByParams pull
|
|
789
918
|
// the resolved (e.g. active) tab into the session group on the service-worker side. We skip
|
|
790
919
|
// tab.* actions: tab.new/group group explicitly, and activate/close/ungroup/list must not.
|
|
791
|
-
const shouldJoinGroup =
|
|
920
|
+
const shouldJoinGroup =
|
|
921
|
+
action.startsWith("page.") && sessionCtx !== undefined && params.sessionGroupTitle === undefined;
|
|
792
922
|
const wireParams = shouldJoinGroup
|
|
793
923
|
? { ...params, sessionGroupTitle: sessionGroupTitle(sessionCtx as ExtensionContext), joinSessionGroup: true }
|
|
794
924
|
: params;
|
|
@@ -857,64 +987,86 @@ Usage rules:
|
|
|
857
987
|
|
|
858
988
|
// Shared handlers, dispatched by the unified /chrome command below.
|
|
859
989
|
const doctorHandler = async (ctx: ExtensionContext) => {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
990
|
+
ctx.ui.notify("Checking pi-chrome…", "info");
|
|
991
|
+
const lines: string[] = [`pi-chrome v${PI_CHROME_VERSION}`];
|
|
992
|
+
const status = bridge.status();
|
|
993
|
+
const roleLabel =
|
|
994
|
+
status.mode === "client"
|
|
995
|
+
? "sharing another pi session's connection"
|
|
996
|
+
: "running the Chrome connection for this machine";
|
|
997
|
+
lines.push(`• This pi session is ${roleLabel}.`);
|
|
998
|
+
let extensionAlive = false;
|
|
999
|
+
let versionMismatch = false;
|
|
1000
|
+
try {
|
|
1001
|
+
const started = Date.now();
|
|
1002
|
+
const version = (await bridge.send("tab.version", {}, 35_000)) as {
|
|
1003
|
+
extensionId?: string;
|
|
1004
|
+
extensionVersion?: string;
|
|
1005
|
+
bridgeUrl?: string;
|
|
1006
|
+
};
|
|
1007
|
+
const latencyMs = Date.now() - started;
|
|
1008
|
+
extensionAlive = true;
|
|
1009
|
+
if (version.extensionVersion && version.extensionVersion !== PI_CHROME_VERSION) {
|
|
1010
|
+
versionMismatch = true;
|
|
1011
|
+
lines.push(
|
|
1012
|
+
`✗ The Chrome companion extension is on an old version (${version.extensionVersion}); this pi-chrome is ${PI_CHROME_VERSION}.`,
|
|
1013
|
+
` Every Chrome action will run with the old code until you reload the extension.`,
|
|
1014
|
+
` Fix: open chrome://extensions and click the refresh icon on 'Pi Chrome Connector'.`,
|
|
1015
|
+
` (After this one-time fix, future updates reload automatically.)`,
|
|
1016
|
+
);
|
|
1017
|
+
} else {
|
|
1018
|
+
lines.push(
|
|
1019
|
+
`✓ Chrome is connected (companion extension v${version.extensionVersion ?? "?"}, responded in ${latencyMs}ms).`,
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
const message = (error as Error).message;
|
|
1024
|
+
lines.push(`✗ Chrome isn't responding: ${message}`);
|
|
1025
|
+
if (message.includes("older pi-chrome without multi-session")) {
|
|
1026
|
+
lines.push(
|
|
1027
|
+
" Fix: quit and restart the pi session that first opened the Chrome connection (it was on an older pi-chrome).",
|
|
1028
|
+
);
|
|
1029
|
+
} else {
|
|
1030
|
+
lines.push(
|
|
1031
|
+
" Fix: run /chrome onboard to install the Chrome companion extension, then keep that Chrome window open.",
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (extensionAlive && !versionMismatch) {
|
|
1037
|
+
// Sanity-check that pi-chrome can actually run code in the active tab.
|
|
867
1038
|
try {
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
extensionAlive = true;
|
|
876
|
-
if (version.extensionVersion && version.extensionVersion !== PI_CHROME_VERSION) {
|
|
877
|
-
versionMismatch = true;
|
|
1039
|
+
const value = await bridge.send(
|
|
1040
|
+
"page.evaluate",
|
|
1041
|
+
{ expression: "1+1", awaitPromise: true, foreground: false },
|
|
1042
|
+
10_000,
|
|
1043
|
+
);
|
|
1044
|
+
if (value === 2) lines.push(`✓ pi-chrome can run code in the active Chrome tab.`);
|
|
1045
|
+
else
|
|
878
1046
|
lines.push(
|
|
879
|
-
|
|
880
|
-
` Every Chrome action will run with the old code until you reload the extension.`,
|
|
881
|
-
` Fix: open chrome://extensions and click the refresh icon on 'Pi Chrome Connector'.`,
|
|
882
|
-
` (After this one-time fix, future updates reload automatically.)`,
|
|
1047
|
+
`⚠ pi-chrome ran code in the active tab but got an unexpected result (${JSON.stringify(value)}). The current tab may be locked-down (a Chrome internal page or a strict site).`,
|
|
883
1048
|
);
|
|
884
|
-
} else {
|
|
885
|
-
lines.push(`✓ Chrome is connected (companion extension v${version.extensionVersion ?? "?"}, responded in ${latencyMs}ms).`);
|
|
886
|
-
}
|
|
887
1049
|
} catch (error) {
|
|
888
|
-
|
|
889
|
-
lines.push(`✗ Chrome isn't responding: ${message}`);
|
|
890
|
-
if (message.includes("older pi-chrome without multi-session")) {
|
|
891
|
-
lines.push(" Fix: quit and restart the pi session that first opened the Chrome connection (it was on an older pi-chrome).");
|
|
892
|
-
} else {
|
|
893
|
-
lines.push(" Fix: run /chrome onboard to install the Chrome companion extension, then keep that Chrome window open.");
|
|
894
|
-
}
|
|
1050
|
+
lines.push(`✗ pi-chrome can't run code in the active tab: ${(error as Error).message}`);
|
|
895
1051
|
}
|
|
896
1052
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
if (probe && probe.arithmetic === 2) lines.push(`✓ The active tab is ${hostnameOf(String(probe.location))} and accepts pi-chrome's commands.`);
|
|
911
|
-
if (probe && probe.webdriver) lines.push(`⚠ Your Chrome is reporting itself as automated to websites. Some sites use this signal to block sign-ins or bot checks.`);
|
|
912
|
-
} catch (error) {
|
|
913
|
-
lines.push(`⚠ Couldn't inspect the active tab: ${(error as Error).message}`);
|
|
914
|
-
}
|
|
915
|
-
} else if (versionMismatch) {
|
|
916
|
-
lines.push(`… Skipped the remaining checks until you reload the Chrome extension.`);
|
|
1053
|
+
// Surface obvious site-side automation flags so the user knows why a site might block pi.
|
|
1054
|
+
try {
|
|
1055
|
+
const probe = (await bridge.send("page.probe", { foreground: false }, 10_000)) as Record<string, unknown>;
|
|
1056
|
+
if (probe && probe.arithmetic === 2)
|
|
1057
|
+
lines.push(
|
|
1058
|
+
`✓ The active tab is ${hostnameOf(String(probe.location))} and accepts pi-chrome's commands.`,
|
|
1059
|
+
);
|
|
1060
|
+
if (probe?.webdriver)
|
|
1061
|
+
lines.push(
|
|
1062
|
+
`⚠ Your Chrome is reporting itself as automated to websites. Some sites use this signal to block sign-ins or bot checks.`,
|
|
1063
|
+
);
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
lines.push(`⚠ Couldn't inspect the active tab: ${(error as Error).message}`);
|
|
917
1066
|
}
|
|
1067
|
+
} else if (versionMismatch) {
|
|
1068
|
+
lines.push(`… Skipped the remaining checks until you reload the Chrome extension.`);
|
|
1069
|
+
}
|
|
918
1070
|
|
|
919
1071
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
920
1072
|
};
|
|
@@ -965,7 +1117,8 @@ Usage rules:
|
|
|
965
1117
|
|
|
966
1118
|
const parseAuthorizeArg = (arg: string): { label: string; until: number | "indefinite" } | undefined => {
|
|
967
1119
|
const normalized = arg.trim().toLowerCase() || "15m";
|
|
968
|
-
if (normalized === "indefinite" || normalized === "forever")
|
|
1120
|
+
if (normalized === "indefinite" || normalized === "forever")
|
|
1121
|
+
return { label: "indefinitely", until: "indefinite" };
|
|
969
1122
|
const minutes = normalized.endsWith("m") ? Number(normalized.slice(0, -1)) : Number(normalized);
|
|
970
1123
|
if (!Number.isFinite(minutes) || minutes <= 0) return undefined;
|
|
971
1124
|
return { label: `${minutes} minutes`, until: Date.now() + minutes * 60_000 };
|
|
@@ -997,9 +1150,18 @@ Usage rules:
|
|
|
997
1150
|
return;
|
|
998
1151
|
}
|
|
999
1152
|
if (process.platform === "darwin") {
|
|
1000
|
-
await pi
|
|
1001
|
-
|
|
1002
|
-
|
|
1153
|
+
await pi
|
|
1154
|
+
.exec("open", ["-a", "Google Chrome", "chrome://extensions"], { cwd: workspaceCwd(ctx), timeout: 5_000 })
|
|
1155
|
+
.catch(() => undefined);
|
|
1156
|
+
await pi
|
|
1157
|
+
.exec("open", ["-R", extensionPath], { cwd: workspaceCwd(ctx), timeout: 5_000 })
|
|
1158
|
+
.catch(() => undefined);
|
|
1159
|
+
await pi
|
|
1160
|
+
.exec("sh", ["-lc", `printf %s ${JSON.stringify(extensionPath)} | pbcopy`], {
|
|
1161
|
+
cwd: workspaceCwd(ctx),
|
|
1162
|
+
timeout: 5_000,
|
|
1163
|
+
})
|
|
1164
|
+
.catch(() => undefined);
|
|
1003
1165
|
}
|
|
1004
1166
|
ctx.ui.notify(
|
|
1005
1167
|
"Chrome and Finder should be open. The extension folder path is on your clipboard. After you click 'Load unpacked' and pick it, run /chrome doctor to confirm everything is connected.",
|
|
@@ -1014,7 +1176,9 @@ Usage rules:
|
|
|
1014
1176
|
try {
|
|
1015
1177
|
const version = (await bridge.send("tab.version", {}, 5_000)) as { extensionVersion?: string };
|
|
1016
1178
|
if (version.extensionVersion && version.extensionVersion !== PI_CHROME_VERSION) {
|
|
1017
|
-
parts.push(
|
|
1179
|
+
parts.push(
|
|
1180
|
+
`⚠ Chrome extension v${version.extensionVersion} (pi-chrome v${PI_CHROME_VERSION}, reload extension)`,
|
|
1181
|
+
);
|
|
1018
1182
|
} else {
|
|
1019
1183
|
parts.push(`✓ Chrome connected`);
|
|
1020
1184
|
}
|
|
@@ -1041,9 +1205,12 @@ Usage rules:
|
|
|
1041
1205
|
]);
|
|
1042
1206
|
if (!choice) return;
|
|
1043
1207
|
switch (choice) {
|
|
1044
|
-
case "15 minutes":
|
|
1045
|
-
|
|
1046
|
-
case "
|
|
1208
|
+
case "15 minutes":
|
|
1209
|
+
return authorizeHandler(ctx, "15m");
|
|
1210
|
+
case "30 minutes":
|
|
1211
|
+
return authorizeHandler(ctx, "30m");
|
|
1212
|
+
case "Indefinite":
|
|
1213
|
+
return authorizeHandler(ctx, "indefinite");
|
|
1047
1214
|
case "Custom minutes": {
|
|
1048
1215
|
const value = await ctx.ui.input("Authorize for how many minutes?", "45");
|
|
1049
1216
|
if (!value) continue;
|
|
@@ -1060,8 +1227,10 @@ Usage rules:
|
|
|
1060
1227
|
]);
|
|
1061
1228
|
if (!choice) return;
|
|
1062
1229
|
switch (choice) {
|
|
1063
|
-
case "Use Chrome in background":
|
|
1064
|
-
|
|
1230
|
+
case "Use Chrome in background":
|
|
1231
|
+
return backgroundHandler(ctx, "on");
|
|
1232
|
+
case "Use Chrome in foreground":
|
|
1233
|
+
return backgroundHandler(ctx, "off");
|
|
1065
1234
|
}
|
|
1066
1235
|
};
|
|
1067
1236
|
|
|
@@ -1077,11 +1246,18 @@ Usage rules:
|
|
|
1077
1246
|
]);
|
|
1078
1247
|
if (!choice) return;
|
|
1079
1248
|
switch (choice) {
|
|
1080
|
-
case "Authorize Chrome control…":
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
case "
|
|
1084
|
-
|
|
1249
|
+
case "Authorize Chrome control…":
|
|
1250
|
+
await openAuthorizeMenu(ctx);
|
|
1251
|
+
continue;
|
|
1252
|
+
case "Lock Chrome control":
|
|
1253
|
+
return revokeHandler(ctx);
|
|
1254
|
+
case "Doctor / troubleshoot":
|
|
1255
|
+
return doctorHandler(ctx);
|
|
1256
|
+
case "Background / watch mode…":
|
|
1257
|
+
await openBackgroundMenu(ctx);
|
|
1258
|
+
continue;
|
|
1259
|
+
case "Install / onboard extension":
|
|
1260
|
+
return onboardHandler(ctx);
|
|
1085
1261
|
}
|
|
1086
1262
|
}
|
|
1087
1263
|
};
|
|
@@ -1105,24 +1281,60 @@ Usage rules:
|
|
|
1105
1281
|
let candidates: Item[] = [];
|
|
1106
1282
|
if (path.length === 0) {
|
|
1107
1283
|
candidates = [
|
|
1108
|
-
{
|
|
1284
|
+
{
|
|
1285
|
+
fullValue: "authorize",
|
|
1286
|
+
label: "authorize",
|
|
1287
|
+
description: "Allow this Pi session to use chrome_* tools.",
|
|
1288
|
+
},
|
|
1109
1289
|
{ fullValue: "revoke", label: "revoke", description: "Lock Chrome control for this Pi session." },
|
|
1110
|
-
{
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1290
|
+
{
|
|
1291
|
+
fullValue: "status",
|
|
1292
|
+
label: "status",
|
|
1293
|
+
description: "One-line summary: connection, auth, and background setting.",
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
fullValue: "doctor",
|
|
1297
|
+
label: "doctor",
|
|
1298
|
+
description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't.",
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
fullValue: "onboard",
|
|
1302
|
+
label: "onboard",
|
|
1303
|
+
description: "Install the Chrome companion extension (first-time setup).",
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
fullValue: "background",
|
|
1307
|
+
label: "background",
|
|
1308
|
+
description: "Run pi-chrome in the background without focusing Chrome?",
|
|
1309
|
+
},
|
|
1114
1310
|
];
|
|
1115
1311
|
} else if (path[0] === "authorize" && path.length === 1) {
|
|
1116
1312
|
candidates = [
|
|
1117
1313
|
{ fullValue: "authorize 15m", label: "15m", description: "Authorize Chrome control for 15 minutes." },
|
|
1118
1314
|
{ fullValue: "authorize 30m", label: "30m", description: "Authorize Chrome control for 30 minutes." },
|
|
1119
|
-
{
|
|
1315
|
+
{
|
|
1316
|
+
fullValue: "authorize indefinite",
|
|
1317
|
+
label: "indefinite",
|
|
1318
|
+
description: "Authorize Chrome control until revoked or Pi exits.",
|
|
1319
|
+
},
|
|
1120
1320
|
];
|
|
1121
1321
|
} else if (path[0] === "background" && path.length === 1) {
|
|
1122
1322
|
candidates = [
|
|
1123
|
-
{
|
|
1124
|
-
|
|
1125
|
-
|
|
1323
|
+
{
|
|
1324
|
+
fullValue: "background on",
|
|
1325
|
+
label: "on",
|
|
1326
|
+
description: "Run in background. Chrome stays in the background. Your editor keeps focus. (default)",
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
fullValue: "background off",
|
|
1330
|
+
label: "off",
|
|
1331
|
+
description: "Bring Chrome to the front so you can watch.",
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
fullValue: "background toggle",
|
|
1335
|
+
label: "toggle",
|
|
1336
|
+
description: "Flip whichever way it's currently set.",
|
|
1337
|
+
},
|
|
1126
1338
|
{ fullValue: "background status", label: "status", description: "Show the current setting." },
|
|
1127
1339
|
];
|
|
1128
1340
|
}
|
|
@@ -1140,11 +1352,16 @@ Usage rules:
|
|
|
1140
1352
|
const [head, ...rest] = tokens;
|
|
1141
1353
|
const subArgs = rest.join(" ");
|
|
1142
1354
|
switch (head) {
|
|
1143
|
-
case "authorize":
|
|
1144
|
-
|
|
1145
|
-
case "
|
|
1146
|
-
|
|
1147
|
-
case "
|
|
1355
|
+
case "authorize":
|
|
1356
|
+
return authorizeHandler(ctx, subArgs);
|
|
1357
|
+
case "revoke":
|
|
1358
|
+
return revokeHandler(ctx);
|
|
1359
|
+
case "status":
|
|
1360
|
+
return statusHandler(ctx);
|
|
1361
|
+
case "doctor":
|
|
1362
|
+
return doctorHandler(ctx);
|
|
1363
|
+
case "onboard":
|
|
1364
|
+
return onboardHandler(ctx);
|
|
1148
1365
|
case "background":
|
|
1149
1366
|
return backgroundHandler(ctx, subArgs);
|
|
1150
1367
|
case "settings": {
|
|
@@ -1155,7 +1372,10 @@ Usage rules:
|
|
|
1155
1372
|
return;
|
|
1156
1373
|
}
|
|
1157
1374
|
default:
|
|
1158
|
-
ctx.ui.notify(
|
|
1375
|
+
ctx.ui.notify(
|
|
1376
|
+
`Unknown subcommand '${head}'. Try: /chrome authorize | revoke | status | doctor | onboard | background.`,
|
|
1377
|
+
"warning",
|
|
1378
|
+
);
|
|
1159
1379
|
}
|
|
1160
1380
|
},
|
|
1161
1381
|
});
|
|
@@ -1164,636 +1384,958 @@ Usage rules:
|
|
|
1164
1384
|
if (chromeToolsRegistered) return;
|
|
1165
1385
|
chromeToolsRegistered = true;
|
|
1166
1386
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
const forwarded = { ...params } as typeof params & { groupTitle?: string };
|
|
1223
|
-
// Default every Pi-opened/explicitly-grouped tab into this session's own group,
|
|
1224
|
-
// named after the session display name (falling back to the session id), unless
|
|
1225
|
-
// the caller specified a group title or opted out with group:false.
|
|
1226
|
-
if ((params.action === "new" || params.action === "group") && params.groupTitle === undefined && params.group !== false) {
|
|
1227
|
-
forwarded.groupTitle = sessionGroupTitle(ctx);
|
|
1228
|
-
}
|
|
1229
|
-
const result = await authorizedBridgeSend(`tab.${params.action}`, forwarded, DEFAULT_TIMEOUT_MS, signal);
|
|
1230
|
-
if (params.action === "list") {
|
|
1231
|
-
const tabs = result as Array<{ id: number; title: string; url: string; active: boolean; windowId: number; group?: { title?: string } | null }>;
|
|
1232
|
-
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.";
|
|
1233
|
-
return { content: [{ type: "text", text }], details: { tabs } };
|
|
1234
|
-
}
|
|
1235
|
-
return { content: [{ type: "text", text: safeJson(result) }], details: { result: result as Json } };
|
|
1236
|
-
},
|
|
1237
|
-
});
|
|
1387
|
+
pi.registerTool({
|
|
1388
|
+
name: "chrome_launch",
|
|
1389
|
+
label: "Chrome Bridge Setup",
|
|
1390
|
+
description:
|
|
1391
|
+
"Start/check the local bridge used by the companion Chrome extension. This does not launch a separate Chrome profile; install the unpacked Chrome extension in your existing Chrome profile to connect.",
|
|
1392
|
+
promptSnippet:
|
|
1393
|
+
"Show instructions for connecting Pi to the user's existing Chrome profile via the companion extension.",
|
|
1394
|
+
parameters: Type.Object({
|
|
1395
|
+
port: Type.Optional(
|
|
1396
|
+
Type.Number({ description: "Ignored. The bundled Chrome extension polls 127.0.0.1:17318." }),
|
|
1397
|
+
),
|
|
1398
|
+
url: Type.Optional(
|
|
1399
|
+
Type.String({
|
|
1400
|
+
description: "Optional URL to open in the existing Chrome profile after the extension is connected.",
|
|
1401
|
+
}),
|
|
1402
|
+
),
|
|
1403
|
+
userDataDir: Type.Optional(
|
|
1404
|
+
Type.String({
|
|
1405
|
+
description:
|
|
1406
|
+
"Ignored. This bridge intentionally uses the user's existing Chrome profile through the companion extension.",
|
|
1407
|
+
}),
|
|
1408
|
+
),
|
|
1409
|
+
useDefaultProfile: Type.Optional(
|
|
1410
|
+
Type.Boolean({
|
|
1411
|
+
description: "Ignored; existing-profile access comes from the companion Chrome extension.",
|
|
1412
|
+
}),
|
|
1413
|
+
),
|
|
1414
|
+
headless: Type.Optional(Type.Boolean({ description: "Ignored." })),
|
|
1415
|
+
}),
|
|
1416
|
+
async execute(_id, params, signal, _onUpdate, _ctx): Promise<ToolTextResult> {
|
|
1417
|
+
if (params.url && bridge.connected) {
|
|
1418
|
+
const result = await authorizedBridgeSend("tab.new", { url: params.url }, DEFAULT_TIMEOUT_MS, signal);
|
|
1419
|
+
return {
|
|
1420
|
+
content: [{ type: "text", text: `Chrome bridge connected; opened ${params.url}` }],
|
|
1421
|
+
details: { status: bridge.status(), result },
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
content: [
|
|
1426
|
+
{
|
|
1427
|
+
type: "text",
|
|
1428
|
+
text:
|
|
1429
|
+
`Chrome profile bridge is listening at ${bridge.url}.\n\n` +
|
|
1430
|
+
`To connect your existing Chrome profile:\n` +
|
|
1431
|
+
`1. Open chrome://extensions in the Chrome profile you normally use.\n` +
|
|
1432
|
+
`2. Enable Developer mode.\n` +
|
|
1433
|
+
`3. Click “Load unpacked”.\n` +
|
|
1434
|
+
`4. Select: ${browserExtensionPath()}\n\n` +
|
|
1435
|
+
`Status: ${bridge.connected ? "connected" : "waiting for extension"}.`,
|
|
1436
|
+
},
|
|
1437
|
+
],
|
|
1438
|
+
details: { status: bridge.status(), extensionPath: browserExtensionPath() },
|
|
1439
|
+
};
|
|
1440
|
+
},
|
|
1441
|
+
});
|
|
1238
1442
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
Type.
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1443
|
+
pi.registerTool({
|
|
1444
|
+
name: "chrome_tab",
|
|
1445
|
+
label: "Chrome Tab",
|
|
1446
|
+
description:
|
|
1447
|
+
"List, create, activate, close, group, ungroup, or inspect tabs in the user's existing Chrome profile via the companion extension.",
|
|
1448
|
+
promptSnippet: "List/open/activate/close/group existing Chrome tabs through the companion extension.",
|
|
1449
|
+
parameters: Type.Object({
|
|
1450
|
+
action: StringEnum(tabActionValues),
|
|
1451
|
+
url: Type.Optional(Type.String({ description: "URL for action=new." })),
|
|
1452
|
+
targetId: Type.Optional(Type.String({ description: "Chrome tab id for activate/close/group/ungroup." })),
|
|
1453
|
+
urlIncludes: Type.Optional(
|
|
1454
|
+
Type.String({ description: "Match the target tab by URL substring for activate/close/group/ungroup." }),
|
|
1455
|
+
),
|
|
1456
|
+
titleIncludes: Type.Optional(
|
|
1457
|
+
Type.String({
|
|
1458
|
+
description: "Match the target tab by title substring for activate/close/group/ungroup.",
|
|
1459
|
+
}),
|
|
1460
|
+
),
|
|
1461
|
+
group: Type.Optional(
|
|
1462
|
+
Type.Boolean({
|
|
1463
|
+
description:
|
|
1464
|
+
"action=new only: pass false to open an ungrouped tab. By default every Pi-opened tab joins this session's own tab group.",
|
|
1465
|
+
}),
|
|
1466
|
+
),
|
|
1467
|
+
groupTitle: Type.Optional(
|
|
1468
|
+
Type.String({
|
|
1469
|
+
description:
|
|
1470
|
+
"Tab group title for action=group/new. Defaults to this Pi session's group ('Pi Session: <name-or-id>'). Pass an empty string on action=new to opt out of grouping.",
|
|
1471
|
+
}),
|
|
1472
|
+
),
|
|
1473
|
+
groupColor: Type.Optional(
|
|
1474
|
+
Type.String({
|
|
1475
|
+
description:
|
|
1476
|
+
"Tab group color for action=group/new: grey, blue, red, yellow, green, pink, purple, cyan, or orange. Defaults to blue.",
|
|
1477
|
+
}),
|
|
1478
|
+
),
|
|
1479
|
+
host: Type.Optional(Type.String()),
|
|
1480
|
+
port: Type.Optional(Type.Number()),
|
|
1481
|
+
}),
|
|
1482
|
+
async execute(_id, params, signal, _onUpdate, ctx): Promise<ToolTextResult> {
|
|
1483
|
+
const forwarded = { ...params } as typeof params & { groupTitle?: string };
|
|
1484
|
+
// Default every Pi-opened/explicitly-grouped tab into this session's own group,
|
|
1485
|
+
// named after the session display name (falling back to the session id), unless
|
|
1486
|
+
// the caller specified a group title or opted out with group:false.
|
|
1487
|
+
if (
|
|
1488
|
+
(params.action === "new" || params.action === "group") &&
|
|
1489
|
+
params.groupTitle === undefined &&
|
|
1490
|
+
params.group !== false
|
|
1491
|
+
) {
|
|
1492
|
+
forwarded.groupTitle = sessionGroupTitle(ctx);
|
|
1493
|
+
}
|
|
1494
|
+
const result = await authorizedBridgeSend(`tab.${params.action}`, forwarded, DEFAULT_TIMEOUT_MS, signal);
|
|
1495
|
+
if (params.action === "list") {
|
|
1496
|
+
const tabs = result as Array<{
|
|
1497
|
+
id: number;
|
|
1498
|
+
title: string;
|
|
1499
|
+
url: string;
|
|
1500
|
+
active: boolean;
|
|
1501
|
+
windowId: number;
|
|
1502
|
+
group?: { title?: string } | null;
|
|
1503
|
+
}>;
|
|
1504
|
+
const text =
|
|
1505
|
+
tabs
|
|
1506
|
+
.map(
|
|
1507
|
+
(tab) =>
|
|
1508
|
+
`${tab.id}\t${tab.active ? "*" : " "}\t${tab.group?.title ? `[${tab.group.title}] ` : ""}${tab.title || "(untitled)"}\t${tab.url}`,
|
|
1509
|
+
)
|
|
1510
|
+
.join("\n") || "No tabs.";
|
|
1511
|
+
return { content: [{ type: "text", text }], details: { tabs } };
|
|
1512
|
+
}
|
|
1513
|
+
return { content: [{ type: "text", text: safeJson(result) }], details: { result: result as Json } };
|
|
1514
|
+
},
|
|
1515
|
+
});
|
|
1272
1516
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1517
|
+
pi.registerTool({
|
|
1518
|
+
name: "chrome_snapshot",
|
|
1519
|
+
label: "Chrome Snapshot",
|
|
1520
|
+
description:
|
|
1521
|
+
"Inspect a page in the user's existing Chrome profile. Default output is a concise, agent-friendly observation with structural layout/context, stable uids, visible actions, form fields, page hints, and changes since the previous snapshot. Use mode/query/nearUid to zoom instead of dumping the whole page. Runs in the background by default; pass background=false to bring Chrome to the foreground so the user can watch.",
|
|
1522
|
+
promptSnippet:
|
|
1523
|
+
"Observe the current Chrome page: concise summary, structural layout, visible actions, forms, page map, query matches, and stable uids.",
|
|
1524
|
+
parameters: Type.Object({
|
|
1525
|
+
targetId: Type.Optional(Type.String()),
|
|
1526
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
1527
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
1528
|
+
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS })),
|
|
1529
|
+
mode: Type.Optional(StringEnum(snapshotModeValues)),
|
|
1530
|
+
query: Type.Optional(
|
|
1531
|
+
Type.String({
|
|
1532
|
+
description:
|
|
1533
|
+
"Find/rank elements, regions, and text matching this phrase, e.g. 'merge button', 'email error', 'approve PR'.",
|
|
1534
|
+
}),
|
|
1535
|
+
),
|
|
1536
|
+
maxTextChars: Type.Optional(
|
|
1537
|
+
Type.Number({
|
|
1538
|
+
description:
|
|
1539
|
+
"Max body text chars included in the underlying snapshot. Defaults are smaller for concise modes.",
|
|
1540
|
+
}),
|
|
1541
|
+
),
|
|
1542
|
+
containingText: Type.Optional(
|
|
1543
|
+
Type.String({
|
|
1544
|
+
description:
|
|
1545
|
+
"Only return elements whose label/text contains this string (case-insensitive). Useful when the page has many controls.",
|
|
1546
|
+
}),
|
|
1547
|
+
),
|
|
1548
|
+
roleFilter: Type.Optional(
|
|
1549
|
+
Type.String({
|
|
1550
|
+
description:
|
|
1551
|
+
"Only return elements matching this ARIA role or tag name (case-insensitive). e.g. 'button', 'link', 'textbox'.",
|
|
1552
|
+
}),
|
|
1553
|
+
),
|
|
1554
|
+
nearUid: Type.Optional(
|
|
1555
|
+
Type.String({
|
|
1556
|
+
description:
|
|
1557
|
+
"Sort elements by proximity to this snapshot uid. Useful for finding controls near a known anchor.",
|
|
1558
|
+
}),
|
|
1559
|
+
),
|
|
1560
|
+
background: Type.Optional(
|
|
1561
|
+
Type.Boolean({
|
|
1562
|
+
description:
|
|
1563
|
+
"If true (the default), run silently in the background without focusing Chrome; pass false so Chrome focuses + the tab activates and the user can watch.",
|
|
1564
|
+
}),
|
|
1565
|
+
),
|
|
1566
|
+
host: Type.Optional(Type.String()),
|
|
1567
|
+
port: Type.Optional(Type.Number()),
|
|
1568
|
+
}),
|
|
1569
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1570
|
+
const snapshot = await authorizedBridgeSend(
|
|
1571
|
+
"page.snapshot",
|
|
1572
|
+
withBackground({ ...params, maxElements: params.maxElements ?? MAX_ELEMENTS }),
|
|
1573
|
+
DEFAULT_TIMEOUT_MS,
|
|
1574
|
+
signal,
|
|
1575
|
+
);
|
|
1576
|
+
return { content: [{ type: "text", text: formatChromeSnapshot(snapshot) }], details: { snapshot } };
|
|
1577
|
+
},
|
|
1578
|
+
});
|
|
1302
1579
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
Type.
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
// Compatibility fallback for a loaded Chrome extension service worker that has not
|
|
1330
|
-
// been reloaded since chrome_inspect was added. It is less rich than page.inspect,
|
|
1331
|
-
// but still gives useful nearby candidates instead of failing the workflow.
|
|
1580
|
+
pi.registerTool({
|
|
1581
|
+
name: "chrome_find",
|
|
1582
|
+
label: "Chrome Find",
|
|
1583
|
+
description:
|
|
1584
|
+
"Find elements, page regions, or text on the current Chrome page by query. Returns ranked matches with stable uids and coordinates. This is a focused wrapper around chrome_snapshot({ query }).",
|
|
1585
|
+
promptSnippet:
|
|
1586
|
+
"Find matching controls/text/regions in Chrome by natural-language query and return stable uids.",
|
|
1587
|
+
parameters: Type.Object({
|
|
1588
|
+
query: Type.String({
|
|
1589
|
+
description: "What to find, e.g. 'merge button', 'email error', 'approve PR', 'search box'.",
|
|
1590
|
+
}),
|
|
1591
|
+
mode: Type.Optional(StringEnum(snapshotModeValues)),
|
|
1592
|
+
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS })),
|
|
1593
|
+
targetId: Type.Optional(Type.String()),
|
|
1594
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
1595
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
1596
|
+
background: Type.Optional(
|
|
1597
|
+
Type.Boolean({
|
|
1598
|
+
description:
|
|
1599
|
+
"If true (the default), run silently in the background without focusing Chrome; pass false so Chrome focuses + the tab activates and the user can watch.",
|
|
1600
|
+
}),
|
|
1601
|
+
),
|
|
1602
|
+
host: Type.Optional(Type.String()),
|
|
1603
|
+
port: Type.Optional(Type.Number()),
|
|
1604
|
+
}),
|
|
1605
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1332
1606
|
const snapshot = await authorizedBridgeSend(
|
|
1333
1607
|
"page.snapshot",
|
|
1334
1608
|
withBackground({
|
|
1335
1609
|
...params,
|
|
1336
|
-
mode: "
|
|
1337
|
-
maxElements: MAX_ELEMENTS,
|
|
1338
|
-
nearUid: params.uid,
|
|
1339
|
-
query: params.selector,
|
|
1610
|
+
mode: params.mode || "auto",
|
|
1611
|
+
maxElements: params.maxElements ?? MAX_ELEMENTS,
|
|
1340
1612
|
}),
|
|
1341
1613
|
DEFAULT_TIMEOUT_MS,
|
|
1342
1614
|
signal,
|
|
1343
1615
|
);
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
},
|
|
1348
|
-
});
|
|
1616
|
+
return { content: [{ type: "text", text: formatChromeSnapshot(snapshot) }], details: { snapshot } };
|
|
1617
|
+
},
|
|
1618
|
+
});
|
|
1349
1619
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
Type.
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1620
|
+
pi.registerTool({
|
|
1621
|
+
name: "chrome_inspect",
|
|
1622
|
+
label: "Chrome Inspect Element",
|
|
1623
|
+
description:
|
|
1624
|
+
"Inspect one snapshot uid or selector deeply: nearby text, nearby actions, form context, ancestors, and suggested click target. Use after chrome_snapshot/chrome_find when you need context around one element.",
|
|
1625
|
+
promptSnippet: "Inspect a Chrome snapshot uid deeply for nearby text, form context, and suggested actions.",
|
|
1626
|
+
parameters: Type.Object({
|
|
1627
|
+
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot/chrome_find." })),
|
|
1628
|
+
selector: Type.Optional(Type.String({ description: "CSS selector if uid is unavailable." })),
|
|
1629
|
+
scrollIntoView: Type.Optional(
|
|
1630
|
+
Type.Boolean({
|
|
1631
|
+
description:
|
|
1632
|
+
"If true, scroll the target into view before inspecting. Default false to avoid changing page state.",
|
|
1633
|
+
}),
|
|
1634
|
+
),
|
|
1635
|
+
targetId: Type.Optional(Type.String()),
|
|
1636
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
1637
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
1638
|
+
background: Type.Optional(
|
|
1639
|
+
Type.Boolean({
|
|
1640
|
+
description:
|
|
1641
|
+
"If true (the default), run silently in the background without focusing Chrome; pass false so Chrome focuses + the tab activates and the user can watch.",
|
|
1642
|
+
}),
|
|
1643
|
+
),
|
|
1644
|
+
host: Type.Optional(Type.String()),
|
|
1645
|
+
port: Type.Optional(Type.Number()),
|
|
1646
|
+
}),
|
|
1647
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1648
|
+
try {
|
|
1649
|
+
const inspect = await authorizedBridgeSend(
|
|
1650
|
+
"page.inspect",
|
|
1651
|
+
withBackground(params),
|
|
1652
|
+
DEFAULT_TIMEOUT_MS,
|
|
1653
|
+
signal,
|
|
1654
|
+
);
|
|
1655
|
+
return { content: [{ type: "text", text: formatChromeInspect(inspect) }], details: { inspect } };
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1658
|
+
if (!/Unknown action: page\.inspect/i.test(message)) throw error;
|
|
1659
|
+
// Compatibility fallback for a loaded Chrome extension service worker that has not
|
|
1660
|
+
// been reloaded since chrome_inspect was added. It is less rich than page.inspect,
|
|
1661
|
+
// but still gives useful nearby candidates instead of failing the workflow.
|
|
1662
|
+
const snapshot = await authorizedBridgeSend(
|
|
1663
|
+
"page.snapshot",
|
|
1664
|
+
withBackground({
|
|
1665
|
+
...params,
|
|
1666
|
+
mode: "interactive",
|
|
1667
|
+
maxElements: MAX_ELEMENTS,
|
|
1668
|
+
nearUid: params.uid,
|
|
1669
|
+
query: params.selector,
|
|
1670
|
+
}),
|
|
1671
|
+
DEFAULT_TIMEOUT_MS,
|
|
1672
|
+
signal,
|
|
1673
|
+
);
|
|
1674
|
+
const text = `chrome_inspect fallback: loaded Chrome extension does not yet support page.inspect; reload it at chrome://extensions for deep inspect.\n\n${formatChromeSnapshot(snapshot)}`;
|
|
1675
|
+
return { content: [{ type: "text", text }], details: { snapshot, fallback: "page.snapshot" } };
|
|
1676
|
+
}
|
|
1677
|
+
},
|
|
1678
|
+
});
|
|
1375
1679
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1680
|
+
pi.registerTool({
|
|
1681
|
+
name: "chrome_navigate",
|
|
1682
|
+
label: "Chrome Navigate",
|
|
1683
|
+
description:
|
|
1684
|
+
"Navigate an existing Chrome tab to a URL via the companion extension. Runs in the background by default; pass background=false to focus Chrome and activate the tab so the user can watch. Optionally waits for load completion.",
|
|
1685
|
+
promptSnippet: "Navigate a Chrome tab in the user's existing profile.",
|
|
1686
|
+
parameters: Type.Object({
|
|
1687
|
+
url: Type.String(),
|
|
1688
|
+
targetId: Type.Optional(Type.String()),
|
|
1689
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
1690
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
1691
|
+
waitUntilLoad: Type.Optional(Type.Boolean({ default: true })),
|
|
1692
|
+
timeoutMs: Type.Optional(Type.Number({ default: 15_000 })),
|
|
1693
|
+
initScript: Type.Optional(
|
|
1694
|
+
Type.String({
|
|
1695
|
+
description:
|
|
1696
|
+
"Optional JavaScript source to run in MAIN world at document_start of the next navigation. Useful for seeding localStorage, stubbing Date.now(), or defining navigator.webdriver=undefined. Requires the companion extension's webNavigation permission.",
|
|
1697
|
+
}),
|
|
1698
|
+
),
|
|
1699
|
+
background: Type.Optional(
|
|
1700
|
+
Type.Boolean({
|
|
1701
|
+
description:
|
|
1702
|
+
"If true, navigate silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch.",
|
|
1703
|
+
}),
|
|
1704
|
+
),
|
|
1705
|
+
host: Type.Optional(Type.String()),
|
|
1706
|
+
port: Type.Optional(Type.Number()),
|
|
1707
|
+
}),
|
|
1708
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1709
|
+
const result = await authorizedBridgeSend(
|
|
1710
|
+
"page.navigate",
|
|
1711
|
+
withBackground(params),
|
|
1712
|
+
(params.timeoutMs ?? 15_000) + 2_000,
|
|
1713
|
+
signal,
|
|
1714
|
+
);
|
|
1715
|
+
return {
|
|
1716
|
+
content: [
|
|
1717
|
+
{ type: "text", text: `Navigated to ${params.url}${params.initScript ? " (with initScript)" : ""}` },
|
|
1718
|
+
],
|
|
1719
|
+
details: { result: result as Json },
|
|
1720
|
+
};
|
|
1721
|
+
},
|
|
1722
|
+
});
|
|
1404
1723
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
Type.
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1724
|
+
pi.registerTool({
|
|
1725
|
+
name: "chrome_evaluate",
|
|
1726
|
+
label: "Chrome Evaluate",
|
|
1727
|
+
description:
|
|
1728
|
+
"Evaluate JavaScript in an existing Chrome tab through the companion extension. Runs in the page context and returns JSON-serializable values when possible. Runs in the background by default; pass background=false to focus Chrome and activate the tab.",
|
|
1729
|
+
promptSnippet: "Evaluate JavaScript in the active Chrome tab through the companion extension.",
|
|
1730
|
+
parameters: Type.Object({
|
|
1731
|
+
expression: Type.String(),
|
|
1732
|
+
awaitPromise: Type.Optional(Type.Boolean({ default: true })),
|
|
1733
|
+
targetId: Type.Optional(Type.String()),
|
|
1734
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
1735
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
1736
|
+
background: Type.Optional(
|
|
1737
|
+
Type.Boolean({
|
|
1738
|
+
description:
|
|
1739
|
+
"If true, evaluate silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch.",
|
|
1740
|
+
}),
|
|
1741
|
+
),
|
|
1742
|
+
host: Type.Optional(Type.String()),
|
|
1743
|
+
port: Type.Optional(Type.Number()),
|
|
1744
|
+
}),
|
|
1745
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1746
|
+
const value = await authorizedBridgeSend(
|
|
1747
|
+
"page.evaluate",
|
|
1748
|
+
withBackground(params),
|
|
1749
|
+
DEFAULT_TIMEOUT_MS,
|
|
1750
|
+
signal,
|
|
1751
|
+
);
|
|
1752
|
+
const text =
|
|
1753
|
+
value === undefined ? "undefined" : typeof value === "string" ? value : (safeJson(value) ?? "undefined");
|
|
1754
|
+
return { content: [{ type: "text", text: truncateText(text) }], details: { value: value as Json } };
|
|
1755
|
+
},
|
|
1756
|
+
});
|
|
1437
1757
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
Type.
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1758
|
+
pi.registerTool({
|
|
1759
|
+
name: "chrome_click",
|
|
1760
|
+
label: "Chrome Click",
|
|
1761
|
+
description:
|
|
1762
|
+
"Click a snapshot uid, CSS selector, or viewport coordinate using Chrome's real input layer. Pass includeSnapshot=true to return a fresh snapshot after the click.",
|
|
1763
|
+
promptSnippet: "Click page elements in Chrome by snapshot uid, selector, or viewport coordinate.",
|
|
1764
|
+
parameters: Type.Object({
|
|
1765
|
+
uid: Type.Optional(
|
|
1766
|
+
Type.String({
|
|
1767
|
+
description:
|
|
1768
|
+
"Stable element uid from chrome_snapshot. Prefer uid over selector after taking a snapshot.",
|
|
1769
|
+
}),
|
|
1770
|
+
),
|
|
1771
|
+
selector: Type.Optional(
|
|
1772
|
+
Type.String({ description: "CSS selector to click. Prefer uid from chrome_snapshot when available." }),
|
|
1773
|
+
),
|
|
1774
|
+
x: Type.Optional(Type.Number({ description: "Viewport x coordinate if uid/selector is omitted." })),
|
|
1775
|
+
y: Type.Optional(Type.Number({ description: "Viewport y coordinate if uid/selector is omitted." })),
|
|
1776
|
+
domFallback: Type.Optional(
|
|
1777
|
+
Type.Boolean({
|
|
1778
|
+
description:
|
|
1779
|
+
"If true (default), fall back to DOM-dispatched click if Chrome's CDP input path is blocked by another extension overlay or debugger failure.",
|
|
1780
|
+
}),
|
|
1781
|
+
),
|
|
1782
|
+
includeSnapshot: Type.Optional(
|
|
1783
|
+
Type.Boolean({ description: "If true, include a fresh chrome_snapshot result after the click." }),
|
|
1784
|
+
),
|
|
1785
|
+
maxElements: Type.Optional(
|
|
1786
|
+
Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." }),
|
|
1787
|
+
),
|
|
1788
|
+
targetId: Type.Optional(Type.String()),
|
|
1789
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
1790
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
1791
|
+
background: Type.Optional(
|
|
1792
|
+
Type.Boolean({
|
|
1793
|
+
description:
|
|
1794
|
+
"If true, click silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch.",
|
|
1795
|
+
}),
|
|
1796
|
+
),
|
|
1797
|
+
host: Type.Optional(Type.String()),
|
|
1798
|
+
port: Type.Optional(Type.Number()),
|
|
1799
|
+
}),
|
|
1800
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1801
|
+
const raw = await authorizedBridgeSend("page.click", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1802
|
+
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
1803
|
+
const summary = summarizeActionResult(result);
|
|
1804
|
+
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
|
1805
|
+
const text = summary ? `Clicked ${target} — ${summary}` : `Clicked ${target}`;
|
|
1806
|
+
return {
|
|
1807
|
+
content: [{ type: "text", text: formatIncludedSnapshotText(raw, text) }],
|
|
1808
|
+
details: { result: raw as Json },
|
|
1809
|
+
};
|
|
1810
|
+
},
|
|
1811
|
+
});
|
|
1470
1812
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
Type.
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1813
|
+
pi.registerTool({
|
|
1814
|
+
name: "chrome_type",
|
|
1815
|
+
label: "Chrome Type",
|
|
1816
|
+
description:
|
|
1817
|
+
"Focus an optional snapshot uid or CSS selector, then type text using Chrome's real keyboard input. Pass includeSnapshot=true to return a fresh snapshot after typing.",
|
|
1818
|
+
promptSnippet: "Type text into Chrome, optionally focusing a snapshot uid or selector first.",
|
|
1819
|
+
parameters: Type.Object({
|
|
1820
|
+
text: Type.String(),
|
|
1821
|
+
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot." })),
|
|
1822
|
+
selector: Type.Optional(Type.String({ description: "CSS selector to focus before typing." })),
|
|
1823
|
+
includeSnapshot: Type.Optional(
|
|
1824
|
+
Type.Boolean({ description: "If true, include a fresh chrome_snapshot result after typing." }),
|
|
1825
|
+
),
|
|
1826
|
+
maxElements: Type.Optional(
|
|
1827
|
+
Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." }),
|
|
1828
|
+
),
|
|
1829
|
+
pressEnter: Type.Optional(Type.Boolean()),
|
|
1830
|
+
targetId: Type.Optional(Type.String()),
|
|
1831
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
1832
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
1833
|
+
background: Type.Optional(
|
|
1834
|
+
Type.Boolean({
|
|
1835
|
+
description:
|
|
1836
|
+
"If true, type silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch.",
|
|
1837
|
+
}),
|
|
1838
|
+
),
|
|
1839
|
+
host: Type.Optional(Type.String()),
|
|
1840
|
+
port: Type.Optional(Type.Number()),
|
|
1841
|
+
}),
|
|
1842
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1843
|
+
const raw = await authorizedBridgeSend("page.type", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1844
|
+
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
1845
|
+
const summary = summarizeActionResult(result);
|
|
1846
|
+
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
|
1847
|
+
const base = `Typed ${params.text.length} character(s)${into}.`;
|
|
1848
|
+
const text = summary ? `${base} (${summary})` : base;
|
|
1849
|
+
return {
|
|
1850
|
+
content: [{ type: "text", text: formatIncludedSnapshotText(raw, text) }],
|
|
1851
|
+
details: { result: raw as Json },
|
|
1852
|
+
};
|
|
1853
|
+
},
|
|
1854
|
+
});
|
|
1504
1855
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1856
|
+
pi.registerTool({
|
|
1857
|
+
name: "chrome_fill",
|
|
1858
|
+
label: "Chrome Fill",
|
|
1859
|
+
description:
|
|
1860
|
+
"Set the full value of a text input, textarea, or contenteditable element using Chrome click/select/delete/type input. Accepts a snapshot uid or CSS selector. Pass includeSnapshot=true to verify after filling.",
|
|
1861
|
+
promptSnippet: "Fill a Chrome form field by snapshot uid or selector, optionally returning a fresh snapshot.",
|
|
1862
|
+
parameters: Type.Object({
|
|
1863
|
+
text: Type.String(),
|
|
1864
|
+
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot." })),
|
|
1865
|
+
selector: Type.Optional(Type.String({ description: "CSS selector to fill if uid is omitted." })),
|
|
1866
|
+
submit: Type.Optional(Type.Boolean({ description: "If true, press Enter after filling." })),
|
|
1867
|
+
domFallback: Type.Optional(
|
|
1868
|
+
Type.Boolean({
|
|
1869
|
+
description:
|
|
1870
|
+
"If true (default), fall back to DOM value-setting if Chrome's CDP input path is blocked by another extension overlay or debugger failure.",
|
|
1871
|
+
}),
|
|
1872
|
+
),
|
|
1873
|
+
includeSnapshot: Type.Optional(
|
|
1874
|
+
Type.Boolean({ description: "If true, include a fresh chrome_snapshot result after filling." }),
|
|
1875
|
+
),
|
|
1876
|
+
maxElements: Type.Optional(
|
|
1877
|
+
Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." }),
|
|
1878
|
+
),
|
|
1879
|
+
targetId: Type.Optional(Type.String()),
|
|
1880
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
1881
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
1882
|
+
background: Type.Optional(
|
|
1883
|
+
Type.Boolean({
|
|
1884
|
+
description:
|
|
1885
|
+
"If true, fill silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch.",
|
|
1886
|
+
}),
|
|
1887
|
+
),
|
|
1888
|
+
host: Type.Optional(Type.String()),
|
|
1889
|
+
port: Type.Optional(Type.Number()),
|
|
1890
|
+
}),
|
|
1891
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1892
|
+
const raw = await authorizedBridgeSend("page.fill", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1893
|
+
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
1894
|
+
const summary = summarizeActionResult(result);
|
|
1895
|
+
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
|
1896
|
+
const base = `Filled ${params.text.length} character(s)${into}.`;
|
|
1897
|
+
const text = summary ? `${base} (${summary})` : base;
|
|
1898
|
+
return {
|
|
1899
|
+
content: [{ type: "text", text: formatIncludedSnapshotText(raw, text) }],
|
|
1900
|
+
details: { result: raw as Json },
|
|
1901
|
+
};
|
|
1902
|
+
},
|
|
1903
|
+
});
|
|
1539
1904
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1905
|
+
pi.registerTool({
|
|
1906
|
+
name: "chrome_key",
|
|
1907
|
+
label: "Chrome Key",
|
|
1908
|
+
description:
|
|
1909
|
+
"Send a keyboard key to an existing Chrome tab (Enter, Escape, Tab, Backspace, Delete, ArrowUp/Down/Left/Right, or one character). Runs in the background by default; pass background=false to focus Chrome and activate the tab so the user can watch. Pass includeSnapshot=true to verify after the keypress.",
|
|
1910
|
+
promptSnippet: "Press keys in Chrome through the companion extension.",
|
|
1911
|
+
parameters: Type.Object({
|
|
1912
|
+
key: Type.String(),
|
|
1913
|
+
modifiers: Type.Optional(
|
|
1914
|
+
Type.Object(
|
|
1915
|
+
{
|
|
1916
|
+
shiftKey: Type.Optional(Type.Boolean()),
|
|
1917
|
+
ctrlKey: Type.Optional(Type.Boolean()),
|
|
1918
|
+
altKey: Type.Optional(Type.Boolean()),
|
|
1919
|
+
metaKey: Type.Optional(Type.Boolean()),
|
|
1920
|
+
},
|
|
1921
|
+
{ description: "Modifier keys to hold while pressing the key (chord)." },
|
|
1922
|
+
),
|
|
1923
|
+
),
|
|
1924
|
+
includeSnapshot: Type.Optional(
|
|
1925
|
+
Type.Boolean({ description: "If true, include a fresh chrome_snapshot result after the keypress." }),
|
|
1926
|
+
),
|
|
1927
|
+
maxElements: Type.Optional(
|
|
1928
|
+
Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." }),
|
|
1929
|
+
),
|
|
1930
|
+
targetId: Type.Optional(Type.String()),
|
|
1931
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
1932
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
1933
|
+
background: Type.Optional(
|
|
1934
|
+
Type.Boolean({
|
|
1935
|
+
description:
|
|
1936
|
+
"If true, send the key silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch.",
|
|
1937
|
+
}),
|
|
1938
|
+
),
|
|
1939
|
+
host: Type.Optional(Type.String()),
|
|
1940
|
+
port: Type.Optional(Type.Number()),
|
|
1941
|
+
}),
|
|
1942
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1943
|
+
const raw = await authorizedBridgeSend("page.key", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1944
|
+
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
1945
|
+
const summary = summarizeActionResult(result);
|
|
1946
|
+
const base = `Pressed ${params.key}.`;
|
|
1947
|
+
const text = summary ? `${base} (${summary})` : base;
|
|
1948
|
+
return {
|
|
1949
|
+
content: [{ type: "text", text: formatIncludedSnapshotText(raw, text) }],
|
|
1950
|
+
details: { result: raw as Json },
|
|
1951
|
+
};
|
|
1952
|
+
},
|
|
1953
|
+
});
|
|
1561
1954
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
"
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1955
|
+
pi.registerTool({
|
|
1956
|
+
name: "chrome_wait_for",
|
|
1957
|
+
label: "Chrome Wait For",
|
|
1958
|
+
description: "Poll an existing Chrome tab until a selector exists or a JavaScript expression returns truthy.",
|
|
1959
|
+
promptSnippet: "Wait for page state in Chrome before further automation.",
|
|
1960
|
+
parameters: Type.Object({
|
|
1961
|
+
kind: StringEnum(waitForValues),
|
|
1962
|
+
value: Type.String({
|
|
1963
|
+
description: "CSS selector when kind=selector; JavaScript expression when kind=expression.",
|
|
1964
|
+
}),
|
|
1965
|
+
timeoutMs: Type.Optional(Type.Number({ default: 10_000 })),
|
|
1966
|
+
intervalMs: Type.Optional(Type.Number({ default: 250 })),
|
|
1967
|
+
targetId: Type.Optional(Type.String()),
|
|
1968
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
1969
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
1970
|
+
host: Type.Optional(Type.String()),
|
|
1971
|
+
port: Type.Optional(Type.Number()),
|
|
1972
|
+
}),
|
|
1973
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1974
|
+
const result = await authorizedBridgeSend(
|
|
1975
|
+
"page.waitFor",
|
|
1976
|
+
params,
|
|
1977
|
+
(params.timeoutMs ?? 10_000) + 2_000,
|
|
1978
|
+
signal,
|
|
1979
|
+
);
|
|
1980
|
+
return {
|
|
1981
|
+
content: [{ type: "text", text: `Observed ${params.kind}: ${params.value}` }],
|
|
1982
|
+
details: { result: result as Json },
|
|
1983
|
+
};
|
|
1984
|
+
},
|
|
1985
|
+
});
|
|
1582
1986
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1987
|
+
pi.registerTool({
|
|
1988
|
+
name: "chrome_list_console_messages",
|
|
1989
|
+
label: "Chrome Console Messages",
|
|
1990
|
+
description:
|
|
1991
|
+
"List console messages captured in the page by the companion extension. Capture starts after any chrome_snapshot, chrome_evaluate, chrome_list_console_messages, or chrome_list_network_requests call installs page instrumentation.",
|
|
1992
|
+
promptSnippet: "List captured console messages from the active Chrome page.",
|
|
1993
|
+
parameters: Type.Object({
|
|
1994
|
+
clear: Type.Optional(Type.Boolean({ description: "Clear the captured console log after reading." })),
|
|
1995
|
+
targetId: Type.Optional(Type.String()),
|
|
1996
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
1997
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
1998
|
+
background: Type.Optional(
|
|
1999
|
+
Type.Boolean({
|
|
2000
|
+
description:
|
|
2001
|
+
"If true, run silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch.",
|
|
2002
|
+
}),
|
|
2003
|
+
),
|
|
2004
|
+
host: Type.Optional(Type.String()),
|
|
2005
|
+
port: Type.Optional(Type.Number()),
|
|
2006
|
+
}),
|
|
2007
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
2008
|
+
const result = await authorizedBridgeSend(
|
|
2009
|
+
"page.console.list",
|
|
2010
|
+
withBackground(params),
|
|
2011
|
+
DEFAULT_TIMEOUT_MS,
|
|
2012
|
+
signal,
|
|
2013
|
+
);
|
|
2014
|
+
return {
|
|
2015
|
+
content: [{ type: "text", text: truncateText(safeJson(result)) }],
|
|
2016
|
+
details: { result: result as Json },
|
|
2017
|
+
};
|
|
2018
|
+
},
|
|
2019
|
+
});
|
|
1604
2020
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
2021
|
+
pi.registerTool({
|
|
2022
|
+
name: "chrome_list_network_requests",
|
|
2023
|
+
label: "Chrome Network Requests",
|
|
2024
|
+
description:
|
|
2025
|
+
"List fetch/XMLHttpRequest activity captured in the page by the companion extension. Capture starts after instrumentation is installed by snapshot/evaluate/network/console tools; browser document/static asset requests are not captured. Use includePreservedRequests=true to keep requests from earlier same-tab navigations that were captured before navigation.",
|
|
2026
|
+
promptSnippet:
|
|
2027
|
+
"List captured XHR/fetch requests from the active Chrome page before doing DOM-heavy debugging.",
|
|
2028
|
+
parameters: Type.Object({
|
|
2029
|
+
includePreservedRequests: Type.Optional(
|
|
2030
|
+
Type.Boolean({
|
|
2031
|
+
description: "Include captured requests from earlier locations in the same tab/session.",
|
|
2032
|
+
}),
|
|
2033
|
+
),
|
|
2034
|
+
clear: Type.Optional(Type.Boolean({ description: "Clear the captured request log after reading." })),
|
|
2035
|
+
targetId: Type.Optional(Type.String()),
|
|
2036
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
2037
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
2038
|
+
background: Type.Optional(
|
|
2039
|
+
Type.Boolean({
|
|
2040
|
+
description:
|
|
2041
|
+
"If true, run silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch.",
|
|
2042
|
+
}),
|
|
2043
|
+
),
|
|
2044
|
+
host: Type.Optional(Type.String()),
|
|
2045
|
+
port: Type.Optional(Type.Number()),
|
|
2046
|
+
}),
|
|
2047
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
2048
|
+
const result = await authorizedBridgeSend(
|
|
2049
|
+
"page.network.list",
|
|
2050
|
+
withBackground(params),
|
|
2051
|
+
DEFAULT_TIMEOUT_MS,
|
|
2052
|
+
signal,
|
|
2053
|
+
);
|
|
2054
|
+
return {
|
|
2055
|
+
content: [{ type: "text", text: truncateText(safeJson(result)) }],
|
|
2056
|
+
details: { result: result as Json },
|
|
2057
|
+
};
|
|
2058
|
+
},
|
|
2059
|
+
});
|
|
1624
2060
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
2061
|
+
pi.registerTool({
|
|
2062
|
+
name: "chrome_get_network_request",
|
|
2063
|
+
label: "Chrome Network Request",
|
|
2064
|
+
description:
|
|
2065
|
+
"Retrieve one captured fetch/XMLHttpRequest entry, including response body when available, by requestId from chrome_list_network_requests.",
|
|
2066
|
+
promptSnippet: "Fetch captured request details and response body by requestId.",
|
|
2067
|
+
parameters: Type.Object({
|
|
2068
|
+
requestId: Type.String({ description: "Request id returned by chrome_list_network_requests." }),
|
|
2069
|
+
targetId: Type.Optional(Type.String()),
|
|
2070
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
2071
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
2072
|
+
background: Type.Optional(
|
|
2073
|
+
Type.Boolean({
|
|
2074
|
+
description:
|
|
2075
|
+
"If true, run silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch.",
|
|
2076
|
+
}),
|
|
2077
|
+
),
|
|
2078
|
+
host: Type.Optional(Type.String()),
|
|
2079
|
+
port: Type.Optional(Type.Number()),
|
|
2080
|
+
}),
|
|
2081
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
2082
|
+
const result = await authorizedBridgeSend(
|
|
2083
|
+
"page.network.get",
|
|
2084
|
+
withBackground(params),
|
|
2085
|
+
DEFAULT_TIMEOUT_MS,
|
|
2086
|
+
signal,
|
|
2087
|
+
);
|
|
2088
|
+
return {
|
|
2089
|
+
content: [{ type: "text", text: truncateText(safeJson(result)) }],
|
|
2090
|
+
details: { result: result as Json },
|
|
2091
|
+
};
|
|
2092
|
+
},
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
pi.registerTool({
|
|
2096
|
+
name: "chrome_screenshot",
|
|
2097
|
+
label: "Chrome Screenshot",
|
|
2098
|
+
description:
|
|
2099
|
+
"Capture a screenshot of an existing Chrome tab via the companion extension and save it to disk. Chrome's extension screenshot API requires the target tab to be the active tab in its window. Runs in the background by default (the tab is briefly activated within its window for the capture, then the previous active tab is restored); pass background=false to focus Chrome so the user can watch.",
|
|
2100
|
+
promptSnippet: "Capture Chrome screenshots and save them under .pi/chrome-screenshots by default.",
|
|
2101
|
+
parameters: Type.Object({
|
|
2102
|
+
path: Type.Optional(
|
|
2103
|
+
Type.String({ description: "Output path. Defaults to .pi/chrome-screenshots/<timestamp>.<format>." }),
|
|
2104
|
+
),
|
|
2105
|
+
format: Type.Optional(StringEnum(imageFormatValues)),
|
|
2106
|
+
quality: Type.Optional(Type.Number({ description: "JPEG quality 0-100." })),
|
|
2107
|
+
fullPage: Type.Optional(
|
|
2108
|
+
Type.Boolean({
|
|
2109
|
+
description: "Not supported by the extension bridge yet; viewport screenshots are captured.",
|
|
2110
|
+
}),
|
|
2111
|
+
),
|
|
2112
|
+
targetId: Type.Optional(Type.String()),
|
|
2113
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
2114
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
2115
|
+
background: Type.Optional(
|
|
2116
|
+
Type.Boolean({
|
|
2117
|
+
description:
|
|
2118
|
+
"If true (the default), capture silently without focusing the Chrome window (the target tab is briefly activated within its window for the capture, then restored); pass false to focus Chrome.",
|
|
2119
|
+
}),
|
|
2120
|
+
),
|
|
2121
|
+
host: Type.Optional(Type.String()),
|
|
2122
|
+
port: Type.Optional(Type.Number()),
|
|
2123
|
+
}),
|
|
2124
|
+
async execute(_id, params, signal, _onUpdate, ctx: ExtensionContext): Promise<ToolTextResult> {
|
|
2125
|
+
const format = params.format ?? "png";
|
|
2126
|
+
const cwd = workspaceCwd(ctx);
|
|
2127
|
+
const defaultPath = join(
|
|
2128
|
+
cwd,
|
|
2129
|
+
".pi",
|
|
2130
|
+
"chrome-screenshots",
|
|
2131
|
+
`${new Date().toISOString().replace(/[:.]/g, "-")}.${format}`,
|
|
2132
|
+
);
|
|
2133
|
+
const outputPath = params.path ? resolve(cwd, params.path) : defaultPath;
|
|
2134
|
+
const result = (await authorizedBridgeSend(
|
|
2135
|
+
"page.screenshot",
|
|
2136
|
+
withBackground(params),
|
|
2137
|
+
params.fullPage ? 120_000 : DEFAULT_TIMEOUT_MS,
|
|
2138
|
+
signal,
|
|
2139
|
+
)) as {
|
|
2140
|
+
dataUrl?: string;
|
|
2141
|
+
tab?: unknown;
|
|
2142
|
+
fullPage?: boolean;
|
|
2143
|
+
dimensions?: { width: number; height: number; viewportHeight: number; dpr: number };
|
|
2144
|
+
tiles?: Array<{ y: number; dataUrl: string }>;
|
|
2145
|
+
};
|
|
2146
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
2147
|
+
if (result.fullPage && result.tiles && result.dimensions) {
|
|
2148
|
+
// Stitch via PNG if format is png; otherwise we fall back to writing tile files and a
|
|
2149
|
+
// manifest. We avoid pulling in an image library by writing each tile next to the main
|
|
2150
|
+
// path with a -tileN suffix and a stitched.json manifest.
|
|
2151
|
+
const { width, height, viewportHeight, dpr } = result.dimensions;
|
|
2152
|
+
const manifest: Array<{ path: string; y: number }> = [];
|
|
2153
|
+
for (let i = 0; i < result.tiles.length; i++) {
|
|
2154
|
+
const tile = result.tiles[i];
|
|
2155
|
+
const tilePath = outputPath.replace(/(\.[^.]+)$/, `-tile${i}$1`);
|
|
2156
|
+
const base64 = tile.dataUrl.replace(/^data:image\/(?:png|jpeg);base64,/, "");
|
|
2157
|
+
await writeFile(tilePath, Buffer.from(base64, "base64"));
|
|
2158
|
+
manifest.push({ path: tilePath, y: tile.y });
|
|
2159
|
+
}
|
|
2160
|
+
await writeFile(
|
|
2161
|
+
`${outputPath}.json`,
|
|
2162
|
+
JSON.stringify({ width, height, viewportHeight, dpr, tiles: manifest }, null, 2),
|
|
2163
|
+
);
|
|
2164
|
+
return {
|
|
2165
|
+
content: [
|
|
2166
|
+
{
|
|
2167
|
+
type: "text",
|
|
2168
|
+
text: `Saved ${result.tiles.length} full-page tile(s) for ${width}×${height}px page. Manifest: ${outputPath}.json`,
|
|
2169
|
+
},
|
|
2170
|
+
],
|
|
2171
|
+
details: {
|
|
2172
|
+
manifest: `${outputPath}.json`,
|
|
2173
|
+
tiles: manifest,
|
|
2174
|
+
dimensions: result.dimensions,
|
|
2175
|
+
tab: result.tab,
|
|
2176
|
+
} as unknown as Record<string, unknown>,
|
|
2177
|
+
};
|
|
1670
2178
|
}
|
|
1671
|
-
|
|
2179
|
+
if (!result.dataUrl) throw new Error("Screenshot returned no dataUrl");
|
|
2180
|
+
const base64 = result.dataUrl.replace(/^data:image\/(?:png|jpeg);base64,/, "");
|
|
2181
|
+
await writeFile(outputPath, Buffer.from(base64, "base64"));
|
|
1672
2182
|
return {
|
|
1673
|
-
content: [{ type: "text", text: `Saved
|
|
1674
|
-
details: {
|
|
2183
|
+
content: [{ type: "text", text: `Saved Chrome screenshot to ${outputPath}` }],
|
|
2184
|
+
details: { path: outputPath, format, tab: result.tab },
|
|
1675
2185
|
};
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
const base64 = result.dataUrl.replace(/^data:image\/(?:png|jpeg);base64,/, "");
|
|
1679
|
-
await writeFile(outputPath, Buffer.from(base64, "base64"));
|
|
1680
|
-
return { content: [{ type: "text", text: `Saved Chrome screenshot to ${outputPath}` }], details: { path: outputPath, format, tab: result.tab } };
|
|
1681
|
-
},
|
|
1682
|
-
});
|
|
2186
|
+
},
|
|
2187
|
+
});
|
|
1683
2188
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2189
|
+
pi.registerTool({
|
|
2190
|
+
name: "chrome_hover",
|
|
2191
|
+
label: "Chrome Hover",
|
|
2192
|
+
description: "Hover over an element by uid, selector, or x/y using Chrome pointer movement.",
|
|
2193
|
+
promptSnippet: "Hover a Chrome element to trigger :hover / mouseover handlers.",
|
|
2194
|
+
parameters: Type.Object({
|
|
2195
|
+
uid: Type.Optional(Type.String()),
|
|
2196
|
+
selector: Type.Optional(Type.String()),
|
|
2197
|
+
x: Type.Optional(Type.Number()),
|
|
2198
|
+
y: Type.Optional(Type.Number()),
|
|
2199
|
+
targetId: Type.Optional(Type.String()),
|
|
2200
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
2201
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
2202
|
+
background: Type.Optional(Type.Boolean()),
|
|
2203
|
+
}),
|
|
2204
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
2205
|
+
const result = await authorizedBridgeSend("page.hover", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
2206
|
+
return {
|
|
2207
|
+
content: [
|
|
2208
|
+
{ type: "text", text: `Hovered ${params.uid ?? params.selector ?? `${params.x},${params.y}`}` },
|
|
2209
|
+
],
|
|
2210
|
+
details: { result: result as Json },
|
|
2211
|
+
};
|
|
2212
|
+
},
|
|
2213
|
+
});
|
|
1704
2214
|
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
2215
|
+
pi.registerTool({
|
|
2216
|
+
name: "chrome_drag",
|
|
2217
|
+
label: "Chrome Drag",
|
|
2218
|
+
description: "Drag from one uid/selector/point to another using Chrome pointer input.",
|
|
2219
|
+
promptSnippet: "Drag a Chrome element from one point to another.",
|
|
2220
|
+
parameters: Type.Object({
|
|
2221
|
+
fromUid: Type.Optional(Type.String()),
|
|
2222
|
+
fromSelector: Type.Optional(Type.String()),
|
|
2223
|
+
fromX: Type.Optional(Type.Number()),
|
|
2224
|
+
fromY: Type.Optional(Type.Number()),
|
|
2225
|
+
toUid: Type.Optional(Type.String()),
|
|
2226
|
+
toSelector: Type.Optional(Type.String()),
|
|
2227
|
+
toX: Type.Optional(Type.Number()),
|
|
2228
|
+
toY: Type.Optional(Type.Number()),
|
|
2229
|
+
steps: Type.Optional(Type.Number({ default: 12 })),
|
|
2230
|
+
targetId: Type.Optional(Type.String()),
|
|
2231
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
2232
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
2233
|
+
background: Type.Optional(Type.Boolean()),
|
|
2234
|
+
}),
|
|
2235
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
2236
|
+
const result = await authorizedBridgeSend("page.drag", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
2237
|
+
return {
|
|
2238
|
+
content: [
|
|
2239
|
+
{
|
|
2240
|
+
type: "text",
|
|
2241
|
+
text: `Dragged from ${params.fromUid ?? params.fromSelector} to ${params.toUid ?? params.toSelector}`,
|
|
2242
|
+
},
|
|
2243
|
+
],
|
|
2244
|
+
details: { result: result as Json },
|
|
2245
|
+
};
|
|
2246
|
+
},
|
|
2247
|
+
});
|
|
1730
2248
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
2249
|
+
pi.registerTool({
|
|
2250
|
+
name: "chrome_tap",
|
|
2251
|
+
label: "Chrome Tap (Touch)",
|
|
2252
|
+
description:
|
|
2253
|
+
"Dispatch a real touchstart/touchend tap through Chrome's input layer. Use for sites that gate on TouchEvent rather than MouseEvent (mobile-first PWAs, swipe carousels). Chrome may show its debugging banner while attached.",
|
|
2254
|
+
promptSnippet: "Tap (real touch) a Chrome element by snapshot uid, selector, or coordinate.",
|
|
2255
|
+
parameters: Type.Object({
|
|
2256
|
+
uid: Type.Optional(Type.String()),
|
|
2257
|
+
selector: Type.Optional(Type.String()),
|
|
2258
|
+
x: Type.Optional(Type.Number()),
|
|
2259
|
+
y: Type.Optional(Type.Number()),
|
|
2260
|
+
targetId: Type.Optional(Type.String()),
|
|
2261
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
2262
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
2263
|
+
background: Type.Optional(Type.Boolean()),
|
|
2264
|
+
}),
|
|
2265
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
2266
|
+
const result = await authorizedBridgeSend("page.tap", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
2267
|
+
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
|
2268
|
+
return {
|
|
2269
|
+
content: [{ type: "text", text: `Tapped ${target} (touch)` }],
|
|
2270
|
+
details: { result: result as Json },
|
|
2271
|
+
};
|
|
2272
|
+
},
|
|
2273
|
+
});
|
|
1753
2274
|
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2275
|
+
pi.registerTool({
|
|
2276
|
+
name: "chrome_scroll",
|
|
2277
|
+
label: "Chrome Scroll",
|
|
2278
|
+
description:
|
|
2279
|
+
"Scroll the page or a specific scrollable element by dispatching real wheel events with momentum-shaped deltas, then applying the scroll. Positive deltaY scrolls down. Pass uid/selector to scroll within a container, otherwise the document scrolls.",
|
|
2280
|
+
promptSnippet: "Scroll a Chrome page or container via wheel events (not raw scrollTop).",
|
|
2281
|
+
parameters: Type.Object({
|
|
2282
|
+
uid: Type.Optional(Type.String()),
|
|
2283
|
+
selector: Type.Optional(Type.String()),
|
|
2284
|
+
deltaY: Type.Optional(Type.Number({ description: "Pixels to scroll vertically. Positive = down." })),
|
|
2285
|
+
deltaX: Type.Optional(Type.Number({ description: "Pixels to scroll horizontally. Positive = right." })),
|
|
2286
|
+
steps: Type.Optional(
|
|
2287
|
+
Type.Number({ description: "Number of wheel events to dispatch. Defaults to ceil(|deltaY|/100)." }),
|
|
2288
|
+
),
|
|
2289
|
+
targetId: Type.Optional(Type.String()),
|
|
2290
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
2291
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
2292
|
+
background: Type.Optional(Type.Boolean()),
|
|
2293
|
+
}),
|
|
2294
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
2295
|
+
const result = await authorizedBridgeSend(
|
|
2296
|
+
"page.scroll",
|
|
2297
|
+
withBackground(params),
|
|
2298
|
+
DEFAULT_TIMEOUT_MS,
|
|
2299
|
+
signal,
|
|
2300
|
+
);
|
|
2301
|
+
return {
|
|
2302
|
+
content: [{ type: "text", text: `Scrolled dy=${params.deltaY ?? 0} dx=${params.deltaX ?? 0}` }],
|
|
2303
|
+
details: { result: result as Json },
|
|
2304
|
+
};
|
|
2305
|
+
},
|
|
2306
|
+
});
|
|
1775
2307
|
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2308
|
+
pi.registerTool({
|
|
2309
|
+
name: "chrome_upload_file",
|
|
2310
|
+
label: "Chrome Upload File",
|
|
2311
|
+
description:
|
|
2312
|
+
"Attach local files to an <input type=file> element using Chrome DevTools file-input control. Does NOT open the native file picker; works with React/Vue/Angular controlled inputs.",
|
|
2313
|
+
promptSnippet: "Attach local files to a Chrome <input type=file> without opening the native file picker.",
|
|
2314
|
+
parameters: Type.Object({
|
|
2315
|
+
uid: Type.Optional(Type.String()),
|
|
2316
|
+
selector: Type.Optional(Type.String()),
|
|
2317
|
+
paths: Type.Array(Type.String(), { description: "Local absolute file paths to upload." }),
|
|
2318
|
+
targetId: Type.Optional(Type.String()),
|
|
2319
|
+
urlIncludes: Type.Optional(Type.String()),
|
|
2320
|
+
titleIncludes: Type.Optional(Type.String()),
|
|
2321
|
+
background: Type.Optional(Type.Boolean()),
|
|
2322
|
+
}),
|
|
2323
|
+
async execute(_id, params, signal, _onUpdate, ctx): Promise<ToolTextResult> {
|
|
2324
|
+
const cwd = workspaceCwd(ctx);
|
|
2325
|
+
const paths = params.paths.map((p) => resolve(cwd, p));
|
|
2326
|
+
const result = await authorizedBridgeSend(
|
|
2327
|
+
"page.upload",
|
|
2328
|
+
withBackground({ ...params, paths }),
|
|
2329
|
+
DEFAULT_TIMEOUT_MS,
|
|
2330
|
+
signal,
|
|
2331
|
+
);
|
|
2332
|
+
return {
|
|
2333
|
+
content: [
|
|
2334
|
+
{ type: "text", text: `Uploaded ${paths.length} file(s) to ${params.uid ?? params.selector}` },
|
|
2335
|
+
],
|
|
2336
|
+
details: { result: result as Json },
|
|
2337
|
+
};
|
|
2338
|
+
},
|
|
2339
|
+
});
|
|
1797
2340
|
}
|
|
1798
|
-
|
|
1799
2341
|
}
|