@extension.dev/mcp 4.7.0 → 4.9.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +70 -0
- package/dist/module.js +683 -130
- package/dist/src/lib/session-browser.d.ts +4 -0
- package/dist/src/lib/validate-input.d.ts +1 -0
- package/dist/src/tools/manifest-validate.d.ts +12 -2
- package/dist/src/tools/open.d.ts +6 -1
- package/dist/src/tools/stop.d.ts +0 -1
- package/package.json +1 -1
- 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 60+ 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": "4.
|
|
13
|
+
"version": "4.9.0",
|
|
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 60+ 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": "4.
|
|
4
|
+
"version": "4.9.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cezar Augusto",
|
|
7
7
|
"email": "boss@cezaraugusto.net",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.9.0
|
|
4
|
+
|
|
5
|
+
A second pass from the persona swarm, closing the gaps 4.8.0 left and the top
|
|
6
|
+
new blockers it surfaced.
|
|
7
|
+
|
|
8
|
+
- **Honest `extension_manifest_validate`.** It now scans the project source for
|
|
9
|
+
permission-gated `chrome.*`/`browser.*` calls and flags any the manifest does
|
|
10
|
+
not declare — an API used without its permission is `undefined` at runtime and
|
|
11
|
+
crashes the context, the exact case where validate used to report `valid:true`.
|
|
12
|
+
The headline is now honest (`valid:false` + `buildBlocking:true` on any error),
|
|
13
|
+
and it accepts singular `browser` as an alias for `browsers`.
|
|
14
|
+
- **`extension_open` can navigate a tab.** Pass a `url` (Chromium, via CDP) to
|
|
15
|
+
drive a content-script test page, a `webNavigation` target, or the popup as a
|
|
16
|
+
page (`chrome-extension://<id>/popup.html`) — the loop the surface-only open
|
|
17
|
+
could not do. `target` is accepted as an alias for `surface`.
|
|
18
|
+
- **`extension_stop` actually reaps the session.** It now terminates the dev CLI
|
|
19
|
+
and both browser families (gecko profile + chromium `--load-extension`, under
|
|
20
|
+
the project's dist) and refuses to report `stopped:true` while any survive.
|
|
21
|
+
- **`extension_wait` won't lie about a dead session.** A `ready.json` whose pid
|
|
22
|
+
is dead now returns `status:"stale"` instead of `ready`, so you don't walk into
|
|
23
|
+
a reload/eval that fails with a misleading control-channel error.
|
|
24
|
+
- **Dropped-channel errors name the real cause.** A `1006` / "no control channel"
|
|
25
|
+
now detects an exited dev server (stale ready.json + dead pid) and says so,
|
|
26
|
+
instead of asking "is the session started with allowControl?" when it was.
|
|
27
|
+
- **`extension_doctor`** surfaces recent error-level logs as a `runtime-errors`
|
|
28
|
+
check (so a background throwing on every event isn't `healthy:true`), keeps the
|
|
29
|
+
project-local engine version in project mode, and flags when that engine
|
|
30
|
+
differs from a pinned `EXTENSION_MCP_CLI_VERSION`.
|
|
31
|
+
- **`extension_build`** lists declared entrypoints in its success output, so a
|
|
32
|
+
content script no longer reads as "didn't build".
|
|
33
|
+
- **`extension_create`** forces non-interactive git (`GIT_TERMINAL_PROMPT=0`) so a
|
|
34
|
+
credential prompt can't hang the template download, retries once on a transient
|
|
35
|
+
network/timeout failure (cleaning the partial dir first), reports a download
|
|
36
|
+
failure as such instead of "choose a valid template name", and warns when the
|
|
37
|
+
scaffold's `extension@latest` pin will win over your pinned CLI.
|
|
38
|
+
- Eval/inspect error guidance now speaks MCP JSON args (`context`, `tab`, `url`)
|
|
39
|
+
instead of CLI flags.
|
|
40
|
+
|
|
41
|
+
## 4.8.0
|
|
42
|
+
|
|
43
|
+
Dev-session ergonomics hardened from a 30-persona agent walk of the toolchain.
|
|
44
|
+
|
|
45
|
+
- **`allowEval` now implies `allowControl`.** Enabling eval on `extension_dev`
|
|
46
|
+
also opens the control channel, so a single `allowEval: true` unlocks
|
|
47
|
+
`extension_storage`/`reload`/`open`/`dom_inspect` too. `extension_dev` now
|
|
48
|
+
returns a `capabilities` block naming exactly which verbs the session unlocked,
|
|
49
|
+
ending the stop-and-restart loop that hit agents who passed one flag and not
|
|
50
|
+
the other.
|
|
51
|
+
- **Session-aware browser default.** `extension_stop` (and the other
|
|
52
|
+
browser-scoped tools) resolve the browser from the one live session for the
|
|
53
|
+
project instead of assuming `chrome`. `extension_stop` also reaps the launched
|
|
54
|
+
browser's process tree and refuses to report `stopped: true` while a process
|
|
55
|
+
survives, fixing orphaned browsers (notably Firefox) after a stop.
|
|
56
|
+
- **Forgiving argument names.** Common synonyms are accepted and normalized:
|
|
57
|
+
`path`/`dir` for `projectPath`, `name` for `projectName`, `template` for
|
|
58
|
+
`slug`, `code` for `expression`, and more, so a reasonable first guess no
|
|
59
|
+
longer 400s.
|
|
60
|
+
- **`extension_manifest_validate`** accepts `projectPath` (it finds the
|
|
61
|
+
manifest) and probes path-valued fields (popup, service worker, icons, content
|
|
62
|
+
scripts) against disk, warning on dangling references instead of a false
|
|
63
|
+
all-clear.
|
|
64
|
+
- **`extension_doctor`** inlines the dev session's own recorded errors so a build
|
|
65
|
+
or load failure no longer reads as healthy.
|
|
66
|
+
- **`extension_inspect`** lists declared entrypoints (so a small content script
|
|
67
|
+
is not buried under assets) and warns when a store-listing promo image is
|
|
68
|
+
shipped inside the package.
|
|
69
|
+
- **`extension_source_inspect`** on a Gecko session now names the working
|
|
70
|
+
alternatives (`extension_logs`, `extension_eval`) instead of pointing back at
|
|
71
|
+
the tool that just refused.
|
|
72
|
+
|
|
3
73
|
## 4.7.0
|
|
4
74
|
|
|
5
75
|
`extension_deploy` now submits **through** extension.dev instead of driving a
|
package/dist/module.js
CHANGED
|
@@ -7,11 +7,11 @@ import node_path, { join } from "node:path";
|
|
|
7
7
|
import { extensionCreate } from "extension-create";
|
|
8
8
|
import node_os from "node:os";
|
|
9
9
|
import cross_spawn from "cross-spawn";
|
|
10
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
10
11
|
import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
|
|
11
12
|
import ws from "ws";
|
|
12
13
|
import node_http from "node:http";
|
|
13
14
|
import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
14
|
-
import { execFile } from "node:child_process";
|
|
15
15
|
import { promisify } from "node:util";
|
|
16
16
|
var add_feature_namespaceObject = {};
|
|
17
17
|
__webpack_require__.r(add_feature_namespaceObject);
|
|
@@ -199,7 +199,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
199
199
|
handler: ()=>whoami_handler,
|
|
200
200
|
schema: ()=>whoami_schema
|
|
201
201
|
});
|
|
202
|
-
var package_namespaceObject = JSON.parse('{"rE":"4.
|
|
202
|
+
var package_namespaceObject = JSON.parse('{"rE":"4.8.0","El":{"OP":"^4.0.11"}}');
|
|
203
203
|
function detectPackageManager(projectPath) {
|
|
204
204
|
const byLockfile = [
|
|
205
205
|
[
|
|
@@ -259,14 +259,27 @@ const create_schema = {
|
|
|
259
259
|
async function create_handler(args) {
|
|
260
260
|
const start = Date.now();
|
|
261
261
|
const projectInput = args.parentDir ? node_path.resolve(args.parentDir, args.projectName) : args.projectName;
|
|
262
|
+
process.env.GIT_TERMINAL_PROMPT = "0";
|
|
263
|
+
if (void 0 === process.env.GIT_ASKPASS) process.env.GIT_ASKPASS = "";
|
|
262
264
|
const logLines = [];
|
|
263
265
|
const capture = (stream)=>(...parts)=>{
|
|
264
266
|
const line = parts.map((p)=>"string" == typeof p ? p : String(p)).join(" ").trim();
|
|
265
267
|
if (line) logLines.push("error" === stream ? `[error] ${line}` : line);
|
|
266
268
|
};
|
|
267
269
|
const logTail = (max = 20)=>logLines.slice(-max);
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
+
const looksTransient = ()=>{
|
|
271
|
+
const blob = logLines.join("\n").toLowerCase();
|
|
272
|
+
return /timed out|timeout|etimedout|econnreset|rate limit|\b429\b|network|could not resolve host|terminal prompts disabled|authentication failed|early eof|rpc failed|remote end hung up/.test(blob);
|
|
273
|
+
};
|
|
274
|
+
const cleanPartial = ()=>{
|
|
275
|
+
try {
|
|
276
|
+
if (args.parentDir && node_fs.existsSync(projectInput)) node_fs.rmSync(projectInput, {
|
|
277
|
+
recursive: true,
|
|
278
|
+
force: true
|
|
279
|
+
});
|
|
280
|
+
} catch {}
|
|
281
|
+
};
|
|
282
|
+
const attempt = ()=>extensionCreate(projectInput, {
|
|
270
283
|
template: args.template ?? "typescript",
|
|
271
284
|
install: args.install ?? true,
|
|
272
285
|
logger: {
|
|
@@ -274,34 +287,53 @@ async function create_handler(args) {
|
|
|
274
287
|
error: capture("error")
|
|
275
288
|
}
|
|
276
289
|
});
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
template: result.template,
|
|
283
|
-
depsInstalled: result.depsInstalled,
|
|
284
|
-
packageManager: result.depsInstalled ? packageManager : null,
|
|
285
|
-
duration: Date.now() - start,
|
|
286
|
-
nextSteps: result.depsInstalled ? [
|
|
287
|
-
`cd ${result.projectPath}`,
|
|
288
|
-
runDev
|
|
289
|
-
] : [
|
|
290
|
-
`cd ${result.projectPath}`,
|
|
291
|
-
"npm install",
|
|
292
|
-
"npm run dev"
|
|
293
|
-
],
|
|
294
|
-
...result.depsInstalled ? {} : {
|
|
295
|
-
warnings: logTail()
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
} catch (err) {
|
|
299
|
-
return JSON.stringify({
|
|
300
|
-
error: err instanceof Error ? err.message : String(err),
|
|
290
|
+
const failure = (err, transient)=>JSON.stringify({
|
|
291
|
+
error: transient ? "Template download failed (network/timeout/rate-limit) — this is not a bad template name. Retry, or check connectivity/GitHub rate limits." : err instanceof Error ? err.message : String(err),
|
|
292
|
+
...transient ? {
|
|
293
|
+
cause: err instanceof Error ? err.message : String(err)
|
|
294
|
+
} : {},
|
|
301
295
|
duration: Date.now() - start,
|
|
302
296
|
log: logTail()
|
|
303
297
|
});
|
|
298
|
+
let result;
|
|
299
|
+
try {
|
|
300
|
+
result = await attempt();
|
|
301
|
+
} catch (err1) {
|
|
302
|
+
if (!looksTransient()) return failure(err1, false);
|
|
303
|
+
logLines.push("[retry] transient template-download failure; retrying once");
|
|
304
|
+
cleanPartial();
|
|
305
|
+
try {
|
|
306
|
+
result = await attempt();
|
|
307
|
+
} catch (err2) {
|
|
308
|
+
return failure(err2, looksTransient());
|
|
309
|
+
}
|
|
304
310
|
}
|
|
311
|
+
const packageManager = result.depsInstalled ? detectPackageManager(result.projectPath) : "npm";
|
|
312
|
+
const runDev = `${packageManager} run dev`;
|
|
313
|
+
const pin = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
|
|
314
|
+
const engineWarning = pin && "latest" !== pin ? `The scaffold pins "extension": "latest"; the project-local engine wins over EXTENSION_MCP_CLI_VERSION=${pin}. Run \`(cd ${result.projectPath} && npm i -D extension@${pin})\` to match the pinned engine.` : void 0;
|
|
315
|
+
return JSON.stringify({
|
|
316
|
+
projectPath: result.projectPath,
|
|
317
|
+
projectName: result.projectName,
|
|
318
|
+
template: result.template,
|
|
319
|
+
depsInstalled: result.depsInstalled,
|
|
320
|
+
packageManager: result.depsInstalled ? packageManager : null,
|
|
321
|
+
duration: Date.now() - start,
|
|
322
|
+
nextSteps: result.depsInstalled ? [
|
|
323
|
+
`cd ${result.projectPath}`,
|
|
324
|
+
runDev
|
|
325
|
+
] : [
|
|
326
|
+
`cd ${result.projectPath}`,
|
|
327
|
+
"npm install",
|
|
328
|
+
"npm run dev"
|
|
329
|
+
],
|
|
330
|
+
...engineWarning ? {
|
|
331
|
+
engineWarning
|
|
332
|
+
} : {},
|
|
333
|
+
...result.depsInstalled ? {} : {
|
|
334
|
+
warnings: logTail()
|
|
335
|
+
}
|
|
336
|
+
});
|
|
305
337
|
}
|
|
306
338
|
const CACHE_DIR = node_path.join(node_os.homedir(), ".cache", "extension-js");
|
|
307
339
|
const CACHE_FILE = node_path.join(CACHE_DIR, "templates-meta.json");
|
|
@@ -535,6 +567,34 @@ function spawnExtensionCli(args, options) {
|
|
|
535
567
|
child.unref();
|
|
536
568
|
return child;
|
|
537
569
|
}
|
|
570
|
+
function builtEntrypoints(distDir) {
|
|
571
|
+
let manifest;
|
|
572
|
+
try {
|
|
573
|
+
manifest = JSON.parse(node_fs.readFileSync(node_path.join(distDir, "manifest.json"), "utf8"));
|
|
574
|
+
} catch {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
const out = [];
|
|
578
|
+
const add = (role, ref)=>{
|
|
579
|
+
if ("string" != typeof ref) return;
|
|
580
|
+
out.push({
|
|
581
|
+
role,
|
|
582
|
+
path: ref,
|
|
583
|
+
present: node_fs.existsSync(node_path.join(distDir, ref.replace(/^\.?\//, "")))
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
const bg = manifest.background;
|
|
587
|
+
if (bg?.service_worker) add("background.service_worker", bg.service_worker);
|
|
588
|
+
if (Array.isArray(bg?.scripts)) bg.scripts.forEach((s)=>add("background.scripts", s));
|
|
589
|
+
const action = manifest.action || manifest.browser_action;
|
|
590
|
+
if (action?.default_popup) add("action.default_popup", action.default_popup);
|
|
591
|
+
const cs = manifest.content_scripts;
|
|
592
|
+
if (Array.isArray(cs)) cs.forEach((c, i)=>{
|
|
593
|
+
if (Array.isArray(c.js)) c.js.forEach((j)=>add(`content_scripts[${i}].js`, j));
|
|
594
|
+
if (Array.isArray(c.css)) c.css.forEach((s)=>add(`content_scripts[${i}].css`, s));
|
|
595
|
+
});
|
|
596
|
+
return out;
|
|
597
|
+
}
|
|
538
598
|
const build_schema = {
|
|
539
599
|
name: "extension_build",
|
|
540
600
|
description: "Build a browser extension for production. Outputs to dist/<browser>/. Optionally creates .zip for store submission.",
|
|
@@ -632,6 +692,7 @@ async function build_handler(args) {
|
|
|
632
692
|
if (0 === code) {
|
|
633
693
|
const size = out.match(/Size:\s*([\d.]+\s*[kKmMgG]?B)/)?.[1];
|
|
634
694
|
const status = out.match(/Build Status:\s*(\w+)/)?.[1];
|
|
695
|
+
const entrypoints = builtEntrypoints(node_path.resolve(args.projectPath, "dist", browser));
|
|
635
696
|
return JSON.stringify({
|
|
636
697
|
success: true,
|
|
637
698
|
browser,
|
|
@@ -641,6 +702,9 @@ async function build_handler(args) {
|
|
|
641
702
|
...status ? {
|
|
642
703
|
status
|
|
643
704
|
} : {},
|
|
705
|
+
...entrypoints.length ? {
|
|
706
|
+
entrypoints
|
|
707
|
+
} : {},
|
|
644
708
|
zip: args.zip ?? false,
|
|
645
709
|
duration,
|
|
646
710
|
output: lastLines(out, 12)
|
|
@@ -769,7 +833,7 @@ const dev_schema = {
|
|
|
769
833
|
allowEval: {
|
|
770
834
|
type: "boolean",
|
|
771
835
|
default: false,
|
|
772
|
-
description: "
|
|
836
|
+
description: "Enable extension_eval (runs code in a context; writes a 0600 session token). Implies allowControl, so a single allowEval: true also unlocks storage/reload/open/dom_inspect — you do not need to pass both."
|
|
773
837
|
}
|
|
774
838
|
},
|
|
775
839
|
required: [
|
|
@@ -779,6 +843,7 @@ const dev_schema = {
|
|
|
779
843
|
};
|
|
780
844
|
async function dev_handler(args) {
|
|
781
845
|
const browser = args.browser ?? "chrome";
|
|
846
|
+
const allowControl = Boolean(args.allowControl || args.allowEval);
|
|
782
847
|
const cliArgs = [
|
|
783
848
|
"dev",
|
|
784
849
|
args.projectPath,
|
|
@@ -789,7 +854,7 @@ async function dev_handler(args) {
|
|
|
789
854
|
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
790
855
|
if (false === args.polyfill) cliArgs.push("--polyfill", "false");
|
|
791
856
|
cliArgs.push(...launchFlagArgs(args));
|
|
792
|
-
if (
|
|
857
|
+
if (allowControl) cliArgs.push("--allow-control");
|
|
793
858
|
if (args.allowEval) cliArgs.push("--allow-eval");
|
|
794
859
|
const child = spawnExtensionCli(cliArgs, {
|
|
795
860
|
projectDir: args.projectPath
|
|
@@ -812,13 +877,20 @@ async function dev_handler(args) {
|
|
|
812
877
|
await new Promise((resolve)=>setTimeout(resolve, 3000));
|
|
813
878
|
child.stdout?.off("data", collector);
|
|
814
879
|
child.stderr?.off("data", collector);
|
|
880
|
+
const controlVerbs = "storage, reload, open, dom_inspect";
|
|
881
|
+
const capabilities = {
|
|
882
|
+
allowControl,
|
|
883
|
+
allowEval: Boolean(args.allowEval),
|
|
884
|
+
unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, source_inspect, wait, doctor)"
|
|
885
|
+
};
|
|
815
886
|
return JSON.stringify({
|
|
816
887
|
pid,
|
|
817
888
|
browser,
|
|
818
889
|
port: args.port ?? 8080,
|
|
819
890
|
projectPath: args.projectPath,
|
|
820
891
|
status: "started",
|
|
821
|
-
|
|
892
|
+
capabilities,
|
|
893
|
+
hint: "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_inspect need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). Restart extension_dev with the flag you need.") + " When you are done, call extension_stop to shut down the dev server and browser.",
|
|
822
894
|
earlyOutput: denoiseEarlyOutput(earlyOutput).slice(0, 500)
|
|
823
895
|
});
|
|
824
896
|
}
|
|
@@ -1003,6 +1075,73 @@ async function preview_handler(args) {
|
|
|
1003
1075
|
hint: "Call extension_stop when you are done to close the preview browser."
|
|
1004
1076
|
});
|
|
1005
1077
|
}
|
|
1078
|
+
function contractSightings(projectPath) {
|
|
1079
|
+
const root = node_path.resolve(projectPath, "dist", "extension-js");
|
|
1080
|
+
let dirs;
|
|
1081
|
+
try {
|
|
1082
|
+
dirs = node_fs.readdirSync(root);
|
|
1083
|
+
} catch {
|
|
1084
|
+
return [];
|
|
1085
|
+
}
|
|
1086
|
+
const sightings = [];
|
|
1087
|
+
for (const dir of dirs){
|
|
1088
|
+
const readyPath = node_path.join(root, dir, "ready.json");
|
|
1089
|
+
try {
|
|
1090
|
+
const stat = node_fs.statSync(readyPath);
|
|
1091
|
+
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1092
|
+
if (contract?.status !== "ready") continue;
|
|
1093
|
+
sightings.push({
|
|
1094
|
+
browser: dir,
|
|
1095
|
+
mtimeMs: stat.mtimeMs,
|
|
1096
|
+
pid: "number" == typeof contract.pid ? contract.pid : void 0
|
|
1097
|
+
});
|
|
1098
|
+
} catch {}
|
|
1099
|
+
}
|
|
1100
|
+
return sightings;
|
|
1101
|
+
}
|
|
1102
|
+
function pidAlive(pid) {
|
|
1103
|
+
try {
|
|
1104
|
+
process.kill(pid, 0);
|
|
1105
|
+
return true;
|
|
1106
|
+
} catch {
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function knownSessionBrowsers(projectPath) {
|
|
1111
|
+
const resolved = node_path.resolve(projectPath);
|
|
1112
|
+
const browsers = [];
|
|
1113
|
+
for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) browsers.push(session.browser);
|
|
1114
|
+
for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
|
|
1115
|
+
return Array.from(new Set(browsers));
|
|
1116
|
+
}
|
|
1117
|
+
function deadReadySession(projectPath) {
|
|
1118
|
+
for (const sighting of contractSightings(projectPath))if (void 0 !== sighting.pid && !pidAlive(sighting.pid)) return {
|
|
1119
|
+
browser: sighting.browser,
|
|
1120
|
+
pid: sighting.pid
|
|
1121
|
+
};
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
1124
|
+
function resolveSessionBrowser(projectPath, explicit, fallback = "chromium") {
|
|
1125
|
+
if (explicit) return {
|
|
1126
|
+
browser: explicit,
|
|
1127
|
+
source: "explicit"
|
|
1128
|
+
};
|
|
1129
|
+
const resolved = node_path.resolve(projectPath);
|
|
1130
|
+
const mine = listSessions().filter((s)=>node_path.resolve(s.projectPath) === resolved);
|
|
1131
|
+
if (mine.length > 0) return {
|
|
1132
|
+
browser: mine[mine.length - 1].browser,
|
|
1133
|
+
source: "session"
|
|
1134
|
+
};
|
|
1135
|
+
const sightings = contractSightings(projectPath).filter((s)=>void 0 === s.pid || pidAlive(s.pid)).sort((a, b)=>b.mtimeMs - a.mtimeMs);
|
|
1136
|
+
if (sightings.length > 0) return {
|
|
1137
|
+
browser: sightings[0].browser,
|
|
1138
|
+
source: "contract"
|
|
1139
|
+
};
|
|
1140
|
+
return {
|
|
1141
|
+
browser: fallback,
|
|
1142
|
+
source: "fallback"
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1006
1145
|
const stop_schema = {
|
|
1007
1146
|
name: "extension_stop",
|
|
1008
1147
|
description: "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched. Counterpart to extension_dev/extension_start. Call it when you are done verifying so sessions do not accumulate.",
|
|
@@ -1015,8 +1154,7 @@ const stop_schema = {
|
|
|
1015
1154
|
},
|
|
1016
1155
|
browser: {
|
|
1017
1156
|
type: "string",
|
|
1018
|
-
|
|
1019
|
-
description: "Browser of the session to stop (matches the browser passed to extension_dev/extension_start)"
|
|
1157
|
+
description: "Browser of the session to stop (matches the browser passed to extension_dev/extension_start). Defaults to the one live session for this project when omitted, instead of assuming chrome."
|
|
1020
1158
|
},
|
|
1021
1159
|
all: {
|
|
1022
1160
|
type: "boolean",
|
|
@@ -1027,6 +1165,37 @@ const stop_schema = {
|
|
|
1027
1165
|
required: []
|
|
1028
1166
|
}
|
|
1029
1167
|
};
|
|
1168
|
+
function pgrepPids(pattern) {
|
|
1169
|
+
try {
|
|
1170
|
+
const out = execFileSync("pgrep", [
|
|
1171
|
+
"-f",
|
|
1172
|
+
pattern
|
|
1173
|
+
], {
|
|
1174
|
+
encoding: "utf8"
|
|
1175
|
+
});
|
|
1176
|
+
return out.split("\n").map((s)=>parseInt(s.trim(), 10)).filter((n)=>Number.isInteger(n) && n > 0 && n !== process.pid);
|
|
1177
|
+
} catch {
|
|
1178
|
+
return [];
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
function sessionProcessPids(projectPath) {
|
|
1182
|
+
const resolved = node_path.resolve(projectPath);
|
|
1183
|
+
const pids = new Set();
|
|
1184
|
+
for (const marker of [
|
|
1185
|
+
`extension dev ${resolved}`,
|
|
1186
|
+
node_path.join(resolved, "dist")
|
|
1187
|
+
])for (const pid of pgrepPids(marker))pids.add(pid);
|
|
1188
|
+
return [
|
|
1189
|
+
...pids
|
|
1190
|
+
];
|
|
1191
|
+
}
|
|
1192
|
+
function reapSessionProcesses(projectPath) {
|
|
1193
|
+
const pids = sessionProcessPids(projectPath);
|
|
1194
|
+
for (const pid of pids)try {
|
|
1195
|
+
process.kill(pid, "SIGKILL");
|
|
1196
|
+
} catch {}
|
|
1197
|
+
return pids;
|
|
1198
|
+
}
|
|
1030
1199
|
function isAlive(pid) {
|
|
1031
1200
|
try {
|
|
1032
1201
|
process.kill(pid, 0);
|
|
@@ -1063,13 +1232,17 @@ function pidFromReadyContract(projectPath, browser) {
|
|
|
1063
1232
|
async function stopOne(projectPath, browser) {
|
|
1064
1233
|
const session = getSession(projectPath, browser);
|
|
1065
1234
|
const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
|
|
1066
|
-
if (null == pid)
|
|
1067
|
-
projectPath
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1235
|
+
if (null == pid) {
|
|
1236
|
+
const reaped = reapSessionProcesses(projectPath);
|
|
1237
|
+
return {
|
|
1238
|
+
projectPath,
|
|
1239
|
+
browser,
|
|
1240
|
+
pid: null,
|
|
1241
|
+
stopped: 0 !== reaped.length,
|
|
1242
|
+
reaped,
|
|
1243
|
+
detail: 0 === reaped.length ? "No known session for this project/browser (nothing registered in this server and no ready.json contract found)." : `No dev pid on record, but reaped ${reaped.length} orphaned browser process(es) from the profile dir.`
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1073
1246
|
let detail;
|
|
1074
1247
|
if (isAlive(pid)) {
|
|
1075
1248
|
signal(pid, "SIGTERM");
|
|
@@ -1080,17 +1253,23 @@ async function stopOne(projectPath, browser) {
|
|
|
1080
1253
|
}
|
|
1081
1254
|
detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
|
|
1082
1255
|
} else detail = "Process was already gone; cleaned up session records.";
|
|
1256
|
+
const reaped = reapSessionProcesses(projectPath);
|
|
1083
1257
|
removeSession(projectPath, browser);
|
|
1084
1258
|
try {
|
|
1085
1259
|
node_fs.rmSync(readyJsonPath(projectPath, browser), {
|
|
1086
1260
|
force: true
|
|
1087
1261
|
});
|
|
1088
1262
|
} catch {}
|
|
1263
|
+
const survivors = sessionProcessPids(projectPath);
|
|
1264
|
+
const stopped = !isAlive(pid) && 0 === survivors.length;
|
|
1265
|
+
if (survivors.length) detail += ` Warning: ${survivors.length} browser process(es) still alive after reap (pids ${survivors.join(", ")}).`;
|
|
1266
|
+
else if (reaped.length) detail += ` Reaped ${reaped.length} browser process(es).`;
|
|
1089
1267
|
return {
|
|
1090
1268
|
projectPath,
|
|
1091
1269
|
browser,
|
|
1092
1270
|
pid,
|
|
1093
|
-
stopped
|
|
1271
|
+
stopped,
|
|
1272
|
+
reaped,
|
|
1094
1273
|
detail
|
|
1095
1274
|
};
|
|
1096
1275
|
}
|
|
@@ -1110,7 +1289,8 @@ async function stop_handler(args) {
|
|
|
1110
1289
|
if (!args.projectPath) return JSON.stringify({
|
|
1111
1290
|
error: "projectPath is required unless all=true. Pass the same projectPath used with extension_dev/extension_start."
|
|
1112
1291
|
});
|
|
1113
|
-
const
|
|
1292
|
+
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
1293
|
+
const outcome = await stopOne(args.projectPath, browser);
|
|
1114
1294
|
return JSON.stringify(outcome);
|
|
1115
1295
|
}
|
|
1116
1296
|
const RAW_BASE = "https://raw.githubusercontent.com/extension-js/examples/main/examples";
|
|
@@ -1298,7 +1478,11 @@ const manifest_validate_schema = {
|
|
|
1298
1478
|
properties: {
|
|
1299
1479
|
manifestPath: {
|
|
1300
1480
|
type: "string",
|
|
1301
|
-
description: "Path to manifest.json"
|
|
1481
|
+
description: "Path to manifest.json. Or pass projectPath and the manifest is located for you."
|
|
1482
|
+
},
|
|
1483
|
+
projectPath: {
|
|
1484
|
+
type: "string",
|
|
1485
|
+
description: "Path to the extension project root; manifest.json is resolved from it (root or src/). Accepted in place of manifestPath."
|
|
1302
1486
|
},
|
|
1303
1487
|
browsers: {
|
|
1304
1488
|
type: "array",
|
|
@@ -1310,14 +1494,154 @@ const manifest_validate_schema = {
|
|
|
1310
1494
|
"firefox"
|
|
1311
1495
|
],
|
|
1312
1496
|
description: "Browsers to validate against"
|
|
1497
|
+
},
|
|
1498
|
+
browser: {
|
|
1499
|
+
type: "string",
|
|
1500
|
+
description: "Single browser to validate against; alias for browsers:[browser] to match the other tools."
|
|
1313
1501
|
}
|
|
1314
1502
|
},
|
|
1315
|
-
required: [
|
|
1316
|
-
|
|
1317
|
-
|
|
1503
|
+
required: []
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
function collectPathRefs(m) {
|
|
1507
|
+
const refs = [];
|
|
1508
|
+
const push = (v)=>{
|
|
1509
|
+
if ("string" == typeof v) refs.push(v);
|
|
1510
|
+
};
|
|
1511
|
+
const action = m.action || m.browser_action;
|
|
1512
|
+
if (action) {
|
|
1513
|
+
push(action.default_popup);
|
|
1514
|
+
if ("string" == typeof action.default_icon) push(action.default_icon);
|
|
1515
|
+
else if (action.default_icon) Object.values(action.default_icon).forEach(push);
|
|
1516
|
+
}
|
|
1517
|
+
const bg = m.background;
|
|
1518
|
+
if (bg) {
|
|
1519
|
+
push(bg.service_worker);
|
|
1520
|
+
push(bg.page);
|
|
1521
|
+
if (Array.isArray(bg.scripts)) bg.scripts.forEach(push);
|
|
1318
1522
|
}
|
|
1523
|
+
if (m.icons) Object.values(m.icons).forEach(push);
|
|
1524
|
+
const cs = m.content_scripts;
|
|
1525
|
+
if (Array.isArray(cs)) for (const c of cs){
|
|
1526
|
+
if (Array.isArray(c.js)) c.js.forEach(push);
|
|
1527
|
+
if (Array.isArray(c.css)) c.css.forEach(push);
|
|
1528
|
+
}
|
|
1529
|
+
push(m.options_page);
|
|
1530
|
+
const oui = m.options_ui;
|
|
1531
|
+
if (oui) push(oui.page);
|
|
1532
|
+
const sp = m.side_panel;
|
|
1533
|
+
if (sp) push(sp.default_path);
|
|
1534
|
+
const sa = m.sidebar_action;
|
|
1535
|
+
if (sa) push(sa.default_panel);
|
|
1536
|
+
const cuo = m.chrome_url_overrides;
|
|
1537
|
+
if (cuo) Object.values(cuo).forEach(push);
|
|
1538
|
+
return refs;
|
|
1539
|
+
}
|
|
1540
|
+
function fileResolvesSomewhere(ref, roots) {
|
|
1541
|
+
if (!ref || ref.includes("*") || /^(https?:|data:)/i.test(ref)) return true;
|
|
1542
|
+
const clean = ref.replace(/^\.?\//, "");
|
|
1543
|
+
return roots.some((root)=>{
|
|
1544
|
+
try {
|
|
1545
|
+
return node_fs.existsSync(node_path.resolve(root, clean));
|
|
1546
|
+
} catch {
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
function findManifest(projectPath) {
|
|
1552
|
+
for (const rel of [
|
|
1553
|
+
"manifest.json",
|
|
1554
|
+
node_path.join("src", "manifest.json")
|
|
1555
|
+
]){
|
|
1556
|
+
const candidate = node_path.resolve(projectPath, rel);
|
|
1557
|
+
if (node_fs.existsSync(candidate)) return candidate;
|
|
1558
|
+
}
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
const API_PERMISSION = {
|
|
1562
|
+
storage: "storage",
|
|
1563
|
+
webNavigation: "webNavigation",
|
|
1564
|
+
history: "history",
|
|
1565
|
+
cookies: "cookies",
|
|
1566
|
+
bookmarks: "bookmarks",
|
|
1567
|
+
alarms: "alarms",
|
|
1568
|
+
contextMenus: "contextMenus",
|
|
1569
|
+
notifications: "notifications",
|
|
1570
|
+
downloads: "downloads",
|
|
1571
|
+
webRequest: "webRequest",
|
|
1572
|
+
tabGroups: "tabGroups",
|
|
1573
|
+
topSites: "topSites",
|
|
1574
|
+
idle: "idle",
|
|
1575
|
+
management: "management",
|
|
1576
|
+
scripting: "scripting",
|
|
1577
|
+
declarativeNetRequest: "declarativeNetRequest",
|
|
1578
|
+
sessions: "sessions",
|
|
1579
|
+
proxy: "proxy",
|
|
1580
|
+
tts: "tts",
|
|
1581
|
+
pageCapture: "pageCapture",
|
|
1582
|
+
desktopCapture: "desktopCapture",
|
|
1583
|
+
debugger: "debugger",
|
|
1584
|
+
geolocation: "geolocation"
|
|
1319
1585
|
};
|
|
1586
|
+
const HARD_APIS = new Set([
|
|
1587
|
+
"history",
|
|
1588
|
+
"cookies",
|
|
1589
|
+
"bookmarks",
|
|
1590
|
+
"webNavigation",
|
|
1591
|
+
"downloads",
|
|
1592
|
+
"webRequest",
|
|
1593
|
+
"topSites",
|
|
1594
|
+
"management",
|
|
1595
|
+
"tabGroups",
|
|
1596
|
+
"sessions",
|
|
1597
|
+
"proxy",
|
|
1598
|
+
"debugger",
|
|
1599
|
+
"pageCapture",
|
|
1600
|
+
"desktopCapture"
|
|
1601
|
+
]);
|
|
1602
|
+
function scanApiUsage(roots) {
|
|
1603
|
+
const used = new Set();
|
|
1604
|
+
let filesRead = 0;
|
|
1605
|
+
const walk = (dir, depth)=>{
|
|
1606
|
+
if (depth > 6 || filesRead > 300) return;
|
|
1607
|
+
let entries;
|
|
1608
|
+
try {
|
|
1609
|
+
entries = node_fs.readdirSync(dir, {
|
|
1610
|
+
withFileTypes: true
|
|
1611
|
+
});
|
|
1612
|
+
} catch {
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
for (const e of entries){
|
|
1616
|
+
if ("node_modules" === e.name || "dist" === e.name || e.name.startsWith(".")) continue;
|
|
1617
|
+
const full = node_path.join(dir, e.name);
|
|
1618
|
+
if (e.isDirectory()) {
|
|
1619
|
+
walk(full, depth + 1);
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
if (!/\.(js|mjs|cjs|ts|tsx|jsx|svelte|vue)$/.test(e.name)) continue;
|
|
1623
|
+
if (filesRead++ > 300) return;
|
|
1624
|
+
let src;
|
|
1625
|
+
try {
|
|
1626
|
+
src = node_fs.readFileSync(full, "utf8");
|
|
1627
|
+
} catch {
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
const re = /\b(?:chrome|browser)\.(\w+)/g;
|
|
1631
|
+
let m;
|
|
1632
|
+
while(m = re.exec(src))if (API_PERMISSION[m[1]]) used.add(m[1]);
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
for (const root of new Set(roots))walk(root, 0);
|
|
1636
|
+
return used;
|
|
1637
|
+
}
|
|
1320
1638
|
async function manifest_validate_handler(args) {
|
|
1639
|
+
if (!args.browsers && "string" == typeof args.browser) args = {
|
|
1640
|
+
...args,
|
|
1641
|
+
browsers: [
|
|
1642
|
+
args.browser
|
|
1643
|
+
]
|
|
1644
|
+
};
|
|
1321
1645
|
const browsers = args.browsers ?? [
|
|
1322
1646
|
"chrome",
|
|
1323
1647
|
"firefox"
|
|
@@ -1329,9 +1653,20 @@ async function manifest_validate_handler(args) {
|
|
|
1329
1653
|
browserSupport: {},
|
|
1330
1654
|
similarTemplates: []
|
|
1331
1655
|
};
|
|
1656
|
+
const manifestPath = args.manifestPath ?? (args.projectPath ? findManifest(args.projectPath) : null);
|
|
1657
|
+
if (!manifestPath) return JSON.stringify({
|
|
1658
|
+
valid: false,
|
|
1659
|
+
errors: [
|
|
1660
|
+
args.projectPath ? `No manifest.json found under ${args.projectPath} (looked in the root and src/).` : "Pass manifestPath (path to manifest.json) or projectPath (project root)."
|
|
1661
|
+
],
|
|
1662
|
+
warnings: [],
|
|
1663
|
+
browserSupport: {},
|
|
1664
|
+
similarTemplates: []
|
|
1665
|
+
});
|
|
1666
|
+
const manifestDir = node_path.dirname(node_path.resolve(manifestPath));
|
|
1332
1667
|
let manifest;
|
|
1333
1668
|
try {
|
|
1334
|
-
const raw = node_fs.readFileSync(node_path.resolve(
|
|
1669
|
+
const raw = node_fs.readFileSync(node_path.resolve(manifestPath), "utf8");
|
|
1335
1670
|
manifest = JSON.parse(raw);
|
|
1336
1671
|
} catch (err) {
|
|
1337
1672
|
return JSON.stringify({
|
|
@@ -1347,6 +1682,25 @@ async function manifest_validate_handler(args) {
|
|
|
1347
1682
|
if (!manifest.name) result.errors.push("Missing required field: name");
|
|
1348
1683
|
if (!manifest.version) result.warnings.push("Missing field: version (required for store submission)");
|
|
1349
1684
|
const chromiumManifest = filterKeysForThisBrowser(manifest, "chrome");
|
|
1685
|
+
const roots = [
|
|
1686
|
+
manifestDir,
|
|
1687
|
+
node_path.join(manifestDir, "src"),
|
|
1688
|
+
..."src" === node_path.basename(manifestDir) ? [
|
|
1689
|
+
node_path.dirname(manifestDir)
|
|
1690
|
+
] : []
|
|
1691
|
+
];
|
|
1692
|
+
for (const ref of new Set(collectPathRefs(chromiumManifest)))if (!fileResolvesSomewhere(ref, roots)) result.warnings.push(`Referenced file "${ref}" was not found near the manifest — this is the kind of dangling reference extension_build fails on. Verify with extension_build.`);
|
|
1693
|
+
const declaredPermSet = new Set([
|
|
1694
|
+
...chromiumManifest.permissions ?? [],
|
|
1695
|
+
...chromiumManifest.optional_permissions ?? []
|
|
1696
|
+
].filter((p)=>"string" == typeof p));
|
|
1697
|
+
for (const api of scanApiUsage(roots)){
|
|
1698
|
+
const perm = API_PERMISSION[api];
|
|
1699
|
+
if (declaredPermSet.has(perm)) continue;
|
|
1700
|
+
const base = `Code calls chrome.${api} but "${perm}" is not in permissions`;
|
|
1701
|
+
if (HARD_APIS.has(api)) result.errors.push(`${base} — chrome.${api} is undefined without it and will crash the context at runtime.`);
|
|
1702
|
+
else result.warnings.push(`${base}; it may be undefined at runtime — add "${perm}" if you use it.`);
|
|
1703
|
+
}
|
|
1350
1704
|
if (!chromiumManifest.manifest_version) result.errors.push('Missing manifest_version. Use "chromium:manifest_version": 3 and "firefox:manifest_version": 2 for cross-browser support.');
|
|
1351
1705
|
const declaredPerms = [
|
|
1352
1706
|
...chromiumManifest.permissions ?? [],
|
|
@@ -1405,8 +1759,11 @@ async function manifest_validate_handler(args) {
|
|
|
1405
1759
|
surfaces: t.surfaces
|
|
1406
1760
|
}));
|
|
1407
1761
|
} catch {}
|
|
1408
|
-
|
|
1409
|
-
return JSON.stringify(
|
|
1762
|
+
result.valid = 0 === result.errors.length && Object.values(result.browserSupport).every((b)=>b.supported);
|
|
1763
|
+
return JSON.stringify({
|
|
1764
|
+
...result,
|
|
1765
|
+
buildBlocking: result.errors.length > 0
|
|
1766
|
+
});
|
|
1410
1767
|
}
|
|
1411
1768
|
const inspect_schema = {
|
|
1412
1769
|
name: "extension_inspect",
|
|
@@ -1525,9 +1882,46 @@ async function inspect_handler(args) {
|
|
|
1525
1882
|
const sourcemapSize = byType.sourcemap?.size ?? 0;
|
|
1526
1883
|
const buildType = sourcemapSize > 0 ? "development" : "production";
|
|
1527
1884
|
const shippableSize = totalSize - sourcemapSize;
|
|
1885
|
+
const sizeByPath = new Map(files.map((f)=>[
|
|
1886
|
+
f.path,
|
|
1887
|
+
f.size
|
|
1888
|
+
]));
|
|
1889
|
+
const entrypoints = [];
|
|
1890
|
+
const addEntry = (role, ref)=>{
|
|
1891
|
+
if ("string" != typeof ref) return;
|
|
1892
|
+
const size = sizeByPath.get(ref.replace(/^\.?\//, ""));
|
|
1893
|
+
entrypoints.push({
|
|
1894
|
+
role,
|
|
1895
|
+
path: ref,
|
|
1896
|
+
present: void 0 !== size,
|
|
1897
|
+
...void 0 !== size ? {
|
|
1898
|
+
sizeFormatted: formatBytes(size)
|
|
1899
|
+
} : {}
|
|
1900
|
+
});
|
|
1901
|
+
};
|
|
1902
|
+
const bg = manifest.background;
|
|
1903
|
+
if (bg?.service_worker) addEntry("background.service_worker", bg.service_worker);
|
|
1904
|
+
if (Array.isArray(bg?.scripts)) bg.scripts.forEach((s)=>addEntry("background.scripts", s));
|
|
1905
|
+
const actionField = manifest.action || manifest.browser_action;
|
|
1906
|
+
if (actionField?.default_popup) addEntry("action.default_popup", actionField.default_popup);
|
|
1907
|
+
const contentScripts = manifest.content_scripts;
|
|
1908
|
+
if (Array.isArray(contentScripts)) contentScripts.forEach((c, i)=>{
|
|
1909
|
+
if (Array.isArray(c.js)) c.js.forEach((j)=>addEntry(`content_scripts[${i}].js`, j));
|
|
1910
|
+
if (Array.isArray(c.css)) c.css.forEach((s)=>addEntry(`content_scripts[${i}].css`, s));
|
|
1911
|
+
});
|
|
1912
|
+
const PROMO_RE = /(screenshot|promo|marquee|tile|banner|preview)[-_.]?\d*\.(png|jpe?g|webp|gif)$/i;
|
|
1913
|
+
const sizeWarnings = [];
|
|
1914
|
+
for (const f of files)if ("sourcemap" !== f.type) {
|
|
1915
|
+
if (PROMO_RE.test(f.path)) sizeWarnings.push(`${f.path} (${formatBytes(f.size)}) looks like a store-listing promo image shipped inside the extension package — move it out of the bundled sources so it does not inflate the store zip.`);
|
|
1916
|
+
else if ("image" === f.type && !f.path.includes("icon") && f.size > 51200 && shippableSize > 0 && f.size / shippableSize > 0.25) sizeWarnings.push(`${f.path} (${formatBytes(f.size)}) is ${Math.round(f.size / shippableSize * 100)}% of the shipped bundle — unusually large for a shipped asset.`);
|
|
1917
|
+
}
|
|
1528
1918
|
const result = {
|
|
1529
1919
|
browser,
|
|
1530
1920
|
distPath,
|
|
1921
|
+
entrypoints,
|
|
1922
|
+
...sizeWarnings.length ? {
|
|
1923
|
+
sizeWarnings
|
|
1924
|
+
} : {},
|
|
1531
1925
|
buildType,
|
|
1532
1926
|
totalSize,
|
|
1533
1927
|
totalSizeFormatted: formatBytes(totalSize),
|
|
@@ -1567,6 +1961,7 @@ async function inspect_handler(args) {
|
|
|
1567
1961
|
hasManifest: node_fs.existsSync(manifestPath),
|
|
1568
1962
|
hasIcons: files.some((f)=>"image" === f.type && f.path.includes("icon")),
|
|
1569
1963
|
noSourceMaps: !files.some((f)=>"sourcemap" === f.type),
|
|
1964
|
+
noPromoAssets: !files.some((f)=>PROMO_RE.test(f.path)),
|
|
1570
1965
|
under10MB: totalSize < 10485760
|
|
1571
1966
|
}
|
|
1572
1967
|
};
|
|
@@ -2027,66 +2422,6 @@ function isCdpEndpoint(port) {
|
|
|
2027
2422
|
});
|
|
2028
2423
|
});
|
|
2029
2424
|
}
|
|
2030
|
-
function contractSightings(projectPath) {
|
|
2031
|
-
const root = node_path.resolve(projectPath, "dist", "extension-js");
|
|
2032
|
-
let dirs;
|
|
2033
|
-
try {
|
|
2034
|
-
dirs = node_fs.readdirSync(root);
|
|
2035
|
-
} catch {
|
|
2036
|
-
return [];
|
|
2037
|
-
}
|
|
2038
|
-
const sightings = [];
|
|
2039
|
-
for (const dir of dirs){
|
|
2040
|
-
const readyPath = node_path.join(root, dir, "ready.json");
|
|
2041
|
-
try {
|
|
2042
|
-
const stat = node_fs.statSync(readyPath);
|
|
2043
|
-
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
2044
|
-
if (contract?.status !== "ready") continue;
|
|
2045
|
-
sightings.push({
|
|
2046
|
-
browser: dir,
|
|
2047
|
-
mtimeMs: stat.mtimeMs,
|
|
2048
|
-
pid: "number" == typeof contract.pid ? contract.pid : void 0
|
|
2049
|
-
});
|
|
2050
|
-
} catch {}
|
|
2051
|
-
}
|
|
2052
|
-
return sightings;
|
|
2053
|
-
}
|
|
2054
|
-
function pidAlive(pid) {
|
|
2055
|
-
try {
|
|
2056
|
-
process.kill(pid, 0);
|
|
2057
|
-
return true;
|
|
2058
|
-
} catch {
|
|
2059
|
-
return false;
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
function knownSessionBrowsers(projectPath) {
|
|
2063
|
-
const resolved = node_path.resolve(projectPath);
|
|
2064
|
-
const browsers = [];
|
|
2065
|
-
for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) browsers.push(session.browser);
|
|
2066
|
-
for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
|
|
2067
|
-
return Array.from(new Set(browsers));
|
|
2068
|
-
}
|
|
2069
|
-
function resolveSessionBrowser(projectPath, explicit, fallback = "chromium") {
|
|
2070
|
-
if (explicit) return {
|
|
2071
|
-
browser: explicit,
|
|
2072
|
-
source: "explicit"
|
|
2073
|
-
};
|
|
2074
|
-
const resolved = node_path.resolve(projectPath);
|
|
2075
|
-
const mine = listSessions().filter((s)=>node_path.resolve(s.projectPath) === resolved);
|
|
2076
|
-
if (mine.length > 0) return {
|
|
2077
|
-
browser: mine[mine.length - 1].browser,
|
|
2078
|
-
source: "session"
|
|
2079
|
-
};
|
|
2080
|
-
const sightings = contractSightings(projectPath).filter((s)=>void 0 === s.pid || pidAlive(s.pid)).sort((a, b)=>b.mtimeMs - a.mtimeMs);
|
|
2081
|
-
if (sightings.length > 0) return {
|
|
2082
|
-
browser: sightings[0].browser,
|
|
2083
|
-
source: "contract"
|
|
2084
|
-
};
|
|
2085
|
-
return {
|
|
2086
|
-
browser: fallback,
|
|
2087
|
-
source: "fallback"
|
|
2088
|
-
};
|
|
2089
|
-
}
|
|
2090
2425
|
const source_inspect_schema = {
|
|
2091
2426
|
name: "extension_source_inspect",
|
|
2092
2427
|
description: "Inspect a running extension's live state via Chrome DevTools Protocol: full HTML (with shadow DOM), DOM structure, content script injection, console messages, and CSS selector queries. Requires an active dev or start session.",
|
|
@@ -2157,8 +2492,8 @@ async function source_inspect_handler(args) {
|
|
|
2157
2492
|
]);
|
|
2158
2493
|
const maxBytes = args.maxBytes ?? 262144;
|
|
2159
2494
|
if (!isChromiumFamily(browser)) return JSON.stringify({
|
|
2160
|
-
error: `Source inspection
|
|
2161
|
-
hint:
|
|
2495
|
+
error: `Source inspection reads the live DOM over Chrome DevTools Protocol, which ${browser} (Gecko) does not expose. This is a capability limit, not a missing session.`,
|
|
2496
|
+
hint: `For the ${browser} session, read runtime state with extension_logs, or extension_eval (context: "content"/"page") which works against Firefox over the control channel. To get CDP DOM inspection, run a Chromium-family dev session (extension_dev with browser: "chrome") in parallel.`
|
|
2162
2497
|
});
|
|
2163
2498
|
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
2164
2499
|
if (!resolved) return JSON.stringify({
|
|
@@ -2634,10 +2969,13 @@ async function logs_handler(args) {
|
|
|
2634
2969
|
return readFromFile(args, browser, limit);
|
|
2635
2970
|
}
|
|
2636
2971
|
function toMcpSpeak(text) {
|
|
2637
|
-
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(/--browser[= ]([\w-]+)/g, 'browser: "$1"').replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev");
|
|
2972
|
+
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(/--context[= ]([\w-]+)/g, 'context: "$1"').replace(/--tab[= ](\d+)/g, "tab: $1").replace(/--url[= ](\S+)/g, 'url: "$1"').replace(/--browser[= ]([\w-]+)/g, 'browser: "$1"').replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev");
|
|
2638
2973
|
}
|
|
2639
2974
|
function withSessionContext(message, projectPath) {
|
|
2640
|
-
|
|
2975
|
+
const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
|
|
2976
|
+
if (!isControlError) return message;
|
|
2977
|
+
const dead = deadReadySession(projectPath);
|
|
2978
|
+
if (dead) return `${message}\nLikely cause: the dev server has exited — ${dead.browser} ready.json still says ready but its pid ${dead.pid} is dead. Restart with extension_dev (this is not an allowControl problem); extension_doctor confirms.`;
|
|
2641
2979
|
const running = knownSessionBrowsers(projectPath);
|
|
2642
2980
|
if (0 === running.length) return message;
|
|
2643
2981
|
return `${message} Active session browser(s) for this project: ${running.join(", ")} — pass that as \`browser\`, or restart it via extension_dev with allowControl: true if the control channel is off.`;
|
|
@@ -2878,6 +3216,64 @@ async function reload_handler(args) {
|
|
|
2878
3216
|
})
|
|
2879
3217
|
], args.projectPath, args.timeout);
|
|
2880
3218
|
}
|
|
3219
|
+
async function navigateToUrl(projectPath, browser, url) {
|
|
3220
|
+
if (!isChromiumFamily(browser)) return JSON.stringify({
|
|
3221
|
+
ok: false,
|
|
3222
|
+
error: {
|
|
3223
|
+
name: "Unsupported",
|
|
3224
|
+
message: `URL navigation drives a tab over Chrome DevTools Protocol, which ${browser} (Gecko) does not expose. On Firefox, drive the page via extension_eval (context: "page"/"content") or read extension_logs.`
|
|
3225
|
+
}
|
|
3226
|
+
});
|
|
3227
|
+
const resolved = await resolveCdpPort(projectPath, browser);
|
|
3228
|
+
if (!resolved) return JSON.stringify({
|
|
3229
|
+
ok: false,
|
|
3230
|
+
error: {
|
|
3231
|
+
name: "NoSession",
|
|
3232
|
+
message: `No active dev session / CDP port for ${browser}. Start extension_dev and extension_wait for ready. ${CDP_PORT_MISSING_HINT}`
|
|
3233
|
+
}
|
|
3234
|
+
});
|
|
3235
|
+
const cdp = new CDPClient();
|
|
3236
|
+
try {
|
|
3237
|
+
const targets = await CDPClient.discoverTargets(resolved.port);
|
|
3238
|
+
const pageTargets = targets.filter((t)=>"page" === t.type && !String(t.url || "").startsWith("devtools://"));
|
|
3239
|
+
if (0 === pageTargets.length) return JSON.stringify({
|
|
3240
|
+
ok: false,
|
|
3241
|
+
error: {
|
|
3242
|
+
name: "NoTab",
|
|
3243
|
+
message: "The dev browser has no open page tab to navigate. Trigger one (e.g. extension_open surface, or open the extension) first."
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
3246
|
+
const target = pageTargets[0];
|
|
3247
|
+
const browserWsUrl = await CDPClient.discoverBrowserWsUrl(resolved.port);
|
|
3248
|
+
await cdp.connect(browserWsUrl);
|
|
3249
|
+
const sessionId = await cdp.attachToTarget(String(target.id));
|
|
3250
|
+
await cdp.navigate(sessionId, url);
|
|
3251
|
+
await new Promise((r)=>setTimeout(r, 1200));
|
|
3252
|
+
const meta = await cdp.getPageMeta(sessionId).catch(()=>({}));
|
|
3253
|
+
return JSON.stringify({
|
|
3254
|
+
ok: true,
|
|
3255
|
+
navigated: url,
|
|
3256
|
+
tab: {
|
|
3257
|
+
id: target.id,
|
|
3258
|
+
title: meta.title,
|
|
3259
|
+
url: meta.url || url
|
|
3260
|
+
},
|
|
3261
|
+
hint: "Inspect it with extension_dom_inspect or extension_source_inspect (context: 'page')."
|
|
3262
|
+
});
|
|
3263
|
+
} catch (e) {
|
|
3264
|
+
return JSON.stringify({
|
|
3265
|
+
ok: false,
|
|
3266
|
+
error: {
|
|
3267
|
+
name: "NavigateError",
|
|
3268
|
+
message: e instanceof Error ? e.message : String(e)
|
|
3269
|
+
}
|
|
3270
|
+
});
|
|
3271
|
+
} finally{
|
|
3272
|
+
try {
|
|
3273
|
+
cdp.disconnect();
|
|
3274
|
+
} catch {}
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
2881
3277
|
const open_schema = {
|
|
2882
3278
|
name: "extension_open",
|
|
2883
3279
|
description: "Open an extension surface or replay an event in a running session. 'popup'/'options'/'sidebar' open UI surfaces. 'action' triggers the toolbar action: opens the action's popup, or (no popup) replays chrome.action.onClicked. 'command' replays a chrome.commands.onCommand keyboard shortcut (pass `name`). NOTE: action/command replay invokes your listener WITHOUT a user gesture, so the gesture-derived activeTab grant does not apply (the result includes gesture:false and a warning when activeTab is declared). Requires the dev session to be started with allowControl: true (extension_dev). Wraps `extension open`.",
|
|
@@ -2903,6 +3299,10 @@ const open_schema = {
|
|
|
2903
3299
|
type: "string",
|
|
2904
3300
|
description: "For surface 'command': the chrome.commands name to trigger."
|
|
2905
3301
|
},
|
|
3302
|
+
url: {
|
|
3303
|
+
type: "string",
|
|
3304
|
+
description: "Navigate a real tab to this URL (Chromium only, via CDP) instead of opening a surface. Use for content-script/webNavigation test pages, or the popup as a page: chrome-extension://<id>/popup.html. Alternative to `surface`."
|
|
3305
|
+
},
|
|
2906
3306
|
browser: {
|
|
2907
3307
|
type: "string",
|
|
2908
3308
|
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
@@ -2913,13 +3313,20 @@ const open_schema = {
|
|
|
2913
3313
|
}
|
|
2914
3314
|
},
|
|
2915
3315
|
required: [
|
|
2916
|
-
"projectPath"
|
|
2917
|
-
"surface"
|
|
3316
|
+
"projectPath"
|
|
2918
3317
|
]
|
|
2919
3318
|
}
|
|
2920
3319
|
};
|
|
2921
3320
|
async function open_handler(args) {
|
|
2922
3321
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
3322
|
+
if (args.url) return navigateToUrl(args.projectPath, browser, args.url);
|
|
3323
|
+
if (!args.surface) return JSON.stringify({
|
|
3324
|
+
ok: false,
|
|
3325
|
+
error: {
|
|
3326
|
+
name: "BadRequest",
|
|
3327
|
+
message: "Pass `surface` (popup/options/sidebar/action/command) to open a surface, or `url` to navigate a tab."
|
|
3328
|
+
}
|
|
3329
|
+
});
|
|
2923
3330
|
const cli = [
|
|
2924
3331
|
"open",
|
|
2925
3332
|
args.surface,
|
|
@@ -3526,6 +3933,14 @@ async function deploy_handler(args) {
|
|
|
3526
3933
|
});
|
|
3527
3934
|
}
|
|
3528
3935
|
const SAFE_CEILING_MS = 50000;
|
|
3936
|
+
function wait_isAlive(pid) {
|
|
3937
|
+
try {
|
|
3938
|
+
process.kill(pid, 0);
|
|
3939
|
+
return true;
|
|
3940
|
+
} catch {
|
|
3941
|
+
return false;
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3529
3944
|
const wait_schema = {
|
|
3530
3945
|
name: "extension_wait",
|
|
3531
3946
|
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status. A single call is bounded to ~50s to stay under the MCP client request timeout; if it returns status:'timeout', call it again to keep waiting (polling resumes on the same contract).",
|
|
@@ -3563,18 +3978,27 @@ async function wait_handler(args) {
|
|
|
3563
3978
|
try {
|
|
3564
3979
|
const raw = node_fs.readFileSync(readyPath, "utf8");
|
|
3565
3980
|
const contract = JSON.parse(raw);
|
|
3566
|
-
if ("ready" === contract.status)
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3981
|
+
if ("ready" === contract.status) {
|
|
3982
|
+
if ("number" == typeof contract.pid && !wait_isAlive(contract.pid)) return JSON.stringify({
|
|
3983
|
+
status: "stale",
|
|
3984
|
+
message: `ready.json reports ready but its dev-server pid ${contract.pid} is dead — the session exited. Restart with extension_dev; extension_doctor will confirm.`,
|
|
3985
|
+
browser: contract.browser,
|
|
3986
|
+
pid: contract.pid,
|
|
3987
|
+
waitDuration: Date.now() - start
|
|
3988
|
+
});
|
|
3989
|
+
return JSON.stringify({
|
|
3990
|
+
status: "ready",
|
|
3991
|
+
command: contract.command,
|
|
3992
|
+
browser: contract.browser,
|
|
3993
|
+
port: contract.port,
|
|
3994
|
+
pid: contract.pid,
|
|
3995
|
+
distPath: contract.distPath,
|
|
3996
|
+
manifestPath: contract.manifestPath,
|
|
3997
|
+
compiledAt: contract.compiledAt,
|
|
3998
|
+
startedAt: contract.startedAt,
|
|
3999
|
+
waitDuration: Date.now() - start
|
|
4000
|
+
});
|
|
4001
|
+
}
|
|
3578
4002
|
if ("error" === contract.status) return JSON.stringify({
|
|
3579
4003
|
status: "error",
|
|
3580
4004
|
message: contract.message,
|
|
@@ -4711,6 +5135,14 @@ async function detect_browsers_handler(args) {
|
|
|
4711
5135
|
hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}.${missing.some((d)=>MANAGED_INSTALLABLE.has(d.browser)) ? ` Use extension_install_browser to install ${missing.filter((d)=>MANAGED_INSTALLABLE.has(d.browser)).map((d)=>d.browser).join(", ")}.` : ""}` : "All requested browsers are available."
|
|
4712
5136
|
});
|
|
4713
5137
|
}
|
|
5138
|
+
function doctor_readReadyContract(projectPath, browser) {
|
|
5139
|
+
try {
|
|
5140
|
+
const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
|
|
5141
|
+
return JSON.parse(raw);
|
|
5142
|
+
} catch {
|
|
5143
|
+
return null;
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
4714
5146
|
const doctor_schema = {
|
|
4715
5147
|
name: "extension_doctor",
|
|
4716
5148
|
description: "Diagnose a dev session end-to-end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, and browser liveness. Returns one {check, status, detail, remediation?} entry per leg in dependency order — a 'skip' names the check that blocked it and is NOT a pass. Run this first when any act tool (storage/reload/eval/open) errors unexpectedly. Wraps `extension doctor`. Call with no projectPath for a pre-flight environment check (node, extension CLI, template cache) before any project exists.",
|
|
@@ -4764,6 +5196,37 @@ async function environmentPreflight() {
|
|
|
4764
5196
|
hint: "Pass projectPath to diagnose a live dev session end-to-end."
|
|
4765
5197
|
});
|
|
4766
5198
|
}
|
|
5199
|
+
function recentErrorLogs(projectPath, browser, max = 5) {
|
|
5200
|
+
const file = node_path.resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
|
|
5201
|
+
let lines;
|
|
5202
|
+
try {
|
|
5203
|
+
lines = node_fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
5204
|
+
} catch {
|
|
5205
|
+
return [];
|
|
5206
|
+
}
|
|
5207
|
+
const errs = [];
|
|
5208
|
+
for (const line of lines){
|
|
5209
|
+
let ev;
|
|
5210
|
+
try {
|
|
5211
|
+
ev = JSON.parse(line);
|
|
5212
|
+
} catch {
|
|
5213
|
+
continue;
|
|
5214
|
+
}
|
|
5215
|
+
if (ev && "error" === ev.level) {
|
|
5216
|
+
const msg = Array.isArray(ev.args) ? ev.args.join(" ") : ev.message || ev.text || "";
|
|
5217
|
+
if (msg) errs.push(String(msg).slice(0, 300));
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
return errs.slice(-max);
|
|
5221
|
+
}
|
|
5222
|
+
function projectEngineVersion(projectPath) {
|
|
5223
|
+
try {
|
|
5224
|
+
const p = node_path.resolve(projectPath, "node_modules", "extension", "package.json");
|
|
5225
|
+
return JSON.parse(node_fs.readFileSync(p, "utf8")).version || null;
|
|
5226
|
+
} catch {
|
|
5227
|
+
return null;
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
4767
5230
|
async function doctor_handler(args) {
|
|
4768
5231
|
if (!args.projectPath) return environmentPreflight();
|
|
4769
5232
|
const projectPath = args.projectPath;
|
|
@@ -4786,9 +5249,48 @@ async function doctor_handler(args) {
|
|
|
4786
5249
|
if ("string" == typeof check.detail) check.detail = toMcpSpeak(check.detail);
|
|
4787
5250
|
if ("string" == typeof check.remediation) check.remediation = toMcpSpeak(check.remediation);
|
|
4788
5251
|
}
|
|
5252
|
+
let healthy = 0 === code;
|
|
5253
|
+
const contract = doctor_readReadyContract(projectPath, browser);
|
|
5254
|
+
if (contract?.status === "error") {
|
|
5255
|
+
healthy = false;
|
|
5256
|
+
const detail = contract.errors && contract.errors.length ? contract.errors.join("; ") : contract.message || "The dev session recorded status: error in ready.json.";
|
|
5257
|
+
checks.push({
|
|
5258
|
+
check: "runtime-errors",
|
|
5259
|
+
status: "fail",
|
|
5260
|
+
detail: toMcpSpeak(detail),
|
|
5261
|
+
remediation: "The build or extension load failed. Fix the reported error, let the dev server recompile, then re-run doctor."
|
|
5262
|
+
});
|
|
5263
|
+
} else {
|
|
5264
|
+
const errs = recentErrorLogs(projectPath, browser);
|
|
5265
|
+
if (errs.length) {
|
|
5266
|
+
healthy = false;
|
|
5267
|
+
checks.push({
|
|
5268
|
+
check: "runtime-errors",
|
|
5269
|
+
status: "fail",
|
|
5270
|
+
detail: `Recent error-level logs: ${errs.join(" | ")}`,
|
|
5271
|
+
remediation: "The extension is throwing at runtime. Inspect with extension_logs; a missing permission for a called chrome.* API is a common cause (extension_manifest_validate now checks this)."
|
|
5272
|
+
});
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
const engineVersion = projectEngineVersion(projectPath);
|
|
5276
|
+
if (engineVersion) {
|
|
5277
|
+
const pin = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
|
|
5278
|
+
const mismatch = "" !== pin && "latest" !== pin && !engineVersion.includes(pin);
|
|
5279
|
+
checks.push({
|
|
5280
|
+
check: "project-engine",
|
|
5281
|
+
status: mismatch ? "warn" : "pass",
|
|
5282
|
+
detail: `project-local extension@${engineVersion}${mismatch ? ` — but EXTENSION_MCP_CLI_VERSION=${pin}; the dev loop uses the project bin, not the pin` : ""}`,
|
|
5283
|
+
...mismatch ? {
|
|
5284
|
+
remediation: `Run \`(cd ${projectPath} && npm i -D extension@${pin})\` to match the pinned engine.`
|
|
5285
|
+
} : {}
|
|
5286
|
+
});
|
|
5287
|
+
}
|
|
4789
5288
|
return JSON.stringify({
|
|
4790
5289
|
browser,
|
|
4791
|
-
|
|
5290
|
+
...engineVersion ? {
|
|
5291
|
+
engineVersion
|
|
5292
|
+
} : {},
|
|
5293
|
+
healthy,
|
|
4792
5294
|
checks
|
|
4793
5295
|
});
|
|
4794
5296
|
} catch {
|
|
@@ -4835,6 +5337,56 @@ function checkPrimitive(path, value, schema, issues) {
|
|
|
4835
5337
|
});
|
|
4836
5338
|
}
|
|
4837
5339
|
}
|
|
5340
|
+
const ARG_ALIASES = {
|
|
5341
|
+
projectPath: [
|
|
5342
|
+
"path",
|
|
5343
|
+
"dir",
|
|
5344
|
+
"projectDir",
|
|
5345
|
+
"cwd"
|
|
5346
|
+
],
|
|
5347
|
+
projectName: [
|
|
5348
|
+
"name"
|
|
5349
|
+
],
|
|
5350
|
+
parentDir: [
|
|
5351
|
+
"parent",
|
|
5352
|
+
"into"
|
|
5353
|
+
],
|
|
5354
|
+
slug: [
|
|
5355
|
+
"template",
|
|
5356
|
+
"templateSlug"
|
|
5357
|
+
],
|
|
5358
|
+
expression: [
|
|
5359
|
+
"code",
|
|
5360
|
+
"js",
|
|
5361
|
+
"script"
|
|
5362
|
+
],
|
|
5363
|
+
manifestPath: [
|
|
5364
|
+
"manifest"
|
|
5365
|
+
],
|
|
5366
|
+
surface: [
|
|
5367
|
+
"view",
|
|
5368
|
+
"target"
|
|
5369
|
+
]
|
|
5370
|
+
};
|
|
5371
|
+
function normalizeArgAliases(inputSchema, args) {
|
|
5372
|
+
const schema = inputSchema;
|
|
5373
|
+
const props = schema.properties ?? {};
|
|
5374
|
+
const out = {
|
|
5375
|
+
...args
|
|
5376
|
+
};
|
|
5377
|
+
for (const [canonical, aliases] of Object.entries(ARG_ALIASES))if (canonical in props) {
|
|
5378
|
+
if (void 0 === out[canonical]) {
|
|
5379
|
+
for (const alias of aliases)if (!(alias in props)) {
|
|
5380
|
+
if (void 0 !== out[alias]) {
|
|
5381
|
+
out[canonical] = out[alias];
|
|
5382
|
+
delete out[alias];
|
|
5383
|
+
break;
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
return out;
|
|
5389
|
+
}
|
|
4838
5390
|
function validateToolInput(inputSchema, args) {
|
|
4839
5391
|
const schema = inputSchema;
|
|
4840
5392
|
const issues = [];
|
|
@@ -4933,7 +5485,8 @@ async function startServer() {
|
|
|
4933
5485
|
],
|
|
4934
5486
|
isError: true
|
|
4935
5487
|
};
|
|
4936
|
-
const
|
|
5488
|
+
const normalizedArgs = normalizeArgAliases(tool.schema.inputSchema, args ?? {});
|
|
5489
|
+
const issues = validateToolInput(tool.schema.inputSchema, normalizedArgs);
|
|
4937
5490
|
if (issues.length) return {
|
|
4938
5491
|
content: [
|
|
4939
5492
|
{
|
|
@@ -4944,7 +5497,7 @@ async function startServer() {
|
|
|
4944
5497
|
isError: true
|
|
4945
5498
|
};
|
|
4946
5499
|
try {
|
|
4947
|
-
const result = await tool.handler(
|
|
5500
|
+
const result = await tool.handler(normalizedArgs);
|
|
4948
5501
|
return {
|
|
4949
5502
|
content: [
|
|
4950
5503
|
{
|
|
@@ -3,4 +3,8 @@ export interface ResolvedBrowser {
|
|
|
3
3
|
source: "explicit" | "session" | "contract" | "fallback";
|
|
4
4
|
}
|
|
5
5
|
export declare function knownSessionBrowsers(projectPath: string): string[];
|
|
6
|
+
export declare function deadReadySession(projectPath: string): {
|
|
7
|
+
browser: string;
|
|
8
|
+
pid: number;
|
|
9
|
+
} | null;
|
|
6
10
|
export declare function resolveSessionBrowser(projectPath: string, explicit: string | undefined, fallback?: string): ResolvedBrowser;
|
|
@@ -2,5 +2,6 @@ export interface InputIssue {
|
|
|
2
2
|
path: string;
|
|
3
3
|
message: string;
|
|
4
4
|
}
|
|
5
|
+
export declare function normalizeArgAliases(inputSchema: Record<string, unknown>, args: Record<string, unknown>): Record<string, unknown>;
|
|
5
6
|
export declare function validateToolInput(inputSchema: Record<string, unknown>, args: Record<string, unknown>): InputIssue[];
|
|
6
7
|
export declare function inputValidationError(toolName: string, issues: InputIssue[]): string;
|
|
@@ -8,6 +8,10 @@ export declare const schema: {
|
|
|
8
8
|
type: string;
|
|
9
9
|
description: string;
|
|
10
10
|
};
|
|
11
|
+
projectPath: {
|
|
12
|
+
type: string;
|
|
13
|
+
description: string;
|
|
14
|
+
};
|
|
11
15
|
browsers: {
|
|
12
16
|
type: string;
|
|
13
17
|
items: {
|
|
@@ -16,11 +20,17 @@ export declare const schema: {
|
|
|
16
20
|
default: string[];
|
|
17
21
|
description: string;
|
|
18
22
|
};
|
|
23
|
+
browser: {
|
|
24
|
+
type: string;
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
19
27
|
};
|
|
20
|
-
required:
|
|
28
|
+
required: never[];
|
|
21
29
|
};
|
|
22
30
|
};
|
|
23
31
|
export declare function handler(args: {
|
|
24
|
-
manifestPath
|
|
32
|
+
manifestPath?: string;
|
|
33
|
+
projectPath?: string;
|
|
34
|
+
browser?: string;
|
|
25
35
|
browsers?: string[];
|
|
26
36
|
}): Promise<string>;
|
package/dist/src/tools/open.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export declare const schema: {
|
|
|
18
18
|
type: string;
|
|
19
19
|
description: string;
|
|
20
20
|
};
|
|
21
|
+
url: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
21
25
|
browser: {
|
|
22
26
|
type: string;
|
|
23
27
|
description: string;
|
|
@@ -31,6 +35,7 @@ export declare const schema: {
|
|
|
31
35
|
};
|
|
32
36
|
};
|
|
33
37
|
export declare function handler(args: ActArgs & {
|
|
34
|
-
surface
|
|
38
|
+
surface?: string;
|
|
35
39
|
name?: string;
|
|
40
|
+
url?: string;
|
|
36
41
|
}): Promise<string>;
|
package/dist/src/tools/stop.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@extension.dev/mcp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.9.0",
|
|
5
5
|
"description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 31 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": "MIT",
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://extension.dev",
|
|
10
|
-
"version": "4.
|
|
10
|
+
"version": "4.9.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@extension.dev/mcp",
|
|
16
|
-
"version": "4.
|
|
16
|
+
"version": "4.9.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|