@extension.dev/mcp 4.3.0 → 4.5.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
@@ -11,8 +11,7 @@ import cross_spawn from "cross-spawn";
11
11
  import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
12
12
  import ws from "ws";
13
13
  import node_http from "node:http";
14
- import { clearCredentials, exchangeAndPersist, fetchLoginConfig, pollForToken, publish, readCredentials, resolveApiBase, resolveToken, safeApiBase, startDeviceCode } from "@extension.dev/core";
15
- import { extensionInstall, getManagedBrowsersCacheRoot } from "extension-install";
14
+ import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
16
15
  import { execFile } from "node:child_process";
17
16
  import { promisify } from "node:util";
18
17
  var add_feature_namespaceObject = {};
@@ -135,9 +134,9 @@ __webpack_require__.d(preview_namespaceObject, {
135
134
  handler: ()=>preview_handler,
136
135
  schema: ()=>preview_schema
137
136
  });
138
- var publish_namespaceObject = {};
139
- __webpack_require__.r(publish_namespaceObject);
140
- __webpack_require__.d(publish_namespaceObject, {
137
+ var tools_publish_namespaceObject = {};
138
+ __webpack_require__.r(tools_publish_namespaceObject);
139
+ __webpack_require__.d(tools_publish_namespaceObject, {
141
140
  handler: ()=>publish_handler,
142
141
  schema: ()=>publish_schema
143
142
  });
@@ -177,6 +176,12 @@ __webpack_require__.d(storage_namespaceObject, {
177
176
  handler: ()=>storage_handler,
178
177
  schema: ()=>storage_schema
179
178
  });
179
+ var uninstall_browser_namespaceObject = {};
180
+ __webpack_require__.r(uninstall_browser_namespaceObject);
181
+ __webpack_require__.d(uninstall_browser_namespaceObject, {
182
+ handler: ()=>uninstall_browser_handler,
183
+ schema: ()=>uninstall_browser_schema
184
+ });
180
185
  var wait_namespaceObject = {};
181
186
  __webpack_require__.r(wait_namespaceObject);
182
187
  __webpack_require__.d(wait_namespaceObject, {
@@ -189,7 +194,7 @@ __webpack_require__.d(whoami_namespaceObject, {
189
194
  handler: ()=>whoami_handler,
190
195
  schema: ()=>whoami_schema
191
196
  });
192
- var package_namespaceObject = JSON.parse('{"rE":"4.3.0","El":{"OP":"^4.0.8"}}');
197
+ var package_namespaceObject = JSON.parse('{"rE":"4.5.0","El":{"OP":"^4.0.11"}}');
193
198
  const create_schema = {
194
199
  name: "extension_create",
195
200
  description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options.",
@@ -379,9 +384,18 @@ const build_schema = {
379
384
  "chrome",
380
385
  "chromium",
381
386
  "edge",
387
+ "brave",
388
+ "opera",
389
+ "vivaldi",
390
+ "yandex",
382
391
  "firefox",
392
+ "waterfox",
393
+ "librewolf",
394
+ "safari",
383
395
  "chromium-based",
384
- "gecko-based"
396
+ "gecko-based",
397
+ "firefox-based",
398
+ "webkit-based"
385
399
  ],
386
400
  default: "chrome",
387
401
  description: "Target browser"
@@ -395,6 +409,30 @@ const build_schema = {
395
409
  type: "boolean",
396
410
  default: false,
397
411
  description: "Include source code zip (required by some stores)"
412
+ },
413
+ zipFilename: {
414
+ type: "string",
415
+ description: "Custom .zip file name (defaults to name and version)"
416
+ },
417
+ polyfill: {
418
+ type: "boolean",
419
+ default: false,
420
+ description: "Apply cross-browser polyfill"
421
+ },
422
+ silent: {
423
+ type: "boolean",
424
+ default: false,
425
+ description: "Suppress build output"
426
+ },
427
+ mode: {
428
+ type: "string",
429
+ enum: [
430
+ "development",
431
+ "production",
432
+ "none"
433
+ ],
434
+ default: "production",
435
+ description: "Bundler mode override (also sets NODE_ENV)"
398
436
  }
399
437
  },
400
438
  required: [
@@ -409,6 +447,18 @@ async function build_handler(args) {
409
447
  browser: args.browser ?? "chrome",
410
448
  zip: args.zip ?? false,
411
449
  zipSource: args.zipSource ?? false,
450
+ ...args.zipFilename ? {
451
+ zipFilename: args.zipFilename
452
+ } : {},
453
+ ...void 0 !== args.polyfill ? {
454
+ polyfill: args.polyfill
455
+ } : {},
456
+ ...void 0 !== args.silent ? {
457
+ silent: args.silent
458
+ } : {},
459
+ ...args.mode ? {
460
+ mode: args.mode
461
+ } : {},
412
462
  exitOnError: false
413
463
  });
414
464
  return JSON.stringify({
@@ -521,6 +571,50 @@ function removeSession(projectPath, browser) {
521
571
  function listSessions() {
522
572
  return Array.from(process_manager_sessions.values());
523
573
  }
574
+ const LAUNCH_FLAG_SCHEMA = {
575
+ profile: {
576
+ type: "string",
577
+ description: 'Browser profile path, or "false" to reuse the default user profile. Omit for a fresh throwaway profile.'
578
+ },
579
+ startingUrl: {
580
+ type: "string",
581
+ description: "URL the browser opens on launch"
582
+ },
583
+ chromiumBinary: {
584
+ type: "string",
585
+ description: "Path to a custom Chromium-based binary (overrides browser)"
586
+ },
587
+ geckoBinary: {
588
+ type: "string",
589
+ description: "Path to a custom Gecko/Firefox binary (overrides browser)"
590
+ },
591
+ host: {
592
+ type: "string",
593
+ description: "Dev server bind host. Use 0.0.0.0 for Docker or devcontainers. Defaults to 127.0.0.1"
594
+ },
595
+ publicHost: {
596
+ type: "string",
597
+ description: "Connectable host the browser dials for HMR and reload when it differs from the bind host"
598
+ },
599
+ extensions: {
600
+ type: "array",
601
+ items: {
602
+ type: "string"
603
+ },
604
+ description: "Companion extension paths or store URLs to load alongside the project"
605
+ }
606
+ };
607
+ function launchFlagArgs(args) {
608
+ const cli = [];
609
+ if (void 0 !== args.profile) cli.push("--profile", args.profile);
610
+ if (args.startingUrl) cli.push("--starting-url", args.startingUrl);
611
+ if (args.chromiumBinary) cli.push("--chromium-binary", args.chromiumBinary);
612
+ if (args.geckoBinary) cli.push("--gecko-binary", args.geckoBinary);
613
+ if (args.host) cli.push("--host", args.host);
614
+ if (args.publicHost) cli.push("--public-host", args.publicHost);
615
+ if (args.extensions?.length) cli.push("--extensions", args.extensions.join(","));
616
+ return cli;
617
+ }
524
618
  const dev_schema = {
525
619
  name: "extension_dev",
526
620
  description: "Start the extension development server with hot module replacement. Launches a browser with the extension loaded. Returns process info for use with extension_wait and extension_source_inspect.",
@@ -537,9 +631,18 @@ const dev_schema = {
537
631
  "chrome",
538
632
  "chromium",
539
633
  "edge",
634
+ "brave",
635
+ "opera",
636
+ "vivaldi",
637
+ "yandex",
540
638
  "firefox",
639
+ "waterfox",
640
+ "librewolf",
641
+ "safari",
541
642
  "chromium-based",
542
- "gecko-based"
643
+ "gecko-based",
644
+ "firefox-based",
645
+ "webkit-based"
543
646
  ],
544
647
  default: "chrome"
545
648
  },
@@ -552,6 +655,12 @@ const dev_schema = {
552
655
  default: false,
553
656
  description: "Start dev server without launching browser"
554
657
  },
658
+ polyfill: {
659
+ type: "boolean",
660
+ default: true,
661
+ description: "Apply cross-browser polyfill"
662
+ },
663
+ ...LAUNCH_FLAG_SCHEMA,
555
664
  allowControl: {
556
665
  type: "boolean",
557
666
  default: false,
@@ -578,6 +687,8 @@ async function dev_handler(args) {
578
687
  ];
579
688
  if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
580
689
  if (args.noBrowser) cliArgs.push("--no-browser");
690
+ if (false === args.polyfill) cliArgs.push("--polyfill", "false");
691
+ cliArgs.push(...launchFlagArgs(args));
581
692
  if (args.allowControl) cliArgs.push("--allow-control");
582
693
  if (args.allowEval) cliArgs.push("--allow-eval");
583
694
  const child = spawnExtensionCli(cliArgs, {
@@ -627,9 +738,18 @@ const start_schema = {
627
738
  "chrome",
628
739
  "chromium",
629
740
  "edge",
741
+ "brave",
742
+ "opera",
743
+ "vivaldi",
744
+ "yandex",
630
745
  "firefox",
746
+ "waterfox",
747
+ "librewolf",
748
+ "safari",
631
749
  "chromium-based",
632
- "gecko-based"
750
+ "gecko-based",
751
+ "firefox-based",
752
+ "webkit-based"
633
753
  ],
634
754
  default: "chrome"
635
755
  },
@@ -637,7 +757,17 @@ const start_schema = {
637
757
  type: "boolean",
638
758
  default: true,
639
759
  description: "Apply cross-browser polyfill"
640
- }
760
+ },
761
+ port: {
762
+ type: "number",
763
+ description: "Server port (0 for auto-assign)"
764
+ },
765
+ noBrowser: {
766
+ type: "boolean",
767
+ default: false,
768
+ description: "Build and serve without launching a browser"
769
+ },
770
+ ...LAUNCH_FLAG_SCHEMA
641
771
  },
642
772
  required: [
643
773
  "projectPath"
@@ -652,7 +782,10 @@ async function start_handler(args) {
652
782
  "--browser",
653
783
  browser
654
784
  ];
655
- if (false === args.polyfill) cliArgs.push("--no-polyfill");
785
+ if (false === args.polyfill) cliArgs.push("--polyfill", "false");
786
+ if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
787
+ if (args.noBrowser) cliArgs.push("--no-browser");
788
+ cliArgs.push(...launchFlagArgs(args));
656
789
  const child = spawnExtensionCli(cliArgs, {
657
790
  projectDir: args.projectPath
658
791
  });
@@ -698,12 +831,31 @@ const preview_schema = {
698
831
  "chrome",
699
832
  "chromium",
700
833
  "edge",
834
+ "brave",
835
+ "opera",
836
+ "vivaldi",
837
+ "yandex",
701
838
  "firefox",
839
+ "waterfox",
840
+ "librewolf",
841
+ "safari",
702
842
  "chromium-based",
703
- "gecko-based"
843
+ "gecko-based",
844
+ "firefox-based",
845
+ "webkit-based"
704
846
  ],
705
847
  default: "chrome"
706
- }
848
+ },
849
+ port: {
850
+ type: "number",
851
+ description: "Server port (0 for auto-assign)"
852
+ },
853
+ noBrowser: {
854
+ type: "boolean",
855
+ default: false,
856
+ description: "Serve the preview without launching a browser"
857
+ },
858
+ ...LAUNCH_FLAG_SCHEMA
707
859
  },
708
860
  required: [
709
861
  "projectPath"
@@ -718,6 +870,9 @@ async function preview_handler(args) {
718
870
  "--browser",
719
871
  browser
720
872
  ];
873
+ if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
874
+ if (args.noBrowser) cliArgs.push("--no-browser");
875
+ cliArgs.push(...launchFlagArgs(args));
721
876
  const child = spawnExtensionCli(cliArgs, {
722
877
  projectDir: args.projectPath
723
878
  });
@@ -916,13 +1071,23 @@ const CHROMIUM_FAMILY = new Set([
916
1071
  "chrome",
917
1072
  "chromium",
918
1073
  "edge",
1074
+ "brave",
1075
+ "opera",
1076
+ "vivaldi",
1077
+ "yandex",
919
1078
  "chromium-based"
920
1079
  ]);
921
1080
  const GECKO_FAMILY = new Set([
922
1081
  "firefox",
1082
+ "waterfox",
1083
+ "librewolf",
923
1084
  "gecko-based",
924
1085
  "firefox-based"
925
1086
  ]);
1087
+ const WEBKIT_FAMILY = new Set([
1088
+ "safari",
1089
+ "webkit-based"
1090
+ ]);
926
1091
  function isChromiumFamily(browser) {
927
1092
  return CHROMIUM_FAMILY.has(browser);
928
1093
  }
@@ -2624,6 +2789,222 @@ async function dom_inspect_handler(args) {
2624
2789
  if (null != args.timeout) cli.push("--timeout", String(args.timeout));
2625
2790
  return runActVerb(cli, args.projectPath, args.timeout);
2626
2791
  }
2792
+ function credentialsPath() {
2793
+ if ("win32" === process.platform) {
2794
+ const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
2795
+ return node_path.join(base, "extension-dev", "auth.json");
2796
+ }
2797
+ const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
2798
+ const base = xdg || node_path.join(node_os.homedir(), ".config");
2799
+ return node_path.join(base, "extension-dev", "auth.json");
2800
+ }
2801
+ function readCredentials() {
2802
+ try {
2803
+ const raw = node_fs.readFileSync(credentialsPath(), "utf8");
2804
+ const data = JSON.parse(raw);
2805
+ if (!data || "object" != typeof data) return null;
2806
+ if (1 !== data.version) return null;
2807
+ const token = String(data.token || "").trim();
2808
+ if (!token) return null;
2809
+ return {
2810
+ version: 1,
2811
+ token,
2812
+ workspaceSlug: String(data.workspaceSlug || ""),
2813
+ projectSlug: String(data.projectSlug || ""),
2814
+ expiresAt: Number(data.expiresAt || 0),
2815
+ api: String(data.api || "")
2816
+ };
2817
+ } catch {
2818
+ return null;
2819
+ }
2820
+ }
2821
+ function writeCredentials(creds) {
2822
+ const file = credentialsPath();
2823
+ const dir = node_path.dirname(file);
2824
+ node_fs.mkdirSync(dir, {
2825
+ recursive: true,
2826
+ mode: 448
2827
+ });
2828
+ try {
2829
+ node_fs.chmodSync(dir, 448);
2830
+ } catch {}
2831
+ node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
2832
+ mode: 384
2833
+ });
2834
+ try {
2835
+ node_fs.chmodSync(file, 384);
2836
+ } catch {}
2837
+ return file;
2838
+ }
2839
+ function clearCredentials() {
2840
+ const file = credentialsPath();
2841
+ try {
2842
+ node_fs.unlinkSync(file);
2843
+ return {
2844
+ cleared: true,
2845
+ path: file
2846
+ };
2847
+ } catch {
2848
+ return {
2849
+ cleared: false,
2850
+ path: file
2851
+ };
2852
+ }
2853
+ }
2854
+ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
2855
+ const creds = readCredentials();
2856
+ if (!creds) return null;
2857
+ if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
2858
+ return creds;
2859
+ }
2860
+ const DEFAULT_API = "https://www.extension.dev";
2861
+ function resolveApiBase(api) {
2862
+ return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
2863
+ }
2864
+ function safeApiBase(raw) {
2865
+ let parsed;
2866
+ try {
2867
+ parsed = new URL(raw);
2868
+ } catch {
2869
+ return {
2870
+ ok: false,
2871
+ message: `Invalid platform URL: ${raw}`
2872
+ };
2873
+ }
2874
+ const isLocalhost = "localhost" === parsed.hostname || "127.0.0.1" === parsed.hostname || "[::1]" === parsed.hostname || "::1" === parsed.hostname;
2875
+ if ("https:" !== parsed.protocol && !("http:" === parsed.protocol && isLocalhost)) return {
2876
+ ok: false,
2877
+ message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
2878
+ };
2879
+ return {
2880
+ ok: true,
2881
+ base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
2882
+ };
2883
+ }
2884
+ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
2885
+ const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
2886
+ if (override) return {
2887
+ clientId: override,
2888
+ scope: "read:user"
2889
+ };
2890
+ const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
2891
+ headers: {
2892
+ accept: "application/json"
2893
+ }
2894
+ });
2895
+ if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
2896
+ const data = await res.json().catch(()=>({}));
2897
+ const clientId = String(data.githubClientId || "").trim();
2898
+ if (!clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
2899
+ return {
2900
+ clientId,
2901
+ scope: String(data.scope || "read:user")
2902
+ };
2903
+ }
2904
+ async function exchangeAndPersist(args) {
2905
+ const doFetch = args.fetchImpl ?? fetch;
2906
+ const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
2907
+ method: "POST",
2908
+ headers: {
2909
+ "content-type": "application/json",
2910
+ accept: "application/json"
2911
+ },
2912
+ body: JSON.stringify({
2913
+ githubToken: args.githubToken,
2914
+ project: args.project
2915
+ })
2916
+ });
2917
+ const text = await res.text();
2918
+ let data;
2919
+ try {
2920
+ data = JSON.parse(text);
2921
+ } catch {
2922
+ data = {
2923
+ message: text
2924
+ };
2925
+ }
2926
+ if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
2927
+ const token = String(data.token || "").trim();
2928
+ if (!token) throw new Error("Login exchange returned no token.");
2929
+ const creds = {
2930
+ version: 1,
2931
+ token,
2932
+ workspaceSlug: String(data.workspaceSlug || ""),
2933
+ projectSlug: String(data.projectSlug || ""),
2934
+ expiresAt: Number(data.expiresAt || 0),
2935
+ api: args.apiBase
2936
+ };
2937
+ writeCredentials(creds);
2938
+ return creds;
2939
+ }
2940
+ function resolveToken() {
2941
+ const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
2942
+ if (fromEnv) return fromEnv;
2943
+ const creds = readValidCredentials();
2944
+ return creds?.token ? String(creds.token).trim() : "";
2945
+ }
2946
+ async function publish(options = {}) {
2947
+ const token = options.token ?? resolveToken();
2948
+ if (!token) return {
2949
+ ok: false,
2950
+ error: {
2951
+ name: "PublishAuthError",
2952
+ message: "No token. Run login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
2953
+ }
2954
+ };
2955
+ const doFetch = options.fetchImpl ?? fetch;
2956
+ const apiCheck = safeApiBase(resolveApiBase(options.api));
2957
+ if (!apiCheck.ok) return {
2958
+ ok: false,
2959
+ error: {
2960
+ name: "PublishConfigError",
2961
+ message: apiCheck.message
2962
+ }
2963
+ };
2964
+ const url = `${apiCheck.base}/api/cli/publish`;
2965
+ const body = {};
2966
+ if (null != options.ttlHours) body.ttlHours = Number(options.ttlHours);
2967
+ if (options.buildSha) body.buildSha = options.buildSha;
2968
+ let res;
2969
+ try {
2970
+ res = await doFetch(url, {
2971
+ method: "POST",
2972
+ headers: {
2973
+ authorization: `Bearer ${token}`,
2974
+ "content-type": "application/json"
2975
+ },
2976
+ body: JSON.stringify(body)
2977
+ });
2978
+ } catch (err) {
2979
+ return {
2980
+ ok: false,
2981
+ error: {
2982
+ name: "PublishNetworkError",
2983
+ message: `Could not reach ${url}: ${err?.message || err}`
2984
+ }
2985
+ };
2986
+ }
2987
+ const text = await res.text();
2988
+ let data;
2989
+ try {
2990
+ data = JSON.parse(text);
2991
+ } catch {
2992
+ data = {
2993
+ message: text
2994
+ };
2995
+ }
2996
+ if (!res.ok) return {
2997
+ ok: false,
2998
+ error: {
2999
+ name: "PublishError",
3000
+ message: `publish failed (${res.status}): ${data?.message || text || "unknown error"}`
3001
+ }
3002
+ };
3003
+ return {
3004
+ ok: true,
3005
+ data
3006
+ };
3007
+ }
2627
3008
  const publish_schema = {
2628
3009
  name: "extension_publish",
2629
3010
  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.",
@@ -2669,7 +3050,7 @@ async function publish_handler(args) {
2669
3050
  });
