@extension.dev/mcp 3.17.0-canary.1779910200.ddcd9eb → 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);
@@ -78,17 +80,35 @@ __webpack_require__.d(list_browsers_namespaceObject, {
78
80
  handler: ()=>list_browsers_handler,
79
81
  schema: ()=>list_browsers_schema
80
82
  });
83
+ var list_extensions_namespaceObject = {};
84
+ __webpack_require__.r(list_extensions_namespaceObject);
85
+ __webpack_require__.d(list_extensions_namespaceObject, {
86
+ handler: ()=>list_extensions_handler,
87
+ schema: ()=>list_extensions_schema
88
+ });
81
89
  var list_templates_namespaceObject = {};
82
90
  __webpack_require__.r(list_templates_namespaceObject);
83
91
  __webpack_require__.d(list_templates_namespaceObject, {
84
92
  handler: ()=>list_templates_handler,
85
93
  schema: ()=>list_templates_schema
86
94
  });
95
+ var login_namespaceObject = {};
96
+ __webpack_require__.r(login_namespaceObject);
97
+ __webpack_require__.d(login_namespaceObject, {
98
+ handler: ()=>login_handler,
99
+ schema: ()=>login_schema
100
+ });
101
+ var logout_namespaceObject = {};
102
+ __webpack_require__.r(logout_namespaceObject);
103
+ __webpack_require__.d(logout_namespaceObject, {
104
+ handler: ()=>logout_handler,
105
+ schema: ()=>logout_schema
106
+ });
87
107
  var logs_namespaceObject = {};
88
108
  __webpack_require__.r(logs_namespaceObject);
89
109
  __webpack_require__.d(logs_namespaceObject, {
90
110
  handler: ()=>logs_handler,
91
- schema: ()=>logs_schema
111
+ schema: ()=>logs_schema_schema
92
112
  });
93
113
  var manifest_validate_namespaceObject = {};
94
114
  __webpack_require__.r(manifest_validate_namespaceObject);
@@ -112,8 +132,16 @@ var publish_namespaceObject = {};
112
132
  __webpack_require__.r(publish_namespaceObject);
113
133
  __webpack_require__.d(publish_namespaceObject, {
114
134
  handler: ()=>publish_handler,
135
+ resolveToken: ()=>resolveToken,
136
+ safeApiBase: ()=>safeApiBase,
115
137
  schema: ()=>publish_schema
116
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
+ });
117
145
  var reload_namespaceObject = {};
118
146
  __webpack_require__.r(reload_namespaceObject);
