@extension.dev/mcp 3.17.0 → 4.0.8

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/dist/module.js CHANGED
@@ -7,10 +7,12 @@ import node_fs, { existsSync, readdirSync, statSync } from "node:fs";
7
7
  import node_path, { join } from "node:path";
8
8
  import node_os from "node:os";
9
9
  import { extensionBuild } from "extension-develop";
10
- import { execFile, spawn } from "node:child_process";
10
+ import cross_spawn from "cross-spawn";
11
+ import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
11
12
  import node_net from "node:net";
12
13
  import ws from "ws";
13
14
  import { extensionInstall, getManagedBrowsersCacheRoot } from "extension-install";
15
+ import { execFile } from "node:child_process";
14
16
  import { promisify } from "node:util";
15
17
  var add_feature_namespaceObject = {};
16
18
  __webpack_require__.r(add_feature_namespaceObject);
@@ -106,7 +108,7 @@ var logs_namespaceObject = {};
106
108
  __webpack_require__.r(logs_namespaceObject);
107
109
  __webpack_require__.d(logs_namespaceObject, {
108
110
  handler: ()=>logs_handler,
109
- schema: ()=>logs_schema
111
+ schema: ()=>logs_schema_schema
110
112
  });
111
113
  var manifest_validate_namespaceObject = {};
112
114
  __webpack_require__.r(manifest_validate_namespaceObject);
@@ -131,8 +133,15 @@ __webpack_require__.r(publish_namespaceObject);
131
133
  __webpack_require__.d(publish_namespaceObject, {
132
134
  handler: ()=>publish_handler,
133
135
  resolveToken: ()=>resolveToken,
136
+ safeApiBase: ()=>safeApiBase,
134
137
  schema: ()=>publish_schema
135
138
  });
139
+ var release_promote_namespaceObject = {};
140
+ __webpack_require__.r(release_promote_namespaceObject);
141
+ __webpack_require__.d(release_promote_namespaceObject, {
142
+ handler: ()=>release_promote_handler,
143
+ schema: ()=>release_promote_schema
144
+ });
136
145
  var reload_namespaceObject = {};
137
146
  __webpack_require__.r(reload_namespaceObject);