2670
3051
  return result.ok ? JSON.stringify(result.data) : JSON.stringify(result);
2671
3052
  }
2672
- const DEFAULT_API = "https://www.extension.dev";
3053
+ const release_promote_DEFAULT_API = "https://www.extension.dev";
2673
3054
  const release_promote_schema = {
2674
3055
  name: "extension_release_promote",
2675
3056
  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).",
@@ -2729,7 +3110,7 @@ async function release_promote_handler(args) {
2729
3110
  const buildId = String(args.buildId || "").trim();
2730
3111
  const channel = String(args.channel || "").trim();
2731
3112
  if (!buildId || !channel) return fail("ReleaseInputError", "buildId and channel are required.");
2732
- const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API));
3113
+ const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
2733
3114
  if (!apiCheck.ok) return fail("ReleaseConfigError", apiCheck.message);
2734
3115
  const url = `${apiCheck.base}/api/cli/release/promote`;
2735
3116
  const body = {
@@ -2832,7 +3213,7 @@ async function wait_handler(args) {
2832
3213
  }
2833
3214
  const add_feature_schema = {
2834
3215
  name: "extension_add_feature",
2835
- description: "Add a new feature surface to an existing extension. Generates the required files and updates manifest.json. Sources patterns from the extension.dev template catalog.",
3216
+ description: "Plan a new feature surface for an existing extension. Returns step-by-step instructions, the manifest additions to make, and reference templates from the extension.dev catalog. Does not modify files; apply the returned plan yourself.",
2836
3217
  inputSchema: {
2837
3218
  type: "object",
2838
3219
  properties: {
@@ -3059,6 +3440,78 @@ async function add_feature_handler(args) {
3059
3440
  hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
3060
3441
  });
3061
3442
  }
3443
+ const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
3444
+ const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
3445
+ const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
3446
+ const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
3447
+ async function startDeviceCode(args) {
3448
+ const doFetch = args.fetchImpl ?? fetch;
3449
+ const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
3450
+ method: "POST",
3451
+ headers: {
3452
+ accept: "application/json",
3453
+ "content-type": "application/json"
3454
+ },
3455
+ body: JSON.stringify({
3456
+ client_id: args.clientId,
3457
+ scope: args.scope || "read:user"
3458
+ })
3459
+ });
3460
+ const data = await res.json().catch(()=>({}));
3461
+ if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
3462
+ return {
3463
+ deviceCode: String(data.device_code || ""),
3464
+ userCode: String(data.user_code || ""),
3465
+ verificationUri: String(data.verification_uri || "https://github.com/login/device"),
3466
+ interval: Number(data.interval || 5),
3467
+ expiresIn: Number(data.expires_in || 900)
3468
+ };
3469
+ }
3470
+ async function pollForToken(args) {
3471
+ const doFetch = args.fetchImpl ?? fetch;
3472
+ const sleep = args.sleepImpl ?? defaultSleep;
3473
+ let intervalMs = 1000 * Math.max(1, args.interval);
3474
+ const deadline = Date.now() + Math.max(0, args.budgetMs);
3475
+ while(Date.now() < deadline){
3476
+ await sleep(intervalMs);
3477
+ const res = await doFetch(GITHUB_TOKEN_URL, {
3478
+ method: "POST",
3479
+ headers: {
3480
+ accept: "application/json",
3481
+ "content-type": "application/json"
3482
+ },
3483
+ body: JSON.stringify({
3484
+ client_id: args.clientId,
3485
+ device_code: args.deviceCode,
3486
+ grant_type: DEVICE_GRANT_TYPE
3487
+ })
3488
+ });
3489
+ const data = await res.json().catch(()=>({}));
3490
+ const token = String(data.access_token || "").trim();
3491
+ if (token) return {
3492
+ ok: true,
3493
+ githubToken: token
3494
+ };
3495
+ const error = String(data.error || "");
3496
+ if ("authorization_pending" === error) continue;
3497
+ if ("slow_down" === error) {
3498
+ intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
3499
+ continue;
3500
+ }
3501
+ if ("expired_token" === error) return {
3502
+ ok: false,
3503
+ reason: "expired"
3504
+ };
3505
+ if ("access_denied" === error) return {
3506
+ ok: false,
3507
+ reason: "denied"
3508
+ };
3509
+ }
3510
+ return {
3511
+ ok: false,
3512
+ reason: "pending"
3513
+ };
3514
+ }
3062
3515
  const login_schema = {
3063
3516
  name: "extension_login",
3064
3517
  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.",
@@ -3268,6 +3721,57 @@ async function install_browser_handler(args) {
3268
3721
  });
