@extension.dev/mcp 10.2.0 → 10.3.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 +53 -0
- package/README.md +2 -2
- package/dist/module.d.ts +1 -1
- package/dist/module.js +184 -24
- package/dist/src/index.d.ts +2 -0
- package/dist/src/lib/artifacts-api.d.ts +1 -0
- package/dist/src/lib/profile-carryover.d.ts +4 -0
- package/dist/src/lib/types.d.ts +1 -0
- package/extensions/live-preview/chromium/action/index.html +0 -3
- package/extensions/live-preview/chromium/sandbox/page-0.html +0 -11
- 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 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.
|
|
13
|
+
"version": "10.3.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.
|
|
4
|
+
"version": "10.3.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cezar Augusto",
|
|
7
7
|
"email": "hello@extension.dev",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 10.3.1
|
|
4
|
+
|
|
5
|
+
The 10.3.0 tarball shipped two comments a stranger could read: an HTML
|
|
6
|
+
comment in the popup markup naming a private package, and an eight line
|
|
7
|
+
internal roadmap comment plus two inline script comments in the live
|
|
8
|
+
preview sandbox page. Both were removed at source right after that
|
|
9
|
+
release. This release exists to take them off the registry, because a
|
|
10
|
+
published tarball cannot be edited in place.
|
|
11
|
+
|
|
12
|
+
- The popup markup no longer names any private package.
|
|
13
|
+
- `extensions/live-preview/chromium/sandbox/page-0.html` ships exactly
|
|
14
|
+
the markup it runs, with no comments at all.
|
|
15
|
+
|
|
16
|
+
## 10.3.0
|
|
17
|
+
|
|
18
|
+
Four fixes for places where a tool answered confidently about something it
|
|
19
|
+
had not actually checked: a login refusal that never said where the slugs
|
|
20
|
+
come from, a doctor report labelled with a browser that had no session, a
|
|
21
|
+
preview probe that certified whatever was listening on the port, and a
|
|
22
|
+
release gate that passed on a stale bundle.
|
|
23
|
+
|
|
24
|
+
- `extension_auth login` refused a malformed `project` without saying
|
|
25
|
+
where the two slugs come from. Both the refusal and the input schema now
|
|
26
|
+
name the console address bar as the source: an existing project's page is
|
|
27
|
+
`console.extension.dev/<workspace>/<project>`, and a project that does
|
|
28
|
+
not exist yet is created at extension.dev/new first.
|
|
29
|
+
- `extension_doctor` diagnoses the session that exists, not a hardcoded
|
|
30
|
+
default. The shared session resolver only counts contracts marked
|
|
31
|
+
"ready", which is exactly wrong for the tool you reach for when a session
|
|
32
|
+
failed: doctor fell back to chrome, labelled the report with it, read the
|
|
33
|
+
absent contract, and returned a healthy verdict over a session that had
|
|
34
|
+
errored. When there is no ready session, the browser with a contract on
|
|
35
|
+
disk wins, newest first and whatever its status.
|
|
36
|
+
- `extension_preview_web` certifies the probe against the build the call
|
|
37
|
+
just minted instead of against the port. Anything can be listening on the
|
|
38
|
+
local preview port, and the old probe read `hostReachable` and
|
|
39
|
+
`previewLoadable` as true off whatever JSON came back, then echoed that
|
|
40
|
+
server's `identifier`, name and version into the envelope unmarked, which
|
|
41
|
+
made any local server a text channel into the agent's context. The name
|
|
42
|
+
and version must now match the `dist` manifest read off disk, and the
|
|
43
|
+
identifier is echoed only when it matches the derivation the real
|
|
44
|
+
middleware uses. A mismatch returns the status
|
|
45
|
+
`host-serving-different-artifact` with `previewLoadable: false`, and
|
|
46
|
+
anything the host claimed that could not be confirmed locally travels
|
|
47
|
+
clipped under `probe.hostReported`, named as the host's claim.
|
|
48
|
+
- The release gate asserts the version the built bundle exports rather than
|
|
49
|
+
grepping the bundle for the version string. The grep greenwashed: the
|
|
50
|
+
bundle carries the pinned engine version and fifty-two `"version":"1.0.0"`
|
|
51
|
+
strings from the template corpus, so a release cut at any of those
|
|
52
|
+
numbers passed the gate over a stale `dist`. `dist/module.js` now exports
|
|
53
|
+
the manifest version it inlined at build time, and the gate reads it back
|
|
54
|
+
and compares.
|
|
55
|
+
|
|
3
56
|
## 10.2.0
|
|
4
57
|
|
|
5
58
|
A production walk of the full share-and-publish loop, then a fix for every
|
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ cp node_modules/@extension.dev/mcp/claude/commands/*.md ~/my-extension/.claude/c
|
|
|
127
127
|
| platform | `extension_shares` | List every link you have shared, and revoke one permanently |
|
|
128
128
|
| platform | `extension_publish` | Publish a shareable preview to extension.dev |
|
|
129
129
|
| platform | `extension_release_promote` | Promote a build to a release channel, headless |
|
|
130
|
-
| platform | `extension_submit` | Submit for store review: Chrome, Firefox
|
|
130
|
+
| platform | `extension_submit` | Submit for store review: Chrome, Firefox and Edge, through extension.dev |
|
|
131
131
|
| platform | `extension_release_status` | Read release channels, recent builds, and store submission and review state |
|
|
132
132
|
|
|
133
133
|
Browser-launching tools (`dev`, `start`) shell out to the `extension` CLI, the project's own `node_modules/.bin/extension` when present, otherwise `npx extension@<pinned>` at the version this package is verified against; everything else runs in-process.
|
|
@@ -142,7 +142,7 @@ That is a different job from shipping. Use `share` for the build you are holding
|
|
|
142
142
|
|
|
143
143
|
## From preview to store
|
|
144
144
|
|
|
145
|
-
The platform tools connect agents to [extension.dev](https://extension.dev): `extension_auth` runs extension.dev's own device flow (you approve the code at [extension.dev/device](https://extension.dev/device), and GitHub is federated server-side, so no GitHub token ever reaches your machine) and stores a project-scoped token locally (never returned to the agent), `extension_publish` turns a build your project has already published into a shareable URL, and `extension_release_promote` promotes a tested build to a release channel from CI or an agent session, no browser required. `extension_submit` submits a built extension to the Chrome Web Store, Edge Add-ons, Firefox AMO
|
|
145
|
+
The platform tools connect agents to [extension.dev](https://extension.dev): `extension_auth` runs extension.dev's own device flow (you approve the code at [extension.dev/device](https://extension.dev/device), and GitHub is federated server-side, so no GitHub token ever reaches your machine) and stores a project-scoped token locally (never returned to the agent), `extension_publish` turns a build your project has already published into a shareable URL, and `extension_release_promote` promotes a tested build to a release channel from CI or an agent session, no browser required. `extension_submit` submits a built extension to the Chrome Web Store, Edge Add-ons, and Firefox AMO through extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI, it defaults to a dry run and store credentials are never tool arguments. Safari and the App Store are one paid lane on the platform, so a free workspace is refused there and the other three stores are unaffected. The two verbs are not interchangeable: `extension_publish` pushes to the extension.dev platform, `extension_submit` sends the build into a store's review queue, which is irreversible. After a real submission, `extension_release_status` reads the recorded outcome, per-store credential health, and review state from the project's public registry, so agents and CI can answer "was it approved?" without a console visit. Access tokens live at most 7 days; CI pipelines re-mint them from the console's Access tokens page.
|
|
146
146
|
|
|
147
147
|
## The extension.dev stack
|
|
148
148
|
|
package/dist/module.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { startServer, runCli } from "./src/index";
|
|
1
|
+
export { startServer, runCli, version } from "./src/index";
|
package/dist/module.js
CHANGED
|
@@ -582,7 +582,7 @@ __webpack_require__.d(wait_namespaceObject, {
|
|
|
582
582
|
handler: ()=>wait_handler,
|
|
583
583
|
schema: ()=>wait_schema
|
|
584
584
|
});
|
|
585
|
-
var package_namespaceObject = JSON.parse('{"rE":"10.
|
|
585
|
+
var package_namespaceObject = JSON.parse('{"rE":"10.3.1","El":{"OP":"4.0.20"}}');
|
|
586
586
|
const ENVELOPE_SCHEMA = 1;
|
|
587
587
|
const collectWarnings = (warnings)=>{
|
|
588
588
|
if (!warnings) return [];
|
|
@@ -3530,6 +3530,26 @@ async function pollBootVerdict(projectPath, browser, options) {
|
|
|
3530
3530
|
warnings: []
|
|
3531
3531
|
};
|
|
3532
3532
|
}
|
|
3533
|
+
const SYSTEM_PROFILE_ARG = "false";
|
|
3534
|
+
function holdsState(dir) {
|
|
3535
|
+
try {
|
|
3536
|
+
return node_fs.readdirSync(dir).length > 0;
|
|
3537
|
+
} catch {
|
|
3538
|
+
return false;
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
function profileCarriesTabsOver(projectPath, browser, profileArg) {
|
|
3542
|
+
const raw = "string" == typeof profileArg ? profileArg.trim() : "";
|
|
3543
|
+
if (raw === SYSTEM_PROFILE_ARG) return true;
|
|
3544
|
+
if (raw) return holdsState(node_path.resolve(projectPath, raw));
|
|
3545
|
+
return holdsState(node_path.join(browserProfileRootDir(projectPath, browser), PERSISTED_PROFILE_DIR_NAME));
|
|
3546
|
+
}
|
|
3547
|
+
function sessionProfileReused(projectPath, browser) {
|
|
3548
|
+
return findSessionInfo(projectPath, browser)?.profileReused === true;
|
|
3549
|
+
}
|
|
3550
|
+
function restoredTabWarning(url) {
|
|
3551
|
+
return `No target was given and this session runs on a browser profile that already held a previous session's state, so ${url} may be a tab restored from that previous session rather than anything this run opened. Nothing here proves the page belongs to the extension you just started: pass url, or open a surface with extension_open, and inspect again.`;
|
|
3552
|
+
}
|
|
3533
3553
|
function sweepCarriers(projectPaths) {
|
|
3534
3554
|
const out = [];
|
|
3535
3555
|
const seen = new Set();
|
|
@@ -3870,6 +3890,7 @@ function launchFlagArgs(args) {
|
|
|
3870
3890
|
if (args.extensions?.length) cli.push("--extensions", args.extensions.join(","));
|
|
3871
3891
|
return cli;
|
|
3872
3892
|
}
|
|
3893
|
+
const REUSED_PROFILE_NOTE = "This session reuses a browser profile that already held a previous session's state, so the browser may restore that session's tabs alongside the extension. extension_inspect with no target ranks whatever is open and can land on one of them; it flags this session as profileReused for that reason. Pass url, or open the surface you mean with extension_open, before reading anything.";
|
|
3873
3894
|
const dev_schema = {
|
|
3874
3895
|
name: "extension_dev",
|
|
3875
3896
|
description: "Run the extension while you edit it: dev build, hot module replacement, and a browser with the extension loaded. Reach for this first when the ask is \"run my extension\". ONLY this tool unlocks the control channel that extension_storage, extension_reload, extension_open and extension_dom_snapshot need (allowControl:true) and the eval channel that extension_eval needs (allowEval:true, which implies allowControl, so you never need to pass both). Use extension_start instead to run the production build in a browser. The result carries the process info that extension_wait and extension_inspect need.",
|
|
@@ -3954,6 +3975,7 @@ async function dev_handler(args) {
|
|
|
3954
3975
|
}
|
|
3955
3976
|
const carrier = args.carrier ? materializeCarrier(args.projectPath, browser) : null;
|
|
3956
3977
|
const allowControl = Boolean(args.allowControl || args.allowEval);
|
|
3978
|
+
const profileReused = profileCarriesTabsOver(args.projectPath, browser, args.profile);
|
|
3957
3979
|
const cliArgs = [
|
|
3958
3980
|
"dev",
|
|
3959
3981
|
args.projectPath,
|
|
@@ -3979,7 +4001,8 @@ async function dev_handler(args) {
|
|
|
3979
4001
|
port: args.port,
|
|
3980
4002
|
projectPath: args.projectPath,
|
|
3981
4003
|
command: "dev",
|
|
3982
|
-
noBrowser: Boolean(args.noBrowser)
|
|
4004
|
+
noBrowser: Boolean(args.noBrowser),
|
|
4005
|
+
profileReused
|
|
3983
4006
|
});
|
|
3984
4007
|
child.on("exit", ()=>{
|
|
3985
4008
|
removeSession(args.projectPath, browser, pid);
|
|
@@ -4104,7 +4127,8 @@ async function dev_handler(args) {
|
|
|
4104
4127
|
port: boundPort,
|
|
4105
4128
|
projectPath: args.projectPath,
|
|
4106
4129
|
command: "dev",
|
|
4107
|
-
noBrowser: Boolean(args.noBrowser)
|
|
4130
|
+
noBrowser: Boolean(args.noBrowser),
|
|
4131
|
+
profileReused
|
|
4108
4132
|
});
|
|
4109
4133
|
const portReport = null !== boundPort ? {
|
|
4110
4134
|
port: boundPort,
|
|
@@ -4138,6 +4162,7 @@ async function dev_handler(args) {
|
|
|
4138
4162
|
},
|
|
4139
4163
|
warnings: [
|
|
4140
4164
|
portNote,
|
|
4165
|
+
profileReused && !args.noBrowser ? REUSED_PROFILE_NOTE : null,
|
|
4141
4166
|
...boot.warnings
|
|
4142
4167
|
],
|
|
4143
4168
|
hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/dom_snapshot/eval) need a live browser and will not work against this session. When you are done, call extension_stop to shut down the dev server." : "Use extension_wait to check when the extension is fully loaded, then extension_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_snapshot need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser."
|
|
@@ -4192,6 +4217,7 @@ async function start_handler(args) {
|
|
|
4192
4217
|
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
4193
4218
|
cliArgs.push(...launchFlagArgs(args));
|
|
4194
4219
|
const stale = removeCarrier(args.projectPath);
|
|
4220
|
+
const profileReused = profileCarriesTabsOver(args.projectPath, browser, args.profile);
|
|
4195
4221
|
const spawnedAt = Date.now();
|
|
4196
4222
|
const spawned = spawnExtensionCli(cliArgs, {
|
|
4197
4223
|
projectDir: args.projectPath
|
|
@@ -4203,7 +4229,8 @@ async function start_handler(args) {
|
|
|
4203
4229
|
pid,
|
|
4204
4230
|
browser,
|
|
4205
4231
|
projectPath: args.projectPath,
|
|
4206
|
-
command
|
|
4232
|
+
command,
|
|
4233
|
+
profileReused
|
|
4207
4234
|
});
|
|
4208
4235
|
child.on("exit", ()=>{
|
|
4209
4236
|
removeSession(args.projectPath, browser, pid);
|
|
@@ -5742,6 +5769,7 @@ async function publish(options = {}) {
|
|
|
5742
5769
|
}
|
|
5743
5770
|
const ARTIFACT_ID = /^gen_(?:[0-9a-f]{32}|[0-9a-f]{64})$/;
|
|
5744
5771
|
const ARTIFACT_ID_CANDIDATE = /gen_[0-9a-f]+/;
|
|
5772
|
+
const ZIP_URL_REDIRECT_NOTE = "zipUrl does not serve the archive itself: it answers 302 with a short-lived presigned storage URL in Location. Follow redirects when you fetch it (curl -L; fetch and most HTTP clients already do), because a client that does not follow them reads 0 bytes and reports the share as empty when it is not.";
|
|
5745
5773
|
function parseArtifactRef(input) {
|
|
5746
5774
|
const raw = String(input ?? "").trim();
|
|
5747
5775
|
if (!raw) return null;
|
|
@@ -6305,6 +6333,14 @@ function safeHostBase(raw) {
|
|
|
6305
6333
|
base: trimmed
|
|
6306
6334
|
};
|
|
6307
6335
|
}
|
|
6336
|
+
const HOST_CLAIM_MAX_CHARS = 120;
|
|
6337
|
+
function clipHostClaim(value) {
|
|
6338
|
+
return value.length > HOST_CLAIM_MAX_CHARS ? value.slice(0, HOST_CLAIM_MAX_CHARS) : value;
|
|
6339
|
+
}
|
|
6340
|
+
function expectedPreviewIdentifier(name, distDir) {
|
|
6341
|
+
const base = (name ?? node_path.basename(distDir)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "extension";
|
|
6342
|
+
return base.startsWith("local-") ? base : `local-${base}`;
|
|
6343
|
+
}
|
|
6308
6344
|
const SURFACE = {
|
|
6309
6345
|
defaultOrigin: DEFAULT_PREVIEW_DEV_URL,
|
|
6310
6346
|
scheme: (encoded)=>`preview://build/${encoded}`,
|
|
@@ -6434,7 +6470,7 @@ async function buildShare(projectPath, distDir, manifest, browser, verifyInBrows
|
|
|
6434
6470
|
browserLoadable: null
|
|
6435
6471
|
},
|
|
6436
6472
|
record,
|
|
6437
|
-
note: "Anyone with this link can open the build you just made, running in the emulator. No install, no sign-in, no dev server. They can also download the whole build as a zip from zipUrl, so the link hands over the built code. It stays live until expiresAt; DELETE revokeUrl with the same token to kill it sooner, and a revoked link stays dead. revokeUrl is the handle that pulls this link early. Re-sharing an unchanged build returns this same link rather than a second one, and only a revoked link is replaced by a different one, so " + (record.recorded ? `it was also written to ${record.path} (record.path), which lists every share from this project.` : `keep it: ${record.note}`) + " To find this link again later, or to pull it back once it has left this conversation, run extension_shares: it lists every link this token has shared with its live or dead state, and revokes one by artifactId or by pasting any of its URLs." + (record.warning ? ` ${record.warning}` : "")
|
|
6473
|
+
note: "Anyone with this link can open the build you just made, running in the emulator. No install, no sign-in, no dev server. They can also download the whole build as a zip from zipUrl, so the link hands over the built code. It stays live until expiresAt; DELETE revokeUrl with the same token to kill it sooner, and a revoked link stays dead. revokeUrl is the handle that pulls this link early. Re-sharing an unchanged build returns this same link rather than a second one, and only a revoked link is replaced by a different one, so " + (record.recorded ? `it was also written to ${record.path} (record.path), which lists every share from this project.` : `keep it: ${record.note}`) + " To find this link again later, or to pull it back once it has left this conversation, run extension_shares: it lists every link this token has shared with its live or dead state, and revokes one by artifactId or by pasting any of its URLs." + (result.data.zipUrl ? ` ${ZIP_URL_REDIRECT_NOTE}` : "") + (record.warning ? ` ${record.warning}` : "")
|
|
6438
6474
|
};
|
|
6439
6475
|
}
|
|
6440
6476
|
function detectSurfaces(manifest) {
|
|
@@ -6651,7 +6687,14 @@ async function preview_web_handler(args) {
|
|
|
6651
6687
|
}
|
|
6652
6688
|
});
|
|
6653
6689
|
const contentType = res.headers.get("content-type") ?? "";
|
|
6654
|
-
|
|
6690
|
+
let raw = null;
|
|
6691
|
+
if (res.ok && contentType.includes("application/json")) try {
|
|
6692
|
+
raw = await res.json();
|
|
6693
|
+
} catch {
|
|
6694
|
+
raw = null;
|
|
6695
|
+
}
|
|
6696
|
+
const payload = raw && "object" == typeof raw && !Array.isArray(raw) ? raw : null;
|
|
6697
|
+
if (!payload) return envelope_envelope({
|
|
6655
6698
|
ok: true,
|
|
6656
6699
|
command: preview_web_COMMAND,
|
|
6657
6700
|
status: "host-not-serving-preview",
|
|
@@ -6670,7 +6713,62 @@ async function preview_web_handler(args) {
|
|
|
6670
6713
|
`${SURFACE.label} answered but not with a preview payload. On the deployed host ${SURFACE.fetchPath} does not exist, because it is dev-only. ${remedy}`
|
|
6671
6714
|
]
|
|
6672
6715
|
});
|
|
6673
|
-
const
|
|
6716
|
+
const localName = "string" == typeof manifest.name ? manifest.name : null;
|
|
6717
|
+
const localVersion = "string" == typeof manifest.version ? manifest.version : null;
|
|
6718
|
+
const remoteName = "string" == typeof payload.manifest?.name ? payload.manifest.name : null;
|
|
6719
|
+
const remoteVersion = "string" == typeof payload.version ? payload.version : null;
|
|
6720
|
+
const remoteIdentifier = "string" == typeof payload.identifier ? payload.identifier : null;
|
|
6721
|
+
const fileCount = Array.isArray(payload.files) ? payload.files.length : 0;
|
|
6722
|
+
const matchesDist = (null === localName || remoteName === localName) && (null === localVersion || remoteVersion === localVersion);
|
|
6723
|
+
if (!matchesDist) {
|
|
6724
|
+
const hostReported = {
|
|
6725
|
+
...null !== remoteIdentifier ? {
|
|
6726
|
+
identifier: clipHostClaim(remoteIdentifier)
|
|
6727
|
+
} : {},
|
|
6728
|
+
...null !== remoteName ? {
|
|
6729
|
+
name: clipHostClaim(remoteName)
|
|
6730
|
+
} : {},
|
|
6731
|
+
...null !== remoteVersion ? {
|
|
6732
|
+
version: clipHostClaim(remoteVersion)
|
|
6733
|
+
} : {}
|
|
6734
|
+
};
|
|
6735
|
+
return envelope_envelope({
|
|
6736
|
+
ok: true,
|
|
6737
|
+
command: preview_web_COMMAND,
|
|
6738
|
+
status: "host-serving-different-artifact",
|
|
6739
|
+
value: {
|
|
6740
|
+
...result,
|
|
6741
|
+
hostReachable: true,
|
|
6742
|
+
previewLoadable: false,
|
|
6743
|
+
probe: {
|
|
6744
|
+
method: "server-fetch",
|
|
6745
|
+
provesBrowserLoad: false,
|
|
6746
|
+
matchesDist: false,
|
|
6747
|
+
fileCount,
|
|
6748
|
+
...Object.keys(hostReported).length ? {
|
|
6749
|
+
hostReported
|
|
6750
|
+
} : {}
|
|
6751
|
+
}
|
|
6752
|
+
},
|
|
6753
|
+
hint,
|
|
6754
|
+
warnings: [
|
|
6755
|
+
...previewWarnings,
|
|
6756
|
+
`Something answered at ${hostBase}${SURFACE.fetchPath} but described a different artifact than the build this call minted, so previewLoadable stays false. probe.hostReported carries that host's own claims, clipped and unverified, not facts about your build; another local server on this port is the usual cause. ${remedy}`
|
|
6757
|
+
]
|
|
6758
|
+
});
|
|
6759
|
+
}
|
|
6760
|
+
const identifierConfirmed = null !== remoteIdentifier && remoteIdentifier === expectedPreviewIdentifier(localName, distDir);
|
|
6761
|
+
const unconfirmed = {
|
|
6762
|
+
...!identifierConfirmed && null !== remoteIdentifier ? {
|
|
6763
|
+
identifier: clipHostClaim(remoteIdentifier)
|
|
6764
|
+
} : {},
|
|
6765
|
+
...null === localName && null !== remoteName ? {
|
|
6766
|
+
name: clipHostClaim(remoteName)
|
|
6767
|
+
} : {},
|
|
6768
|
+
...null === localVersion && null !== remoteVersion ? {
|
|
6769
|
+
version: clipHostClaim(remoteVersion)
|
|
6770
|
+
} : {}
|
|
6771
|
+
};
|
|
6674
6772
|
return envelope_envelope({
|
|
6675
6773
|
ok: true,
|
|
6676
6774
|
command: preview_web_COMMAND,
|
|
@@ -6681,12 +6779,22 @@ async function preview_web_handler(args) {
|
|
|
6681
6779
|
previewLoadable: true,
|
|
6682
6780
|
previewLoadableLane: "local-dev-host",
|
|
6683
6781
|
probe: {
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6782
|
+
...identifierConfirmed ? {
|
|
6783
|
+
identifier: remoteIdentifier
|
|
6784
|
+
} : {},
|
|
6785
|
+
...null !== localName ? {
|
|
6786
|
+
loadedName: localName
|
|
6787
|
+
} : {},
|
|
6788
|
+
...null !== localVersion ? {
|
|
6789
|
+
loadedVersion: localVersion
|
|
6790
|
+
} : {},
|
|
6791
|
+
fileCount,
|
|
6688
6792
|
method: "server-fetch",
|
|
6689
|
-
provesBrowserLoad: false
|
|
6793
|
+
provesBrowserLoad: false,
|
|
6794
|
+
matchesDist: true,
|
|
6795
|
+
...Object.keys(unconfirmed).length ? {
|
|
6796
|
+
hostReported: unconfirmed
|
|
6797
|
+
} : {}
|
|
6690
6798
|
}
|
|
6691
6799
|
},
|
|
6692
6800
|
hint,
|
|
@@ -6930,6 +7038,7 @@ async function listShares(args) {
|
|
|
6930
7038
|
} : {},
|
|
6931
7039
|
status: localOnlyStatus(entry, completeness, liveFiltered, now)
|
|
6932
7040
|
}));
|
|
7041
|
+
const handsOutAZip = shares.some((share)=>Boolean(share.zipUrl)) || localOnly.some((entry)=>Boolean(entry.zipUrl));
|
|
6933
7042
|
const liveCount = shares.filter((share)=>share.live).length;
|
|
6934
7043
|
const ownership = {
|
|
6935
7044
|
project: shares.filter((s)=>"project" === s.attribution.ownership).length,
|
|
@@ -6966,6 +7075,7 @@ async function listShares(args) {
|
|
|
6966
7075
|
warnings: [
|
|
6967
7076
|
"previewUrl and zipUrl are null for a share that is no longer live, because a revoked or expired link cannot resolve for anyone. revokeUrl stays on every row. Revocation is permanent: a revoked id is burned, and re-sharing that build mints a different link. Re-sharing a build that was NOT revoked returns its existing link instead.",
|
|
6968
7077
|
"attribution.ownership says who the share belongs to and therefore who may revoke it: project means the owning workspace holds it and any member can pull it back, personal means one person holds it alone. attribution.credit names the publisher and is attribution only, granting and restricting nothing. A credit of \"CLI token ...\" means the platform could not resolve which human minted that token, and a credit of \"not recorded\" means it never knew; neither is a name, and neither should be reported as one.",
|
|
7078
|
+
handsOutAZip ? ZIP_URL_REDIRECT_NOTE : null,
|
|
6969
7079
|
truncatedNote
|
|
6970
7080
|
]
|
|
6971
7081
|
});
|
|
@@ -9172,6 +9282,10 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
9172
9282
|
if (jsLooking.length) probeWarning = `Probes are CSS selectors run through querySelectorAll against the live page, NOT JavaScript expressions. ${jsLooking.map((s)=>`"${s}"`).join(", ")} parsed as selectors and will match nothing. To evaluate JS, use extension_eval.`;
|
|
9173
9283
|
}
|
|
9174
9284
|
const urlFilter = args.url ?? ("string" == typeof value.meta?.url ? value.meta.url : void 0);
|
|
9285
|
+
if (!args.url && sessionProfileReused(args.projectPath, browser)) {
|
|
9286
|
+
result.profileReused = true;
|
|
9287
|
+
notes.push(restoredTabWarning(urlFilter ?? "the tab this read landed on"));
|
|
9288
|
+
}
|
|
9175
9289
|
if (include.has("console")) await collectGeckoConsole(args, browser, urlFilter, result, notes);
|
|
9176
9290
|
if (args.deepDom) {
|
|
9177
9291
|
const cap = maxBytes > 0 ? maxBytes : 65536;
|
|
@@ -9271,9 +9385,13 @@ async function inspect_handler(args) {
|
|
|
9271
9385
|
}
|
|
9272
9386
|
toolchainWarning = isEngineCompanionUrl(documentUrl) || isEngineCompanionUrl(target.url) ? `Inspected ${target.url}, which is rendered by the Extension.js toolchain's own companion extension, not by this project. Pass url, or open one of the extension's surfaces first (extension_open), then inspect again.` : `No url was given and only override pages were open, so this inspected ${target.url}. Unless this extension provides that override itself, this is the toolchain's welcome surface, not your extension: pass url or open a surface with extension_open.`;
|
|
9273
9387
|
}
|
|
9388
|
+
const profileReused = !args.url && sessionProfileReused(args.projectPath, browser);
|
|
9274
9389
|
const result = {
|
|
9275
9390
|
cdpPort,
|
|
9276
9391
|
browser,
|
|
9392
|
+
...profileReused ? {
|
|
9393
|
+
profileReused: true
|
|
9394
|
+
} : {},
|
|
9277
9395
|
target: {
|
|
9278
9396
|
id: target.id,
|
|
9279
9397
|
url: target.url,
|
|
@@ -9335,7 +9453,8 @@ async function inspect_handler(args) {
|
|
|
9335
9453
|
value: result,
|
|
9336
9454
|
warnings: [
|
|
9337
9455
|
"string" == typeof probeWarning ? probeWarning : null,
|
|
9338
|
-
toolchainWarning
|
|
9456
|
+
toolchainWarning,
|
|
9457
|
+
profileReused ? restoredTabWarning(documentUrl || target.url) : null
|
|
9339
9458
|
]
|
|
9340
9459
|
});
|
|
9341
9460
|
} catch (err) {
|
|
@@ -11456,9 +11575,26 @@ function readContractForDiagnosis(projectPath, browser) {
|
|
|
11456
11575
|
return null;
|
|
11457
11576
|
}
|
|
11458
11577
|
}
|
|
11578
|
+
function sightedContractBrowser(projectPath) {
|
|
11579
|
+
let dirs;
|
|
11580
|
+
try {
|
|
11581
|
+
dirs = node_fs.readdirSync(sessionArtifactsRootDir(projectPath));
|
|
11582
|
+
} catch {
|
|
11583
|
+
return null;
|
|
11584
|
+
}
|
|
11585
|
+
let best = null;
|
|
11586
|
+
for (const dir of dirs)try {
|
|
11587
|
+
const stat = node_fs.statSync(bridge_readyContractPath(projectPath, dir));
|
|
11588
|
+
if (!best || stat.mtimeMs > best.mtimeMs) best = {
|
|
11589
|
+
browser: dir,
|
|
11590
|
+
mtimeMs: stat.mtimeMs
|
|
11591
|
+
};
|
|
11592
|
+
} catch {}
|
|
11593
|
+
return best ? best.browser : null;
|
|
11594
|
+
}
|
|
11459
11595
|
const doctor_schema = {
|
|
11460
11596
|
name: "extension_doctor",
|
|
11461
|
-
description: "Diagnose a dev session end to end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, browser liveness. This returns one {check, status, detail, remediation?} per leg, in dependency order. Read a 'skip' as blocked, not as a pass: it names the check that blocked it. Run this first when any act tool (storage, reload, eval, open) errors unexpectedly. Call it with no projectPath for a pre-flight environment check (node, the Extension.js CLI, the template cache) before any project exists.",
|
|
11597
|
+
description: "Diagnose a dev session end to end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, browser liveness. This returns one {check, status, detail, remediation?} per leg, in dependency order. Read a 'skip' as blocked, not as a pass: it names the check that blocked it. A session started without allowControl comes back ok:true with status 'read-only', not as an error: its control channel is off by choice. Run this first when any act tool (storage, reload, eval, open) errors unexpectedly. Call it with no projectPath for a pre-flight environment check (node, the Extension.js CLI, the template cache) before any project exists.",
|
|
11462
11598
|
inputSchema: {
|
|
11463
11599
|
type: "object",
|
|
11464
11600
|
properties: {
|
|
@@ -11546,12 +11682,20 @@ function projectEngineVersion(projectPath) {
|
|
|
11546
11682
|
}
|
|
11547
11683
|
}
|
|
11548
11684
|
function capabilityProbeChecks(parsed) {
|
|
11549
|
-
|
|
11685
|
+
if (!isEnvelope(parsed)) return parsed;
|
|
11686
|
+
const value = parsed.value;
|
|
11687
|
+
if (Array.isArray(value)) return value;
|
|
11688
|
+
return value?.checks;
|
|
11689
|
+
}
|
|
11690
|
+
const CONTROL_OFF_BY_CHOICE = /\bwas not started with --allow-control\b/i;
|
|
11691
|
+
function controlOffByChoiceLeg(checks) {
|
|
11692
|
+
return checks.find((leg)=>"control-channel" === leg.check && "fail" === leg.status && "string" == typeof leg.detail && CONTROL_OFF_BY_CHOICE.test(leg.detail)) ?? null;
|
|
11550
11693
|
}
|
|
11551
11694
|
async function doctor_handler(args) {
|
|
11552
11695
|
if (!args.projectPath) return environmentPreflight();
|
|
11553
11696
|
const projectPath = args.projectPath;
|
|
11554
|
-
const
|
|
11697
|
+
const resolved = resolveSessionBrowser(projectPath, args.browser);
|
|
11698
|
+
const browser = "fallback" === resolved.source ? sightedContractBrowser(projectPath) ?? resolved.browser : resolved.browser;
|
|
11555
11699
|
const { code, stdout, stderr } = await runExtensionCli([
|
|
11556
11700
|
"doctor",
|
|
11557
11701
|
projectPath,
|
|
@@ -11576,8 +11720,10 @@ async function doctor_handler(args) {
|
|
|
11576
11720
|
const out = stdout.trim();
|
|
11577
11721
|
try {
|
|
11578
11722
|
const parsed = JSON.parse(out);
|
|
11579
|
-
const
|
|
11580
|
-
if (!Array.isArray(
|
|
11723
|
+
const probed = capabilityProbeChecks(parsed);
|
|
11724
|
+
if (!Array.isArray(probed)) throw new Error("not a check array");
|
|
11725
|
+
const checks = probed;
|
|
11726
|
+
const readOnlyLeg = controlOffByChoiceLeg(checks);
|
|
11581
11727
|
for (const check of checks){
|
|
11582
11728
|
if ("string" == typeof check.detail) check.detail = toMcpSpeak(check.detail);
|
|
11583
11729
|
if ("string" == typeof check.remediation) check.remediation = toMcpSpeak(check.remediation);
|
|
@@ -11619,17 +11765,30 @@ async function doctor_handler(args) {
|
|
|
11619
11765
|
} : {}
|
|
11620
11766
|
});
|
|
11621
11767
|
}
|
|
11768
|
+
const failures = checks.filter((leg)=>"fail" === leg.status);
|
|
11769
|
+
const readOnly = null !== readOnlyLeg && 1 === failures.length && failures[0] === readOnlyLeg;
|
|
11770
|
+
if (readOnly && readOnlyLeg) {
|
|
11771
|
+
readOnlyLeg.status = "warn";
|
|
11772
|
+
readOnlyLeg.detail = `read-only by choice: ${readOnlyLeg.detail}`;
|
|
11773
|
+
readOnlyLeg.remediation = "Nothing failed. To unlock the control verbs, call extension_dev again with allowControl: true (or allowEval: true) plus replace: true, which stops this session first; a plain second call is refused so the session does not fork.";
|
|
11774
|
+
}
|
|
11622
11775
|
return envelope_envelope({
|
|
11623
|
-
ok: healthy,
|
|
11776
|
+
ok: healthy || readOnly,
|
|
11624
11777
|
command: doctor_schema.name,
|
|
11625
|
-
status: healthy ? "healthy" : "unhealthy",
|
|
11778
|
+
status: healthy ? "healthy" : readOnly ? "read-only" : "unhealthy",
|
|
11626
11779
|
value: {
|
|
11627
11780
|
browser,
|
|
11628
11781
|
...engineVersion ? {
|
|
11629
11782
|
engineVersion
|
|
11630
11783
|
} : {},
|
|
11784
|
+
...readOnly ? {
|
|
11785
|
+
readOnly: true
|
|
11786
|
+
} : {},
|
|
11631
11787
|
checks
|
|
11632
|
-
}
|
|
11788
|
+
},
|
|
11789
|
+
...readOnly ? {
|
|
11790
|
+
hint: "This session is read-only because it was started without allowControl, not because anything is wrong: logs, inspect, wait and doctor all work, while storage, reload, open, dom_snapshot and eval stay locked."
|
|
11791
|
+
} : {}
|
|
11633
11792
|
});
|
|
11634
11793
|
} catch {
|
|
11635
11794
|
const message = stderr.trim() || `extension exited with code ${code}`;
|
|
@@ -12321,7 +12480,7 @@ function resumePending(deviceCode, verificationUri) {
|
|
|
12321
12480
|
}
|
|
12322
12481
|
async function loginToProject(args) {
|
|
12323
12482
|
const project = String(args.project || "").trim();
|
|
12324
|
-
if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.", "bad-request", "E_BAD_REQUEST");
|
|
12483
|
+
if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'. The slug pair is the console address bar: an existing project's page is console.extension.dev/<workspace>/<project>. If the project does not exist yet, create it at extension.dev/new, then log in with the slugs the console shows.", "bad-request", "E_BAD_REQUEST");
|
|
12325
12484
|
const apiCheck = safeApiBase(resolveApiBase(args.api), args.api);
|
|
12326
12485
|
if (!apiCheck.ok) return login_fail("LoginConfigError", apiCheck.message, "login-failed", "E_AUTH_FAILED");
|
|
12327
12486
|
const apiBase = apiCheck.base;
|
|
@@ -12562,7 +12721,7 @@ const auth_schema = {
|
|
|
12562
12721
|
},
|
|
12563
12722
|
project: {
|
|
12564
12723
|
type: "string",
|
|
12565
|
-
description: "login: target project as '<workspace>/<project>'; the token is scoped to it."
|
|
12724
|
+
description: "login: target project as '<workspace>/<project>'; the token is scoped to it. The slug pair is the console address bar: an existing project's page is console.extension.dev/<workspace>/<project>. Create one at extension.dev/new if none exists yet."
|
|
12566
12725
|
},
|
|
12567
12726
|
deviceCode: {
|
|
12568
12727
|
type: "string",
|
|
@@ -13461,4 +13620,5 @@ async function runCli(cmd, args) {
|
|
|
13461
13620
|
log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami, release.`);
|
|
13462
13621
|
return 1;
|
|
13463
13622
|
}
|
|
13464
|
-
|
|
13623
|
+
var version_0 = package_namespaceObject.rE;
|
|
13624
|
+
export { runCli, startServer, version_0 as version };
|
package/dist/src/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface ArtifactPublisher {
|
|
|
13
13
|
project: string | null;
|
|
14
14
|
tokenId: string | null;
|
|
15
15
|
}
|
|
16
|
+
export declare const ZIP_URL_REDIRECT_NOTE = "zipUrl does not serve the archive itself: it answers 302 with a short-lived presigned storage URL in Location. Follow redirects when you fetch it (curl -L; fetch and most HTTP clients already do), because a client that does not follow them reads 0 bytes and reports the share as empty when it is not.";
|
|
16
17
|
export interface ListedArtifact {
|
|
17
18
|
artifactId: string;
|
|
18
19
|
kind?: string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const SYSTEM_PROFILE_ARG = "false";
|
|
2
|
+
export declare function profileCarriesTabsOver(projectPath: string, browser: string, profileArg?: string): boolean;
|
|
3
|
+
export declare function sessionProfileReused(projectPath: string, browser: string): boolean;
|
|
4
|
+
export declare function restoredTabWarning(url: string): string;
|
package/dist/src/lib/types.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
<!DOCTYPE html><html lang="en"><head>
|
|
2
2
|
<meta charset="utf-8">
|
|
3
3
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4
|
-
<!-- Dark-only surface: signals the popup chrome up front so there is no
|
|
5
|
-
white flash before styles.css loads. The brand tokens live in
|
|
6
|
-
./styles.css (synced with @extensiondev/ui), not inline here. -->
|
|
7
4
|
<meta name="color-scheme" content="dark">
|
|
8
5
|
<title>Extension.dev Live Preview</title>
|
|
9
6
|
<link rel="stylesheet" href="/action/index.css"></head>
|
|
@@ -3,19 +3,8 @@
|
|
|
3
3
|
<title>Extension preview host</title>
|
|
4
4
|
</head>
|
|
5
5
|
<body>
|
|
6
|
-
<!--
|
|
7
|
-
Sandbox execution surface for guest extension code.
|
|
8
|
-
Declared in manifest.json under chromium:sandbox.pages with a CSP
|
|
9
|
-
that allows unsafe-eval / blob: so guest code can run for real inside this
|
|
10
|
-
installed extension. This file exists so the sandbox reference resolves and
|
|
11
|
-
the build has a host page to mount; the full executor wiring (receiving a
|
|
12
|
-
guest PreviewSession and running its surfaces here) is the next increment.
|
|
13
|
-
Until then, guest surfaces still render through the creator-preview iframes.
|
|
14
|
-
-->
|
|
15
6
|
<div id="preview-host-root"></div>
|
|
16
7
|
<script>
|
|
17
|
-
// Signal the parent (the newtab runtime) that the sandbox host is mounted
|
|
18
|
-
// and ready to receive a guest preview to execute.
|
|
19
8
|
if (window.parent) {
|
|
20
9
|
window.parent.postMessage(
|
|
21
10
|
{ type: "EXTENSION_PREVIEW_HOST_READY" },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@extension.dev/mcp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "10.
|
|
4
|
+
"version": "10.3.1",
|
|
5
5
|
"description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 28 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",
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://extension.dev",
|
|
10
|
-
"version": "10.
|
|
10
|
+
"version": "10.3.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.
|
|
16
|
+
"version": "10.3.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|