138
147
  __webpack_require__.d(reload_namespaceObject, {
@@ -169,6 +178,9 @@ __webpack_require__.d(whoami_namespaceObject, {
169
178
  handler: ()=>whoami_handler,
170
179
  schema: ()=>whoami_schema
171
180
  });
181
+ var package_namespaceObject = {
182
+ rE: "4.0.8"
183
+ };
172
184
  const schema = {
173
185
  name: "extension_create",
174
186
  description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options.",
@@ -398,7 +410,7 @@ async function build_handler(args) {
398
410
  }
399
411
  function runExtensionCli(args, options) {
400
412
  return new Promise((resolve)=>{
401
- const child = spawn("npx", [
413
+ const child = cross_spawn("npx", [
402
414
  "extension",
403
415
  ...args
404
416
  ], {
@@ -413,7 +425,6 @@ function runExtensionCli(args, options) {
413
425
  FORCE_COLOR: "0",
414
426
  NO_COLOR: "1"
415
427
  },
416
- shell: true,
417
428
  timeout: options?.timeoutMs ?? 30000
418
429
  });
419
430
  let stdout = "";
@@ -433,7 +444,7 @@ function runExtensionCli(args, options) {
433
444
  });
434
445
  }
435
446
  function spawnExtensionCli(args, options) {
436
- const child = spawn("npx", [
447
+ const child = cross_spawn("npx", [
437
448
  "extension",
438
449
  ...args
439
450
  ], {
@@ -448,8 +459,7 @@ function spawnExtensionCli(args, options) {
448
459
  ...process.env,
449
460
  FORCE_COLOR: "0",
450
461
  NO_COLOR: "1"
451
- },
452
- shell: true
462
+ }
453
463
  });
454
464
  child.unref();
455
465
  return child;
@@ -778,9 +788,8 @@ async function manifest_validate_handler(args) {
778
788
  }
779
789
  if (!manifest.name) result.errors.push("Missing required field: name");
780
790
  if (!manifest.version) result.warnings.push("Missing field: version (required for store submission)");
781
- const chromiumMv = manifest["chromium:manifest_version"] ?? manifest.manifest_version;
782
- manifest["firefox:manifest_version"] ?? manifest.manifest_version;
783
- if (!chromiumMv && !manifest.manifest_version) result.errors.push('Missing manifest_version. Use "chromium:manifest_version": 3 and "firefox:manifest_version": 2 for cross-browser support.');
791
+ const chromiumManifest = filterKeysForThisBrowser(manifest, "chrome");
792
+ if (!chromiumManifest.manifest_version) result.errors.push('Missing manifest_version. Use "chromium:manifest_version": 3 and "firefox:manifest_version": 2 for cross-browser support.');
784
793
  for (const browser of browsers){
785
794
  const isChromium = [
786
795
  "chrome",
@@ -791,25 +800,26 @@ async function manifest_validate_handler(args) {
791
800
  "firefox",
792
801
  "gecko-based"
793
802
  ].includes(browser);
803
+ const effective = filterKeysForThisBrowser(manifest, browser);
794
804
  const issues = [];
795
805
  if (isChromium) {
796
- const mv = chromiumMv;
806
+ const mv = effective.manifest_version;
797
807
  if (mv && mv < 3) issues.push("Manifest V2 is deprecated on Chromium. Use chromium:manifest_version: 3.");
798
- if (manifest["chromium:side_panel"] || manifest.side_panel) {
799
- const perms = manifest["chromium:permissions"] ?? manifest.permissions ?? [];
808
+ if (effective.side_panel) {
809
+ const perms = effective.permissions ?? [];
800
810
  if (!perms.includes("sidePanel")) issues.push('Side panel declared but "sidePanel" permission is missing.');
801
811
  }
802
- if (manifest["firefox:browser_action"] && !manifest["chromium:action"] && !manifest.action) issues.push('Firefox browser_action found but no chromium:action. Chromium MV3 uses "action" instead of "browser_action".');
812
+ if (manifest["firefox:browser_action"] && !effective.action) issues.push('Firefox browser_action found but no chromium:action. Chromium MV3 uses "action" instead of "browser_action".');
803
813
  }
804
814
  if (isFirefox) {
805
- const contentScripts = manifest.content_scripts;
815
+ const contentScripts = effective.content_scripts;
806
816
  if (contentScripts) {
807
817
  for (const cs of contentScripts)if ("MAIN" === cs.world || "MAIN" === cs["world"]) issues.push('content_scripts.world: "MAIN" is Chromium-only. Use "chromium:world": "MAIN" and provide a Firefox fallback.');
808
818
  }
809
- if (manifest["chromium:side_panel"] && !manifest["firefox:sidebar_action"]) issues.push("Chromium side_panel declared but no firefox:sidebar_action. Firefox uses sidebar_action for sidebars.");
810
- const bg = manifest.background;
819
+ if (chromiumManifest.side_panel && !effective.sidebar_action) issues.push("Chromium side_panel declared but no firefox:sidebar_action. Firefox uses sidebar_action for sidebars.");
820
+ const bg = effective.background;
811
821
  if (bg) {
812
- if (bg.service_worker && !bg["firefox:scripts"] && !bg.scripts) issues.push('Background service_worker declared but no firefox:scripts. Firefox uses "scripts" (array) instead of "service_worker".');
822
+ if (bg.service_worker && !bg.scripts) issues.push('Background service_worker declared but no firefox:scripts. Firefox uses "scripts" (array) instead of "service_worker".');
813
823
  }
814
824
  }
815
825
  result.browserSupport[browser] = {
@@ -819,11 +829,11 @@ async function manifest_validate_handler(args) {
819
829
  if (issues.length) result.valid = false;
820
830
  }
821
831
  const surfaces = [];
822
- if (manifest.content_scripts) surfaces.push("content");
823
- if (manifest["chromium:side_panel"] || manifest.side_panel) surfaces.push("sidebar");
824
- if (manifest.action || manifest["chromium:action"] || manifest["firefox:browser_action"]) surfaces.push("action");
825
- if (manifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
826
- if (manifest.background) surfaces.push("background");
832
+ if (chromiumManifest.content_scripts) surfaces.push("content");
833
+ if (chromiumManifest.side_panel) surfaces.push("sidebar");
834
+ if (chromiumManifest.action || manifest["firefox:browser_action"]) surfaces.push("action");
835
+ if (chromiumManifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
836
+ if (chromiumManifest.background) surfaces.push("background");
827
837
  if (surfaces.length) try {
828
838
  const templates = await listTemplates();
829
839
  result.similarTemplates = templates.filter((t)=>t.surfaces.some((s)=>surfaces.includes(s))).slice(0, 5).map((t)=>({
@@ -1000,7 +1010,7 @@ function _define_property(obj, key, value) {
1000
1010
  return obj;
1001
1011
  }
1002
1012
  const COMMAND_TIMEOUT_MS = 15000;
1003
- class CDPClient {
1013
+ class CDPConnection {
1004
1014
  async connect(wsUrl) {
1005
1015
  return new Promise((resolve, reject)=>{
1006
1016
  this.ws = new ws(wsUrl);
@@ -1092,61 +1102,52 @@ class CDPClient {
1092
1102
  this.ws.send(JSON.stringify(message));
1093
1103
  });
1094
1104
  }
1095
- static async discoverBrowserWsUrl(port, host = "127.0.0.1") {
1096
- const res = await fetch(`http://${host}:${port}/json/version`);
1097
- if (!res.ok) throw new Error(`CDP /json/version failed: ${res.status}`);
1098
- const data = await res.json();
1099
- if ("string" == typeof data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
1100
- throw new Error("No webSocketDebuggerUrl in /json/version response");
1101
- }
1102
- static async discoverTargets(port, host = "127.0.0.1") {
1103
- const res = await fetch(`http://${host}:${port}/json`);
1104
- if (!res.ok) throw new Error(`CDP /json failed: ${res.status}`);
1105
- return await res.json();
1106
- }
1107
- async getTargets() {
1108
- const response = await this.sendCommand("Target.getTargets");
1109
- return response?.targetInfos ?? [];
1110
- }
1111
- async attachToTarget(targetId) {
1112
- const response = await this.sendCommand("Target.attachToTarget", {
1113
- targetId,
1114
- flatten: true
1115
- });
1116
- return response.sessionId ?? "";
1117
- }
1118
- async enableDomains(sessionId) {
1119
- await Promise.all([
1120
- this.sendCommand("Runtime.enable", {}, sessionId),
1121
- this.sendCommand("Log.enable", {}, sessionId),
1122
- this.sendCommand("Page.enable", {}, sessionId)
1123
- ]);
1105
+ getConsoleMessages() {
1106
+ return [
1107
+ ...this.consoleMessages
1108
+ ];
1124
1109
  }
1125
- async navigate(sessionId, url) {
1126
- await this.sendCommand("Page.navigate", {
1127
- url
1128
- }, sessionId);
1129
- await new Promise((resolve)=>{
1130
- const timeout = setTimeout(resolve, 5000);
1131
- const unsubscribe = this.onEvent((msg)=>{
1132
- if ("Page.loadEventFired" === msg.method) {
1133
- clearTimeout(timeout);
1134
- unsubscribe();
1135
- resolve();
1136
- }
1110
+ getConsoleSummary() {
1111
+ const counts = {};
1112
+ const uniqueByLevel = {};
1113
+ for (const msg of this.consoleMessages){
1114
+ counts[msg.level] = (counts[msg.level] ?? 0) + 1;
1115
+ if (!uniqueByLevel[msg.level]) uniqueByLevel[msg.level] = new Map();
1116
+ const key = msg.text.slice(0, 200);
1117
+ uniqueByLevel[msg.level].set(key, (uniqueByLevel[msg.level].get(key) ?? 0) + 1);
1118
+ }
1119
+ const topMessages = [];
1120
+ for (const [level, msgs] of Object.entries(uniqueByLevel)){
1121
+ const sorted = [
1122
+ ...msgs.entries()
1123
+ ].sort((a, b)=>b[1] - a[1]);
1124
+ for (const [text, count] of sorted.slice(0, 5))topMessages.push({
1125
+ level,
1126
+ text,
1127
+ count
1137
1128
  });
1138
- });
1129
+ }
1130
+ return {
1131
+ total: this.consoleMessages.length,
1132
+ counts,
1133
+ topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
1134
+ };
1139
1135
  }
1140
- async evaluate(sessionId, expression) {
1141
- const response = await this.sendCommand("Runtime.evaluate", {
1142
- expression,
1143
- returnByValue: true,
1144
- awaitPromise: false
1145
- }, sessionId);
1146
- return response.result?.value;
1136
+ onEvent(handler) {
1137
+ this.eventListeners.add(handler);
1138
+ return ()=>{
1139
+ this.eventListeners.delete(handler);
1140
+ };
1147
1141
  }
1148
- async getPageHTML(sessionId) {
1149
- const result = await this.evaluate(sessionId, `(() => {
1142
+ constructor(){
1143
+ _define_property(this, "ws", null);
1144
+ _define_property(this, "messageId", 0);
1145
+ _define_property(this, "eventListeners", new Set());
1146
+ _define_property(this, "pendingRequests", new Map());
1147
+ _define_property(this, "consoleMessages", []);
1148
+ }
1149
+ }
1150
+ const PAGE_HTML_SCRIPT = `(() => {
1150
1151
  try {
1151
1152
  const doctype = document.doctype;
1152
1153
  const dt = doctype
@@ -1179,61 +1180,8 @@ class CDPClient {
1179
1180
  }
1180
1181
  return dt + '\\n' + document.documentElement.outerHTML;
1181
1182
  } catch (e) { return ''; }
1182
- })()`);
1183
- return "string" == typeof result ? result : "";
1184
- }
1185
- async getClosedShadowRoots(sessionId, maxBytes = 65536) {
1186
- await this.sendCommand("DOM.enable", {}, sessionId);
1187
- const doc = await this.sendCommand("DOM.getDocument", {
1188
- depth: -1,
1189
- pierce: true
1190
- }, sessionId);
1191
- const found = [];
1192
- const walk = (node, hostName)=>{
1193
- if (!node || "object" != typeof node) return;
1194
- const name = node.localName || node.nodeName || hostName;
1195
- if (Array.isArray(node.shadowRoots)) for (const sr of node.shadowRoots){
1196
- if (sr && "closed" === sr.shadowRootType && "number" == typeof sr.nodeId) found.push({
1197
- nodeId: sr.nodeId,
1198
- host: String(name),
1199
- type: "closed"
1200
- });
1201
- walk(sr, name);
1202
- }
1203
- if (Array.isArray(node.children)) for (const c of node.children)walk(c, name);
1204
- if (node.contentDocument) walk(node.contentDocument, name);
1205
- };
1206
- walk(doc.root, "html");
1207
- const out = [];
1208
- for (const f of found)try {
1209
- const oh = await this.sendCommand("DOM.getOuterHTML", {
1210
- nodeId: f.nodeId
1211
- }, sessionId);
1212
- let html = String(oh?.outerHTML ?? "");
1213
- let truncated = false;
1214
- if (maxBytes > 0 && html.length > maxBytes) {
1215
- html = html.slice(0, maxBytes);
1216
- truncated = true;
1217
- }
1218
- out.push({
1219
- host: f.host,
1220
- type: f.type,
1221
- html,
1222
- ...truncated ? {
1223
- truncated
1224
- } : {}
1225
- });
1226
- } catch {
1227
- out.push({
1228
- host: f.host,
1229
- type: f.type,
1230
- html: ""
1231
- });
1232
- }
1233
- return out;
1234
- }
1235
- async getPageMeta(sessionId) {
1236
- const result = await this.evaluate(sessionId, `(() => {
1183
+ })()`;
1184
+ const PAGE_META_SCRIPT = `(() => {
1237
1185
  try {
1238
1186
  return {
1239
1187
  title: document.title,
@@ -1249,11 +1197,42 @@ class CDPClient {
1249
1197
  styleCount: document.querySelectorAll('style,link[rel="stylesheet"]').length
1250
1198
  };
1251
1199
  } catch { return {}; }
1252
- })()`);
1253
- return result ?? {};
1254
- }
1255
- async probeSelectors(sessionId, selectors) {
1256
- const result = await this.evaluate(sessionId, `(() => {
1200
+ })()`;
1201
+ const EXTENSION_ROOT_META_SCRIPT = `(() => {
1202
+ try {
1203
+ const readGeneration = (node) => {
1204
+ const raw = node.getAttribute && node.getAttribute('data-extjs-reinject-generation');
1205
+ const parsed = Number(raw);
1206
+ return Number.isFinite(parsed) ? parsed : undefined;
1207
+ };
1208
+ const normalize = (node) => ({
1209
+ tag: node.tagName ? String(node.tagName).toLowerCase() : 'unknown',
1210
+ id: node.id || undefined,
1211
+ key: node.getAttribute ? node.getAttribute('data-extjs-reinject-key') || undefined : undefined,
1212
+ generation: readGeneration(node),
1213
+ status: node.getAttribute ? node.getAttribute('data-extjs-reinject-status') || undefined : undefined
1214
+ });
1215
+ const roots = Array.from(
1216
+ document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])')
1217
+ ).slice(0, 10).map(normalize);
1218
+ const markers = Array.from(
1219
+ document.querySelectorAll('[data-extjs-reinject-marker="true"]')
1220
+ ).slice(0, 10).map(normalize);
1221
+ if (!roots.length && !markers.length) return null;
1222
+ const generations = [...roots, ...markers]
1223
+ .map(e => e.generation)
1224
+ .filter(g => typeof g === 'number');
1225
+ return {
1226
+ rootCount: roots.length,
1227
+ markerCount: markers.length,
1228
+ latestGeneration: generations.length ? Math.max(...generations) : 0,
1229
+ roots,
1230
+ markers
1231
+ };
1232
+ } catch { return null; }
1233
+ })()`;
1234
+ function probeSelectorsScript(selectors) {
1235
+ return `(() => {
1257
1236
  const selectors = ${JSON.stringify(selectors)};
1258
1237
  return selectors.map(selector => {
1259
1238
  try {
@@ -1275,11 +1254,10 @@ class CDPClient {
1275
1254
  return { selector, count: 0, samples: [], error: String(e) };
1276
1255
  }
1277
1256
  });
1278
- })()`);
1279
- return result ?? [];
1280
- }
1281
- async getDomSnapshot(sessionId, maxNodes = 500) {
1282
- const result = await this.evaluate(sessionId, `(() => {
1257
+ })()`;
1258
+ }
1259
+ function domSnapshotScript(maxNodes) {
1260
+ return `(() => {
1283
1261
  const maxNodes = ${maxNodes};
1284
1262
  const nodes = [];
1285
1263
  const walk = (node, depth) => {
@@ -1301,88 +1279,131 @@ class CDPClient {
1301
1279
  };
1302
1280
  walk(document.documentElement, 0);
1303
1281
  return nodes;
1304
- })()`);
1305
- return result ?? [];
1282
+ })()`;
1283
+ }
1284
+ class CDPClient extends CDPConnection {
1285
+ static async discoverBrowserWsUrl(port, host = "127.0.0.1") {
1286
+ const res = await fetch(`http://${host}:${port}/json/version`);
1287
+ if (!res.ok) throw new Error(`CDP /json/version failed: ${res.status}`);
1288
+ const data = await res.json();
1289
+ if ("string" == typeof data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
1290
+ throw new Error("No webSocketDebuggerUrl in /json/version response");
1306
1291
  }
1307
- async getExtensionRootMeta(sessionId) {
1308
- const result = await this.evaluate(sessionId, `(() => {
1309
- try {
1310
- const readGeneration = (node) => {
1311
- const raw = node.getAttribute && node.getAttribute('data-extjs-reinject-generation');
1312
- const parsed = Number(raw);
1313
- return Number.isFinite(parsed) ? parsed : undefined;
1314
- };
1315
- const normalize = (node) => ({
1316
- tag: node.tagName ? String(node.tagName).toLowerCase() : 'unknown',
1317
- id: node.id || undefined,
1318
- key: node.getAttribute ? node.getAttribute('data-extjs-reinject-key') || undefined : undefined,
1319
- generation: readGeneration(node),
1320
- status: node.getAttribute ? node.getAttribute('data-extjs-reinject-status') || undefined : undefined
1321
- });
1322
- const roots = Array.from(
1323
- document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])')
1324
- ).slice(0, 10).map(normalize);
1325
- const markers = Array.from(
1326
- document.querySelectorAll('[data-extjs-reinject-marker="true"]')
1327
- ).slice(0, 10).map(normalize);
1328
- if (!roots.length && !markers.length) return null;
1329
- const generations = [...roots, ...markers]
1330
- .map(e => e.generation)
1331
- .filter(g => typeof g === 'number');
1332
- return {
1333
- rootCount: roots.length,
1334
- markerCount: markers.length,
1335
- latestGeneration: generations.length ? Math.max(...generations) : 0,
1336
- roots,
1337
- markers
1338
- };
1339
- } catch { return null; }
1340
- })()`);
1341
- return result ?? null;
1292
+ static async discoverTargets(port, host = "127.0.0.1") {
1293
+ const res = await fetch(`http://${host}:${port}/json`);
1294
+ if (!res.ok) throw new Error(`CDP /json failed: ${res.status}`);
1295
+ return await res.json();
1342
1296
  }
1343
- getConsoleMessages() {
1344
- return [
1345
- ...this.consoleMessages
1346
- ];
1297
+ async getTargets() {
1298
+ const response = await this.sendCommand("Target.getTargets");
1299
+ return response?.targetInfos ?? [];
1347
1300
  }
1348
- getConsoleSummary() {
1349
- const counts = {};
1350
- const uniqueByLevel = {};
1351
- for (const msg of this.consoleMessages){
1352
- counts[msg.level] = (counts[msg.level] ?? 0) + 1;
1353
- if (!uniqueByLevel[msg.level]) uniqueByLevel[msg.level] = new Map();
1354
- const key = msg.text.slice(0, 200);
1355
- uniqueByLevel[msg.level].set(key, (uniqueByLevel[msg.level].get(key) ?? 0) + 1);
1356
- }
1357
- const topMessages = [];
1358
- for (const [level, msgs] of Object.entries(uniqueByLevel)){
1359
- const sorted = [
1360
- ...msgs.entries()
1361
- ].sort((a, b)=>b[1] - a[1]);
1362
- for (const [text, count] of sorted.slice(0, 5))topMessages.push({
1363
- level,
1364
- text,
1365
- count
1301
+ async attachToTarget(targetId) {
1302
+ const response = await this.sendCommand("Target.attachToTarget", {
1303
+ targetId,
1304
+ flatten: true
1305
+ });
1306
+ return response.sessionId ?? "";
1307
+ }
1308
+ async enableDomains(sessionId) {
1309
+ await Promise.all([
1310
+ this.sendCommand("Runtime.enable", {}, sessionId),
1311
+ this.sendCommand("Log.enable", {}, sessionId),
1312
+ this.sendCommand("Page.enable", {}, sessionId)
1313
+ ]);
1314
+ }
1315
+ async navigate(sessionId, url) {
1316
+ await this.sendCommand("Page.navigate", {
1317
+ url
1318
+ }, sessionId);
1319
+ await new Promise((resolve)=>{
1320
+ const timeout = setTimeout(resolve, 5000);
1321
+ const unsubscribe = this.onEvent((msg)=>{
1322
+ if ("Page.loadEventFired" === msg.method) {
1323
+ clearTimeout(timeout);
1324
+ unsubscribe();
1325
+ resolve();
1326
+ }
1327
+ });
1328
+ });
1329
+ }
1330
+ async evaluate(sessionId, expression) {
1331
+ const response = await this.sendCommand("Runtime.evaluate", {
1332
+ expression,
1333
+ returnByValue: true,
1334
+ awaitPromise: false
1335
+ }, sessionId);
1336
+ return response.result?.value;
1337
+ }
1338
+ async getPageHTML(sessionId) {
1339
+ const result = await this.evaluate(sessionId, PAGE_HTML_SCRIPT);
1340
+ return "string" == typeof result ? result : "";
1341
+ }
1342
+ async getClosedShadowRoots(sessionId, maxBytes = 65536) {
1343
+ await this.sendCommand("DOM.enable", {}, sessionId);
1344
+ const doc = await this.sendCommand("DOM.getDocument", {
1345
+ depth: -1,
1346
+ pierce: true
1347
+ }, sessionId);
1348
+ const found = [];
1349
+ const walk = (node, hostName)=>{
1350
+ if (!node || "object" != typeof node) return;
1351
+ const name = node.localName || node.nodeName || hostName;
1352
+ if (Array.isArray(node.shadowRoots)) for (const sr of node.shadowRoots){
1353
+ if (sr && "closed" === sr.shadowRootType && "number" == typeof sr.nodeId) found.push({
1354
+ nodeId: sr.nodeId,
1355
+ host: String(name),
1356
+ type: "closed"
1357
+ });
1358
+ walk(sr, name);
1359
+ }
1360
+ if (Array.isArray(node.children)) for (const c of node.children)walk(c, name);
1361
+ if (node.contentDocument) walk(node.contentDocument, name);
1362
+ };
1363
+ walk(doc.root, "html");
1364
+ const out = [];
1365
+ for (const f of found)try {
1366
+ const oh = await this.sendCommand("DOM.getOuterHTML", {
1367
+ nodeId: f.nodeId
1368
+ }, sessionId);
1369
+ let html = String(oh?.outerHTML ?? "");
1370
+ let truncated = false;
1371
+ if (maxBytes > 0 && html.length > maxBytes) {
1372
+ html = html.slice(0, maxBytes);
1373
+ truncated = true;
1374
+ }
1375
+ out.push({
1376
+ host: f.host,
1377
+ type: f.type,
1378
+ html,
1379
+ ...truncated ? {
1380
+ truncated
1381
+ } : {}
1382
+ });
1383
+ } catch {
1384
+ out.push({
1385
+ host: f.host,
1386
+ type: f.type,
1387
+ html: ""
1366
1388
  });
1367
1389
  }
1368
- return {
1369
- total: this.consoleMessages.length,
1370
- counts,
1371
- topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
1372
- };
1390
+ return out;
1373
1391
  }
1374
- onEvent(handler) {
1375
- this.eventListeners.add(handler);
1376
- return ()=>{
1377
- this.eventListeners.delete(handler);
1378
- };
1392
+ async getPageMeta(sessionId) {
1393
+ const result = await this.evaluate(sessionId, PAGE_META_SCRIPT);
1394
+ return result ?? {};
1379
1395
  }
1380
- constructor(){
1381
- _define_property(this, "ws", null);
1382
- _define_property(this, "messageId", 0);
1383
- _define_property(this, "eventListeners", new Set());
1384
- _define_property(this, "pendingRequests", new Map());
1385
- _define_property(this, "consoleMessages", []);
1396
+ async probeSelectors(sessionId, selectors) {
1397
+ const result = await this.evaluate(sessionId, probeSelectorsScript(selectors));
1398
+ return result ?? [];
1399
+ }
1400
+ async getDomSnapshot(sessionId, maxNodes = 500) {
1401
+ const result = await this.evaluate(sessionId, domSnapshotScript(maxNodes));
1402
+ return result ?? [];
1403
+ }
1404
+ async getExtensionRootMeta(sessionId) {
1405
+ const result = await this.evaluate(sessionId, EXTENSION_ROOT_META_SCRIPT);
1406
+ return result ?? null;
1386
1407
  }
1387
1408
  }
1388
1409
  const source_inspect_schema = {
@@ -1685,13 +1706,57 @@ async function list_extensions_findCdpPort(projectPath, browser) {
1685
1706
  socket.connect(defaultPort, "127.0.0.1");
1686
1707
  });
1687
1708
  }
1688
- const CONTROL_ENVELOPE_VERSION = 1;
1689
1709
  const CONTROL_WS_PATH = "/extjs-control";
1690
- const DEFAULT_LIMIT = 200;
1691
- const DEFAULT_FOLLOW_MS = 4000;
1692
- const MIN_FOLLOW_MS = 500;
1693
- const MAX_FOLLOW_MS = 15000;
1694
- const logs_schema = {
1710
+ const LEVEL_ORDER = [
1711
+ "error",
1712
+ "warn",
1713
+ "info",
1714
+ "debug",
1715
+ "trace"
1716
+ ];
1717
+ function levelRank(level) {
1718
+ const l = "log" === level ? "info" : level;
1719
+ const i = LEVEL_ORDER.indexOf(l);
1720
+ return -1 === i ? LEVEL_ORDER.length : i;
1721
+ }
1722
+ function makeUrlMatcher(pattern) {
1723
+ const hasGlob = pattern.includes("*");
1724
+ let re = null;
1725
+ if (hasGlob) {
1726
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1727
+ re = new RegExp(escaped);
1728
+ }
1729
+ return (event)=>{
1730
+ const candidates = [
1731
+ event.url,
1732
+ event.hostname
1733
+ ].filter((v)=>"string" == typeof v);
1734
+ if (0 === candidates.length) return false;
1735
+ return candidates.some((c)=>re ? re.test(c) : c.includes(pattern));
1736
+ };
1737
+ }
1738
+ function makeFilter(args) {
1739
+ const minLevel = String(args.level || "all").toLowerCase();
1740
+ const rawContexts = Array.isArray(args.context) ? args.context : "string" == typeof args.context ? args.context.split(",") : null;
1741
+ const contexts = rawContexts && rawContexts.length ? new Set(rawContexts.map((c)=>c.trim()).filter(Boolean)) : null;
1742
+ const sinceSeq = null != args.since ? Number(args.since) : null;
1743
+ const urlMatches = args.url ? makeUrlMatcher(args.url) : null;
1744
+ const tabId = null != args.tab ? Number(args.tab) : null;
1745
+ return (event)=>{
1746
+ if (!event || "object" != typeof event) return false;
1747
+ if ("header" === event.type) return false;
1748
+ if (args.signalsOnly && "dx.signal" !== event.eventType) return false;
1749
+ if (contexts && !contexts.has(event.context)) return false;
1750
+ if ("all" !== minLevel && "off" !== minLevel) {
1751
+ if (levelRank(event.level) > levelRank(minLevel)) return false;
1752
+ }
1753
+ if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
1754
+ if (urlMatches && !urlMatches(event)) return false;
1755
+ if (null != tabId && Number.isFinite(tabId) && event.tabId !== tabId) return false;
1756
+ return true;
1757
+ };
1758
+ }
1759
+ const logs_schema_schema = {
1695
1760
  name: "extension_logs",
1696
1761
  description: "Read or stream logs from every context of a running dev session (service worker, content scripts, popup, options, sidebar, devtools, pages) in one ordered timeline. Reads the same agent-bridge plane as the `extension logs` CLI: a one-shot returns the most recent matching lines from logs.ndjson; `follow:true` collects from the live control channel for a bounded window. Requires an active `extension dev` session.",
1697
1762
  inputSchema: {
@@ -1760,12 +1825,12 @@ const logs_schema = {
1760
1825
  },
1761
1826
  followMs: {
1762
1827
  type: "number",
1763
- default: DEFAULT_FOLLOW_MS,
1764
- description: `How long to collect live frames when follow=true (clamped ${MIN_FOLLOW_MS}–${MAX_FOLLOW_MS}ms).`
1828
+ default: 4000,
1829
+ description: "How long to collect live frames when follow=true (clamped 500–15000ms)."
1765
1830
  },
1766
1831
  limit: {
1767
1832
  type: "number",
1768
- default: DEFAULT_LIMIT,
1833
+ default: 200,
1769
1834
  description: "Maximum number of (most recent) events to return."
1770
1835
  }
1771
1836
  },
@@ -1774,55 +1839,6 @@ const logs_schema = {
1774
1839
  ]
1775
1840
  }
1776
1841
  };
1777
- const LEVEL_ORDER = [
1778
- "error",
1779
- "warn",
1780
- "info",
1781
- "debug",
1782
- "trace"
1783
- ];
1784
- function levelRank(level) {
1785
- const l = "log" === level ? "info" : level;
1786
- const i = LEVEL_ORDER.indexOf(l);
1787
- return -1 === i ? LEVEL_ORDER.length : i;
1788
- }
1789
- function makeUrlMatcher(pattern) {
1790
- const hasGlob = pattern.includes("*");
1791
- let re = null;
1792
- if (hasGlob) {
1793
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1794
- re = new RegExp(escaped);
1795
- }
1796
- return (event)=>{
1797
- const candidates = [
1798
- event.url,
1799
- event.hostname
1800
- ].filter((v)=>"string" == typeof v);
1801
- if (0 === candidates.length) return false;
1802
- return candidates.some((c)=>re ? re.test(c) : c.includes(pattern));
1803
- };
1804
- }
1805
- function makeFilter(args) {
1806
- const minLevel = String(args.level || "all").toLowerCase();
1807
- const rawContexts = Array.isArray(args.context) ? args.context : "string" == typeof args.context ? args.context.split(",") : null;
1808
- const contexts = rawContexts && rawContexts.length ? new Set(rawContexts.map((c)=>c.trim()).filter(Boolean)) : null;
1809
- const sinceSeq = null != args.since ? Number(args.since) : null;
1810
- const urlMatches = args.url ? makeUrlMatcher(args.url) : null;
1811
- const tabId = null != args.tab ? Number(args.tab) : null;
1812
- return (event)=>{
1813
- if (!event || "object" != typeof event) return false;
1814
- if ("header" === event.type) return false;
1815
- if (args.signalsOnly && "dx.signal" !== event.eventType) return false;
1816
- if (contexts && !contexts.has(event.context)) return false;
1817
- if ("all" !== minLevel && "off" !== minLevel) {
1818
- if (levelRank(event.level) > levelRank(minLevel)) return false;
1819
- }
1820
- if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
1821
- if (urlMatches && !urlMatches(event)) return false;
1822
- if (null != tabId && Number.isFinite(tabId) && event.tabId !== tabId) return false;
1823
- return true;
1824
- };
1825
- }
1826
1842
  function logsFilePath(projectPath, browser) {
1827
1843
  return node_path.resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
1828
1844
  }
@@ -1898,7 +1914,7 @@ async function readFromStream(args, browser, limit) {
1898
1914
  error: `No active control channel found for ${browser}.`,
1899
1915
  hint: `Run extension_dev (browser: ${browser}) and wait for it to be ready, then retry. For past logs without a live channel, call without follow.`
1900
1916
  });
1901
- const followMs = Math.min(Math.max(args.followMs ?? DEFAULT_FOLLOW_MS, MIN_FOLLOW_MS), MAX_FOLLOW_MS);
1917
+ const followMs = Math.min(Math.max(args.followMs ?? 4000, 500), 15000);
1902
1918
  const matches = makeFilter(args);
1903
1919
  const events = [];
1904
1920
  let dropped = 0;
@@ -1929,7 +1945,7 @@ async function readFromStream(args, browser, limit) {
1929
1945
  try {
1930
1946
  socket.send(JSON.stringify({
1931
1947
  type: "hello",
1932
- v: CONTROL_ENVELOPE_VERSION,
1948
+ v: 1,
1933
1949
  role: "consumer",
1934
1950
  instanceId: ready.instanceId
1935
1951
  }));
@@ -1961,7 +1977,7 @@ async function readFromStream(args, browser, limit) {
1961
1977
  }
1962
1978
  async function logs_handler(args) {
1963
1979
  const browser = args.browser ?? "chromium";
1964
- const limit = args.limit && args.limit > 0 ? args.limit : DEFAULT_LIMIT;
1980
+ const limit = args.limit && args.limit > 0 ? args.limit : 200;
1965
1981
  if (args.follow) return readFromStream(args, browser, limit);
1966
1982
  return readFromFile(args, browser, limit);
1967
1983
  }
@@ -2423,6 +2439,26 @@ function resolveToken() {
2423
2439
  const creds = readValidCredentials();
2424
2440
  return creds?.token ? String(creds.token).trim() : "";
2425
2441
  }
2442
+ function safeApiBase(raw) {
2443
+ let parsed;
2444
+ try {
2445
+ parsed = new URL(raw);
2446
+ } catch {
2447
+ return {
2448
+ ok: false,
2449
+ message: `Invalid platform URL: ${raw}`
2450
+ };
2451
+ }
2452
+ const isLocalhost = "localhost" === parsed.hostname || "127.0.0.1" === parsed.hostname || "[::1]" === parsed.hostname || "::1" === parsed.hostname;
2453
+ if ("https:" !== parsed.protocol && !("http:" === parsed.protocol && isLocalhost)) return {
2454
+ ok: false,
2455
+ message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
2456
+ };
2457
+ return {
2458
+ ok: true,
2459
+ base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
2460
+ };
2461
+ }
2426
2462
  function fail(name, message) {
2427
2463
  return JSON.stringify({
2428
2464
  ok: false,
@@ -2435,8 +2471,9 @@ function fail(name, message) {
2435
2471
  async function publish_handler(args) {
2436
2472
  const token = resolveToken();
2437
2473
  if (!token) return fail("PublishAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
2438
- const base = String(args.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
2439
- const url = `${base}/api/cli/publish`;
2474
+ const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API));
2475
+ if (!apiCheck.ok) return fail("PublishConfigError", apiCheck.message);
2476
+ const url = `${apiCheck.base}/api/cli/publish`;
2440
2477
  const body = {};
2441
2478
  if (null != args.ttlHours) body.ttlHours = Number(args.ttlHours);
2442
2479
  if (args.buildSha) body.buildSha = args.buildSha;
@@ -2465,6 +2502,102 @@ async function publish_handler(args) {
2465
2502
  if (!res.ok) return fail("PublishError", `publish failed (${res.status}): ${data?.message || text || "unknown error"}`);
2466
2503
  return JSON.stringify(data);
2467
2504
  }
2505
+ const release_promote_DEFAULT_API = "https://www.extension.dev";
2506
+ const release_promote_schema = {
2507
+ name: "extension_release_promote",
2508
+ description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: requires a release token in EXTENSION_DEV_TOKEN (mint and revoke it in the dashboard under project settings -> Access tokens). Posts to the platform's CLI release endpoint; the project is identified by the token. Cutting a version-bump PR is not available headlessly (it writes to your source repo and needs an interactive login).",
2509
+ inputSchema: {
2510
+ type: "object",
2511
+ properties: {
2512
+ buildId: {
2513
+ type: "string",
2514
+ description: "Build commit SHA to promote (a 7-char short SHA is fine)"
2515
+ },
2516
+ channel: {
2517
+ type: "string",
2518
+ description: "Target release channel, e.g. stable, preview, beta"
2519
+ },
2520
+ sourceChannel: {
2521
+ type: "string",
2522
+ description: "Channel to promote from (optional; inferred otherwise)"
2523
+ },
2524
+ browsers: {
2525
+ type: "array",
2526
+ items: {
2527
+ type: "string"
2528
+ },
2529
+ description: "Browsers to release (optional; auto-detected from the build)"
2530
+ },
2531
+ version: {
2532
+ type: "string",
2533
+ description: "Version label for the release (optional)"
2534
+ },
2535
+ releaseNotes: {
2536
+ type: "string",
2537
+ description: "Release notes markdown (optional)"
2538
+ },
2539
+ api: {
2540
+ type: "string",
2541
+ description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
2542
+ }
2543
+ },
2544
+ required: [
2545
+ "buildId",
2546
+ "channel"
2547
+ ]
2548
+ }
2549
+ };
2550
+ function release_promote_fail(name, message) {
2551
+ return JSON.stringify({
2552
+ ok: false,
2553
+ error: {
2554
+ name,
2555
+ message
2556
+ }
2557
+ });
2558
+ }
2559
+ async function release_promote_handler(args) {
2560
+ const token = resolveToken();
2561
+ if (!token) return release_promote_fail("ReleaseAuthError", "No token. Set EXTENSION_DEV_TOKEN to a release token (create one in the extension.dev dashboard under project settings -> Access tokens), or run extension_login.");
2562
+ const buildId = String(args.buildId || "").trim();
2563
+ const channel = String(args.channel || "").trim();
2564
+ if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
2565
+ const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
2566
+ if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
2567
+ const url = `${apiCheck.base}/api/cli/release/promote`;
2568
+ const body = {
2569
+ buildId,
2570
+ channel
2571
+ };
2572
+ if (args.sourceChannel) body.sourceChannel = String(args.sourceChannel).trim();
2573
+ if (Array.isArray(args.browsers) && args.browsers.length) body.browsers = args.browsers.map((b)=>String(b).trim()).filter(Boolean);
2574
+ if (args.version) body.version = String(args.version).trim();
2575
+ if (args.releaseNotes) body.releaseNotes = String(args.releaseNotes);
2576
+ let res;
2577
+ try {
2578
+ res = await fetch(url, {
2579
+ method: "POST",
2580
+ headers: {
2581
+ authorization: `Bearer ${token}`,
2582
+ "content-type": "application/json"
2583
+ },
2584
+ body: JSON.stringify(body)
2585
+ });
2586
+ } catch (err) {
2587
+ return release_promote_fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}`);
2588
+ }
2589
+ const text = await res.text();
2590
+ let data;
2591
+ try {
2592
+ data = JSON.parse(text);
2593
+ } catch {
2594
+ data = {
2595
+ message: text
2596
+ };
2597
+ }
2598
+ if (!res.ok) return release_promote_fail("ReleaseError", `promote failed (${res.status}): ${data?.message || text || "unknown error"}`);
2599
+ return JSON.stringify(data);
2600
+ }
2468
2601
  const wait_schema = {
2469
2602
  name: "extension_wait",
2470
2603
  description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status.",
@@ -3404,6 +3537,7 @@ const tools = [
3404
3537
  open_namespaceObject,
3405
3538
  dom_inspect_namespaceObject,
3406
3539
  publish_namespaceObject,
3540
+ release_promote_namespaceObject,
3407
3541
  wait_namespaceObject,
3408
3542
  add_feature_namespaceObject,
3409
3543
  login_namespaceObject,
@@ -3418,7 +3552,7 @@ for (const tool of tools)toolMap.set(tool.schema.name, tool);
3418
3552
  async function startServer() {
3419
3553
  const server = new Server({
3420
3554
  name: "extension-dev",
3421
- version: "3.13.5"
3555
+ version: package_namespaceObject.rE
3422
3556
  }, {
3423
3557
  capabilities: {
3424
3558
  tools: {}
@@ -3484,6 +3618,34 @@ async function runCli(cmd, args) {
3484
3618
  log(await whoami_handler());
3485
3619
  return 0;
3486
3620
  }
3621
+ if ("release" === cmd) {
3622
+ const sub = String(args[0] || "").trim();
3623
+ if ("promote" === sub) {
3624
+ const buildId = String(flag("build") || flag("build-id") || "").trim();
3625
+ const channel = String(flag("channel") || "").trim();
3626
+ if (!buildId || !channel) {
3627
+ log("Usage: extension-mcp release promote --build <sha> --channel <channel> [--source-channel <c>] [--version <v>] [--api <url>]");
3628
+ return 1;
3629
+ }
3630
+ const out = await release_promote_handler({
3631
+ buildId,
3632
+ channel,
3633
+ sourceChannel: flag("source-channel"),
3634
+ version: flag("version"),
3635
+ api: flag("api")
3636
+ });
3637
+ log(out);
3638
+ let parsed = null;
3639
+ try {
3640
+ parsed = JSON.parse(out);
3641
+ } catch {
3642
+ parsed = null;
3643
+ }
3644
+ return parsed?.ok === false ? 1 : 0;
3645
+ }
3646
+ log("Usage: extension-mcp release promote --build <sha> --channel <channel>");
3647
+ return 1;
3648
+ }
3487
3649
  if ("logout" === cmd) {
3488
3650
  log(await logout_handler());
3489
3651
  return 0;
@@ -3527,7 +3689,7 @@ async function runCli(cmd, args) {
3527
3689
  return 1;
3528
3690
  }
3529
3691
  }
3530
- log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami.`);
3692
+ log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami, release.`);
3531
3693
  return 1;
3532
3694
  }
3533
3695
  export { runCli, startServer };