3269
3722
  }
3270
3723
  }
3724
+ const uninstall_browser_schema = {
3725
+ name: "extension_uninstall_browser",
3726
+ description: "Remove a managed browser binary from the Extension.js cache. Only touches the managed cache, never system-installed browsers.",
3727
+ inputSchema: {
3728
+ type: "object",
3729
+ properties: {
3730
+ browser: {
3731
+ type: "string",
3732
+ enum: [
3733
+ "chrome",
3734
+ "chromium",
3735
+ "edge",
3736
+ "firefox"
3737
+ ],
3738
+ description: "Managed browser to remove"
3739
+ },
3740
+ all: {
3741
+ type: "boolean",
3742
+ default: false,
3743
+ description: "Remove every managed browser binary"
3744
+ }
3745
+ },
3746
+ required: []
3747
+ }
3748
+ };
3749
+ async function uninstall_browser_handler(args) {
3750
+ const start = Date.now();
3751
+ if (!args.browser && !args.all) return JSON.stringify({
3752
+ status: "error",
3753
+ message: "Provide a browser to remove, or set all: true."
3754
+ });
3755
+ try {
3756
+ await extensionUninstall({
3757
+ browser: args.browser,
3758
+ all: args.all
3759
+ });
3760
+ return JSON.stringify({
3761
+ status: "uninstalled",
3762
+ target: args.all ? "all" : args.browser,
3763
+ duration: Date.now() - start,
3764
+ hint: "Use extension_list_browsers to confirm what remains in the managed cache."
3765
+ });
3766
+ } catch (err) {
3767
+ return JSON.stringify({
3768
+ status: "error",
3769
+ target: args.all ? "all" : args.browser,
3770
+ message: err instanceof Error ? err.message : String(err),
3771
+ duration: Date.now() - start
3772
+ });
3773
+ }
3774
+ }
3271
3775
  const list_browsers_schema = {
3272
3776
  name: "extension_list_browsers",
3273
3777
  description: "List managed browser binaries installed by the extension.dev platform. Shows what browsers are available in the managed cache without checking system-installed browsers.",
@@ -3341,7 +3845,14 @@ const detect_browsers_schema = {
3341
3845
  "chrome",
3342
3846
  "chromium",
3343
3847
  "edge",
3344
- "firefox"
3848
+ "brave",
3849
+ "opera",
3850
+ "vivaldi",
3851
+ "yandex",
3852
+ "firefox",
3853
+ "waterfox",
3854
+ "librewolf",
3855
+ "safari"
3345
3856
  ]
3346
3857
  },
