@extension.dev/mcp 3.17.0 → 4.1.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/dist/module.js CHANGED
@@ -7,10 +7,13 @@ 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";
14
+ import { clearCredentials, exchangeAndPersist, fetchLoginConfig, pollForToken, publish, readCredentials, resolveApiBase, resolveToken, safeApiBase, startDeviceCode } from "@extension.dev/core";
13
15
  import { extensionInstall, getManagedBrowsersCacheRoot } from "extension-install";
16
+ import { execFile } from "node:child_process";
14
17
  import { promisify } from "node:util";
15
18
  var add_feature_namespaceObject = {};
16
19
  __webpack_require__.r(add_feature_namespaceObject);
@@ -106,7 +109,7 @@ var logs_namespaceObject = {};
106
109
  __webpack_require__.r(logs_namespaceObject);
107
110
  __webpack_require__.d(logs_namespaceObject, {
108
111
  handler: ()=>logs_handler,
109
- schema: ()=>logs_schema
112
+ schema: ()=>logs_schema_schema
110
113
  });
111
114
  var manifest_validate_namespaceObject = {};
112
115
  __webpack_require__.r(manifest_validate_namespaceObject);
@@ -130,9 +133,14 @@ var publish_namespaceObject = {};
130
133
  __webpack_require__.r(publish_namespaceObject);
131
134
  __webpack_require__.d(publish_namespaceObject, {
132
135
  handler: ()=>publish_handler,
133
- resolveToken: ()=>resolveToken,
134
136
  schema: ()=>publish_schema
135
137
  });
138
+ var release_promote_namespaceObject = {};
139
+ __webpack_require__.r(release_promote_namespaceObject);
140
+ __webpack_require__.d(release_promote_namespaceObject, {
141
+ handler: ()=>release_promote_handler,
142
+ schema: ()=>release_promote_schema
143
+ });
136
144
  var reload_namespaceObject = {};
137
145
  __webpack_require__.r(reload_namespaceObject);