119
147
  __webpack_require__.d(reload_namespaceObject, {
@@ -144,6 +172,15 @@ __webpack_require__.d(wait_namespaceObject, {
144
172
  handler: ()=>wait_handler,
145
173
  schema: ()=>wait_schema
146
174
  });
175
+ var whoami_namespaceObject = {};
176
+ __webpack_require__.r(whoami_namespaceObject);
177
+ __webpack_require__.d(whoami_namespaceObject, {
178
+ handler: ()=>whoami_handler,
179
+ schema: ()=>whoami_schema
180
+ });
181
+ var package_namespaceObject = {
182
+ rE: "4.0.8"
183
+ };
147
184
  const schema = {
148
185
  name: "extension_create",
149
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.",
@@ -373,7 +410,7 @@ async function build_handler(args) {
373
410
  }
374
411
  function runExtensionCli(args, options) {
375
412
  return new Promise((resolve)=>{
376
- const child = spawn("npx", [
413
+ const child = cross_spawn("npx", [
377
414
  "extension",
378
415
  ...args
379
416
  ], {
@@ -388,7 +425,6 @@ function runExtensionCli(args, options) {
388
425
  FORCE_COLOR: "0",
389
426
  NO_COLOR: "1"
390
427
  },
391
- shell: true,
392
428
  timeout: options?.timeoutMs ?? 30000
393
429
  });
394
430
  let stdout = "";
@@ -408,7 +444,7 @@ function runExtensionCli(args, options) {
408
444
  });
409
445
  }
410
446
  function spawnExtensionCli(args, options) {
411
- const child = spawn("npx", [
447
+ const child = cross_spawn("npx", [
412
448
  "extension",
413
449
  ...args
414
450
  ], {
@@ -423,8 +459,7 @@ function spawnExtensionCli(args, options) {
423
459
  ...process.env,
424
460
  FORCE_COLOR: "0",
425
461
  NO_COLOR: "1"
426
- },
427
- shell: true
462
+ }
428
463
  });
429
464
  child.unref();
430
465
  return child;
@@ -753,9 +788,8 @@ async function manifest_validate_handler(args) {
753
788
  }
754
789
  if (!manifest.name) result.errors.push("Missing required field: name");
755
790
  if (!manifest.version) result.warnings.push("Missing field: version (required for store submission)");
756
- const chromiumMv = manifest["chromium:manifest_version"] ?? manifest.manifest_version;
757
- manifest["firefox:manifest_version"] ?? manifest.manifest_version;
758
- 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.');
759
793
  for (const browser of browsers){
760
794
  const isChromium = [
761
795
  "chrome",
@@ -766,25 +800,26 @@ async function manifest_validate_handler(args) {
766
800
  "firefox",
767
801
  "gecko-based"
768
802
  ].includes(browser);
803
+ const effective = filterKeysForThisBrowser(manifest, browser);
769
804
  const issues = [];
770
805
  if (isChromium) {
771
- const mv = chromiumMv;
806
+ const mv = effective.manifest_version;
772
807
  if (mv && mv < 3) issues.push("Manifest V2 is deprecated on Chromium. Use chromium:manifest_version: 3.");
773
- if (manifest["chromium:side_panel"] || manifest.side_panel) {
774
- const perms = manifest["chromium:permissions"] ?? manifest.permissions ?? [];
808
+ if (effective.side_panel) {
809
+ const perms = effective.permissions ?? [];
775
810
  if (!perms.includes("sidePanel")) issues.push('Side panel declared but "sidePanel" permission is missing.');
776
811
  }
777
- 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".');
778
813
  }
779
814
  if (isFirefox) {
780
- const contentScripts = manifest.content_scripts;
815
+ const contentScripts = effective.content_scripts;
781
816
  if (contentScripts) {
782
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.');
783
818
  }
784
- 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.");
785
- 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;
786
821
  if (bg) {
787
- 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".');
788
823
  }
789
824
  }
790
825
  result.browserSupport[browser] = {
@@ -794,11 +829,11 @@ async function manifest_validate_handler(args) {
794
829
  if (issues.length) result.valid = false;
795
830
  }
796
831
  const surfaces = [];
797
- if (manifest.content_scripts) surfaces.push("content");
798
- if (manifest["chromium:side_panel"] || manifest.side_panel) surfaces.push("sidebar");
799
- if (manifest.action || manifest["chromium:action"] || manifest["firefox:browser_action"]) surfaces.push("action");
800
- if (manifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
801
- 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");
802
837
  if (surfaces.length) try {
803
838
  const templates = await listTemplates();
804
839
  result.similarTemplates = templates.filter((t)=>t.surfaces.some((s)=>surfaces.includes(s))).slice(0, 5).map((t)=>({
@@ -975,7 +1010,7 @@ function _define_property(obj, key, value) {
975
1010
  return obj;
976
1011
  }
977
1012
  const COMMAND_TIMEOUT_MS = 15000;
978
- class CDPClient {
1013
+ class CDPConnection {
979
1014
  async connect(wsUrl) {
980
1015
  return new Promise((resolve, reject)=>{
981
1016
  this.ws = new ws(wsUrl);
@@ -1067,6 +1102,186 @@ class CDPClient {
1067
1102
  this.ws.send(JSON.stringify(message));
1068
1103
  });
1069
1104
  }
1105
+ getConsoleMessages() {
1106
+ return [
1107
+ ...this.consoleMessages
1108
+ ];
1109
+ }
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
1128
+ });
1129
+ }
1130
+ return {
1131
+ total: this.consoleMessages.length,
1132
+ counts,
1133
+ topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
1134
+ };
1135
+ }
1136
+ onEvent(handler) {
1137
+ this.eventListeners.add(handler);
1138
+ return ()=>{
1139
+ this.eventListeners.delete(handler);
1140
+ };
1141
+ }
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 = `(() => {
1151
+ try {
1152
+ const doctype = document.doctype;
1153
+ const dt = doctype
1154
+ ? '<!DOCTYPE ' + doctype.name
1155
+ + (doctype.publicId ? ' PUBLIC "' + doctype.publicId + '"' : '')
1156
+ + (doctype.systemId ? ' "' + doctype.systemId + '"' : '')
1157
+ + '>'
1158
+ : '';
1159
+ // Include shadow DOM content from extension roots
1160
+ const roots = Array.from(document.querySelectorAll(
1161
+ '#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])'
1162
+ ));
1163
+ if (roots.length) {
1164
+ const clone = document.documentElement.cloneNode(true);
1165
+ const clonedRoots = Array.from(clone.querySelectorAll(
1166
+ '#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])'
1167
+ ));
1168
+ const s = new XMLSerializer();
1169
+ for (let i = 0; i < Math.min(roots.length, clonedRoots.length); i++) {
1170
+ const sr = roots[i].shadowRoot;
1171
+ if (!sr) continue;
1172
+ try {
1173
+ const shadow = Array.from(sr.childNodes).map(n => {
1174
+ try { return s.serializeToString(n); } catch { return ''; }
1175
+ }).join('');
1176
+ if (shadow) clonedRoots[i].innerHTML = shadow;
1177
+ } catch {}
1178
+ }
1179
+ return dt + '\\n' + clone.outerHTML;
1180
+ }
1181
+ return dt + '\\n' + document.documentElement.outerHTML;
1182
+ } catch (e) { return ''; }
1183
+ })()`;
1184
+ const PAGE_META_SCRIPT = `(() => {
1185
+ try {
1186
+ return {
1187
+ title: document.title,
1188
+ url: location.href,
1189
+ readyState: document.readyState,
1190
+ viewport: {
1191
+ width: window.innerWidth,
1192
+ height: window.innerHeight,
1193
+ devicePixelRatio: window.devicePixelRatio
1194
+ },
1195
+ frameCount: window.frames.length,
1196
+ scriptCount: document.querySelectorAll('script').length,
1197
+ styleCount: document.querySelectorAll('style,link[rel="stylesheet"]').length
1198
+ };
1199
+ } catch { return {}; }
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 `(() => {
1236
+ const selectors = ${JSON.stringify(selectors)};
1237
+ return selectors.map(selector => {
1238
+ try {
1239
+ const els = Array.from(document.querySelectorAll(selector));
1240
+ return {
1241
+ selector,
1242
+ count: els.length,
1243
+ samples: els.slice(0, 3).map(el => ({
1244
+ tag: el.tagName.toLowerCase(),
1245
+ id: el.id || undefined,
1246
+ classes: Array.from(el.classList).join(' ') || undefined,
1247
+ role: el.getAttribute('role') || undefined,
1248
+ ariaLabel: el.getAttribute('aria-label') || undefined,
1249
+ textLength: (el.textContent || '').length,
1250
+ textSnippet: (el.textContent || '').trim().slice(0, 80)
1251
+ }))
1252
+ };
1253
+ } catch (e) {
1254
+ return { selector, count: 0, samples: [], error: String(e) };
1255
+ }
1256
+ });
1257
+ })()`;
1258
+ }
1259
+ function domSnapshotScript(maxNodes) {
1260
+ return `(() => {
1261
+ const maxNodes = ${maxNodes};
1262
+ const nodes = [];
1263
+ const walk = (node, depth) => {
1264
+ if (nodes.length >= maxNodes || depth > 20) return;
1265
+ if (node.nodeType !== 1) return;
1266
+ const el = node;
1267
+ nodes.push({
1268
+ tag: el.tagName.toLowerCase(),
1269
+ depth,
1270
+ id: el.id || undefined,
1271
+ classes: Array.from(el.classList).slice(0, 5).join(' ') || undefined,
1272
+ role: el.getAttribute('role') || undefined,
1273
+ ariaLabel: el.getAttribute('aria-label') || undefined,
1274
+ childCount: el.children.length
1275
+ });
1276
+ for (const child of el.children) {
1277
+ walk(child, depth + 1);
1278
+ }
1279
+ };
1280
+ walk(document.documentElement, 0);
1281
+ return nodes;
1282
+ })()`;
1283
+ }
1284
+ class CDPClient extends CDPConnection {
1070
1285
  static async discoverBrowserWsUrl(port, host = "127.0.0.1") {
1071
1286
  const res = await fetch(`http://${host}:${port}/json/version`);
1072
1287
  if (!res.ok) throw new Error(`CDP /json/version failed: ${res.status}`);
@@ -1121,40 +1336,7 @@ class CDPClient {
1121
1336
  return response.result?.value;
1122
1337
  }
1123
1338
  async getPageHTML(sessionId) {
1124
- const result = await this.evaluate(sessionId, `(() => {
1125
- try {
1126
- const doctype = document.doctype;
1127
- const dt = doctype
1128
- ? '<!DOCTYPE ' + doctype.name
1129
- + (doctype.publicId ? ' PUBLIC "' + doctype.publicId + '"' : '')
1130
- + (doctype.systemId ? ' "' + doctype.systemId + '"' : '')
1131
- + '>'
1132
- : '';
1133
- // Include shadow DOM content from extension roots
1134
- const roots = Array.from(document.querySelectorAll(
1135
- '#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])'
1136
- ));
1137
- if (roots.length) {
1138
- const clone = document.documentElement.cloneNode(true);
1139
- const clonedRoots = Array.from(clone.querySelectorAll(
1140
- '#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])'
1141
- ));
1142
- const s = new XMLSerializer();
1143
- for (let i = 0; i < Math.min(roots.length, clonedRoots.length); i++) {
1144
- const sr = roots[i].shadowRoot;
1145
- if (!sr) continue;
1146
- try {
1147
- const shadow = Array.from(sr.childNodes).map(n => {
1148
- try { return s.serializeToString(n); } catch { return ''; }
1149
- }).join('');
1150
- if (shadow) clonedRoots[i].innerHTML = shadow;
1151
- } catch {}
1152
- }
1153
- return dt + '\\n' + clone.outerHTML;
1154
- }
1155
- return dt + '\\n' + document.documentElement.outerHTML;
1156
- } catch (e) { return ''; }
1157
- })()`);
1339
+ const result = await this.evaluate(sessionId, PAGE_HTML_SCRIPT);
1158
1340
  return "string" == typeof result ? result : "";
1159
1341
  }
1160
1342
  async getClosedShadowRoots(sessionId, maxBytes = 65536) {
@@ -1208,157 +1390,21 @@ class CDPClient {
1208
1390
  return out;
1209
1391
  }
1210
1392
  async getPageMeta(sessionId) {
1211
- const result = await this.evaluate(sessionId, `(() => {
1212
- try {
1213
- return {
1214
- title: document.title,
1215
- url: location.href,
1216
- readyState: document.readyState,
1217
- viewport: {
1218
- width: window.innerWidth,
1219
- height: window.innerHeight,
1220
- devicePixelRatio: window.devicePixelRatio
1221
- },
1222
- frameCount: window.frames.length,
1223
- scriptCount: document.querySelectorAll('script').length,
1224
- styleCount: document.querySelectorAll('style,link[rel="stylesheet"]').length
1225
- };
1226
- } catch { return {}; }
1227
- })()`);
1393
+ const result = await this.evaluate(sessionId, PAGE_META_SCRIPT);
1228
1394
  return result ?? {};
1229
1395
  }
1230
1396
  async probeSelectors(sessionId, selectors) {
1231
- const result = await this.evaluate(sessionId, `(() => {
1232
- const selectors = ${JSON.stringify(selectors)};
1233
- return selectors.map(selector => {
1234
- try {
1235
- const els = Array.from(document.querySelectorAll(selector));
1236
- return {
1237
- selector,
1238
- count: els.length,
1239
- samples: els.slice(0, 3).map(el => ({
1240
- tag: el.tagName.toLowerCase(),
1241
- id: el.id || undefined,
1242
- classes: Array.from(el.classList).join(' ') || undefined,
1243
- role: el.getAttribute('role') || undefined,
1244
- ariaLabel: el.getAttribute('aria-label') || undefined,
1245
- textLength: (el.textContent || '').length,
1246
- textSnippet: (el.textContent || '').trim().slice(0, 80)
1247
- }))
1248
- };
1249
- } catch (e) {
1250
- return { selector, count: 0, samples: [], error: String(e) };
1251
- }
1252
- });
1253
- })()`);
1397
+ const result = await this.evaluate(sessionId, probeSelectorsScript(selectors));
1254
1398
  return result ?? [];
1255
1399
  }
1256
1400
  async getDomSnapshot(sessionId, maxNodes = 500) {
1257
- const result = await this.evaluate(sessionId, `(() => {
1258
- const maxNodes = ${maxNodes};
1259
- const nodes = [];
1260
- const walk = (node, depth) => {
1261
- if (nodes.length >= maxNodes || depth > 20) return;
1262
- if (node.nodeType !== 1) return;
1263
- const el = node;
1264
- nodes.push({
1265
- tag: el.tagName.toLowerCase(),
1266
- depth,
1267
- id: el.id || undefined,
1268
- classes: Array.from(el.classList).slice(0, 5).join(' ') || undefined,
1269
- role: el.getAttribute('role') || undefined,
1270
- ariaLabel: el.getAttribute('aria-label') || undefined,
1271
- childCount: el.children.length
1272
- });
1273
- for (const child of el.children) {
1274
- walk(child, depth + 1);
1275
- }
1276
- };
1277
- walk(document.documentElement, 0);
1278
- return nodes;
1279
- })()`);
1401
+ const result = await this.evaluate(sessionId, domSnapshotScript(maxNodes));
1280
1402
  return result ?? [];
1281
1403
  }
1282
1404
  async getExtensionRootMeta(sessionId) {
1283
- const result = await this.evaluate(sessionId, `(() => {
1284
- try {
1285
- const readGeneration = (node) => {
1286
- const raw = node.getAttribute && node.getAttribute('data-extjs-reinject-generation');
1287
- const parsed = Number(raw);
1288
- return Number.isFinite(parsed) ? parsed : undefined;
1289
- };
1290
- const normalize = (node) => ({
1291
- tag: node.tagName ? String(node.tagName).toLowerCase() : 'unknown',
1292
- id: node.id || undefined,
1293
- key: node.getAttribute ? node.getAttribute('data-extjs-reinject-key') || undefined : undefined,
1294
- generation: readGeneration(node),
1295
- status: node.getAttribute ? node.getAttribute('data-extjs-reinject-status') || undefined : undefined
1296
- });
1297
- const roots = Array.from(
1298
- document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])')
1299
- ).slice(0, 10).map(normalize);
1300
- const markers = Array.from(
1301
- document.querySelectorAll('[data-extjs-reinject-marker="true"]')
1302
- ).slice(0, 10).map(normalize);
1303
- if (!roots.length && !markers.length) return null;
1304
- const generations = [...roots, ...markers]
1305
- .map(e => e.generation)
1306
- .filter(g => typeof g === 'number');
1307
- return {
1308
- rootCount: roots.length,
1309
- markerCount: markers.length,
1310
- latestGeneration: generations.length ? Math.max(...generations) : 0,
1311
- roots,
1312
- markers
1313
- };
1314
- } catch { return null; }
1315
- })()`);
1405
+ const result = await this.evaluate(sessionId, EXTENSION_ROOT_META_SCRIPT);
1316
1406
  return result ?? null;
1317
1407
  }
1318
- getConsoleMessages() {
1319
- return [
1320
- ...this.consoleMessages
1321
- ];
1322
- }
1323
- getConsoleSummary() {
1324
- const counts = {};
1325
- const uniqueByLevel = {};
1326
- for (const msg of this.consoleMessages){
1327
- counts[msg.level] = (counts[msg.level] ?? 0) + 1;
1328
- if (!uniqueByLevel[msg.level]) uniqueByLevel[msg.level] = new Map();
1329
- const key = msg.text.slice(0, 200);
1330
- uniqueByLevel[msg.level].set(key, (uniqueByLevel[msg.level].get(key) ?? 0) + 1);
1331
- }
1332
- const topMessages = [];
1333
- for (const [level, msgs] of Object.entries(uniqueByLevel)){
1334
- const sorted = [
1335
- ...msgs.entries()
1336
- ].sort((a, b)=>b[1] - a[1]);
1337
- for (const [text, count] of sorted.slice(0, 5))topMessages.push({
1338
- level,
1339
- text,
1340
- count
1341
- });
1342
- }
1343
- return {
1344
- total: this.consoleMessages.length,
1345
- counts,
1346
- topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
1347
- };
1348
- }
1349
- onEvent(handler) {
1350
- this.eventListeners.add(handler);
1351
- return ()=>{
1352
- this.eventListeners.delete(handler);
1353
- };
1354
- }
1355
- constructor(){
1356
- _define_property(this, "ws", null);
1357
- _define_property(this, "messageId", 0);
1358
- _define_property(this, "eventListeners", new Set());
1359
- _define_property(this, "pendingRequests", new Map());
1360
- _define_property(this, "consoleMessages", []);
1361
- }
1362
1408
  }
1363
1409
  const source_inspect_schema = {
1364
1410
  name: "extension_source_inspect",
@@ -1546,13 +1592,171 @@ async function source_inspect_handler(args) {
1546
1592
  cdp.disconnect();
1547
1593
  }
1548
1594
  }
1549
- const CONTROL_ENVELOPE_VERSION = 1;
1595
+ const list_extensions_schema = {
1596
+ name: "extension_list_extensions",
1597
+ description: "List the extensions with a live context in the running dev browser via Chrome DevTools Protocol. Returns each extension's id, name, version, and live contexts (service worker, page). Identity is read read-only via the Extensions domain — other extensions' contexts are never attached to or evaluated in. A dormant MV3 service worker with no open page may be absent until it wakes. Chromium only (Firefox uses RDP, not yet supported). Requires an active dev or start session.",
1598
+ inputSchema: {
1599
+ type: "object",
1600
+ properties: {
1601
+ projectPath: {
1602
+ type: "string",
1603
+ description: "Path to the extension project root (must have an active dev session)"
1604
+ },
1605
+ browser: {
1606
+ type: "string",
1607
+ default: "chrome"
1608
+ }
1609
+ },
1610
+ required: [
1611
+ "projectPath"
1612
+ ]
1613
+ }
1614
+ };
1615
+ async function list_extensions_handler(args) {
1616
+ const browser = args.browser ?? "chrome";
1617
+ const isChromium = [
1618
+ "chrome",
1619
+ "edge",
1620
+ "chromium-based"
1621
+ ].includes(browser);
1622
+ if (!isChromium) return JSON.stringify({
1623
+ error: `Listing extensions for ${browser} uses RDP (Remote Debug Protocol). Currently only Chromium CDP is supported.`,
1624
+ hint: "Use --browser=chrome."
1625
+ });
1626
+ const cdpPort = await list_extensions_findCdpPort(args.projectPath, browser);
1627
+ if (!cdpPort) return JSON.stringify({
1628
+ error: "No active dev session found. Cannot connect to Chrome DevTools Protocol.",
1629
+ hint: "Start a dev session first with extension_dev, then use extension_wait to confirm it is ready."
1630
+ });
1631
+ const cdp = new CDPClient();
1632
+ try {
1633
+ const browserWsUrl = await CDPClient.discoverBrowserWsUrl(cdpPort);
1634
+ await cdp.connect(browserWsUrl);
1635
+ const targets = await cdp.getTargets();
1636
+ const byId = new Map();
1637
+ for (const t of targets){
1638
+ const url = String(t.url ?? "");
1639
+ if (!url.startsWith("chrome-extension://")) continue;
1640
+ const id = url.slice(19).split("/")[0];
1641
+ if (!id) continue;
1642
+ const list = byId.get(id) ?? [];
1643
+ list.push({
1644
+ type: String(t.type ?? ""),
1645
+ url
1646
+ });
1647
+ byId.set(id, list);
1648
+ }
1649
+ const extensions = [];
1650
+ for (const [id, ctxTargets] of byId){
1651
+ const entry = {
1652
+ id,
1653
+ contexts: ctxTargets.map((c)=>({
1654
+ type: c.type,
1655
+ url: c.url
1656
+ })),
1657
+ source: "target-only"
1658
+ };
1659
+ try {
1660
+ const info = await cdp.sendCommand("Extensions.getExtensionInfo", {
1661
+ extensionId: id
1662
+ });
1663
+ if (info?.extensionInfo) {
1664
+ entry.name = info.extensionInfo.name;
1665
+ entry.version = info.extensionInfo.version;
1666
+ entry.source = "extensions-domain";
1667
+ }
1668
+ } catch {}
1669
+ extensions.push(entry);
1670
+ }
1671
+ extensions.sort((a, b)=>(a.name ?? a.id).localeCompare(b.name ?? b.id));
1672
+ return JSON.stringify({
1673
+ cdpPort,
1674
+ browser,
1675
+ count: extensions.length,
1676
+ extensions,
1677
+ note: "Lists extensions that currently have at least one live context (service worker or open page). An MV3 service worker that has gone dormant with no open page may be absent until it wakes. Identity is read read-only via the Extensions domain; other extensions' contexts are never attached to or evaluated in."
1678
+ });
1679
+ } catch (error) {
1680
+ return JSON.stringify({
1681
+ error: `Failed to list extensions: ${error.message}`
1682
+ });
1683
+ } finally{
1684
+ cdp.disconnect();
1685
+ }
1686
+ }
1687
+ async function list_extensions_findCdpPort(projectPath, browser) {
1688
+ const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
1689
+ try {
1690
+ const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
1691
+ if ("number" == typeof contract.cdpPort) return contract.cdpPort;
1692
+ } catch {}
1693
+ const defaultPort = 9222;
1694
+ return new Promise((resolve)=>{
1695
+ const socket = new node_net.Socket();
1696
+ socket.setTimeout(1000);
1697
+ socket.on("connect", ()=>{
1698
+ socket.destroy();
1699
+ resolve(defaultPort);
1700
+ });
1701
+ socket.on("error", ()=>resolve(null));
1702
+ socket.on("timeout", ()=>{
1703
+ socket.destroy();
1704
+ resolve(null);
1705
+ });
1706
+ socket.connect(defaultPort, "127.0.0.1");
1707
+ });
1708
+ }
1550
1709
  const CONTROL_WS_PATH = "/extjs-control";
1551
- const DEFAULT_LIMIT = 200;
1552
- const DEFAULT_FOLLOW_MS = 4000;
1553
- const MIN_FOLLOW_MS = 500;
1554
- const MAX_FOLLOW_MS = 15000;
1555
- 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 = {
1556
1760
  name: "extension_logs",
1557
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.",
1558
1762
  inputSchema: {
@@ -1621,12 +1825,12 @@ const logs_schema = {
1621
1825
  },
1622
1826
  followMs: {
1623
1827
  type: "number",
1624
- default: DEFAULT_FOLLOW_MS,
1625
- 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)."
1626
1830
  },
1627
1831
  limit: {
1628
1832
  type: "number",
1629
- default: DEFAULT_LIMIT,
1833
+ default: 200,
1630
1834
  description: "Maximum number of (most recent) events to return."
1631
1835
  }
1632
1836
  },
@@ -1634,56 +1838,7 @@ const logs_schema = {
1634
1838
  "projectPath"
1635
1839
  ]
1636
1840
  }
1637
- };
1638
- const LEVEL_ORDER = [
1639
- "error",
1640
- "warn",
1641
- "info",
1642
- "debug",
1643
- "trace"
1644
- ];
1645
- function levelRank(level) {
1646
- const l = "log" === level ? "info" : level;
1647
- const i = LEVEL_ORDER.indexOf(l);
1648
- return -1 === i ? LEVEL_ORDER.length : i;
1649
- }
1650
- function makeUrlMatcher(pattern) {
1651
- const hasGlob = pattern.includes("*");
1652
- let re = null;
1653
- if (hasGlob) {
1654
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1655
- re = new RegExp(escaped);
1656
- }
1657
- return (event)=>{
1658
- const candidates = [
1659
- event.url,
1660
- event.hostname
1661
- ].filter((v)=>"string" == typeof v);
1662
- if (0 === candidates.length) return false;
1663
- return candidates.some((c)=>re ? re.test(c) : c.includes(pattern));
1664
- };
1665
- }
1666
- function makeFilter(args) {
1667
- const minLevel = String(args.level || "all").toLowerCase();
1668
- const rawContexts = Array.isArray(args.context) ? args.context : "string" == typeof args.context ? args.context.split(",") : null;
1669
- const contexts = rawContexts && rawContexts.length ? new Set(rawContexts.map((c)=>c.trim()).filter(Boolean)) : null;
1670
- const sinceSeq = null != args.since ? Number(args.since) : null;
1671
- const urlMatches = args.url ? makeUrlMatcher(args.url) : null;
1672
- const tabId = null != args.tab ? Number(args.tab) : null;
1673
- return (event)=>{
1674
- if (!event || "object" != typeof event) return false;
1675
- if ("header" === event.type) return false;
1676
- if (args.signalsOnly && "dx.signal" !== event.eventType) return false;
1677
- if (contexts && !contexts.has(event.context)) return false;
1678
- if ("all" !== minLevel && "off" !== minLevel) {
1679
- if (levelRank(event.level) > levelRank(minLevel)) return false;
1680
- }
1681
- if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
1682
- if (urlMatches && !urlMatches(event)) return false;
1683
- if (null != tabId && Number.isFinite(tabId) && event.tabId !== tabId) return false;
1684
- return true;
1685
- };
1686
- }
1841
+ };
1687
1842
  function logsFilePath(projectPath, browser) {
1688
1843
  return node_path.resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
1689
1844
  }
@@ -1759,7 +1914,7 @@ async function readFromStream(args, browser, limit) {
1759
1914
  error: `No active control channel found for ${browser}.`,
1760
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.`
1761
1916
  });
1762
- 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);
1763
1918
  const matches = makeFilter(args);
1764
1919
  const events = [];
1765
1920
  let dropped = 0;
@@ -1790,7 +1945,7 @@ async function readFromStream(args, browser, limit) {
1790
1945
  try {
1791
1946
  socket.send(JSON.stringify({
1792
1947
  type: "hello",
1793
- v: CONTROL_ENVELOPE_VERSION,
1948
+ v: 1,
1794
1949
  role: "consumer",
1795
1950
  instanceId: ready.instanceId
1796
1951
  }));
@@ -1822,7 +1977,7 @@ async function readFromStream(args, browser, limit) {
1822
1977
  }
1823
1978
  async function logs_handler(args) {
1824
1979
  const browser = args.browser ?? "chromium";
1825
- const limit = args.limit && args.limit > 0 ? args.limit : DEFAULT_LIMIT;
1980
+ const limit = args.limit && args.limit > 0 ? args.limit : 200;
1826
1981
  if (args.follow) return readFromStream(args, browser, limit);
1827
1982
  return readFromFile(args, browser, limit);
1828
1983
  }
@@ -2046,7 +2201,7 @@ async function reload_handler(args) {
2046
2201
  }
2047
2202
  const open_schema = {
2048
2203
  name: "extension_open",
2049
- description: "Open an extension surface popup, options, or sidebar in a running session. Requires the dev session to be started with --allow-control. Wraps `extension open`.",
2204
+ description: "Open an extension surface or replay an event in a running session. 'popup'/'options'/'sidebar' open UI surfaces. 'action' triggers the toolbar action: opens the action's popup, or (no popup) replays chrome.action.onClicked. 'command' replays a chrome.commands.onCommand keyboard shortcut (pass `name`). NOTE: action/command replay invokes your listener WITHOUT a user gesture, so the gesture-derived activeTab grant does not apply (the result includes gesture:false and a warning when activeTab is declared). Requires --allow-control. Wraps `extension open`.",
2050
2205
  inputSchema: {
2051
2206
  type: "object",
2052
2207
  properties: {
@@ -2059,9 +2214,15 @@ const open_schema = {
2059
2214
  enum: [
2060
2215
  "popup",
2061
2216
  "options",
2062
- "sidebar"
2217
+ "sidebar",
2218
+ "action",
2219
+ "command"
2063
2220
  ],
2064
- description: "Which surface to open"
2221
+ description: "Which surface to open or event to replay. 'action' triggers the toolbar action; 'command' replays a keyboard-shortcut command (requires `name`)."
2222
+ },
2223
+ name: {
2224
+ type: "string",
2225
+ description: "For surface 'command': the chrome.commands name to trigger."
2065
2226
  },
2066
2227
  browser: {
2067
2228
  type: "string",
@@ -2084,6 +2245,7 @@ async function open_handler(args) {
2084
2245
  args.surface,
2085
2246
  args.projectPath
2086
2247
  ];
2248
+ if ("command" === args.surface && args.name) cli.push("--name", args.name);
2087
2249
  if (args.browser) cli.push("--browser", args.browser);
2088
2250
  if (null != args.timeout) cli.push("--timeout", String(args.timeout));
2089
2251
  return runActVerb(cli, args.projectPath, args.timeout);
@@ -2179,9 +2341,73 @@ async function dom_inspect_handler(args) {
2179
2341
  if (null != args.timeout) cli.push("--timeout", String(args.timeout));
2180
2342
  return runActVerb(cli, args.projectPath, args.timeout);
2181
2343
  }
2344
+ function credentialsPath() {
2345
+ if ("win32" === process.platform) {
2346
+ const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
2347
+ return node_path.join(base, "extension-dev", "auth.json");
2348
+ }
2349
+ const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
2350
+ const base = xdg || node_path.join(node_os.homedir(), ".config");
2351
+ return node_path.join(base, "extension-dev", "auth.json");
2352
+ }
2353
+ function readCredentials() {
2354
+ try {
2355
+ const raw = node_fs.readFileSync(credentialsPath(), "utf8");
2356
+ const data = JSON.parse(raw);
2357
+ if (!data || "object" != typeof data) return null;
2358
+ if (1 !== data.version) return null;
2359
+ const token = String(data.token || "").trim();
2360
+ if (!token) return null;
2361
+ return {
2362
+ version: 1,
2363
+ token,
2364
+ workspaceSlug: String(data.workspaceSlug || ""),
2365
+ projectSlug: String(data.projectSlug || ""),
2366
+ expiresAt: Number(data.expiresAt || 0),
2367
+ api: String(data.api || "")
2368
+ };
2369
+ } catch {
2370
+ return null;
2371
+ }
2372
+ }
2373
+ function writeCredentials(creds) {
2374
+ const file = credentialsPath();
2375
+ node_fs.mkdirSync(node_path.dirname(file), {
2376
+ recursive: true
2377
+ });
2378
+ node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
2379
+ mode: 384
2380
+ });
2381
+ try {
2382
+ node_fs.chmodSync(file, 384);
2383
+ } catch {}
2384
+ return file;
2385
+ }
2386
+ function clearCredentials() {
2387
+ const file = credentialsPath();
2388
+ try {
2389
+ node_fs.unlinkSync(file);
2390
+ return {
2391
+ cleared: true,
2392
+ path: file
2393
+ };
2394
+ } catch {
2395
+ return {
2396
+ cleared: false,
2397
+ path: file
2398
+ };
2399
+ }
2400
+ }
2401
+ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
2402
+ const creds = readCredentials();
2403
+ if (!creds) return null;
2404
+ if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
2405
+ return creds;
2406
+ }
2407
+ const DEFAULT_API = "https://www.extension.dev";
2182
2408
  const publish_schema = {
2183
2409
  name: "extension_publish",
2184
- 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. Wraps `extension publish` (which posts to the platform's CLI publish endpoint). This is the only tool that talks to the hosted platform rather than the local browser.",
2410
+ 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.",
2185
2411
  inputSchema: {
2186
2412
  type: "object",
2187
2413
  properties: {
@@ -2207,32 +2433,171 @@ const publish_schema = {
2207
2433
  ]
2208
2434
  }
2209
2435
  };
2210
- async function publish_handler(args) {
2211
- const cli = [
2212
- "publish",
2213
- args.projectPath,
2214
- "--output",
2215
- "json"
2216
- ];
2217
- if (null != args.ttlHours) cli.push("--ttl", String(args.ttlHours));
2218
- if (args.buildSha) cli.push("--build-sha", args.buildSha);
2219
- if (args.api) cli.push("--api", args.api);
2220
- const { code, stdout, stderr } = await runExtensionCli(cli, {
2221
- cwd: args.projectPath
2436
+ function resolveToken() {
2437
+ const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
2438
+ if (fromEnv) return fromEnv;
2439
+ const creds = readValidCredentials();
2440
+ return creds?.token ? String(creds.token).trim() : "";
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
+ }
2462
+ function fail(name, message) {
2463
+ return JSON.stringify({
2464
+ ok: false,
2465
+ error: {
2466
+ name,
2467
+ message
2468
+ }
2222
2469
  });
2223
- const out = stdout.trim();
2224
- if (out) try {
2225
- JSON.parse(out);
2226
- return out;
2227
- } catch {}
2470
+ }
2471
+ async function publish_handler(args) {
2472
+ const token = resolveToken();
2473
+ if (!token) return fail("PublishAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
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`;
2477
+ const body = {};
2478
+ if (null != args.ttlHours) body.ttlHours = Number(args.ttlHours);
2479
+ if (args.buildSha) body.buildSha = args.buildSha;
2480
+ let res;
2481
+ try {
2482
+ res = await fetch(url, {
2483
+ method: "POST",
2484
+ headers: {
2485
+ authorization: `Bearer ${token}`,
2486
+ "content-type": "application/json"
2487
+ },
2488
+ body: JSON.stringify(body)
2489
+ });
2490
+ } catch (err) {
2491
+ return fail("PublishNetworkError", `Could not reach ${url}: ${err?.message || err}`);
2492
+ }
2493
+ const text = await res.text();
2494
+ let data;
2495
+ try {
2496
+ data = JSON.parse(text);
2497
+ } catch {
2498
+ data = {
2499
+ message: text
2500
+ };
2501
+ }
2502
+ if (!res.ok) return fail("PublishError", `publish failed (${res.status}): ${data?.message || text || "unknown error"}`);
2503
+ return JSON.stringify(data);
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) {
2228
2551
  return JSON.stringify({
2229
2552
  ok: false,
2230
2553
  error: {
2231
- name: "PublishError",
2232
- message: stderr.trim() || `extension publish exited with code ${code}`
2554
+ name,
2555
+ message
2233
2556
  }
2234
2557
  });
2235
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
+ }
2236
2601
  const wait_schema = {
2237
2602
  name: "extension_wait",
2238
2603
  description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status.",
@@ -2528,6 +2893,303 @@ async function add_feature_handler(args) {
2528
2893
  hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
2529
2894
  });
2530
2895
  }
2896
+ const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
2897
+ const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
2898
+ const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
2899
+ const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
2900
+ async function startDeviceCode(args) {
2901
+ const doFetch = args.fetchImpl ?? fetch;
2902
+ const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
2903
+ method: "POST",
2904
+ headers: {
2905
+ accept: "application/json",
2906
+ "content-type": "application/json"
2907
+ },
2908
+ body: JSON.stringify({
2909
+ client_id: args.clientId,
2910
+ scope: args.scope || "read:user"
2911
+ })
2912
+ });
2913
+ const data = await res.json().catch(()=>({}));
2914
+ if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
2915
+ return {
2916
+ deviceCode: String(data.device_code || ""),
2917
+ userCode: String(data.user_code || ""),
2918
+ verificationUri: String(data.verification_uri || "https://github.com/login/device"),
2919
+ interval: Number(data.interval || 5),
2920
+ expiresIn: Number(data.expires_in || 900)
2921
+ };
2922
+ }
2923
+ async function pollForToken(args) {
2924
+ const doFetch = args.fetchImpl ?? fetch;
2925
+ const sleep = args.sleepImpl ?? defaultSleep;
2926
+ let intervalMs = 1000 * Math.max(1, args.interval);
2927
+ const deadline = Date.now() + Math.max(0, args.budgetMs);
2928
+ while(Date.now() < deadline){
2929
+ await sleep(intervalMs);
2930
+ const res = await doFetch(GITHUB_TOKEN_URL, {
2931
+ method: "POST",
2932
+ headers: {
2933
+ accept: "application/json",
2934
+ "content-type": "application/json"
2935
+ },
2936
+ body: JSON.stringify({
2937
+ client_id: args.clientId,
2938
+ device_code: args.deviceCode,
2939
+ grant_type: DEVICE_GRANT_TYPE
2940
+ })
2941
+ });
2942
+ const data = await res.json().catch(()=>({}));
2943
+ const token = String(data.access_token || "").trim();
2944
+ if (token) return {
2945
+ ok: true,
2946
+ githubToken: token
2947
+ };
2948
+ const error = String(data.error || "");
2949
+ if ("authorization_pending" === error) continue;
2950
+ if ("slow_down" === error) {
2951
+ intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
2952
+ continue;
2953
+ }
2954
+ if ("expired_token" === error) return {
2955
+ ok: false,
2956
+ reason: "expired"
2957
+ };
2958
+ if ("access_denied" === error) return {
2959
+ ok: false,
2960
+ reason: "denied"
2961
+ };
2962
+ }
2963
+ return {
2964
+ ok: false,
2965
+ reason: "pending"
2966
+ };
2967
+ }
2968
+ const login_flow_DEFAULT_API = "https://www.extension.dev";
2969
+ function resolveApiBase(api) {
2970
+ return String(api || process.env.EXTENSION_DEV_API_URL || login_flow_DEFAULT_API).replace(/\/+$/, "");
2971
+ }
2972
+ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
2973
+ const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
2974
+ if (override) return {
2975
+ clientId: override,
2976
+ scope: "read:user"
2977
+ };
2978
+ const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
2979
+ headers: {
2980
+ accept: "application/json"
2981
+ }
2982
+ });
2983
+ if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
2984
+ const data = await res.json().catch(()=>({}));
2985
+ const clientId = String(data.githubClientId || "").trim();
2986
+ if (!clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
2987
+ return {
2988
+ clientId,
2989
+ scope: String(data.scope || "read:user")
2990
+ };
2991
+ }
2992
+ async function exchangeAndPersist(args) {
2993
+ const doFetch = args.fetchImpl ?? fetch;
2994
+ const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
2995
+ method: "POST",
2996
+ headers: {
2997
+ "content-type": "application/json",
2998
+ accept: "application/json"
2999
+ },
3000
+ body: JSON.stringify({
3001
+ githubToken: args.githubToken,
3002
+ project: args.project
3003
+ })
3004
+ });
3005
+ const text = await res.text();
3006
+ let data;
3007
+ try {
3008
+ data = JSON.parse(text);
3009
+ } catch {
3010
+ data = {
3011
+ message: text
3012
+ };
3013
+ }
3014
+ if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
3015
+ const token = String(data.token || "").trim();
3016
+ if (!token) throw new Error("Login exchange returned no token.");
3017
+ const creds = {
3018
+ version: 1,
3019
+ token,
3020
+ workspaceSlug: String(data.workspaceSlug || ""),
3021
+ projectSlug: String(data.projectSlug || ""),
3022
+ expiresAt: Number(data.expiresAt || 0),
3023
+ api: args.apiBase
3024
+ };
3025
+ writeCredentials(creds);
3026
+ return creds;
3027
+ }
3028
+ const login_schema = {
3029
+ name: "extension_login",
3030
+ 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.",
3031
+ inputSchema: {
3032
+ type: "object",
3033
+ properties: {
3034
+ project: {
3035
+ type: "string",
3036
+ description: "Target project as '<workspace>/<project>' (the token is scoped to it)"
3037
+ },
3038
+ deviceCode: {
3039
+ type: "string",
3040
+ description: "Resume token from a prior call's `deviceCode`; omit on the first call"
3041
+ },
3042
+ api: {
3043
+ type: "string",
3044
+ description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
3045
+ }
3046
+ },
3047
+ required: [
3048
+ "project"
3049
+ ]
3050
+ }
3051
+ };
3052
+ const FIRST_CALL_BUDGET_MS = 8000;
3053
+ const RESUME_BUDGET_MS = 22000;
3054
+ function login_fail(name, message) {
3055
+ return JSON.stringify({
3056
+ ok: false,
3057
+ error: {
3058
+ name,
3059
+ message
3060
+ }
3061
+ });
3062
+ }
3063
+ function success(creds) {
3064
+ return JSON.stringify({
3065
+ ok: true,
3066
+ status: "logged-in",
3067
+ workspaceSlug: creds.workspaceSlug,
3068
+ projectSlug: creds.projectSlug,
3069
+ expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
3070
+ message: `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}. extension_publish can now use the stored token.`
3071
+ });
3072
+ }
3073
+ function login_pending(start) {
3074
+ return JSON.stringify({
3075
+ ok: false,
3076
+ status: "authorization_pending",
3077
+ userCode: start.userCode,
3078
+ verificationUri: start.verificationUri,
3079
+ deviceCode: start.deviceCode,
3080
+ message: `Open ${start.verificationUri} and enter code ${start.userCode}, then call extension_login again with this deviceCode (and the same project).`
3081
+ });
3082
+ }
3083
+ async function login_handler(args) {
3084
+ const project = String(args.project || "").trim();
3085
+ if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.");
3086
+ const apiBase = resolveApiBase(args.api);
3087
+ let config;
3088
+ try {
3089
+ config = await fetchLoginConfig(apiBase);
3090
+ } catch (err) {
3091
+ return login_fail("LoginConfigError", err?.message || "Could not load login config.");
3092
+ }
3093
+ if (args.deviceCode) {
3094
+ const poll = await pollForToken({
3095
+ clientId: config.clientId,
3096
+ deviceCode: String(args.deviceCode),
3097
+ interval: 5,
3098
+ budgetMs: RESUME_BUDGET_MS
3099
+ });
3100
+ if (!poll.ok) {
3101
+ if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
3102
+ if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
3103
+ return login_pending({
3104
+ deviceCode: String(args.deviceCode),
3105
+ userCode: "(see the previous response)",
3106
+ verificationUri: "https://github.com/login/device"
3107
+ });
3108
+ }
3109
+ try {
3110
+ const creds = await exchangeAndPersist({
3111
+ apiBase,
3112
+ githubToken: poll.githubToken,
3113
+ project
3114
+ });
3115
+ return success(creds);
3116
+ } catch (err) {
3117
+ return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
3118
+ }
3119
+ }
3120
+ let start;
3121
+ try {
3122
+ start = await startDeviceCode({
3123
+ clientId: config.clientId,
3124
+ scope: config.scope
3125
+ });
3126
+ } catch (err) {
3127
+ return login_fail("LoginStartError", err?.message || "Could not start device flow.");
3128
+ }
3129
+ const poll = await pollForToken({
3130
+ clientId: config.clientId,
3131
+ deviceCode: start.deviceCode,
3132
+ interval: start.interval,
3133
+ budgetMs: FIRST_CALL_BUDGET_MS
3134
+ });
3135
+ if (poll.ok) try {
3136
+ const creds = await exchangeAndPersist({
3137
+ apiBase,
3138
+ githubToken: poll.githubToken,
3139
+ project
3140
+ });
3141
+ return success(creds);
3142
+ } catch (err) {
3143
+ return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
3144
+ }
3145
+ if (!poll.ok && "denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
3146
+ return login_pending(start);
3147
+ }
3148
+ const whoami_schema = {
3149
+ name: "extension_whoami",
3150
+ description: "Report the locally stored extension.dev login (workspace/project and token expiry) without revealing the token. Returns logged-out status when no credentials are stored.",
3151
+ inputSchema: {
3152
+ type: "object",
3153
+ properties: {}
3154
+ }
3155
+ };
3156
+ async function whoami_handler() {
3157
+ const creds = readCredentials();
3158
+ if (!creds) return JSON.stringify({
3159
+ ok: true,
3160
+ status: "logged-out",
3161
+ message: "No stored credentials. Run extension_login to authenticate."
3162
+ });
3163
+ const now = Math.floor(Date.now() / 1000);
3164
+ const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
3165
+ return JSON.stringify({
3166
+ ok: true,
3167
+ status: expired ? "expired" : "logged-in",
3168
+ workspaceSlug: creds.workspaceSlug,
3169
+ projectSlug: creds.projectSlug,
3170
+ api: creds.api,
3171
+ expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
3172
+ expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
3173
+ expired,
3174
+ message: expired ? "Stored token has expired. Run extension_login to refresh it." : `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}.`
3175
+ });
3176
+ }
3177
+ const logout_schema = {
3178
+ name: "extension_logout",
3179
+ description: "Delete the locally stored extension.dev credentials. Does not revoke the token server-side (revoke from the dashboard if needed); only removes it from this machine.",
3180
+ inputSchema: {
3181
+ type: "object",
3182
+ properties: {}
3183
+ }
3184
+ };
3185
+ async function logout_handler() {
3186
+ const result = clearCredentials();
3187
+ return JSON.stringify({
3188
+ ok: true,
3189
+ cleared: result.cleared,
3190
+ message: result.cleared ? "Local credentials removed." : "No stored credentials to remove."
3191
+ });
3192
+ }
2531
3193
  const install_browser_schema = {
2532
3194
  name: "extension_install_browser",
2533
3195
  description: "Install a managed browser binary for extension testing. Useful in CI, Docker, or fresh environments where browsers are not pre-installed.",
@@ -2867,6 +3529,7 @@ const tools = [
2867
3529
  manifest_validate_namespaceObject,
2868
3530
  inspect_namespaceObject,
2869
3531
  source_inspect_namespaceObject,
3532
+ list_extensions_namespaceObject,
2870
3533
  logs_namespaceObject,
2871
3534
  eval_namespaceObject,
2872
3535
  storage_namespaceObject,
@@ -2874,8 +3537,12 @@ const tools = [
2874
3537
  open_namespaceObject,
2875
3538
  dom_inspect_namespaceObject,
2876
3539
  publish_namespaceObject,
3540
+ release_promote_namespaceObject,
2877
3541
  wait_namespaceObject,
2878
3542
  add_feature_namespaceObject,
3543
+ login_namespaceObject,
3544
+ whoami_namespaceObject,
3545
+ logout_namespaceObject,
2879
3546
  install_browser_namespaceObject,
2880
3547
  list_browsers_namespaceObject,
2881
3548
  detect_browsers_namespaceObject
@@ -2885,7 +3552,7 @@ for (const tool of tools)toolMap.set(tool.schema.name, tool);
2885
3552
  async function startServer() {
2886
3553
  const server = new Server({
2887
3554
  name: "extension-dev",
2888
- version: "3.13.5"
3555
+ version: package_namespaceObject.rE
2889
3556
  }, {
2890
3557
  capabilities: {
2891
3558
  tools: {}
@@ -2941,4 +3608,88 @@ async function startServer() {
2941
3608
  const transport = new StdioServerTransport();
2942
3609
  await server.connect(transport);
2943
3610
  }
2944
- export { startServer };
3611
+ async function runCli(cmd, args) {
3612
+ const log = (msg)=>process.stderr.write(`${msg}\n`);
3613
+ const flag = (name)=>{
3614
+ const i = args.indexOf(`--${name}`);
3615
+ return i >= 0 ? args[i + 1] : void 0;
3616
+ };
3617
+ if ("whoami" === cmd) {
3618
+ log(await whoami_handler());
3619
+ return 0;
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
+ }
3649
+ if ("logout" === cmd) {
3650
+ log(await logout_handler());
3651
+ return 0;
3652
+ }
3653
+ if ("login" === cmd) {
3654
+ const project = String(flag("project") || "").trim();
3655
+ if (!/^[^/]+\/[^/]+$/.test(project)) {
3656
+ log("Usage: extension-mcp login --project <workspace>/<project> [--api <url>]");
3657
+ return 1;
3658
+ }
3659
+ const apiBase = resolveApiBase(flag("api"));
3660
+ try {
3661
+ const config = await fetchLoginConfig(apiBase);
3662
+ const start = await startDeviceCode({
3663
+ clientId: config.clientId,
3664
+ scope: config.scope
3665
+ });
3666
+ log("");
3667
+ log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
3668
+ log("");
3669
+ log(" Waiting for authorization...");
3670
+ const poll = await pollForToken({
3671
+ clientId: config.clientId,
3672
+ deviceCode: start.deviceCode,
3673
+ interval: start.interval,
3674
+ budgetMs: 1000 * start.expiresIn
3675
+ });
3676
+ if (!poll.ok) {
3677
+ log("denied" === poll.reason ? "Authorization was denied on GitHub." : "Timed out waiting for authorization. Run login again.");
3678
+ return 1;
3679
+ }
3680
+ const creds = await exchangeAndPersist({
3681
+ apiBase,
3682
+ githubToken: poll.githubToken,
3683
+ project
3684
+ });
3685
+ log(`Logged in to ${creds.workspaceSlug}/${creds.projectSlug}.`);
3686
+ return 0;
3687
+ } catch (err) {
3688
+ log(err instanceof Error ? err.message : String(err));
3689
+ return 1;
3690
+ }
3691
+ }
3692
+ log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami, release.`);
3693
+ return 1;
3694
+ }
3695
+ export { runCli, startServer };