3347
3858
  description: "Browsers to check. If omitted, checks all."
@@ -3353,8 +3864,21 @@ const ALL_BROWSERS = [
3353
3864
  "chrome",
3354
3865
  "chromium",
3355
3866
  "edge",
3356
- "firefox"
3867
+ "brave",
3868
+ "opera",
3869
+ "vivaldi",
3870
+ "yandex",
3871
+ "firefox",
3872
+ "waterfox",
3873
+ "librewolf",
3874
+ "safari"
3357
3875
  ];
3876
+ const MANAGED_INSTALLABLE = new Set([
3877
+ "chrome",
3878
+ "chromium",
3879
+ "edge",
3880
+ "firefox"
3881
+ ]);
3358
3882
  const SYSTEM_PATHS = {
3359
3883
  darwin: {
3360
3884
  chrome: [
@@ -3372,6 +3896,33 @@ const SYSTEM_PATHS = {
3372
3896
  firefox: [
3373
3897
  "/Applications/Firefox.app/Contents/MacOS/firefox",
3374
3898
  `${process.env.HOME}/Applications/Firefox.app/Contents/MacOS/firefox`
3899
+ ],
3900
+ brave: [
3901
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
3902
+ `${process.env.HOME}/Applications/Brave Browser.app/Contents/MacOS/Brave Browser`
3903
+ ],
3904
+ opera: [
3905
+ "/Applications/Opera.app/Contents/MacOS/Opera",
3906
+ `${process.env.HOME}/Applications/Opera.app/Contents/MacOS/Opera`
3907
+ ],
3908
+ vivaldi: [
3909
+ "/Applications/Vivaldi.app/Contents/MacOS/Vivaldi",
3910
+ `${process.env.HOME}/Applications/Vivaldi.app/Contents/MacOS/Vivaldi`
3911
+ ],
3912
+ yandex: [
3913
+ "/Applications/Yandex.app/Contents/MacOS/Yandex",
3914
+ `${process.env.HOME}/Applications/Yandex.app/Contents/MacOS/Yandex`
3915
+ ],
3916
+ waterfox: [
3917
+ "/Applications/Waterfox.app/Contents/MacOS/waterfox",
3918
+ `${process.env.HOME}/Applications/Waterfox.app/Contents/MacOS/waterfox`
3919
+ ],
3920
+ librewolf: [
3921
+ "/Applications/LibreWolf.app/Contents/MacOS/librewolf",
3922
+ `${process.env.HOME}/Applications/LibreWolf.app/Contents/MacOS/librewolf`
3923
+ ],
3924
+ safari: [
3925
+ "/Applications/Safari.app/Contents/MacOS/Safari"
3375
3926
  ]
3376
3927
  },
3377
3928
  linux: {
@@ -3394,6 +3945,31 @@ const SYSTEM_PATHS = {
3394
3945
  "/usr/bin/firefox",
3395
3946
  "/snap/bin/firefox",
3396
3947
  "/usr/lib/firefox/firefox"
3948
+ ],
3949
+ brave: [
3950
+ "/usr/bin/brave-browser",
3951
+ "/usr/bin/brave",
3952
+ "/snap/bin/brave"
3953
+ ],
3954
+ opera: [
3955
+ "/usr/bin/opera",
3956
+ "/snap/bin/opera"
3957
+ ],
3958
+ vivaldi: [
3959
+ "/usr/bin/vivaldi",
3960
+ "/usr/bin/vivaldi-stable"
3961
+ ],
3962
+ yandex: [
3963
+ "/usr/bin/yandex-browser",
3964
+ "/usr/bin/yandex-browser-stable"
3965
+ ],
3966
+ waterfox: [
3967
+ "/usr/bin/waterfox",
3968
+ "/opt/waterfox/waterfox"
3969
+ ],
3970
+ librewolf: [
3971
+ "/usr/bin/librewolf",
3972
+ "/opt/librewolf/librewolf"
3397
3973
  ]
3398
3974
  },
3399
3975
  win32: {
@@ -3411,6 +3987,30 @@ const SYSTEM_PATHS = {
3411
3987
  firefox: [
3412
3988
  "C:\\Program Files\\Mozilla Firefox\\firefox.exe",
3413
3989
  "C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe"
3990
+ ],
3991
+ brave: [
3992
+ "C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe",
3993
+ "C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
3994
+ ],
3995
+ opera: [
3996
+ `${process.env.LOCALAPPDATA || ""}\\Programs\\Opera\\opera.exe`,
3997
+ "C:\\Program Files\\Opera\\opera.exe"
3998
+ ],
3999
+ vivaldi: [
4000
+ `${process.env.LOCALAPPDATA || ""}\\Vivaldi\\Application\\vivaldi.exe`,
4001
+ "C:\\Program Files\\Vivaldi\\Application\\vivaldi.exe"
4002
+ ],
4003
+ yandex: [
4004
+ `${process.env.LOCALAPPDATA || ""}\\Yandex\\YandexBrowser\\Application\\browser.exe`,
4005
+ "C:\\Program Files (x86)\\Yandex\\YandexBrowser\\Application\\browser.exe"
4006
+ ],
4007
+ waterfox: [
4008
+ "C:\\Program Files\\Waterfox\\waterfox.exe",
4009
+ "C:\\Program Files (x86)\\Waterfox\\waterfox.exe"
4010
+ ],
4011
+ librewolf: [
4012
+ "C:\\Program Files\\LibreWolf\\librewolf.exe",
4013
+ "C:\\Program Files (x86)\\LibreWolf\\librewolf.exe"
3414
4014
  ]
3415
4015
  }
3416
4016
  };
@@ -3521,7 +4121,8 @@ async function detect_browsers_handler(args) {
3521
4121
  if (node_fs.existsSync(browserDir)) managed.installed.push(browser);
3522
4122
  }
3523
4123
  for (const browser of browsersToCheck){
3524
- const isGecko = "firefox" === browser;
4124
+ const isGecko = isGeckoFamily(browser);
4125
+ const isWebkit = WEBKIT_FAMILY.has(browser);
3525
4126
  let binaryPath = findManagedBinary(browser);
3526
4127
  let source = "managed";
3527
4128
  if (!binaryPath) {
@@ -3529,14 +4130,14 @@ async function detect_browsers_handler(args) {
3529
4130
  source = binaryPath ? "system" : "not_found";
3530
4131
  }
3531
4132
  let version = null;
3532
- if (binaryPath) version = await getVersion(binaryPath, browser);
4133
+ if (binaryPath && !isWebkit) version = await getVersion(binaryPath, browser);
3533
4134
  detected.push({
3534
4135
  browser,
3535
4136
  binaryPath,
3536
4137
  source,
3537
- engine: isGecko ? "gecko" : "chromium",
4138
+ engine: isWebkit ? "webkit" : isGecko ? "gecko" : "chromium",
3538
4139
  version,
3539
- cdpSupport: !isGecko,
4140
+ cdpSupport: !isGecko && !isWebkit,
3540
4141
  rdpSupport: isGecko
3541
4142
  });
3542
4143
  }
@@ -3549,7 +4150,7 @@ async function detect_browsers_handler(args) {
3549
4150
  available: available.map((d)=>d.browser),
3550
4151
  missing: missing.map((d)=>d.browser)
3551
4152
  },
3552
- hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}. Use extension_install_browser to install them.` : "All requested browsers are available."
4153
+ hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}.${missing.some((d)=>MANAGED_INSTALLABLE.has(d.browser)) ? ` Use extension_install_browser to install ${missing.filter((d)=>MANAGED_INSTALLABLE.has(d.browser)).map((d)=>d.browser).join(", ")}.` : ""}` : "All requested browsers are available."
3553
4154
  });
3554
4155
  }
3555
4156
  const doctor_schema = {
@@ -3692,7 +4293,7 @@ const tools = [
3692
4293
  reload_namespaceObject,
3693
4294
  open_namespaceObject,
3694
4295
  dom_inspect_namespaceObject,
3695
- publish_namespaceObject,
4296
+ tools_publish_namespaceObject,
3696
4297
  release_promote_namespaceObject,
3697
4298
  wait_namespaceObject,
3698
4299
  add_feature_namespaceObject,
@@ -3700,6 +4301,7 @@ const tools = [
3700
4301
  whoami_namespaceObject,
3701
4302
  logout_namespaceObject,
3702
4303
  install_browser_namespaceObject,
4304
+ uninstall_browser_namespaceObject,
3703
4305
  list_browsers_namespaceObject,
3704
4306
  detect_browsers_namespaceObject,
3705
4307
  doctor_namespaceObject