@extension.dev/mcp 10.10.0 → 10.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +71 -0
- package/dist/module.js +410 -63
- package/dist/src/lib/extension-surfaces.d.ts +7 -0
- package/dist/src/lib/relay-eval.d.ts +12 -0
- package/dist/src/lib/types.d.ts +3 -0
- package/dist/src/tools/detect-browsers.d.ts +1 -0
- package/dist/src/tools/eval.d.ts +2 -1
- package/dist/src/tools/inspect-gecko.d.ts +2 -0
- package/dist/src/tools/stop.d.ts +5 -0
- package/package.json +4 -4
- package/server.json +2 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "extension-mcp",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "MCP tools for browser extension development: scaffold from 50+ templates, run the dev server with HMR, inspect the live DOM and logs, and publish store-ready builds for Chrome, Edge, and Firefox.",
|
|
13
|
-
"version": "10.10.
|
|
13
|
+
"version": "10.10.1",
|
|
14
14
|
"category": "development",
|
|
15
15
|
"author": {
|
|
16
16
|
"name": "Cezar Augusto"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "extension-mcp",
|
|
3
3
|
"description": "MCP tools for browser extension development: scaffold from 50+ templates, run the dev server with HMR, inspect the live DOM and logs, and publish store-ready builds for Chrome, Edge, and Firefox. Ships /extension, /extension-add, /extension-debug, and /extension-publish commands.",
|
|
4
|
-
"version": "10.10.
|
|
4
|
+
"version": "10.10.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cezar Augusto",
|
|
7
7
|
"email": "hello@extension.dev",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 10.10.1
|
|
4
|
+
|
|
5
|
+
Five Gecko findings from one agent session on Firefox and Waterfox
|
|
6
|
+
(BUGS_TO_FIX_MCP.md entries 5 to 9), each replayed against a live Firefox
|
|
7
|
+
Nightly before and after the fix where the shape allowed it, plus four more
|
|
8
|
+
that followed them the same week, and the engine release that carries the
|
|
9
|
+
root-cause fix for the first of them.
|
|
10
|
+
|
|
11
|
+
- `extension-create`, `extension-develop` and `extension-install` move from
|
|
12
|
+
4.1.29 to 4.1.30. That engine settles a promise before a surface relay
|
|
13
|
+
replies to an eval (the root cause behind the first entry below, filed
|
|
14
|
+
from this repo as extension.js 512), names a missing tab, a refused url,
|
|
15
|
+
a closed surface and a browser exit on every act verb instead of a
|
|
16
|
+
generic refusal, and lets `reload` resolve a surface's tab. The relay
|
|
17
|
+
wrapper below stays: a project that pins an older engine in its own
|
|
18
|
+
node_modules still drives that engine.
|
|
19
|
+
|
|
20
|
+
- `extension_eval` on a surface that answers through the in-bundle relay
|
|
21
|
+
(every Gecko session, and Chromium MV2) never hands the relay a promise.
|
|
22
|
+
The relay evaluates synchronously and passes the raw value to
|
|
23
|
+
`sendResponse`; a promise is not cloneable, Firefox reports that to the
|
|
24
|
+
sender as no reply, and the bridge then called the surface "not open"
|
|
25
|
+
while the page was still running the expression. The wrapper now settles
|
|
26
|
+
a thenable inside the page under a token, and the tool polls until it is
|
|
27
|
+
done: an async expression returns its value, a rejection answers `E_EVAL`
|
|
28
|
+
with the page's own message, and one that never settles answers
|
|
29
|
+
`E_WAIT_TIMEOUT` naming where the result will land. The engine's "open it
|
|
30
|
+
first: extension open newtab" hint is now spoken as the tool call it means.
|
|
31
|
+
- `extension_inspect` on Gecko reads a page inside the extension through
|
|
32
|
+
its surface relay: a url that matches a declared surface document (the
|
|
33
|
+
full `moz-extension://` address, the document path, or its file name)
|
|
34
|
+
routes to that context instead of the tab injection, which Firefox
|
|
35
|
+
refuses into extension pages with "Missing host permission for the tab".
|
|
36
|
+
An extension url that matches no declared surface says so and names the
|
|
37
|
+
ones the manifest declares.
|
|
38
|
+
- `extension_stop` reaps the browser the launcher recorded in `ready.json`
|
|
39
|
+
(`browserPid`, `launcherPid`, and any process holding `profilePath`), so
|
|
40
|
+
a Firefox started on a custom profile, or one whose argv never names the
|
|
41
|
+
project, no longer survives a `stopped` answer with `reaped: []`.
|
|
42
|
+
- `extension_doctor` no longer calls a Gecko session unhealthy over a
|
|
43
|
+
browser exit that the executor outlived: Firefox hands a fresh profile to
|
|
44
|
+
a relaunched process and the first one exits 0, which the engine's browser
|
|
45
|
+
leg reports as a failure while storage probes and evals keep answering.
|
|
46
|
+
That leg becomes a warning that says so, and any other failure still
|
|
47
|
+
counts.
|
|
48
|
+
- `extension_browsers` detect finds a managed binary as deep as the cache
|
|
49
|
+
writes it (a Firefox Nightly sits six levels down inside its app bundle),
|
|
50
|
+
so it no longer reports the system Firefox as available while
|
|
51
|
+
`extension_dev` launches the cached Nightly. When both exist, the entry
|
|
52
|
+
carries `systemBinaryPath` and a `devLaunches` line saying which one dev
|
|
53
|
+
starts and how to choose the other. `extension_wait` reports `browserPid`
|
|
54
|
+
and `profilePath` from the contract.
|
|
55
|
+
- `extension_open` with surface `sidebar` on Gecko no longer stops at the
|
|
56
|
+
engine's "sidePanel not available", a Chromium API named on a Firefox
|
|
57
|
+
engine. It asks the sidebar relay whether the `sidebar_action` panel is
|
|
58
|
+
open and reports `status: "already-open"` when it is (Firefox opens the
|
|
59
|
+
panel at install, so it usually is); when it is not, the document is
|
|
60
|
+
rendered as a tab with the gesture rule stated (Firefox opens the panel
|
|
61
|
+
only from the toolbar button or View > Sidebar, Bugzilla 1392624).
|
|
62
|
+
- `extension_eval` with context `page` and a `moz-extension://` url on
|
|
63
|
+
Gecko evaluates through the surface relay of the document that url names,
|
|
64
|
+
with a warning naming the context to pass next time, instead of "chrome.
|
|
65
|
+
scripting is not available ... use context background". An extension url
|
|
66
|
+
that matches no declared surface says which surfaces the manifest
|
|
67
|
+
declares. The url-to-surface mapping now lives in one place for inspect
|
|
68
|
+
and eval.
|
|
69
|
+
- `extension_dev` with `port: 0` no longer warns "Requested port 0 was not
|
|
70
|
+
available": 0 asks the engine for any free port, so the note now says
|
|
71
|
+
which port it picked, and the collision wording stays for a numbered port
|
|
72
|
+
the server could not bind.
|
|
73
|
+
|
|
3
74
|
## 10.10.0
|
|
4
75
|
|
|
5
76
|
Safari was the one engine this server treated as a dead end, while the
|
package/dist/module.js
CHANGED
|
@@ -192,6 +192,7 @@ __webpack_require__.d(start_namespaceObject, {
|
|
|
192
192
|
var stop_namespaceObject = {};
|
|
193
193
|
__webpack_require__.r(stop_namespaceObject);
|
|
194
194
|
__webpack_require__.d(stop_namespaceObject, {
|
|
195
|
+
contractProcessHints: ()=>contractProcessHints,
|
|
195
196
|
handler: ()=>stop_handler,
|
|
196
197
|
schema: ()=>stop_schema,
|
|
197
198
|
stopOne: ()=>stopOne
|
|
@@ -227,7 +228,7 @@ __webpack_require__.d(wait_namespaceObject, {
|
|
|
227
228
|
handler: ()=>wait_handler,
|
|
228
229
|
schema: ()=>wait_schema
|
|
229
230
|
});
|
|
230
|
-
var package_namespaceObject = JSON.parse('{"rE":"10.10.
|
|
231
|
+
var package_namespaceObject = JSON.parse('{"rE":"10.10.1","El":{"OP":"4.1.30"}}');
|
|
231
232
|
const ENVELOPE_SCHEMA = 1;
|
|
232
233
|
const collectWarnings = (warnings)=>{
|
|
233
234
|
if (!warnings) return [];
|
|
@@ -4104,8 +4105,31 @@ function processCommand(pid) {
|
|
|
4104
4105
|
return "";
|
|
4105
4106
|
}
|
|
4106
4107
|
}
|
|
4107
|
-
function
|
|
4108
|
+
function contractProcessHints(projectPath, browser) {
|
|
4109
|
+
try {
|
|
4110
|
+
const contract = JSON.parse(node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8"));
|
|
4111
|
+
const pids = [
|
|
4112
|
+
contract.browserPid,
|
|
4113
|
+
contract.launcherPid
|
|
4114
|
+
].filter((pid)=>"number" == typeof pid && Number.isInteger(pid) && pid > 0);
|
|
4115
|
+
return {
|
|
4116
|
+
..."string" == typeof contract.profilePath && contract.profilePath.trim() ? {
|
|
4117
|
+
profilePath: contract.profilePath
|
|
4118
|
+
} : {},
|
|
4119
|
+
pids
|
|
4120
|
+
};
|
|
4121
|
+
} catch {
|
|
4122
|
+
return {
|
|
4123
|
+
pids: []
|
|
4124
|
+
};
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
function sessionProcessPids(projectPath, hints = {
|
|
4128
|
+
pids: []
|
|
4129
|
+
}) {
|
|
4108
4130
|
const pids = new Set();
|
|
4131
|
+
if (hints.profilePath) for (const pid of pgrepPids(escapeRegex(hints.profilePath)))pids.add(pid);
|
|
4132
|
+
for (const pid of hints.pids)if (pid !== process.pid && isAlive(pid)) pids.add(pid);
|
|
4109
4133
|
for (const form of projectPathForms(projectPath)){
|
|
4110
4134
|
const escaped = escapeRegex(form);
|
|
4111
4135
|
const patterns = [
|
|
@@ -4118,8 +4142,10 @@ function sessionProcessPids(projectPath) {
|
|
|
4118
4142
|
...pids
|
|
4119
4143
|
].filter((pid)=>PLAUSIBLE_SESSION_BINARY.test(processCommand(pid)));
|
|
4120
4144
|
}
|
|
4121
|
-
function reapSessionProcesses(projectPath
|
|
4122
|
-
|
|
4145
|
+
function reapSessionProcesses(projectPath, hints = {
|
|
4146
|
+
pids: []
|
|
4147
|
+
}) {
|
|
4148
|
+
const pids = sessionProcessPids(projectPath, hints);
|
|
4123
4149
|
for (const pid of pids)try {
|
|
4124
4150
|
process.kill(pid, "SIGKILL");
|
|
4125
4151
|
} catch {}
|
|
@@ -4158,8 +4184,9 @@ function pidFromReadyContract(projectPath, browser) {
|
|
|
4158
4184
|
async function stopOne(projectPath, browser) {
|
|
4159
4185
|
const session = findSessionInfo(projectPath, browser);
|
|
4160
4186
|
const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
|
|
4187
|
+
const hints = contractProcessHints(projectPath, browser);
|
|
4161
4188
|
if (null == pid) {
|
|
4162
|
-
const reaped = reapSessionProcesses(projectPath);
|
|
4189
|
+
const reaped = reapSessionProcesses(projectPath, hints);
|
|
4163
4190
|
removeSessionMarker(projectPath, browser);
|
|
4164
4191
|
return {
|
|
4165
4192
|
projectPath,
|
|
@@ -4181,7 +4208,7 @@ async function stopOne(projectPath, browser) {
|
|
|
4181
4208
|
}
|
|
4182
4209
|
detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
|
|
4183
4210
|
} else detail = "Process was already gone; cleaned up session records.";
|
|
4184
|
-
const reaped = reapSessionProcesses(projectPath);
|
|
4211
|
+
const reaped = reapSessionProcesses(projectPath, hints);
|
|
4185
4212
|
removeSession(projectPath, browser);
|
|
4186
4213
|
removeSessionMarker(projectPath, browser);
|
|
4187
4214
|
try {
|
|
@@ -4189,7 +4216,7 @@ async function stopOne(projectPath, browser) {
|
|
|
4189
4216
|
force: true
|
|
4190
4217
|
});
|
|
4191
4218
|
} catch {}
|
|
4192
|
-
const survivors = sessionProcessPids(projectPath);
|
|
4219
|
+
const survivors = sessionProcessPids(projectPath, hints);
|
|
4193
4220
|
const stopped = !isAlive(pid) && 0 === survivors.length;
|
|
4194
4221
|
if (survivors.length) detail += ` Warning: ${survivors.length} browser process(es) still alive after reap (pids ${survivors.join(", ")}).`;
|
|
4195
4222
|
else if (reaped.length) detail += ` Reaped ${reaped.length} browser process(es).`;
|
|
@@ -4553,7 +4580,8 @@ async function dev_handler(args) {
|
|
|
4553
4580
|
} : {
|
|
4554
4581
|
requestedPort: args.port ?? 8080
|
|
4555
4582
|
};
|
|
4556
|
-
const
|
|
4583
|
+
const autoAssigned = 0 === args.port;
|
|
4584
|
+
const portNote = null !== boundPort ? void 0 !== args.port && args.port !== boundPort ? autoAssigned ? `Port 0 asked the engine for any free port and it picked ${boundPort} (read from the engine's ready.json contract, the same source extension_wait reports).` : `Requested port ${args.port} was not available; the dev server bound ${boundPort} (read from the engine's ready.json contract, the same source extension_wait reports).` : null : autoAssigned ? "Port 0 asked the engine for any free port, and it has not stamped its ready.json contract yet, so the port it picked is not known at response time. extension_wait reports it from that contract once it lands." : "The engine has not stamped its ready.json contract yet, so the bound port is not known at response time (a taken port makes the server bind the next free one). extension_wait reports the bound port from that contract once it lands; requestedPort above is only what was asked for.";
|
|
4557
4585
|
return envelope_envelope({
|
|
4558
4586
|
ok: true,
|
|
4559
4587
|
command: dev_schema.name,
|
|
@@ -4743,7 +4771,7 @@ async function start_handler(args) {
|
|
|
4743
4771
|
});
|
|
4744
4772
|
}
|
|
4745
4773
|
function toMcpSpeak(text) {
|
|
4746
|
-
return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
|
|
4774
|
+
return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/\bextension open (popup|options|sidebar|newtab|history|bookmarks)\b/g, 'extension_open with surface: "$1"').replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
|
|
4747
4775
|
}
|
|
4748
4776
|
const CONTROL_CHANNEL_DOWN_CODES = new Set([
|
|
4749
4777
|
"E_CONTROL_DENIED",
|
|
@@ -6606,6 +6634,10 @@ async function open_handler(args) {
|
|
|
6606
6634
|
const refusal = readGestureRefusal(raw);
|
|
6607
6635
|
if (refusal) return openSidebarThroughGesture(args.projectPath, browser, refusal);
|
|
6608
6636
|
}
|
|
6637
|
+
if ("sidebar" === args.surface && isGeckoFamily(browser)) {
|
|
6638
|
+
const refusal = readUnsupportedRefusal(raw);
|
|
6639
|
+
if (refusal) return openGeckoSidebar(args.projectPath, browser, refusal, args.timeout);
|
|
6640
|
+
}
|
|
6609
6641
|
return AS_TAB_SURFACES.includes(args.surface) ? confirmSurfaceTarget(args.projectPath, browser, args.surface, raw) : raw;
|
|
6610
6642
|
}
|
|
6611
6643
|
const E_USER_GESTURE_REQUIRED = "E_USER_GESTURE_REQUIRED";
|
|
@@ -6621,6 +6653,73 @@ function readGestureRefusal(raw) {
|
|
|
6621
6653
|
const message = String(parsed.error?.message ?? "");
|
|
6622
6654
|
return code === E_USER_GESTURE_REQUIRED || /user gesture/i.test(message) ? parsed : null;
|
|
6623
6655
|
}
|
|
6656
|
+
function readUnsupportedRefusal(raw) {
|
|
6657
|
+
let parsed;
|
|
6658
|
+
try {
|
|
6659
|
+
parsed = JSON.parse(raw);
|
|
6660
|
+
} catch {
|
|
6661
|
+
return null;
|
|
6662
|
+
}
|
|
6663
|
+
if (parsed?.ok !== false) return null;
|
|
6664
|
+
const code = "string" == typeof parsed.error?.code ? parsed.error.code : "";
|
|
6665
|
+
const message = String(parsed.error?.message ?? "");
|
|
6666
|
+
return "E_NOT_IMPLEMENTED" === code || /not available/i.test(message) ? parsed : null;
|
|
6667
|
+
}
|
|
6668
|
+
const GECKO_SIDEBAR_GESTURE = "Firefox opens a sidebar_action panel only from a user gesture (the toolbar button or View > Sidebar; Bugzilla 1392624), and the engine's open verb carries none, so it cannot open the panel";
|
|
6669
|
+
async function openGeckoSidebar(projectPath, browser, refusal, timeout) {
|
|
6670
|
+
const doc = surfaceDocument(projectPath, browser, "sidebar");
|
|
6671
|
+
if (!doc) return missingSurfaceError(projectPath, browser, "sidebar", "so there is no sidebar panel to open");
|
|
6672
|
+
const probe = await runActVerb([
|
|
6673
|
+
"inspect",
|
|
6674
|
+
projectPath,
|
|
6675
|
+
"--context",
|
|
6676
|
+
"sidebar",
|
|
6677
|
+
"--include",
|
|
6678
|
+
"summary",
|
|
6679
|
+
"--browser",
|
|
6680
|
+
browser,
|
|
6681
|
+
...null != timeout ? [
|
|
6682
|
+
"--timeout",
|
|
6683
|
+
String(timeout)
|
|
6684
|
+
] : []
|
|
6685
|
+
], projectPath, timeout, open_schema.name);
|
|
6686
|
+
let open = null;
|
|
6687
|
+
try {
|
|
6688
|
+
const parsed = JSON.parse(probe);
|
|
6689
|
+
if (parsed?.ok === true) open = parsed;
|
|
6690
|
+
} catch {}
|
|
6691
|
+
if (open) {
|
|
6692
|
+
const url = "string" == typeof open.value?.url ? open.value.url : "string" == typeof open.value?.meta?.url ? open.value.meta.url : void 0;
|
|
6693
|
+
return envelope_envelope({
|
|
6694
|
+
ok: true,
|
|
6695
|
+
command: open_schema.name,
|
|
6696
|
+
status: "already-open",
|
|
6697
|
+
value: {
|
|
6698
|
+
surface: "sidebar",
|
|
6699
|
+
document: doc,
|
|
6700
|
+
alreadyOpen: true,
|
|
6701
|
+
...url ? {
|
|
6702
|
+
url
|
|
6703
|
+
} : {}
|
|
6704
|
+
},
|
|
6705
|
+
hint: `The sidebar panel is open in the ${browser} window already: read it with extension_dom_snapshot context: 'sidebar' or run code in it with extension_eval context: 'sidebar'. ${GECKO_SIDEBAR_GESTURE}, and it did not need to.`
|
|
6706
|
+
});
|
|
6707
|
+
}
|
|
6708
|
+
const fallback = await openSurfaceAsTab(projectPath, browser, "sidebar");
|
|
6709
|
+
try {
|
|
6710
|
+
const parsedFallback = JSON.parse(fallback);
|
|
6711
|
+
if (parsedFallback?.ok) {
|
|
6712
|
+
addWarning(parsedFallback, `${GECKO_SIDEBAR_GESTURE}, and the panel is not open now, so the sidebar document was rendered as a tab instead. The DOM is the same document the panel would show; the panel hosting stays unverified. A person opens the real panel from the toolbar button or View > Sidebar.`);
|
|
6713
|
+
return actFrameJson(parsedFallback);
|
|
6714
|
+
}
|
|
6715
|
+
} catch {}
|
|
6716
|
+
refusal.error = {
|
|
6717
|
+
...refusal.error ?? {},
|
|
6718
|
+
message: `${GECKO_SIDEBAR_GESTURE}. Rendering the document ${doc} as a tab failed as well.`
|
|
6719
|
+
};
|
|
6720
|
+
refusal.hint = "Start the session with allowEval: true so the document can be opened by url through the bridge, or open the panel from the toolbar button in the dev browser and read it with extension_dom_snapshot context: 'sidebar'.";
|
|
6721
|
+
return actFrameJson(refusal);
|
|
6722
|
+
}
|
|
6624
6723
|
const SIDEBAR_GESTURE_WARNING = "Chrome opens the side panel only from a user gesture and the engine's open verb carries none, so the server opened the extension's own sidebar page in a tab, dispatched a synthetic click on it over CDP and called chrome.sidePanel.open from inside that click, then closed the tab. The panel that opened is the real one; the toolbar wiring (action.onClicked or sidePanel.setPanelBehavior) was not exercised.";
|
|
6625
6724
|
async function openSidebarThroughGesture(projectPath, browser, refusal) {
|
|
6626
6725
|
const doc = surfaceDocument(projectPath, browser, "sidebar");
|
|
@@ -8949,6 +9048,25 @@ function capabilityProbeChecks(parsed) {
|
|
|
8949
9048
|
return value?.checks;
|
|
8950
9049
|
}
|
|
8951
9050
|
const CONTROL_OFF_BY_CHOICE = /\bwas not started with --allow-control\b/i;
|
|
9051
|
+
function pidIsAlive(pid) {
|
|
9052
|
+
try {
|
|
9053
|
+
process.kill(pid, 0);
|
|
9054
|
+
return true;
|
|
9055
|
+
} catch {
|
|
9056
|
+
return false;
|
|
9057
|
+
}
|
|
9058
|
+
}
|
|
9059
|
+
function reconcileRelaunchedBrowser(checks, contract) {
|
|
9060
|
+
const exitedLeg = checks.find((leg)=>"browser" === leg.check && "fail" === leg.status && /\bexited\b/i.test(String(leg.detail ?? "")));
|
|
9061
|
+
if (!exitedLeg) return false;
|
|
9062
|
+
const executorAnswered = checks.some((leg)=>("executor" === leg.check || "control-channel" === leg.check) && "pass" === leg.status);
|
|
9063
|
+
const browserAlive = "number" == typeof contract?.browserPid && pidIsAlive(contract.browserPid);
|
|
9064
|
+
if (!executorAnswered && !browserAlive) return false;
|
|
9065
|
+
exitedLeg.status = "warn";
|
|
9066
|
+
exitedLeg.detail = `${exitedLeg.detail ?? "browser exited"}. ${browserAlive ? `The browser pid the launcher recorded (${contract?.browserPid}) is alive` : "The executor answered a probe after that exit"}, so the session is live and the recorded exit was an earlier process: Firefox hands a fresh profile to a relaunched process and the first one exits 0.`;
|
|
9067
|
+
exitedLeg.remediation = "Nothing to do. If a later call finds the session unreachable, extension_stop and extension_dev again.";
|
|
9068
|
+
return true;
|
|
9069
|
+
}
|
|
8952
9070
|
function controlOffByChoiceLeg(checks) {
|
|
8953
9071
|
return checks.find((leg)=>"control-channel" === leg.check && "fail" === leg.status && "string" == typeof leg.detail && CONTROL_OFF_BY_CHOICE.test(leg.detail)) ?? null;
|
|
8954
9072
|
}
|
|
@@ -8991,17 +9109,9 @@ async function doctor_handler(args) {
|
|
|
8991
9109
|
}
|
|
8992
9110
|
let healthy = 0 === code;
|
|
8993
9111
|
const contract = readContractForDiagnosis(projectPath, browser);
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
const detail = browserExited ? `The ${browser} browser for this session exited unexpectedly${null != contract.browserExitCode ? ` (exit code ${contract.browserExitCode})` : ""}; the extension may have been rejected or the browser crashed. The session cannot be driven.` : contract.errors && contract.errors.length ? contract.errors.join("; ") : contract.message || "The dev session recorded status: error in ready.json.";
|
|
8998
|
-
checks.push({
|
|
8999
|
-
check: "runtime-errors",
|
|
9000
|
-
status: "fail",
|
|
9001
|
-
detail: toMcpSpeak(detail),
|
|
9002
|
-
remediation: browserExited ? "Read extension_logs and the session log for the rejection cause, call extension_stop to clean up, then relaunch." : "The build or extension load failed. Fix the reported error, let the dev server recompile, then re-run doctor."
|
|
9003
|
-
});
|
|
9004
|
-
} else {
|
|
9112
|
+
const relaunched = reconcileRelaunchedBrowser(checks, contract);
|
|
9113
|
+
if (relaunched) healthy = !checks.some((leg)=>"fail" === leg.status);
|
|
9114
|
+
if (contract?.status !== "error" || relaunched) {
|
|
9005
9115
|
const errs = recentErrorLogs(projectPath, browser);
|
|
9006
9116
|
if (errs.length) {
|
|
9007
9117
|
healthy = false;
|
|
@@ -9012,6 +9122,16 @@ async function doctor_handler(args) {
|
|
|
9012
9122
|
remediation: "The extension is throwing at runtime. Inspect with extension_logs. A chrome.* API called without its permission is a common cause: extension_manifest_validate catches a permission MISSING FROM permissions[], but it does not model host-permission scope (e.g. webRequest with no matching host_permissions) or gesture requirements (e.g. activeTab without a user gesture), so a valid:true there does not rule those out."
|
|
9013
9123
|
});
|
|
9014
9124
|
}
|
|
9125
|
+
} else {
|
|
9126
|
+
healthy = false;
|
|
9127
|
+
const browserExited = "browser_exited" === contract.code || void 0 !== contract.browserExitCode;
|
|
9128
|
+
const detail = browserExited ? `The ${browser} browser for this session exited unexpectedly${null != contract.browserExitCode ? ` (exit code ${contract.browserExitCode})` : ""}; the extension may have been rejected or the browser crashed. The session cannot be driven.` : contract.errors && contract.errors.length ? contract.errors.join("; ") : contract.message || "The dev session recorded status: error in ready.json.";
|
|
9129
|
+
checks.push({
|
|
9130
|
+
check: "runtime-errors",
|
|
9131
|
+
status: "fail",
|
|
9132
|
+
detail: toMcpSpeak(detail),
|
|
9133
|
+
remediation: browserExited ? "Read extension_logs and the session log for the rejection cause, call extension_stop to clean up, then relaunch." : "The build or extension load failed. Fix the reported error, let the dev server recompile, then re-run doctor."
|
|
9134
|
+
});
|
|
9015
9135
|
}
|
|
9016
9136
|
const engineVersion = projectEngineVersion(projectPath);
|
|
9017
9137
|
if (engineVersion) {
|
|
@@ -11967,6 +12087,32 @@ const inspect_schema_schema = {
|
|
|
11967
12087
|
]
|
|
11968
12088
|
}
|
|
11969
12089
|
};
|
|
12090
|
+
const EXTENSION_PAGE_CONTEXTS = [
|
|
12091
|
+
"popup",
|
|
12092
|
+
"options",
|
|
12093
|
+
"sidebar",
|
|
12094
|
+
"newtab",
|
|
12095
|
+
"history",
|
|
12096
|
+
"bookmarks"
|
|
12097
|
+
];
|
|
12098
|
+
const EXTENSION_ORIGIN = /^(moz|chrome|safari-web)-extension:\/\//;
|
|
12099
|
+
function isExtensionUrl(url) {
|
|
12100
|
+
return "string" == typeof url && EXTENSION_ORIGIN.test(url);
|
|
12101
|
+
}
|
|
12102
|
+
function surfaceForExtensionUrl(projectPath, browser, url) {
|
|
12103
|
+
const bare = url.replace(EXTENSION_ORIGIN, "").replace(/^[^/]*\//, (m)=>EXTENSION_ORIGIN.test(url) ? "" : m).replace(/^\.?\//, "").replace(/[?#].*$/, "");
|
|
12104
|
+
if (!bare) return null;
|
|
12105
|
+
for (const context of EXTENSION_PAGE_CONTEXTS){
|
|
12106
|
+
const document = surfaceDocument(projectPath, browser, context);
|
|
12107
|
+
if (document) {
|
|
12108
|
+
if (bare === document || bare.endsWith(`/${document}`) || document.endsWith(`/${bare}`)) return {
|
|
12109
|
+
context,
|
|
12110
|
+
document
|
|
12111
|
+
};
|
|
12112
|
+
}
|
|
12113
|
+
}
|
|
12114
|
+
return null;
|
|
12115
|
+
}
|
|
11970
12116
|
const inspect_gecko_TOOL = inspect_schema_schema.name;
|
|
11971
12117
|
function buildBridgeInspectExpression(opts) {
|
|
11972
12118
|
const parts = [
|
|
@@ -12084,7 +12230,22 @@ async function collectGeckoConsole(args, browser, urlFilter, result, notes) {
|
|
|
12084
12230
|
}
|
|
12085
12231
|
async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
12086
12232
|
const notes = [];
|
|
12087
|
-
|
|
12233
|
+
const surface = args.url ? surfaceForExtensionUrl(args.projectPath, browser, args.url) : null;
|
|
12234
|
+
if (args.url && !surface && EXTENSION_ORIGIN.test(args.url)) {
|
|
12235
|
+
const declared = declaredSurfaces(args.projectPath, browser) ?? [];
|
|
12236
|
+
return envelope_envelope({
|
|
12237
|
+
ok: false,
|
|
12238
|
+
command: inspect_gecko_TOOL,
|
|
12239
|
+
status: "no-surface",
|
|
12240
|
+
error: {
|
|
12241
|
+
code: "E_NO_SURFACE_DOCUMENT",
|
|
12242
|
+
name: "NoSurfaceDocument",
|
|
12243
|
+
message: `${args.url} is a page inside the extension, and on ${browser} a page inside the extension is read through its own surface relay, but that path matches none of the surface documents the manifest declares${declared.length ? ` (${declared.join(", ")})` : ""}. Script injection cannot reach an extension page at all.`
|
|
12244
|
+
},
|
|
12245
|
+
hint: declared.length ? `Pass the declared document path (extension_open surface: "${declared[0]}" opens one), or read it with extension_dom_snapshot context: "${declared[0]}".` : "Declare the page as a surface in the manifest (action.default_popup, options_ui.page, sidebar_action.default_panel or chrome_url_overrides) and rebuild."
|
|
12246
|
+
});
|
|
12247
|
+
}
|
|
12248
|
+
if (args.url && !surface) {
|
|
12088
12249
|
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout, inspect_gecko_TOOL);
|
|
12089
12250
|
if ("error" in listed) return listed.error;
|
|
12090
12251
|
const already = listed.tabs.some((t)=>t.url.includes(args.url));
|
|
@@ -12111,8 +12272,8 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
12111
12272
|
expression,
|
|
12112
12273
|
args.projectPath,
|
|
12113
12274
|
"--context",
|
|
12114
|
-
"page",
|
|
12115
|
-
...args.url ? [
|
|
12275
|
+
surface ? surface.context : "page",
|
|
12276
|
+
...args.url && !surface ? [
|
|
12116
12277
|
"--url",
|
|
12117
12278
|
args.url
|
|
12118
12279
|
] : [],
|
|
@@ -12166,7 +12327,11 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
12166
12327
|
if (null === value) return raw;
|
|
12167
12328
|
const result = {
|
|
12168
12329
|
browser,
|
|
12169
|
-
transport: "bridge"
|
|
12330
|
+
transport: "bridge",
|
|
12331
|
+
...surface ? {
|
|
12332
|
+
surface: surface.context,
|
|
12333
|
+
document: surface.document
|
|
12334
|
+
} : {}
|
|
12170
12335
|
};
|
|
12171
12336
|
if (value.meta) {
|
|
12172
12337
|
result.target = {
|
|
@@ -12642,6 +12807,69 @@ async function listGeckoExtensions(projectPath, browser) {
|
|
|
12642
12807
|
});
|
|
12643
12808
|
}
|
|
12644
12809
|
}
|
|
12810
|
+
const RELAY_MARK = "__extensionDevRelay";
|
|
12811
|
+
function cloneHelper() {
|
|
12812
|
+
return `var clone = function (v) {
|
|
12813
|
+
if (v === undefined) return null;
|
|
12814
|
+
try {
|
|
12815
|
+
if (typeof structuredClone === "function") { structuredClone(v); return v; }
|
|
12816
|
+
return JSON.parse(JSON.stringify(v));
|
|
12817
|
+
} catch (e) { return String(v); }
|
|
12818
|
+
};`;
|
|
12819
|
+
}
|
|
12820
|
+
function relaySafeExpression(expression, token) {
|
|
12821
|
+
const src = JSON.stringify(expression);
|
|
12822
|
+
const key = JSON.stringify(token);
|
|
12823
|
+
return `(function () {
|
|
12824
|
+
${cloneHelper()}
|
|
12825
|
+
var value = (0, eval)(${src});
|
|
12826
|
+
if (value && typeof value.then === "function") {
|
|
12827
|
+
var store = globalThis.${RELAY_MARK} = globalThis.${RELAY_MARK} || {};
|
|
12828
|
+
store[${key}] = { done: false };
|
|
12829
|
+
Promise.resolve(value).then(function (settled) {
|
|
12830
|
+
store[${key}] = { done: true, ok: true, value: clone(settled) };
|
|
12831
|
+
}, function (error) {
|
|
12832
|
+
store[${key}] = { done: true, ok: false, name: (error && error.name) || "EvalError", message: (error && error.message) || String(error) };
|
|
12833
|
+
});
|
|
12834
|
+
return { ${RELAY_MARK}: 1, done: false, token: ${key} };
|
|
12835
|
+
}
|
|
12836
|
+
return { ${RELAY_MARK}: 1, done: true, ok: true, value: clone(value) };
|
|
12837
|
+
})()`;
|
|
12838
|
+
}
|
|
12839
|
+
function relayPollExpression(token) {
|
|
12840
|
+
const key = JSON.stringify(token);
|
|
12841
|
+
return `(function () {
|
|
12842
|
+
var store = globalThis.${RELAY_MARK};
|
|
12843
|
+
var entry = store && store[${key}];
|
|
12844
|
+
if (!entry) return { ${RELAY_MARK}: 1, done: true, ok: false, name: "RelayLost", message: "the page reloaded or navigated before the expression settled, so its result is gone" };
|
|
12845
|
+
if (!entry.done) return { ${RELAY_MARK}: 1, done: false, token: ${key} };
|
|
12846
|
+
delete store[${key}];
|
|
12847
|
+
return { ${RELAY_MARK}: 1, done: true, ok: entry.ok, value: entry.value, name: entry.name, message: entry.message };
|
|
12848
|
+
})()`;
|
|
12849
|
+
}
|
|
12850
|
+
function readRelayFrame(value) {
|
|
12851
|
+
if (!value || "object" != typeof value || Array.isArray(value)) return null;
|
|
12852
|
+
const frame = value;
|
|
12853
|
+
if (1 !== frame[RELAY_MARK] || "boolean" != typeof frame.done) return null;
|
|
12854
|
+
return {
|
|
12855
|
+
done: frame.done,
|
|
12856
|
+
..."boolean" == typeof frame.ok ? {
|
|
12857
|
+
ok: frame.ok
|
|
12858
|
+
} : {},
|
|
12859
|
+
..."value" in frame ? {
|
|
12860
|
+
value: frame.value
|
|
12861
|
+
} : {},
|
|
12862
|
+
..."string" == typeof frame.name ? {
|
|
12863
|
+
name: frame.name
|
|
12864
|
+
} : {},
|
|
12865
|
+
..."string" == typeof frame.message ? {
|
|
12866
|
+
message: frame.message
|
|
12867
|
+
} : {},
|
|
12868
|
+
..."string" == typeof frame.token ? {
|
|
12869
|
+
token: frame.token
|
|
12870
|
+
} : {}
|
|
12871
|
+
};
|
|
12872
|
+
}
|
|
12645
12873
|
const eval_schema = {
|
|
12646
12874
|
name: "extension_eval",
|
|
12647
12875
|
description: "Evaluate an expression in a running extension context. Start the session with allowEval:true (extension_dev), which writes a 0600 session token. Context defaults to 'background', except on a Chromium MV3 session (the default template) where it defaults to 'page', the active tab, because the MV3 service worker CSP blocks eval; pass context:'background' to target the worker anyway and get that explanation back. For content and page, pass `url` to pick the tab, or omit both `url` and `tab` for the active tab; a numeric `tab` only disambiguates. Extension surfaces (popup, options, sidebar, devtools) and override pages (newtab, history, bookmarks) need no tab id but must already be open: open one with extension_open first, because a closed one returns an explicit error. On a Chromium MV3 session those pages, and context:'page' with a chrome-extension:// url, evaluate over CDP, the inspector path the extension page CSP does not govern; elsewhere they evaluate over the in-bundle relay. Call extension_dom_snapshot with listTabs:true to enumerate {tabId, url, title}.",
|
|
@@ -12704,14 +12932,6 @@ function resolveDefaultEvalContext(projectPath, browser) {
|
|
|
12704
12932
|
if (!isChromiumFamily(browser)) return "background";
|
|
12705
12933
|
return 3 === chromiumManifestVersion(projectPath, browser) ? "page" : "background";
|
|
12706
12934
|
}
|
|
12707
|
-
const EXTENSION_PAGE_CONTEXTS = [
|
|
12708
|
-
"popup",
|
|
12709
|
-
"options",
|
|
12710
|
-
"sidebar",
|
|
12711
|
-
"newtab",
|
|
12712
|
-
"history",
|
|
12713
|
-
"bookmarks"
|
|
12714
|
-
];
|
|
12715
12935
|
function wantsExtensionPageOverCdp(projectPath, browser, context, url) {
|
|
12716
12936
|
if (!isChromiumFamily(browser) || !context) return false;
|
|
12717
12937
|
if ("page" === context) return "string" == typeof url && /^chrome-extension:\/\//.test(url);
|
|
@@ -12834,6 +13054,78 @@ async function evaluateOnWebKitPage(args, browser, info) {
|
|
|
12834
13054
|
});
|
|
12835
13055
|
}
|
|
12836
13056
|
}
|
|
13057
|
+
const RELAY_POLL_MS = 300;
|
|
13058
|
+
const RELAY_DEFAULT_BUDGET_MS = 30000;
|
|
13059
|
+
function tryParseFrame(raw) {
|
|
13060
|
+
try {
|
|
13061
|
+
const parsed = JSON.parse(raw);
|
|
13062
|
+
return parsed && "object" == typeof parsed ? parsed : null;
|
|
13063
|
+
} catch {
|
|
13064
|
+
return null;
|
|
13065
|
+
}
|
|
13066
|
+
}
|
|
13067
|
+
async function evaluateThroughRelay(args, browser, context) {
|
|
13068
|
+
const token = node_crypto.randomUUID();
|
|
13069
|
+
const budgetMs = args.timeout ?? RELAY_DEFAULT_BUDGET_MS;
|
|
13070
|
+
const deadline = Date.now() + budgetMs;
|
|
13071
|
+
const run = (expression)=>runActVerb([
|
|
13072
|
+
"eval",
|
|
13073
|
+
...commonFlags({
|
|
13074
|
+
...args,
|
|
13075
|
+
context,
|
|
13076
|
+
browser
|
|
13077
|
+
}),
|
|
13078
|
+
"--",
|
|
13079
|
+
expression,
|
|
13080
|
+
args.projectPath
|
|
13081
|
+
], args.projectPath, args.timeout, eval_schema.name);
|
|
13082
|
+
let raw = await run(relaySafeExpression(args.expression, token));
|
|
13083
|
+
let parsed = tryParseFrame(raw);
|
|
13084
|
+
if (!parsed || true !== parsed.ok) return raw;
|
|
13085
|
+
let frame = readRelayFrame(parsed.value);
|
|
13086
|
+
if (!frame) return raw;
|
|
13087
|
+
let polls = 0;
|
|
13088
|
+
while(!frame.done){
|
|
13089
|
+
if (Date.now() >= deadline) return envelope_envelope({
|
|
13090
|
+
ok: false,
|
|
13091
|
+
command: eval_schema.name,
|
|
13092
|
+
status: "eval-pending",
|
|
13093
|
+
error: {
|
|
13094
|
+
code: "E_WAIT_TIMEOUT",
|
|
13095
|
+
name: "EvalPending",
|
|
13096
|
+
message: `The expression returned a promise that had not settled after ${budgetMs} ms; it is still running in the ${context} page.`
|
|
13097
|
+
},
|
|
13098
|
+
value: {
|
|
13099
|
+
context,
|
|
13100
|
+
token,
|
|
13101
|
+
polls
|
|
13102
|
+
},
|
|
13103
|
+
hint: `Its outcome lands in globalThis.${RELAY_MARK}[${JSON.stringify(token)}] inside the ${context} page when it settles ({done, ok, value}): read it with extension_eval in the same context, or pass a larger timeout to wait here.`
|
|
13104
|
+
});
|
|
13105
|
+
await new Promise((r)=>setTimeout(r, RELAY_POLL_MS));
|
|
13106
|
+
polls += 1;
|
|
13107
|
+
raw = await run(relayPollExpression(token));
|
|
13108
|
+
parsed = tryParseFrame(raw);
|
|
13109
|
+
if (!parsed || true !== parsed.ok) return raw;
|
|
13110
|
+
const next = readRelayFrame(parsed.value);
|
|
13111
|
+
if (!next) return raw;
|
|
13112
|
+
frame = next;
|
|
13113
|
+
}
|
|
13114
|
+
if (false === frame.ok) return envelope_envelope({
|
|
13115
|
+
ok: false,
|
|
13116
|
+
command: eval_schema.name,
|
|
13117
|
+
status: "eval-failed",
|
|
13118
|
+
error: {
|
|
13119
|
+
code: "E_EVAL",
|
|
13120
|
+
name: frame.name || "EvalError",
|
|
13121
|
+
message: frame.message || "the expression rejected"
|
|
13122
|
+
},
|
|
13123
|
+
hint: `The expression threw, or the promise it returned rejected, inside the ${context} page.`
|
|
13124
|
+
});
|
|
13125
|
+
parsed.value = void 0 === frame.value ? null : frame.value;
|
|
13126
|
+
if (polls > 0) parsed.hint = `The expression returned a promise; the ${context} page settled it and this call polled ${polls} time${1 === polls ? "" : "s"} for the result. ` + ("string" == typeof parsed.hint ? parsed.hint : "");
|
|
13127
|
+
return actFrameJson(parsed);
|
|
13128
|
+
}
|
|
12837
13129
|
async function eval_handler(args) {
|
|
12838
13130
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
12839
13131
|
if (WEBKIT_FAMILY.has(browser) && "page" === args.context) {
|
|
@@ -12843,6 +13135,35 @@ async function eval_handler(args) {
|
|
|
12843
13135
|
const defaulted = !args.context && "page" === resolveDefaultEvalContext(args.projectPath, browser);
|
|
12844
13136
|
const context = defaulted ? "page" : args.context;
|
|
12845
13137
|
if (wantsExtensionPageOverCdp(args.projectPath, browser, context, args.url)) return evaluateOnChromiumExtensionPage(args, browser, context);
|
|
13138
|
+
if ("page" === context && isExtensionUrl(args.url)) {
|
|
13139
|
+
const surface = surfaceForExtensionUrl(args.projectPath, browser, args.url);
|
|
13140
|
+
if (surface) {
|
|
13141
|
+
const raw = await evaluateThroughRelay({
|
|
13142
|
+
...args,
|
|
13143
|
+
url: void 0,
|
|
13144
|
+
tab: void 0
|
|
13145
|
+
}, browser, surface.context);
|
|
13146
|
+
const parsed = tryParseFrame(raw);
|
|
13147
|
+
if (parsed) {
|
|
13148
|
+
addWarning(parsed, `${args.url} is the extension's own ${surface.context} document (${surface.document}), which script injection cannot reach on any engine, so this evaluated through the ${surface.context} surface relay. Pass context: "${surface.context}" directly next time.`);
|
|
13149
|
+
return actFrameJson(parsed);
|
|
13150
|
+
}
|
|
13151
|
+
return raw;
|
|
13152
|
+
}
|
|
13153
|
+
const declared = declaredSurfaces(args.projectPath, browser) ?? [];
|
|
13154
|
+
return envelope_envelope({
|
|
13155
|
+
ok: false,
|
|
13156
|
+
command: eval_schema.name,
|
|
13157
|
+
status: "no-surface",
|
|
13158
|
+
error: {
|
|
13159
|
+
code: "E_NO_SURFACE_DOCUMENT",
|
|
13160
|
+
name: "NoSurfaceDocument",
|
|
13161
|
+
message: `${args.url} is a page inside the extension, which script injection cannot reach, and it matches none of the surface documents the manifest declares${declared.length ? ` (${declared.join(", ")})` : ""}.`
|
|
13162
|
+
},
|
|
13163
|
+
hint: declared.length ? `Evaluate in a declared surface with context: "${declared[0]}" (open it first with extension_open), or read a web page by url.` : "Declare the page as a surface in the manifest (action.default_popup, options_ui.page, sidebar_action.default_panel or chrome_url_overrides) and rebuild."
|
|
13164
|
+
});
|
|
13165
|
+
}
|
|
13166
|
+
if (context && EXTENSION_PAGE_CONTEXTS.includes(context)) return evaluateThroughRelay(args, browser, context);
|
|
12846
13167
|
const raw = await runActVerb([
|
|
12847
13168
|
"eval",
|
|
12848
13169
|
...commonFlags({
|
|
@@ -14531,6 +14852,12 @@ async function wait_handler(args) {
|
|
|
14531
14852
|
pid: contract.pid,
|
|
14532
14853
|
distPath: contract.distPath,
|
|
14533
14854
|
manifestPath: contract.manifestPath,
|
|
14855
|
+
..."number" == typeof contract.browserPid ? {
|
|
14856
|
+
browserPid: contract.browserPid
|
|
14857
|
+
} : {},
|
|
14858
|
+
..."string" == typeof contract.profilePath && contract.profilePath ? {
|
|
14859
|
+
profilePath: contract.profilePath
|
|
14860
|
+
} : {},
|
|
14534
14861
|
compiledAt: contract.compiledAt,
|
|
14535
14862
|
startedAt: contract.startedAt,
|
|
14536
14863
|
budgetMs,
|
|
@@ -15387,44 +15714,58 @@ function resolveCacheRoot() {
|
|
|
15387
15714
|
if (home) return node_path.join(home, ".cache", "extension.js", "browsers");
|
|
15388
15715
|
return node_path.resolve(process.cwd(), ".cache", "extension.js", "browsers");
|
|
15389
15716
|
}
|
|
15390
|
-
|
|
15391
|
-
|
|
15717
|
+
const MANAGED_SEARCH_DEPTH = 8;
|
|
15718
|
+
const MANAGED_EXEC_NAMES = {
|
|
15719
|
+
chrome: [
|
|
15720
|
+
"chrome",
|
|
15721
|
+
"chrome.exe",
|
|
15722
|
+
"Google Chrome for Testing"
|
|
15723
|
+
],
|
|
15724
|
+
chromium: [
|
|
15725
|
+
"chrome",
|
|
15726
|
+
"chromium",
|
|
15727
|
+
"chrome.exe",
|
|
15728
|
+
"chromium.exe",
|
|
15729
|
+
"Chromium"
|
|
15730
|
+
],
|
|
15731
|
+
edge: [
|
|
15732
|
+
"msedge",
|
|
15733
|
+
"msedge.exe",
|
|
15734
|
+
"microsoft-edge",
|
|
15735
|
+
"Microsoft Edge"
|
|
15736
|
+
],
|
|
15737
|
+
firefox: [
|
|
15738
|
+
"firefox",
|
|
15739
|
+
"firefox.exe",
|
|
15740
|
+
"firefox-bin"
|
|
15741
|
+
]
|
|
15742
|
+
};
|
|
15743
|
+
function executableInsideBundle(bundle, names) {
|
|
15744
|
+
const macos = node_path.join(bundle, "Contents", "MacOS");
|
|
15745
|
+
try {
|
|
15746
|
+
for (const entry of node_fs.readdirSync(macos, {
|
|
15747
|
+
withFileTypes: true
|
|
15748
|
+
}))if (entry.isFile() && names.includes(entry.name)) return node_path.join(macos, entry.name);
|
|
15749
|
+
} catch {}
|
|
15750
|
+
return null;
|
|
15751
|
+
}
|
|
15752
|
+
function findManagedBinary(browser, cacheRoot = resolveCacheRoot()) {
|
|
15753
|
+
const browserDir = node_path.join(cacheRoot, browser);
|
|
15392
15754
|
if (!node_fs.existsSync(browserDir)) return null;
|
|
15393
|
-
const
|
|
15394
|
-
chrome: [
|
|
15395
|
-
"chrome",
|
|
15396
|
-
"chrome.exe",
|
|
15397
|
-
"Google Chrome for Testing"
|
|
15398
|
-
],
|
|
15399
|
-
chromium: [
|
|
15400
|
-
"chrome",
|
|
15401
|
-
"chromium",
|
|
15402
|
-
"chrome.exe",
|
|
15403
|
-
"chromium.exe",
|
|
15404
|
-
"Chromium.app"
|
|
15405
|
-
],
|
|
15406
|
-
edge: [
|
|
15407
|
-
"msedge",
|
|
15408
|
-
"msedge.exe",
|
|
15409
|
-
"microsoft-edge",
|
|
15410
|
-
"Microsoft Edge"
|
|
15411
|
-
],
|
|
15412
|
-
firefox: [
|
|
15413
|
-
"firefox",
|
|
15414
|
-
"firefox.exe",
|
|
15415
|
-
"Firefox.app"
|
|
15416
|
-
]
|
|
15417
|
-
};
|
|
15418
|
-
const names = execNames[browser] ?? [];
|
|
15755
|
+
const names = MANAGED_EXEC_NAMES[browser] ?? [];
|
|
15419
15756
|
function search(dir, depth) {
|
|
15420
|
-
if (depth >
|
|
15757
|
+
if (depth > MANAGED_SEARCH_DEPTH) return null;
|
|
15421
15758
|
try {
|
|
15422
15759
|
for (const entry of node_fs.readdirSync(dir, {
|
|
15423
15760
|
withFileTypes: true
|
|
15424
15761
|
})){
|
|
15425
15762
|
const full = node_path.join(dir, entry.name);
|
|
15426
15763
|
if (entry.isFile() && names.includes(entry.name)) return full;
|
|
15427
|
-
if (entry.isDirectory() &&
|
|
15764
|
+
if (entry.isDirectory() && entry.name.endsWith(".app")) {
|
|
15765
|
+
const inside = executableInsideBundle(full, names);
|
|
15766
|
+
if (inside) return inside;
|
|
15767
|
+
}
|
|
15768
|
+
if (entry.isDirectory() && depth < MANAGED_SEARCH_DEPTH) {
|
|
15428
15769
|
const found = search(full, depth + 1);
|
|
15429
15770
|
if (found) return found;
|
|
15430
15771
|
}
|
|
@@ -15475,14 +15816,16 @@ async function detectBrowsers(browsers) {
|
|
|
15475
15816
|
const isWebkit = WEBKIT_FAMILY.has(browser);
|
|
15476
15817
|
let binaryPath = findManagedBinary(browser);
|
|
15477
15818
|
let source = "managed";
|
|
15819
|
+
const systemBinaryPath = findSystemBinary(browser);
|
|
15478
15820
|
if (!binaryPath) {
|
|
15479
|
-
binaryPath =
|
|
15821
|
+
binaryPath = systemBinaryPath;
|
|
15480
15822
|
source = binaryPath ? "system" : "not_found";
|
|
15481
15823
|
}
|
|
15482
15824
|
let version = null;
|
|
15483
15825
|
if (binaryPath && !isWebkit) version = await getVersion(binaryPath, browser);
|
|
15484
15826
|
else if (binaryPath && isWebkit) version = await readSafariVersion(binaryPath);
|
|
15485
15827
|
const automation = isWebkit && binaryPath ? await detectSafariAutomation(binaryPath) : null;
|
|
15828
|
+
const shadowedSystem = "managed" === source && systemBinaryPath && systemBinaryPath !== binaryPath ? systemBinaryPath : null;
|
|
15486
15829
|
detected.push({
|
|
15487
15830
|
browser,
|
|
15488
15831
|
binaryPath,
|
|
@@ -15493,6 +15836,10 @@ async function detectBrowsers(browsers) {
|
|
|
15493
15836
|
rdpSupport: isGecko,
|
|
15494
15837
|
...automation ? {
|
|
15495
15838
|
automation
|
|
15839
|
+
} : {},
|
|
15840
|
+
...shadowedSystem ? {
|
|
15841
|
+
systemBinaryPath: shadowedSystem,
|
|
15842
|
+
devLaunches: `extension_dev launches the managed binary above, not the system install at ${shadowedSystem}; pass ${isGecko ? "geckoBinary" : "chromiumBinary"} to extension_dev to use the system one.`
|
|
15496
15843
|
} : {}
|
|
15497
15844
|
});
|
|
15498
15845
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const EXTENSION_PAGE_CONTEXTS: string[];
|
|
2
|
+
export declare const EXTENSION_ORIGIN: RegExp;
|
|
3
|
+
export declare function isExtensionUrl(url: string | undefined): boolean;
|
|
4
|
+
export declare function surfaceForExtensionUrl(projectPath: string, browser: string, url: string): {
|
|
5
|
+
context: string;
|
|
6
|
+
document: string;
|
|
7
|
+
} | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const RELAY_MARK = "__extensionDevRelay";
|
|
2
|
+
export interface RelayFrame {
|
|
3
|
+
done: boolean;
|
|
4
|
+
ok?: boolean;
|
|
5
|
+
value?: unknown;
|
|
6
|
+
name?: string;
|
|
7
|
+
message?: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function relaySafeExpression(expression: string, token: string): string;
|
|
11
|
+
export declare function relayPollExpression(token: string): string;
|
|
12
|
+
export declare function readRelayFrame(value: unknown): RelayFrame | null;
|
package/dist/src/lib/types.d.ts
CHANGED
package/dist/src/tools/eval.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ActArgs } from "../lib/act";
|
|
2
|
+
import { EXTENSION_PAGE_CONTEXTS } from "../lib/extension-surfaces";
|
|
2
3
|
export declare const schema: {
|
|
3
4
|
name: string;
|
|
4
5
|
description: string;
|
|
@@ -40,7 +41,7 @@ export declare const schema: {
|
|
|
40
41
|
};
|
|
41
42
|
export declare function chromiumManifestVersion(projectPath: string, browser: string): 2 | 3 | null;
|
|
42
43
|
export declare function resolveDefaultEvalContext(projectPath: string, browser: string): "background" | "page";
|
|
43
|
-
export
|
|
44
|
+
export { EXTENSION_PAGE_CONTEXTS };
|
|
44
45
|
export declare function wantsExtensionPageOverCdp(projectPath: string, browser: string, context: string | undefined, url: string | undefined): boolean;
|
|
45
46
|
export declare function handler(args: ActArgs & {
|
|
46
47
|
expression: string;
|
package/dist/src/tools/stop.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ interface StopOutcome {
|
|
|
30
30
|
detail: string;
|
|
31
31
|
carrierRemoved?: string;
|
|
32
32
|
}
|
|
33
|
+
export interface ContractProcessHints {
|
|
34
|
+
profilePath?: string;
|
|
35
|
+
pids: number[];
|
|
36
|
+
}
|
|
37
|
+
export declare function contractProcessHints(projectPath: string, browser: string): ContractProcessHints;
|
|
33
38
|
export declare function stopOne(projectPath: string, browser: string): Promise<StopOutcome>;
|
|
34
39
|
export declare function handler(args: {
|
|
35
40
|
projectPath?: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@extension.dev/mcp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "10.10.
|
|
4
|
+
"version": "10.10.1",
|
|
5
5
|
"description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 30 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, Firefox, Safari, and every Chromium- or Gecko-based browser (Brave, Opera, Vivaldi, Yandex, Waterfox, LibreWolf). Powered by extension.dev and Extension.js.",
|
|
6
6
|
"mcpName": "io.github.extensiondev/mcp",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -91,9 +91,9 @@
|
|
|
91
91
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
92
92
|
"browser-extension-manifest-fields": "^2.2.9",
|
|
93
93
|
"cross-spawn": "^7.0.6",
|
|
94
|
-
"extension-create": "4.1.
|
|
95
|
-
"extension-develop": "4.1.
|
|
96
|
-
"extension-install": "4.1.
|
|
94
|
+
"extension-create": "4.1.30",
|
|
95
|
+
"extension-develop": "4.1.30",
|
|
96
|
+
"extension-install": "4.1.30",
|
|
97
97
|
"ws": "^8.20.0"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://extension.dev",
|
|
10
|
-
"version": "10.10.
|
|
10
|
+
"version": "10.10.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@extension.dev/mcp",
|
|
16
|
-
"version": "10.10.
|
|
16
|
+
"version": "10.10.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|