pi-chrome 0.14.0 → 0.14.2
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Pi Chrome Connector",
|
|
4
|
-
"version": "0.14.
|
|
4
|
+
"version": "0.14.2",
|
|
5
5
|
"description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
|
|
6
6
|
"permissions": ["tabs", "scripting", "storage", "activeTab", "alarms", "webNavigation", "debugger"],
|
|
7
7
|
"host_permissions": ["<all_urls>", "http://127.0.0.1:17318/*"],
|
|
@@ -60,6 +60,13 @@ async function maybeUpgradeToTrusted(kind, params, syntheticResult, trustedFn) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Last few attach failures, kept for /chrome doctor + trusted.debug diagnostics.
|
|
64
|
+
const attachDebugLog = [];
|
|
65
|
+
function recordAttachEvent(entry) {
|
|
66
|
+
attachDebugLog.push({ ...entry, t: Date.now() });
|
|
67
|
+
if (attachDebugLog.length > 20) attachDebugLog.shift();
|
|
68
|
+
}
|
|
69
|
+
|
|
63
70
|
async function attachDebugger(tabId) {
|
|
64
71
|
if (!chrome.debugger) throw new Error("chrome.debugger API unavailable; reload the extension to grant the new permission");
|
|
65
72
|
if (attachedTabs.has(tabId)) {
|
|
@@ -67,28 +74,75 @@ async function attachDebugger(tabId) {
|
|
|
67
74
|
entry.detachAt = Date.now() + TRUSTED_IDLE_DETACH_MS;
|
|
68
75
|
return entry;
|
|
69
76
|
}
|
|
77
|
+
// Before each attach, force-detach any stale CDP target this extension owns on the tab.
|
|
78
|
+
// Chrome sometimes keeps a half-dead session around (extension reload mid-attach, etc.) and
|
|
79
|
+
// surfaces it as "Cannot access a chrome-extension://" on the next attach attempt.
|
|
70
80
|
try {
|
|
71
|
-
await chrome.debugger.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
const targets = await new Promise((resolve) => chrome.debugger.getTargets((t) => resolve(t || [])));
|
|
82
|
+
for (const tgt of targets) {
|
|
83
|
+
if (tgt.tabId === tabId && tgt.attached) {
|
|
84
|
+
recordAttachEvent({ kind: "stale-target-found", tabId, target: { id: tgt.id, type: tgt.type, url: tgt.url, extensionId: tgt.extensionId } });
|
|
85
|
+
try { await chrome.debugger.detach({ tabId }); } catch {}
|
|
86
|
+
await sleep(80);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
const attemptAttach = async () => {
|
|
92
|
+
try {
|
|
93
|
+
await chrome.debugger.attach({ tabId }, CDP_VERSION);
|
|
94
|
+
return null;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return error;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
let err = await attemptAttach();
|
|
100
|
+
if (err) {
|
|
101
|
+
const msg = String(err?.message || err);
|
|
77
102
|
const transient = /Cannot access a chrome-extension|Cannot access contents of|No tab with id|Debugger is not attached|Another debugger|Target closed/i.test(msg);
|
|
78
|
-
|
|
103
|
+
const tabSnapshot = await chrome.tabs.get(tabId).catch(() => null);
|
|
104
|
+
recordAttachEvent({ kind: "attach-failed", tabId, message: msg, tabUrl: tabSnapshot?.url, transient });
|
|
105
|
+
if (!transient) throw err;
|
|
106
|
+
if (!tabSnapshot || (tabSnapshot.url || "").startsWith("chrome://") || (tabSnapshot.url || "").startsWith("chrome-extension://")) {
|
|
107
|
+
throw new Error(`Chrome can't attach the debugger to this tab (${tabSnapshot?.url ?? "unknown"}). Open a normal http(s) tab and try again.`);
|
|
108
|
+
}
|
|
79
109
|
await sleep(180);
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
110
|
+
err = await attemptAttach();
|
|
111
|
+
if (err) {
|
|
112
|
+
recordAttachEvent({ kind: "attach-retry-failed", tabId, message: String(err.message || err), tabUrl: tabSnapshot?.url });
|
|
113
|
+
// One more try after a longer settle. Some Chrome builds need ~500ms after a navigation
|
|
114
|
+
// for content-script registration on the tab to drain before chrome.debugger.attach
|
|
115
|
+
// will accept the target.
|
|
116
|
+
await sleep(500);
|
|
117
|
+
err = await attemptAttach();
|
|
118
|
+
if (err) {
|
|
119
|
+
recordAttachEvent({ kind: "attach-retry2-failed", tabId, message: String(err.message || err), tabUrl: tabSnapshot?.url });
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
83
122
|
}
|
|
84
|
-
await chrome.debugger.attach({ tabId }, CDP_VERSION);
|
|
85
123
|
}
|
|
124
|
+
recordAttachEvent({ kind: "attached", tabId });
|
|
86
125
|
// Seed pointer in a plausible "just left the address bar" location.
|
|
87
126
|
const entry = { detachAt: Date.now() + TRUSTED_IDLE_DETACH_MS, pointer: { x: 120 + Math.random() * 200, y: 80 + Math.random() * 120 } };
|
|
88
127
|
attachedTabs.set(tabId, entry);
|
|
89
128
|
return entry;
|
|
90
129
|
}
|
|
91
130
|
|
|
131
|
+
async function trustedDebug(params) {
|
|
132
|
+
const tab = params?.targetId ? await chrome.tabs.get(Number(params.targetId)).catch(() => null) : null;
|
|
133
|
+
let targets = [];
|
|
134
|
+
try { targets = await new Promise((resolve) => chrome.debugger.getTargets((t) => resolve(t || []))); } catch {}
|
|
135
|
+
return {
|
|
136
|
+
extensionVersion: chrome.runtime.getManifest().version,
|
|
137
|
+
extensionId: chrome.runtime.id,
|
|
138
|
+
trustedMode: TRUSTED_MODE,
|
|
139
|
+
attachedTabs: Array.from(attachedTabs.keys()),
|
|
140
|
+
requestedTab: tab ? { id: tab.id, url: tab.url, status: tab.status, title: tab.title } : null,
|
|
141
|
+
cdpTargets: targets,
|
|
142
|
+
recentAttachEvents: attachDebugLog.slice(),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
92
146
|
async function detachDebugger(tabId) {
|
|
93
147
|
if (!attachedTabs.has(tabId)) return;
|
|
94
148
|
attachedTabs.delete(tabId);
|
|
@@ -131,12 +185,72 @@ function cdpRaw(tabId, method, params) {
|
|
|
131
185
|
// chrome.debugger.attach can stay cached in attachedTabs even after Chrome killed
|
|
132
186
|
// the session (tab nav, devtools opened/closed, etc). Recover by detaching the
|
|
133
187
|
// stale entry and re-attaching, then retry the command once.
|
|
188
|
+
// Find foreign chrome-extension targets currently anchored to the tab. Password managers,
|
|
189
|
+
// autofill helpers, and other input-attached extensions create type:"other" CDP targets
|
|
190
|
+
// whose URL is chrome-extension://<otherId>/... When that target is in focus, CDP refuses
|
|
191
|
+
// our Input.dispatchMouseEvent calls with "Cannot access a chrome-extension:// URL of
|
|
192
|
+
// different extension" — surfacing a cryptic error to the user.
|
|
193
|
+
async function findForeignExtensionTargets() {
|
|
194
|
+
try {
|
|
195
|
+
const targets = await new Promise((resolve) => chrome.debugger.getTargets((t) => resolve(t || [])));
|
|
196
|
+
return targets.filter((t) => {
|
|
197
|
+
const url = String(t.url || "");
|
|
198
|
+
if (!url.startsWith("chrome-extension://")) return false;
|
|
199
|
+
if (t.extensionId === chrome.runtime.id) return false;
|
|
200
|
+
return true;
|
|
201
|
+
});
|
|
202
|
+
} catch {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function extractForeignExtId(targets) {
|
|
208
|
+
for (const t of targets) {
|
|
209
|
+
if (t.extensionId && t.extensionId !== chrome.runtime.id) return t.extensionId;
|
|
210
|
+
const m = String(t.url || "").match(/chrome-extension:\/\/([a-p]+)\//);
|
|
211
|
+
if (m && m[1] !== chrome.runtime.id) return m[1];
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function dismissOverlayViaEscape(tabId) {
|
|
217
|
+
// Esc routes through key dispatcher (target-by-focus), not by mouse coordinates, so it
|
|
218
|
+
// works even when a foreign chrome-extension popup is intercepting pointer events.
|
|
219
|
+
try {
|
|
220
|
+
await cdpRaw(tabId, "Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
|
|
221
|
+
await cdpRaw(tabId, "Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
|
|
222
|
+
await sleep(120);
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
225
|
+
|
|
134
226
|
async function cdp(tabId, method, params) {
|
|
135
227
|
try {
|
|
136
228
|
return await cdpRaw(tabId, method, params);
|
|
137
229
|
} catch (error) {
|
|
138
230
|
const msg = String(error?.message || error);
|
|
139
231
|
const isStale = /Debugger is not attached|Detached while|Target closed|No tab with id/i.test(msg);
|
|
232
|
+
const isForeignExtBlock = /Cannot access a chrome-extension:\/\/ URL of different extension/i.test(msg);
|
|
233
|
+
if (isForeignExtBlock && /Input\./.test(method)) {
|
|
234
|
+
// Foreign chrome-extension popup (autofill, password manager) is hijacking input.
|
|
235
|
+
// Try once: dismiss via Esc, then retry.
|
|
236
|
+
const before = await findForeignExtensionTargets();
|
|
237
|
+
recordAttachEvent({ kind: "foreign-ext-detected", tabId, method, foreignExtId: extractForeignExtId(before), targetCount: before.length });
|
|
238
|
+
await dismissOverlayViaEscape(tabId);
|
|
239
|
+
try {
|
|
240
|
+
return await cdpRaw(tabId, method, params);
|
|
241
|
+
} catch (retryErr) {
|
|
242
|
+
const retryMsg = String(retryErr?.message || retryErr);
|
|
243
|
+
if (/Cannot access a chrome-extension:\/\/ URL of different extension/i.test(retryMsg)) {
|
|
244
|
+
const after = await findForeignExtensionTargets();
|
|
245
|
+
const id = extractForeignExtId(after) || extractForeignExtId(before) || "unknown";
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Another Chrome extension (${id}) has an input overlay on this page (e.g. a password manager / autofill popup). \n` +
|
|
248
|
+
`pi-chrome tried to dismiss it with Escape but it reappeared. Disable that extension on this page, focus the field via Tab instead of clicking, or run /chrome quiet off so the agent uses synthetic input here.`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
throw retryErr;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
140
254
|
if (!isStale) throw error;
|
|
141
255
|
attachedTabs.delete(tabId);
|
|
142
256
|
await chrome.debugger.attach({ tabId }, CDP_VERSION).catch(() => undefined);
|
|
@@ -639,6 +753,8 @@ async function dispatch(action, params) {
|
|
|
639
753
|
return setTrustedMode(params.mode);
|
|
640
754
|
case "trusted.status":
|
|
641
755
|
return trustedStatus();
|
|
756
|
+
case "trusted.debug":
|
|
757
|
+
return trustedDebug(params);
|
|
642
758
|
case "page.console.list":
|
|
643
759
|
return executeInTab(params, listConsoleMessages, [params.clear === true]);
|
|
644
760
|
case "page.network.list":
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
4
3
|
import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
|
|
5
4
|
import { Type } from "typebox";
|
|
6
5
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
@@ -381,6 +380,10 @@ const tabActionValues = ["list", "new", "activate", "close", "version"] as const
|
|
|
381
380
|
const imageFormatValues = ["png", "jpeg"] as const;
|
|
382
381
|
const waitForValues = ["selector", "expression"] as const;
|
|
383
382
|
|
|
383
|
+
function StringEnum<T extends readonly [string, ...string[]]>(values: T) {
|
|
384
|
+
return Type.Union(values.map((value) => Type.Literal(value)) as [ReturnType<typeof Type.Literal>, ...ReturnType<typeof Type.Literal>[]]);
|
|
385
|
+
}
|
|
386
|
+
|
|
384
387
|
export default function (pi: ExtensionAPI): void {
|
|
385
388
|
const globalState = globalThis as typeof globalThis & {
|
|
386
389
|
[PI_CHROME_GLOBAL_KEY]?: { version: string; root: string };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-chrome",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "Drive your existing logged-in Chrome from Pi — no re-login, no throwaway profile, watch the agent work in real time (or toggle quiet background mode).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -24,8 +24,15 @@
|
|
|
24
24
|
]
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@earendil-works/pi-ai": "*",
|
|
28
27
|
"@earendil-works/pi-coding-agent": "*",
|
|
29
28
|
"typebox": "*"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"@earendil-works/pi-coding-agent": {
|
|
32
|
+
"optional": true
|
|
33
|
+
},
|
|
34
|
+
"typebox": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
}
|