138
146
  __webpack_require__.d(reload_namespaceObject, {
@@ -169,6 +177,9 @@ __webpack_require__.d(whoami_namespaceObject, {
169
177
  handler: ()=>whoami_handler,
170
178
  schema: ()=>whoami_schema
171
179
  });
180
+ var package_namespaceObject = {
181
+ rE: "4.1.0"
182
+ };
172
183
  const schema = {
173
184
  name: "extension_create",
174
185
  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 +409,7 @@ async function build_handler(args) {
398
409
  }
399
410
  function runExtensionCli(args, options) {
400
411
  return new Promise((resolve)=>{
401
- const child = spawn("npx", [
412
+ const child = cross_spawn("npx", [
402
413
  "extension",
403
414
  ...args
404
415
  ], {
@@ -413,7 +424,6 @@ function runExtensionCli(args, options) {
413
424
  FORCE_COLOR: "0",
414
425
  NO_COLOR: "1"
415
426
  },
416
- shell: true,
417
427
  timeout: options?.timeoutMs ?? 30000
418
428
  });
419
429
  let stdout = "";
@@ -433,7 +443,7 @@ function runExtensionCli(args, options) {
433
443
  });
434
444
  }
435
445
  function spawnExtensionCli(args, options) {
436
- const child = spawn("npx", [
446
+ const child = cross_spawn("npx", [
437
447
  "extension",
438
448
  ...args
439
449
  ], {
@@ -448,8 +458,7 @@ function spawnExtensionCli(args, options) {
448
458
  ...process.env,
449
459
  FORCE_COLOR: "0",
450
460
  NO_COLOR: "1"
451
- },
452
- shell: true
461
+ }
453
462
  });
454
463
  child.unref();
455
464
  return child;
@@ -778,9 +787,8 @@ async function manifest_validate_handler(args) {
778
787
  }
779
788
  if (!manifest.name) result.errors.push("Missing required field: name");
780
789
  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.');
790
+ const chromiumManifest = filterKeysForThisBrowser(manifest, "chrome");
791
+ 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
792
  for (const browser of browsers){
785
793
  const isChromium = [
786
794
  "chrome",
@@ -791,25 +799,26 @@ async function manifest_validate_handler(args) {
791
799
  "firefox",
792
800
  "gecko-based"
793
801
  ].includes(browser);
802
+ const effective = filterKeysForThisBrowser(manifest, browser);
794
803
  const issues = [];
795
804
  if (isChromium) {
796
- const mv = chromiumMv;
805
+ const mv = effective.manifest_version;
797
806
  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 ?? [];
807
+ if (effective.side_panel) {
808
+ const perms = effective.permissions ?? [];
800
809
  if (!perms.includes("sidePanel")) issues.push('Side panel declared but "sidePanel" permission is missing.');
801
810
  }
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".');
811
+ 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
812
  }
804
813
  if (isFirefox) {
805
- const contentScripts = manifest.content_scripts;
814
+ const contentScripts = effective.content_scripts;
806
815
  if (contentScripts) {
807
816
  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
817
  }
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;
818
+ if (chromiumManifest.side_panel && !effective.sidebar_action) issues.push("Chromium side_panel declared but no firefox:sidebar_action. Firefox uses sidebar_action for sidebars.");
819
+ const bg = effective.background;
811
820
  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".');
821
+ 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
822
  }
814
823
  }
815
824
  result.browserSupport[browser] = {
@@ -819,11 +828,11 @@ async function manifest_validate_handler(args) {
819
828
  if (issues.length) result.valid = false;
820
829
  }
821
830
  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");
831
+ if (chromiumManifest.content_scripts) surfaces.push("content");
832
+ if (chromiumManifest.side_panel) surfaces.push("sidebar");
833
+ if (chromiumManifest.action || manifest["firefox:browser_action"]) surfaces.push("action");
834
+ if (chromiumManifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
835
+ if (chromiumManifest.background) surfaces.push("background");
827
836
  if (surfaces.length) try {
828
837
  const templates = await listTemplates();
829
838
  result.similarTemplates = templates.filter((t)=>t.surfaces.some((s)=>surfaces.includes(s))).slice(0, 5).map((t)=>({
@@ -1000,7 +1009,7 @@ function _define_property(obj, key, value) {
1000
1009
  return obj;
1001
1010
  }
1002
1011
  const COMMAND_TIMEOUT_MS = 15000;
1003
- class CDPClient {
1012
+ class CDPConnection {
1004
1013
  async connect(wsUrl) {
1005
1014
  return new Promise((resolve, reject)=>{
1006
1015
  this.ws = new ws(wsUrl);
@@ -1092,61 +1101,52 @@ class CDPClient {
1092
1101
  this.ws.send(JSON.stringify(message));
1093
1102
  });
1094
1103
  }
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
- ]);
1104
+ getConsoleMessages() {
1105
+ return [
1106
+ ...this.consoleMessages
1107
+ ];
1124
1108
  }
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
- }
1109
+ getConsoleSummary() {
1110
+ const counts = {};
1111
+ const uniqueByLevel = {};
1112
+ for (const msg of this.consoleMessages){
1113
+ counts[msg.level] = (counts[msg.level] ?? 0) + 1;
1114
+ if (!uniqueByLevel[msg.level]) uniqueByLevel[msg.level] = new Map();
1115
+ const key = msg.text.slice(0, 200);
1116
+ uniqueByLevel[msg.level].set(key, (uniqueByLevel[msg.level].get(key) ?? 0) + 1);
1117
+ }
1118
+ const topMessages = [];
1119
+ for (const [level, msgs] of Object.entries(uniqueByLevel)){
1120
+ const sorted = [
1121
+ ...msgs.entries()
1122
+ ].sort((a, b)=>b[1] - a[1]);
1123
+ for (const [text, count] of sorted.slice(0, 5))topMessages.push({
1124
+ level,
1125
+ text,
1126
+ count
1137
1127
  });
1138
- });
1128
+ }
1129
+ return {
1130
+ total: this.consoleMessages.length,
1131
+ counts,
1132
+ topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
1133
+ };
1139
1134
  }
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;
1135
+ onEvent(handler) {
1136
+ this.eventListeners.add(handler);
1137
+ return ()=>{
1138
+ this.eventListeners.delete(handler);
1139
+ };
1147
1140
  }
1148
- async getPageHTML(sessionId) {
1149
- const result = await this.evaluate(sessionId, `(() => {
1141
+ constructor(){
1142
+ _define_property(this, "ws", null);
1143
+ _define_property(this, "messageId", 0);
1144
+ _define_property(this, "eventListeners", new Set());
1145
+ _define_property(this, "pendingRequests", new Map());
1146
+ _define_property(this, "consoleMessages", []);
1147
+ }
1148
+ }
1149
+ const PAGE_HTML_SCRIPT = `(() => {
1150
1150
  try {
1151
1151
  const doctype = document.doctype;
1152
1152
  const dt = doctype
@@ -1179,61 +1179,8 @@ class CDPClient {
1179
1179
  }
1180
1180
  return dt + '\\n' + document.documentElement.outerHTML;
1181
1181
  } 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, `(() => {
1182
+ })()`;
1183
+ const PAGE_META_SCRIPT = `(() => {
1237
1184
  try {
1238
1185
  return {
1239
1186
  title: document.title,
@@ -1249,11 +1196,42 @@ class CDPClient {
1249
1196
  styleCount: document.querySelectorAll('style,link[rel="stylesheet"]').length
1250
1197
  };
1251
1198
  } catch { return {}; }
1252
- })()`);
1253
- return result ?? {};
1254
- }
1255
- async probeSelectors(sessionId, selectors) {
1256
- const result = await this.evaluate(sessionId, `(() => {
1199
+ })()`;
1200
+ const EXTENSION_ROOT_META_SCRIPT = `(() => {
1201
+ try {
1202
+ const readGeneration = (node) => {
1203
+ const raw = node.getAttribute && node.getAttribute('data-extjs-reinject-generation');
1204
+ const parsed = Number(raw);
1205
+ return Number.isFinite(parsed) ? parsed : undefined;
1206
+ };
1207
+ const normalize = (node) => ({
1208
+ tag: node.tagName ? String(node.tagName).toLowerCase() : 'unknown',
1209
+ id: node.id || undefined,
1210
+ key: node.getAttribute ? node.getAttribute('data-extjs-reinject-key') || undefined : undefined,
1211
+ generation: readGeneration(node),
1212
+ status: node.getAttribute ? node.getAttribute('data-extjs-reinject-status') || undefined : undefined
1213
+ });
1214
+ const roots = Array.from(
1215
+ document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])')
1216
+ ).slice(0, 10).map(normalize);
1217
+ const markers = Array.from(
1218
+ document.querySelectorAll('[data-extjs-reinject-marker="true"]')
1219
+ ).slice(0, 10).map(normalize);
1220
+ if (!roots.length && !markers.length) return null;
1221
+ const generations = [...roots, ...markers]
1222
+ .map(e => e.generation)
1223
+ .filter(g => typeof g === 'number');
1224
+ return {
1225
+ rootCount: roots.length,
1226
+ markerCount: markers.length,
1227
+ latestGeneration: generations.length ? Math.max(...generations) : 0,
1228
+ roots,
1229
+ markers
1230
+ };
1231
+ } catch { return null; }
1232
+ })()`;
1233
+ function probeSelectorsScript(selectors) {
1234
+ return `(() => {
1257
1235
  const selectors = ${JSON.stringify(selectors)};
1258
1236
  return selectors.map(selector => {
1259
1237
  try {
@@ -1275,11 +1253,10 @@ class CDPClient {
1275
1253
  return { selector, count: 0, samples: [], error: String(e) };
1276
1254
  }
1277
1255
  });
1278
- })()`);
1279
- return result ?? [];
1280
- }
1281
- async getDomSnapshot(sessionId, maxNodes = 500) {
1282
- const result = await this.evaluate(sessionId, `(() => {
1256
+ })()`;
1257
+ }
1258
+ function domSnapshotScript(maxNodes) {
1259
+ return `(() => {
1283
1260
  const maxNodes = ${maxNodes};
1284
1261
  const nodes = [];
1285
1262
  const walk = (node, depth) => {
@@ -1301,88 +1278,131 @@ class CDPClient {
1301
1278
  };
1302
1279
  walk(document.documentElement, 0);
1303
1280
  return nodes;
1304
- })()`);
1305
- return result ?? [];
1281
+ })()`;
1282
+ }
1283
+ class CDPClient extends CDPConnection {
1284
+ static async discoverBrowserWsUrl(port, host = "127.0.0.1") {
1285
+ const res = await fetch(`http://${host}:${port}/json/version`);
1286
+ if (!res.ok) throw new Error(`CDP /json/version failed: ${res.status}`);
1287
+ const data = await res.json();
1288
+ if ("string" == typeof data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
1289
+ throw new Error("No webSocketDebuggerUrl in /json/version response");
1306
1290
  }
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;
1291
+ static async discoverTargets(port, host = "127.0.0.1") {
1292
+ const res = await fetch(`http://${host}:${port}/json`);
1293
+ if (!res.ok) throw new Error(`CDP /json failed: ${res.status}`);
1294
+ return await res.json();
1342
1295
  }
1343
- getConsoleMessages() {
1344
- return [
1345
- ...this.consoleMessages
1346
- ];
1296
+ async getTargets() {
1297
+ const response = await this.sendCommand("Target.getTargets");
1298
+ return response?.targetInfos ?? [];
1347
1299
  }
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
1300
+ async attachToTarget(targetId) {
1301
+ const response = await this.sendCommand("Target.attachToTarget", {
1302
+ targetId,
1303
+ flatten: true
1304
+ });
1305
+ return response.sessionId ?? "";
1306
+ }
1307
+ async enableDomains(sessionId) {
1308
+ await Promise.all([
1309
+ this.sendCommand("Runtime.enable", {}, sessionId),
1310
+ this.sendCommand("Log.enable", {}, sessionId),
1311
+ this.sendCommand("Page.enable", {}, sessionId)
1312
+ ]);
1313
+ }
1314
+ async navigate(sessionId, url) {
1315
+ await this.sendCommand("Page.navigate", {
1316
+ url
1317
+ }, sessionId);
1318
+ await new Promise((resolve)=>{
1319
+ const timeout = setTimeout(resolve, 5000);
1320
+ const unsubscribe = this.onEvent((msg)=>{
1321
+ if ("Page.loadEventFired" === msg.method) {
1322
+ clearTimeout(timeout);
1323
+ unsubscribe();
1324
+ resolve();
1325
+ }
1326
+ });
1327
+ });
1328
+ }
1329
+ async evaluate(sessionId, expression) {
1330
+ const response = await this.sendCommand("Runtime.evaluate", {
1331
+ expression,
1332
+ returnByValue: true,
1333
+ awaitPromise: false
1334
+ }, sessionId);
1335
+ return response.result?.value;
1336
+ }
1337
+ async getPageHTML(sessionId) {
1338
+ const result = await this.evaluate(sessionId, PAGE_HTML_SCRIPT);
1339
+ return "string" == typeof result ? result : "";
1340
+ }
1341
+ async getClosedShadowRoots(sessionId, maxBytes = 65536) {
1342
+ await this.sendCommand("DOM.enable", {}, sessionId);
1343
+ const doc = await this.sendCommand("DOM.getDocument", {
1344
+ depth: -1,
1345
+ pierce: true
1346
+ }, sessionId);
1347
+ const found = [];
1348
+ const walk = (node, hostName)=>{
1349
+ if (!node || "object" != typeof node) return;
1350
+ const name = node.localName || node.nodeName || hostName;
1351
+ if (Array.isArray(node.shadowRoots)) for (const sr of node.shadowRoots){
1352
+ if (sr && "closed" === sr.shadowRootType && "number" == typeof sr.nodeId) found.push({
1353
+ nodeId: sr.nodeId,
1354
+ host: String(name),
1355
+ type: "closed"
1356
+ });
1357
+ walk(sr, name);
1358
+ }
1359
+ if (Array.isArray(node.children)) for (const c of node.children)walk(c, name);
1360
+ if (node.contentDocument) walk(node.contentDocument, name);
1361
+ };
1362
+ walk(doc.root, "html");
1363
+ const out = [];
1364
+ for (const f of found)try {
1365
+ const oh = await this.sendCommand("DOM.getOuterHTML", {
1366
+ nodeId: f.nodeId
1367
+ }, sessionId);
1368
+ let html = String(oh?.outerHTML ?? "");
1369
+ let truncated = false;
1370
+ if (maxBytes > 0 && html.length > maxBytes) {
1371
+ html = html.slice(0, maxBytes);
1372
+ truncated = true;
1373
+ }
1374
+ out.push({
1375
+ host: f.host,
1376
+ type: f.type,
1377
+ html,
1378
+ ...truncated ? {
1379
+ truncated
1380
+ } : {}
1381
+ });
1382
+ } catch {
1383
+ out.push({
1384
+ host: f.host,
1385
+ type: f.type,
1386
+ html: ""
1366
1387
  });
1367
1388
  }
1368
- return {
1369
- total: this.consoleMessages.length,
1370
- counts,
1371
- topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
1372
- };
1389
+ return out;
1373
1390
  }
1374
- onEvent(handler) {
1375
- this.eventListeners.add(handler);
1376
- return ()=>{
1377
- this.eventListeners.delete(handler);
1378
- };
1391
+ async getPageMeta(sessionId) {
1392
+ const result = await this.evaluate(sessionId, PAGE_META_SCRIPT);
1393
+ return result ?? {};
1379
1394
  }
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", []);
1395
+ async probeSelectors(sessionId, selectors) {
1396
+ const result = await this.evaluate(sessionId, probeSelectorsScript(selectors));
1397
+ return result ?? [];
1398
+ }
1399
+ async getDomSnapshot(sessionId, maxNodes = 500) {
1400
+ const result = await this.evaluate(sessionId, domSnapshotScript(maxNodes));
1401
+ return result ?? [];
1402
+ }
1403
+ async getExtensionRootMeta(sessionId) {
1404
+ const result = await this.evaluate(sessionId, EXTENSION_ROOT_META_SCRIPT);
1405
+ return result ?? null;
1386
1406
  }
1387
1407
  }
1388
1408
  const source_inspect_schema = {
@@ -1685,13 +1705,57 @@ async function list_extensions_findCdpPort(projectPath, browser) {
1685
1705
  socket.connect(defaultPort, "127.0.0.1");
1686
1706
  });
1687
1707
  }
1688
- const CONTROL_ENVELOPE_VERSION = 1;
1689
1708
  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 = {
1709
+ const LEVEL_ORDER = [
1710
+ "error",
1711
+ "warn",
1712
+ "info",
1713
+ "debug",
1714
+ "trace"
1715
+ ];
1716
+ function levelRank(level) {
1717
+ const l = "log" === level ? "info" : level;
1718
+ const i = LEVEL_ORDER.indexOf(l);
1719
+ return -1 === i ? LEVEL_ORDER.length : i;
1720
+ }
1721
+ function makeUrlMatcher(pattern) {
1722
+ const hasGlob = pattern.includes("*");
1723
+ let re = null;
1724
+ if (hasGlob) {
1725
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1726
+ re = new RegExp(escaped);
1727
+ }
1728
+ return (event)=>{
1729
+ const candidates = [
1730
+ event.url,
1731
+ event.hostname
1732
+ ].filter((v)=>"string" == typeof v);
1733
+ if (0 === candidates.length) return false;
1734
+ return candidates.some((c)=>re ? re.test(c) : c.includes(pattern));
1735
+ };
1736
+ }
1737
+ function makeFilter(args) {
1738
+ const minLevel = String(args.level || "all").toLowerCase();
1739
+ const rawContexts = Array.isArray(args.context) ? args.context : "string" == typeof args.context ? args.context.split(",") : null;
1740
+ const contexts = rawContexts && rawContexts.length ? new Set(rawContexts.map((c)=>c.trim()).filter(Boolean)) : null;
1741
+ const sinceSeq = null != args.since ? Number(args.since) : null;
1742
+ const urlMatches = args.url ? makeUrlMatcher(args.url) : null;
1743
+ const tabId = null != args.tab ? Number(args.tab) : null;
1744
+ return (event)=>{
1745
+ if (!event || "object" != typeof event) return false;
1746
+ if ("header" === event.type) return false;
1747
+ if (args.signalsOnly && "dx.signal" !== event.eventType) return false;
1748
+ if (contexts && !contexts.has(event.context)) return false;
1749
+ if ("all" !== minLevel && "off" !== minLevel) {
1750
+ if (levelRank(event.level) > levelRank(minLevel)) return false;
1751
+ }
1752
+ if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
1753
+ if (urlMatches && !urlMatches(event)) return false;
1754
+ if (null != tabId && Number.isFinite(tabId) && event.tabId !== tabId) return false;
1755
+ return true;
1756
+ };
1757
+ }
1758
+ const logs_schema_schema = {
1695
1759
  name: "extension_logs",
1696
1760
  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
1761
  inputSchema: {
@@ -1760,12 +1824,12 @@ const logs_schema = {
1760
1824
  },
1761
1825
  followMs: {
1762
1826
  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).`
1827
+ default: 4000,
1828
+ description: "How long to collect live frames when follow=true (clamped 500–15000ms)."
1765
1829
  },
1766
1830
  limit: {
1767
1831
  type: "number",
1768
- default: DEFAULT_LIMIT,
1832
+ default: 200,
1769
1833
  description: "Maximum number of (most recent) events to return."
1770
1834
  }
1771
1835
  },
@@ -1774,55 +1838,6 @@ const logs_schema = {
1774
1838
  ]
1775
1839
  }
1776
1840
  };
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
1841
  function logsFilePath(projectPath, browser) {
1827
1842
  return node_path.resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
1828
1843
  }
@@ -1898,7 +1913,7 @@ async function readFromStream(args, browser, limit) {
1898
1913
  error: `No active control channel found for ${browser}.`,
1899
1914
  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
1915
  });
1901
- const followMs = Math.min(Math.max(args.followMs ?? DEFAULT_FOLLOW_MS, MIN_FOLLOW_MS), MAX_FOLLOW_MS);
1916
+ const followMs = Math.min(Math.max(args.followMs ?? 4000, 500), 15000);
1902
1917
  const matches = makeFilter(args);
1903
1918
  const events = [];
1904
1919
  let dropped = 0;
@@ -1929,7 +1944,7 @@ async function readFromStream(args, browser, limit) {
1929
1944
  try {
1930
1945
  socket.send(JSON.stringify({
1931
1946
  type: "hello",
1932
- v: CONTROL_ENVELOPE_VERSION,
1947
+ v: 1,
1933
1948
  role: "consumer",
1934
1949
  instanceId: ready.instanceId
1935
1950
  }));
@@ -1961,7 +1976,7 @@ async function readFromStream(args, browser, limit) {
1961
1976
  }
1962
1977
  async function logs_handler(args) {
1963
1978
  const browser = args.browser ?? "chromium";
1964
- const limit = args.limit && args.limit > 0 ? args.limit : DEFAULT_LIMIT;
1979
+ const limit = args.limit && args.limit > 0 ? args.limit : 200;
1965
1980
  if (args.follow) return readFromStream(args, browser, limit);
1966
1981
  return readFromFile(args, browser, limit);
1967
1982
  }
@@ -2325,70 +2340,6 @@ async function dom_inspect_handler(args) {
2325
2340
  if (null != args.timeout) cli.push("--timeout", String(args.timeout));
2326
2341
  return runActVerb(cli, args.projectPath, args.timeout);
2327
2342
  }
2328
- function credentialsPath() {
2329
- if ("win32" === process.platform) {
2330
- const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
2331
- return node_path.join(base, "extension-dev", "auth.json");
2332
- }
2333
- const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
2334
- const base = xdg || node_path.join(node_os.homedir(), ".config");
2335
- return node_path.join(base, "extension-dev", "auth.json");
2336
- }
2337
- function readCredentials() {
2338
- try {
2339
- const raw = node_fs.readFileSync(credentialsPath(), "utf8");
2340
- const data = JSON.parse(raw);
2341
- if (!data || "object" != typeof data) return null;
2342
- if (1 !== data.version) return null;
2343
- const token = String(data.token || "").trim();
2344
- if (!token) return null;
2345
- return {
2346
- version: 1,
2347
- token,
2348
- workspaceSlug: String(data.workspaceSlug || ""),
2349
- projectSlug: String(data.projectSlug || ""),
2350
- expiresAt: Number(data.expiresAt || 0),
2351
- api: String(data.api || "")
2352
- };
2353
- } catch {
2354
- return null;
2355
- }
2356
- }
2357
- function writeCredentials(creds) {
2358
- const file = credentialsPath();
2359
- node_fs.mkdirSync(node_path.dirname(file), {
2360
- recursive: true
2361
- });
2362
- node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
2363
- mode: 384
2364
- });
2365
- try {
2366
- node_fs.chmodSync(file, 384);
2367
- } catch {}
2368
- return file;
2369
- }
2370
- function clearCredentials() {
2371
- const file = credentialsPath();
2372
- try {
2373
- node_fs.unlinkSync(file);
2374
- return {
2375
- cleared: true,
2376
- path: file
2377
- };
2378
- } catch {
2379
- return {
2380
- cleared: false,
2381
- path: file
2382
- };
2383
- }
2384
- }
2385
- function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
2386
- const creds = readCredentials();
2387
- if (!creds) return null;
2388
- if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
2389
- return creds;
2390
- }
2391
- const DEFAULT_API = "https://www.extension.dev";
2392
2343
  const publish_schema = {
2393
2344
  name: "extension_publish",
2394
2345
  description: "Publish a project to extension.dev and return a shareable URL. Auth-gated: requires EXTENSION_DEV_TOKEN (a workspace/project access token) in the environment. Posts to the platform's CLI publish endpoint directly. This is the only tool that talks to the hosted platform rather than the local browser.",
@@ -2417,12 +2368,68 @@ const publish_schema = {
2417
2368
  ]
2418
2369
  }
2419
2370
  };
2420
- function resolveToken() {
2421
- const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
2422
- if (fromEnv) return fromEnv;
2423
- const creds = readValidCredentials();
2424
- return creds?.token ? String(creds.token).trim() : "";
2371
+ async function publish_handler(args) {
2372
+ const token = resolveToken();
2373
+ if (!token) return JSON.stringify({
2374
+ ok: false,
2375
+ error: {
2376
+ name: "PublishAuthError",
2377
+ message: "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
2378
+ }
2379
+ });
2380
+ const result = await publish({
2381
+ ttlHours: args.ttlHours,
2382
+ buildSha: args.buildSha,
2383
+ api: args.api,
2384
+ token
2385
+ });
2386
+ return result.ok ? JSON.stringify(result.data) : JSON.stringify(result);
2425
2387
  }
2388
+ const DEFAULT_API = "https://www.extension.dev";
2389
+ const release_promote_schema = {
2390
+ name: "extension_release_promote",
2391
+ 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).",
2392
+ inputSchema: {
2393
+ type: "object",
2394
+ properties: {
2395
+ buildId: {
2396
+ type: "string",
2397
+ description: "Build commit SHA to promote (a 7-char short SHA is fine)"
2398
+ },
2399
+ channel: {
2400
+ type: "string",
2401
+ description: "Target release channel, e.g. stable, preview, beta"
2402
+ },
2403
+ sourceChannel: {
2404
+ type: "string",
2405
+ description: "Channel to promote from (optional; inferred otherwise)"
2406
+ },
2407
+ browsers: {
2408
+ type: "array",
2409
+ items: {
2410
+ type: "string"
2411
+ },
2412
+ description: "Browsers to release (optional; auto-detected from the build)"
2413
+ },
2414
+ version: {
2415
+ type: "string",
2416
+ description: "Version label for the release (optional)"
2417
+ },
2418
+ releaseNotes: {
2419
+ type: "string",
2420
+ description: "Release notes markdown (optional)"
2421
+ },
2422
+ api: {
2423
+ type: "string",
2424
+ description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
2425
+ }
2426
+ },
2427
+ required: [
2428
+ "buildId",
2429
+ "channel"
2430
+ ]
2431
+ }
2432
+ };
2426
2433
  function fail(name, message) {
2427
2434
  return JSON.stringify({
2428
2435
  ok: false,
@@ -2432,14 +2439,23 @@ function fail(name, message) {
2432
2439
  }
2433
2440
  });
2434
2441
  }
2435
- async function publish_handler(args) {
2442
+ async function release_promote_handler(args) {
2436
2443
  const token = resolveToken();
2437
- 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`;
2440
- const body = {};
2441
- if (null != args.ttlHours) body.ttlHours = Number(args.ttlHours);
2442
- if (args.buildSha) body.buildSha = args.buildSha;
2444
+ if (!token) return 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.");
2445
+ const buildId = String(args.buildId || "").trim();
2446
+ const channel = String(args.channel || "").trim();
2447
+ if (!buildId || !channel) return fail("ReleaseInputError", "buildId and channel are required.");
2448
+ const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API));
2449
+ if (!apiCheck.ok) return fail("ReleaseConfigError", apiCheck.message);
2450
+ const url = `${apiCheck.base}/api/cli/release/promote`;
2451
+ const body = {
2452
+ buildId,
2453
+ channel
2454
+ };
2455
+ if (args.sourceChannel) body.sourceChannel = String(args.sourceChannel).trim();
2456
+ if (Array.isArray(args.browsers) && args.browsers.length) body.browsers = args.browsers.map((b)=>String(b).trim()).filter(Boolean);
2457
+ if (args.version) body.version = String(args.version).trim();
2458
+ if (args.releaseNotes) body.releaseNotes = String(args.releaseNotes);
2443
2459
  let res;
2444
2460
  try {
2445
2461
  res = await fetch(url, {
@@ -2451,7 +2467,7 @@ async function publish_handler(args) {
2451
2467
  body: JSON.stringify(body)
2452
2468
  });
2453
2469
  } catch (err) {
2454
- return fail("PublishNetworkError", `Could not reach ${url}: ${err?.message || err}`);
2470
+ return fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}`);
2455
2471
  }
2456
2472
  const text = await res.text();
2457
2473
  let data;
@@ -2462,7 +2478,7 @@ async function publish_handler(args) {
2462
2478
  message: text
2463
2479
  };
2464
2480
  }
2465
- if (!res.ok) return fail("PublishError", `publish failed (${res.status}): ${data?.message || text || "unknown error"}`);
2481
+ if (!res.ok) return fail("ReleaseError", `promote failed (${res.status}): ${data?.message || text || "unknown error"}`);
2466
2482
  return JSON.stringify(data);
2467
2483
  }
2468
2484
  const wait_schema = {
@@ -2760,138 +2776,6 @@ async function add_feature_handler(args) {
2760
2776
  hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
2761
2777
  });
2762
2778
  }
2763
- const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
2764
- const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
2765
- const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
2766
- const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
2767
- async function startDeviceCode(args) {
2768
- const doFetch = args.fetchImpl ?? fetch;
2769
- const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
2770
- method: "POST",
2771
- headers: {
2772
- accept: "application/json",
2773
- "content-type": "application/json"
2774
- },
2775
- body: JSON.stringify({
2776
- client_id: args.clientId,
2777
- scope: args.scope || "read:user"
2778
- })
2779
- });
2780
- const data = await res.json().catch(()=>({}));
2781
- if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
2782
- return {
2783
- deviceCode: String(data.device_code || ""),
2784
- userCode: String(data.user_code || ""),
2785
- verificationUri: String(data.verification_uri || "https://github.com/login/device"),
2786
- interval: Number(data.interval || 5),
2787
- expiresIn: Number(data.expires_in || 900)
2788
- };
2789
- }
2790
- async function pollForToken(args) {
2791
- const doFetch = args.fetchImpl ?? fetch;
2792
- const sleep = args.sleepImpl ?? defaultSleep;
2793
- let intervalMs = 1000 * Math.max(1, args.interval);
2794
- const deadline = Date.now() + Math.max(0, args.budgetMs);
2795
- while(Date.now() < deadline){
2796
- await sleep(intervalMs);
2797
- const res = await doFetch(GITHUB_TOKEN_URL, {
2798
- method: "POST",
2799
- headers: {
2800
- accept: "application/json",
2801
- "content-type": "application/json"
2802
- },
2803
- body: JSON.stringify({
2804
- client_id: args.clientId,
2805
- device_code: args.deviceCode,
2806
- grant_type: DEVICE_GRANT_TYPE
2807
- })
2808
- });
2809
- const data = await res.json().catch(()=>({}));
2810
- const token = String(data.access_token || "").trim();
2811
- if (token) return {
2812
- ok: true,
2813
- githubToken: token
2814
- };
2815
- const error = String(data.error || "");
2816
- if ("authorization_pending" === error) continue;
2817
- if ("slow_down" === error) {
2818
- intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
2819
- continue;
2820
- }
2821
- if ("expired_token" === error) return {
2822
- ok: false,
2823
- reason: "expired"
2824
- };
2825
- if ("access_denied" === error) return {
2826
- ok: false,
2827
- reason: "denied"
2828
- };
2829
- }
2830
- return {
2831
- ok: false,
2832
- reason: "pending"
2833
- };
2834
- }
2835
- const login_flow_DEFAULT_API = "https://www.extension.dev";
2836
- function resolveApiBase(api) {
2837
- return String(api || process.env.EXTENSION_DEV_API_URL || login_flow_DEFAULT_API).replace(/\/+$/, "");
2838
- }
2839
- async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
2840
- const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
2841
- if (override) return {
2842
- clientId: override,
2843
- scope: "read:user"
2844
- };
2845
- const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
2846
- headers: {
2847
- accept: "application/json"
2848
- }
2849
- });
2850
- if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
2851
- const data = await res.json().catch(()=>({}));
2852
- const clientId = String(data.githubClientId || "").trim();
2853
- if (!clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
2854
- return {
2855
- clientId,
2856
- scope: String(data.scope || "read:user")
2857
- };
2858
- }
2859
- async function exchangeAndPersist(args) {
2860
- const doFetch = args.fetchImpl ?? fetch;
2861
- const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
2862
- method: "POST",
2863
- headers: {
2864
- "content-type": "application/json",
2865
- accept: "application/json"
2866
- },
2867
- body: JSON.stringify({
2868
- githubToken: args.githubToken,
2869
- project: args.project
2870
- })
2871
- });
2872
- const text = await res.text();
2873
- let data;
2874
- try {
2875
- data = JSON.parse(text);
2876
- } catch {
2877
- data = {
2878
- message: text
2879
- };
2880
- }
2881
- if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
2882
- const token = String(data.token || "").trim();
2883
- if (!token) throw new Error("Login exchange returned no token.");
2884
- const creds = {
2885
- version: 1,
2886
- token,
2887
- workspaceSlug: String(data.workspaceSlug || ""),
2888
- projectSlug: String(data.projectSlug || ""),
2889
- expiresAt: Number(data.expiresAt || 0),
2890
- api: args.apiBase
2891
- };
2892
- writeCredentials(creds);
2893
- return creds;
2894
- }
2895
2779
  const login_schema = {
2896
2780
  name: "extension_login",
2897
2781
  description: "Authenticate to extension.dev via GitHub device-code and store a project-scoped access token locally so extension_publish can use it. Two-phase: call with `project` to get a code + URL for the user to authorize, then call again with the returned `deviceCode` to finish. Never returns the token. This is the only tool besides extension_publish that talks to the hosted platform.",
@@ -3404,6 +3288,7 @@ const tools = [
3404
3288
  open_namespaceObject,
3405
3289
  dom_inspect_namespaceObject,
3406
3290
  publish_namespaceObject,
3291
+ release_promote_namespaceObject,
3407
3292
  wait_namespaceObject,
3408
3293
  add_feature_namespaceObject,
3409
3294
  login_namespaceObject,
@@ -3418,7 +3303,7 @@ for (const tool of tools)toolMap.set(tool.schema.name, tool);
3418
3303
  async function startServer() {
3419
3304
  const server = new Server({
3420
3305
  name: "extension-dev",
3421
- version: "3.13.5"
3306
+ version: package_namespaceObject.rE
3422
3307
  }, {
3423
3308
  capabilities: {
3424
3309
  tools: {}
@@ -3484,6 +3369,34 @@ async function runCli(cmd, args) {
3484
3369
  log(await whoami_handler());
3485
3370
  return 0;
3486
3371
  }
3372
+ if ("release" === cmd) {
3373
+ const sub = String(args[0] || "").trim();
3374
+ if ("promote" === sub) {
3375
+ const buildId = String(flag("build") || flag("build-id") || "").trim();
3376
+ const channel = String(flag("channel") || "").trim();
3377
+ if (!buildId || !channel) {
3378
+ log("Usage: extension-mcp release promote --build <sha> --channel <channel> [--source-channel <c>] [--version <v>] [--api <url>]");
3379
+ return 1;
3380
+ }
3381
+ const out = await release_promote_handler({
3382
+ buildId,
3383
+ channel,
3384
+ sourceChannel: flag("source-channel"),
3385
+ version: flag("version"),
3386
+ api: flag("api")
3387
+ });
3388
+ log(out);
3389
+ let parsed = null;
3390
+ try {
3391
+ parsed = JSON.parse(out);
3392
+ } catch {
3393
+ parsed = null;
3394
+ }
3395
+ return parsed?.ok === false ? 1 : 0;
3396
+ }
3397
+ log("Usage: extension-mcp release promote --build <sha> --channel <channel>");
3398
+ return 1;
3399
+ }
3487
3400
  if ("logout" === cmd) {
3488
3401
  log(await logout_handler());
3489
3402
  return 0;
@@ -3527,7 +3440,7 @@ async function runCli(cmd, args) {
3527
3440
  return 1;
3528
3441
  }
3529
3442
  }
3530
- log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami.`);
3443
+ log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami, release.`);
3531
3444
  return 1;
3532
3445
  }
3533
3446
  export { runCli, startServer };