@extension.dev/mcp 6.6.0 → 9.0.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.
Files changed (64) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +259 -0
  4. package/README.md +13 -21
  5. package/claude/ARCHITECTURE.md +4 -4
  6. package/claude/CLAUDE.md +2 -2
  7. package/claude/README.md +1 -1
  8. package/claude/commands/extension-add.md +1 -1
  9. package/claude/commands/extension-debug.md +1 -1
  10. package/claude/commands/extension-publish.md +1 -1
  11. package/claude/commands/extension.md +3 -3
  12. package/claude/rules/mcp-tools.md +45 -49
  13. package/dist/module.js +1041 -1423
  14. package/dist/src/lib/common-schema.d.ts +28 -0
  15. package/dist/src/lib/credentials.d.ts +1 -1
  16. package/dist/src/lib/launch-flags.d.ts +6 -6
  17. package/dist/src/lib/login-flow.d.ts +0 -10
  18. package/dist/src/lib/session-identity.d.ts +16 -0
  19. package/dist/src/tools/add-feature.d.ts +2 -2
  20. package/dist/src/tools/analyze.d.ts +29 -0
  21. package/dist/src/tools/auth.d.ts +33 -0
  22. package/dist/src/tools/browsers.d.ts +39 -0
  23. package/dist/src/tools/build.d.ts +5 -6
  24. package/dist/src/tools/detect-browsers.d.ts +1 -20
  25. package/dist/src/tools/dev.d.ts +11 -11
  26. package/dist/src/tools/{dom-inspect.d.ts → dom-snapshot.d.ts} +6 -6
  27. package/dist/src/tools/eval.d.ts +6 -6
  28. package/dist/src/tools/get-template-source.d.ts +1 -22
  29. package/dist/src/tools/inspect.d.ts +33 -5
  30. package/dist/src/tools/install-browser.d.ts +1 -18
  31. package/dist/src/tools/list-browsers.d.ts +1 -9
  32. package/dist/src/tools/list-extensions.d.ts +4 -4
  33. package/dist/src/tools/list-templates.d.ts +1 -35
  34. package/dist/src/tools/login.d.ts +1 -23
  35. package/dist/src/tools/logout.d.ts +1 -9
  36. package/dist/src/tools/logs-schema.d.ts +2 -2
  37. package/dist/src/tools/open.d.ts +6 -6
  38. package/dist/src/tools/preview-web.d.ts +4 -16
  39. package/dist/src/tools/publish.d.ts +2 -2
  40. package/dist/src/tools/release-list.d.ts +1 -23
  41. package/dist/src/tools/release-promote.d.ts +2 -2
  42. package/dist/src/tools/release-status.d.ts +37 -0
  43. package/dist/src/tools/reload.d.ts +6 -6
  44. package/dist/src/tools/shares.d.ts +2 -2
  45. package/dist/src/tools/start.d.ts +16 -10
  46. package/dist/src/tools/stop.d.ts +2 -2
  47. package/dist/src/tools/storage.d.ts +6 -6
  48. package/dist/src/tools/store-status.d.ts +1 -23
  49. package/dist/src/tools/{deploy.d.ts → submit.d.ts} +4 -4
  50. package/dist/src/tools/{source-inspect.d.ts → templates.d.ts} +27 -23
  51. package/dist/src/tools/uninstall-browser.d.ts +1 -21
  52. package/dist/src/tools/wait.d.ts +4 -4
  53. package/dist/src/tools/whoami.d.ts +1 -9
  54. package/extensions/live-preview/chromium/action/index.css +1 -1
  55. package/extensions/live-preview/chromium/action/index.js +1 -9
  56. package/extensions/live-preview/chromium/background/service_worker.js +4 -12
  57. package/extensions/live-preview/chromium/manifest.json +1 -2
  58. package/package.json +2 -2
  59. package/server.json +3 -3
  60. package/dist/src/__tests__/fixtures/ready-contract.d.ts +0 -7
  61. package/dist/src/__tests__/setup-session-dir.d.ts +0 -1
  62. package/dist/src/lib/github-device.d.ts +0 -31
  63. package/dist/src/tools/preview.d.ts +0 -66
  64. /package/dist/src/tools/{source-inspect-gecko.d.ts → inspect-gecko.d.ts} +0 -0
package/dist/module.js CHANGED
@@ -6,16 +6,16 @@ import node_fs, { existsSync, promises, readdirSync, statSync } from "node:fs";
6
6
  import node_path, { join } from "node:path";
7
7
  import { extensionCreate } from "extension-create";
8
8
  import node_os from "node:os";
9
- import cross_spawn from "cross-spawn";
10
9
  import node_crypto, { createHash } from "node:crypto";
10
+ import cross_spawn from "cross-spawn";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { execFile, execFileSync } from "node:child_process";
13
13
  import ws_0 from "ws";
14
14
  import node_http from "node:http";
15
15
  import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
16
16
  import node_net from "node:net";
17
- import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
18
17
  import { promisify } from "node:util";
18
+ import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
19
19
  __webpack_require__.add({
20
20
  "./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/origins.cjs" (__unused_rspack_module, exports) {
21
21
  var __nested_rspack_require_18_37__ = {};
@@ -410,6 +410,24 @@ __webpack_require__.d(add_feature_namespaceObject, {
410
410
  handler: ()=>add_feature_handler,
411
411
  schema: ()=>add_feature_schema
412
412
  });
413
+ var analyze_namespaceObject = {};
414
+ __webpack_require__.r(analyze_namespaceObject);
415
+ __webpack_require__.d(analyze_namespaceObject, {
416
+ handler: ()=>analyze_handler,
417
+ schema: ()=>analyze_schema
418
+ });
419
+ var auth_namespaceObject = {};
420
+ __webpack_require__.r(auth_namespaceObject);
421
+ __webpack_require__.d(auth_namespaceObject, {
422
+ handler: ()=>auth_handler,
423
+ schema: ()=>auth_schema
424
+ });
425
+ var browsers_namespaceObject = {};
426
+ __webpack_require__.r(browsers_namespaceObject);
427
+ __webpack_require__.d(browsers_namespaceObject, {
428
+ handler: ()=>browsers_handler,
429
+ schema: ()=>browsers_schema
430
+ });
413
431
  var build_namespaceObject = {};
414
432
  __webpack_require__.r(build_namespaceObject);
415
433
  __webpack_require__.d(build_namespaceObject, {
@@ -422,19 +440,6 @@ __webpack_require__.d(create_namespaceObject, {
422
440
  handler: ()=>create_handler,
423
441
  schema: ()=>create_schema
424
442
  });
425
- var deploy_namespaceObject = {};
426
- __webpack_require__.r(deploy_namespaceObject);
427
- __webpack_require__.d(deploy_namespaceObject, {
428
- handler: ()=>deploy_handler,
429
- schema: ()=>deploy_schema,
430
- storeMdWarnings: ()=>storeMdWarnings
431
- });
432
- var detect_browsers_namespaceObject = {};
433
- __webpack_require__.r(detect_browsers_namespaceObject);
434
- __webpack_require__.d(detect_browsers_namespaceObject, {
435
- handler: ()=>detect_browsers_handler,
436
- schema: ()=>detect_browsers_schema
437
- });
438
443
  var dev_namespaceObject = {};
439
444
  __webpack_require__.r(dev_namespaceObject);
440
445
  __webpack_require__.d(dev_namespaceObject, {
@@ -448,11 +453,11 @@ __webpack_require__.d(doctor_namespaceObject, {
448
453
  recentErrorLogs: ()=>recentErrorLogs,
449
454
  schema: ()=>doctor_schema
450
455
  });
451
- var dom_inspect_namespaceObject = {};
452
- __webpack_require__.r(dom_inspect_namespaceObject);
453
- __webpack_require__.d(dom_inspect_namespaceObject, {
454
- handler: ()=>dom_inspect_handler,
455
- schema: ()=>dom_inspect_schema
456
+ var dom_snapshot_namespaceObject = {};
457
+ __webpack_require__.r(dom_snapshot_namespaceObject);
458
+ __webpack_require__.d(dom_snapshot_namespaceObject, {
459
+ handler: ()=>dom_snapshot_handler,
460
+ schema: ()=>dom_snapshot_schema
456
461
  });
457
462
  var eval_namespaceObject = {};
458
463
  __webpack_require__.r(eval_namespaceObject);
@@ -461,54 +466,18 @@ __webpack_require__.d(eval_namespaceObject, {
461
466
  resolveDefaultEvalContext: ()=>resolveDefaultEvalContext,
462
467
  schema: ()=>eval_schema
463
468
  });
464
- var get_template_source_namespaceObject = {};
465
- __webpack_require__.r(get_template_source_namespaceObject);
466
- __webpack_require__.d(get_template_source_namespaceObject, {
467
- handler: ()=>get_template_source_handler,
468
- schema: ()=>get_template_source_schema
469
- });
470
469
  var inspect_namespaceObject = {};
471
470
  __webpack_require__.r(inspect_namespaceObject);
472
471
  __webpack_require__.d(inspect_namespaceObject, {
473
472
  handler: ()=>inspect_handler,
474
473
  schema: ()=>inspect_schema
475
474
  });
476
- var install_browser_namespaceObject = {};
477
- __webpack_require__.r(install_browser_namespaceObject);
478
- __webpack_require__.d(install_browser_namespaceObject, {
479
- handler: ()=>install_browser_handler,
480
- schema: ()=>install_browser_schema
481
- });
482
- var list_browsers_namespaceObject = {};
483
- __webpack_require__.r(list_browsers_namespaceObject);
484
- __webpack_require__.d(list_browsers_namespaceObject, {
485
- handler: ()=>list_browsers_handler,
486
- schema: ()=>list_browsers_schema
487
- });
488
475
  var list_extensions_namespaceObject = {};
489
476
  __webpack_require__.r(list_extensions_namespaceObject);
490
477
  __webpack_require__.d(list_extensions_namespaceObject, {
491
478
  handler: ()=>list_extensions_handler,
492
479
  schema: ()=>list_extensions_schema
493
480
  });
494
- var list_templates_namespaceObject = {};
495
- __webpack_require__.r(list_templates_namespaceObject);
496
- __webpack_require__.d(list_templates_namespaceObject, {
497
- handler: ()=>list_templates_handler,
498
- schema: ()=>list_templates_schema
499
- });
500
- var login_namespaceObject = {};
501
- __webpack_require__.r(login_namespaceObject);
502
- __webpack_require__.d(login_namespaceObject, {
503
- handler: ()=>login_handler,
504
- schema: ()=>login_schema
505
- });
506
- var logout_namespaceObject = {};
507
- __webpack_require__.r(logout_namespaceObject);
508
- __webpack_require__.d(logout_namespaceObject, {
509
- handler: ()=>logout_handler,
510
- schema: ()=>logout_schema
511
- });
512
481
  var logs_namespaceObject = {};
513
482
  __webpack_require__.r(logs_namespaceObject);
514
483
  __webpack_require__.d(logs_namespaceObject, {
@@ -536,30 +505,24 @@ __webpack_require__.d(preview_web_namespaceObject, {
536
505
  handler: ()=>preview_web_handler,
537
506
  schema: ()=>preview_web_schema
538
507
  });
539
- var preview_namespaceObject = {};
540
- __webpack_require__.r(preview_namespaceObject);
541
- __webpack_require__.d(preview_namespaceObject, {
542
- handler: ()=>preview_handler,
543
- schema: ()=>preview_schema
544
- });
545
508
  var tools_publish_namespaceObject = {};
546
509
  __webpack_require__.r(tools_publish_namespaceObject);
547
510
  __webpack_require__.d(tools_publish_namespaceObject, {
548
511
  handler: ()=>publish_handler,
549
512
  schema: ()=>publish_schema
550
513
  });
551
- var release_list_namespaceObject = {};
552
- __webpack_require__.r(release_list_namespaceObject);
553
- __webpack_require__.d(release_list_namespaceObject, {
554
- handler: ()=>release_list_handler,
555
- schema: ()=>release_list_schema
556
- });
557
514
  var release_promote_namespaceObject = {};
558
515
  __webpack_require__.r(release_promote_namespaceObject);
559
516
  __webpack_require__.d(release_promote_namespaceObject, {
560
517
  handler: ()=>release_promote_handler,
561
518
  schema: ()=>release_promote_schema
562
519
  });
520
+ var release_status_namespaceObject = {};
521
+ __webpack_require__.r(release_status_namespaceObject);
522
+ __webpack_require__.d(release_status_namespaceObject, {
523
+ handler: ()=>release_status_handler,
524
+ schema: ()=>release_status_schema
525
+ });
563
526
  var reload_namespaceObject = {};
564
527
  __webpack_require__.r(reload_namespaceObject);
565
528
  __webpack_require__.d(reload_namespaceObject, {
@@ -572,12 +535,6 @@ __webpack_require__.d(shares_namespaceObject, {
572
535
  handler: ()=>shares_handler,
573
536
  schema: ()=>shares_schema
574
537
  });
575
- var source_inspect_namespaceObject = {};
576
- __webpack_require__.r(source_inspect_namespaceObject);
577
- __webpack_require__.d(source_inspect_namespaceObject, {
578
- handler: ()=>source_inspect_handler,
579
- schema: ()=>source_inspect_schema
580
- });
581
538
  var start_namespaceObject = {};
582
539
  __webpack_require__.r(start_namespaceObject);
583
540
  __webpack_require__.d(start_namespaceObject, {
@@ -597,13 +554,18 @@ __webpack_require__.d(storage_namespaceObject, {
597
554
  handler: ()=>storage_handler,
598
555
  schema: ()=>storage_schema
599
556
  });
600
- var store_status_namespaceObject = {};
601
- __webpack_require__.r(store_status_namespaceObject);
602
- __webpack_require__.d(store_status_namespaceObject, {
603
- handler: ()=>store_status_handler,
604
- latestSubmissionsByStore: ()=>latestSubmissionsByStore,
605
- normalizeStoresStatus: ()=>normalizeStoresStatus,
606
- schema: ()=>store_status_schema
557
+ var submit_namespaceObject = {};
558
+ __webpack_require__.r(submit_namespaceObject);
559
+ __webpack_require__.d(submit_namespaceObject, {
560
+ handler: ()=>submit_handler,
561
+ schema: ()=>submit_schema,
562
+ storeMdWarnings: ()=>storeMdWarnings
563
+ });
564
+ var templates_namespaceObject = {};
565
+ __webpack_require__.r(templates_namespaceObject);
566
+ __webpack_require__.d(templates_namespaceObject, {
567
+ handler: ()=>templates_handler,
568
+ schema: ()=>templates_schema
607
569
  });
608
570
  var theme_verify_namespaceObject = {};
609
571
  __webpack_require__.r(theme_verify_namespaceObject);
@@ -611,25 +573,13 @@ __webpack_require__.d(theme_verify_namespaceObject, {
611
573
  handler: ()=>theme_verify_handler,
612
574
  schema: ()=>theme_verify_schema
613
575
  });
614
- var uninstall_browser_namespaceObject = {};
615
- __webpack_require__.r(uninstall_browser_namespaceObject);
616
- __webpack_require__.d(uninstall_browser_namespaceObject, {
617
- handler: ()=>uninstall_browser_handler,
618
- schema: ()=>uninstall_browser_schema
619
- });
620
576
  var wait_namespaceObject = {};
621
577
  __webpack_require__.r(wait_namespaceObject);
622
578
  __webpack_require__.d(wait_namespaceObject, {
623
579
  handler: ()=>wait_handler,
624
580
  schema: ()=>wait_schema
625
581
  });
626
- var whoami_namespaceObject = {};
627
- __webpack_require__.r(whoami_namespaceObject);
628
- __webpack_require__.d(whoami_namespaceObject, {
629
- handler: ()=>whoami_handler,
630
- schema: ()=>whoami_schema
631
- });
632
- var package_namespaceObject = JSON.parse('{"rE":"6.5.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
582
+ var package_namespaceObject = JSON.parse('{"rE":"9.0.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
633
583
  function credentialsPath() {
634
584
  if ("win32" === process.platform) {
635
585
  const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
@@ -647,7 +597,7 @@ function readCredentials() {
647
597
  if (1 !== data.version) return null;
648
598
  const token = String(data.token || "").trim();
649
599
  if (!token) return null;
650
- const provider = "extensiondev" === data.provider || "github" === data.provider ? data.provider : void 0;
600
+ const provider = "extensiondev" === data.provider ? data.provider : void 0;
651
601
  return {
652
602
  version: 1,
653
603
  token,
@@ -735,15 +685,6 @@ function safeApiBase(raw) {
735
685
  };
736
686
  }
737
687
  async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
738
- const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
739
- if (override) return {
740
- provider: "github",
741
- clientId: override,
742
- scope: "read:user",
743
- deviceCodeUrl: "/api/cli/device/code",
744
- deviceTokenUrl: "/api/cli/device/token",
745
- verificationUri: "https://github.com/login/device"
746
- };
747
688
  const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
748
689
  headers: {
749
690
  accept: "application/json"
@@ -751,16 +692,10 @@ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
751
692
  });
752
693
  if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
753
694
  const data = await res.json().catch(()=>({}));
754
- const provider = "extensiondev" === data.provider ? "extensiondev" : "github";
755
- const clientId = String(data.githubClientId || "").trim();
756
- if ("github" === provider && !clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
757
695
  return {
758
- provider,
759
- clientId,
760
- scope: String(data.scope || "read:user"),
761
696
  deviceCodeUrl: String(data.deviceCodeUrl || "/api/cli/device/code"),
762
697
  deviceTokenUrl: String(data.deviceTokenUrl || "/api/cli/device/token"),
763
- verificationUri: String(data.verificationUri || "https://github.com/login/device")
698
+ verificationUri: String(data.verificationUri || `${apiBase.replace(/\/+$/, "")}/device`)
764
699
  };
765
700
  }
766
701
  function persistTokenResponse(args) {
@@ -773,39 +708,115 @@ function persistTokenResponse(args) {
773
708
  projectSlug: String(args.data.projectSlug || ""),
774
709
  expiresAt: Number(args.data.expiresAt || 0),
775
710
  api: args.apiBase,
776
- provider: args.provider
711
+ provider: "extensiondev"
777
712
  };
778
713
  writeCredentials(creds);
779
714
  return creds;
780
715
  }
781
- async function exchangeAndPersist(args) {
782
- const doFetch = args.fetchImpl ?? fetch;
783
- const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
784
- method: "POST",
785
- headers: {
786
- "content-type": "application/json",
787
- accept: "application/json"
788
- },
789
- body: JSON.stringify({
790
- githubToken: args.githubToken,
791
- project: args.project
792
- })
793
- });
794
- const text = await res.text();
795
- let data;
716
+ const INSTALL_HEADER = "x-extensiondev-install";
717
+ const SESSION_HEADER = "x-extensiondev-session";
718
+ const TOOL_HEADER = "x-extensiondev-tool";
719
+ const ROTATE_AFTER_MS = 2592000000;
720
+ function installIdentityDir() {
721
+ if ("win32" === process.platform) {
722
+ const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
723
+ return node_path.join(base, "extension-dev");
724
+ }
725
+ const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
726
+ const base = xdg || node_path.join(node_os.homedir(), ".config");
727
+ return node_path.join(base, "extension-dev");
728
+ }
729
+ function installIdentityPath() {
730
+ return node_path.join(installIdentityDir(), "install.json");
731
+ }
732
+ function telemetryDisabled() {
733
+ const off = (value)=>{
734
+ const raw = String(value || "").trim().toLowerCase();
735
+ return "" !== raw && "0" !== raw && "false" !== raw;
736
+ };
737
+ return off(process.env.EXTENSION_DEV_NO_TELEMETRY) || off(process.env.DO_NOT_TRACK);
738
+ }
739
+ function randomId() {
740
+ return node_crypto.randomBytes(16).toString("hex");
741
+ }
742
+ function readStoredInstallIdentity() {
796
743
  try {
797
- data = JSON.parse(text);
744
+ const raw = node_fs.readFileSync(installIdentityPath(), "utf8");
745
+ const data = JSON.parse(raw);
746
+ if (!data || "object" != typeof data) return null;
747
+ if (1 !== data.version) return null;
748
+ const installId = String(data.installId || "").trim();
749
+ if (!/^[0-9a-f]{32}$/.test(installId)) return null;
750
+ const rotatedAt = Number(data.rotatedAt || 0);
751
+ if (!Number.isFinite(rotatedAt) || rotatedAt <= 0) return null;
752
+ return {
753
+ version: 1,
754
+ installId,
755
+ rotatedAt
756
+ };
798
757
  } catch {
799
- data = {
800
- message: text
758
+ return null;
759
+ }
760
+ }
761
+ function writeStoredInstallIdentity(identity) {
762
+ const file = installIdentityPath();
763
+ try {
764
+ node_fs.mkdirSync(node_path.dirname(file), {
765
+ recursive: true,
766
+ mode: 448
767
+ });
768
+ node_fs.writeFileSync(file, JSON.stringify(identity, null, 2) + "\n", {
769
+ mode: 384
770
+ });
771
+ return true;
772
+ } catch {
773
+ return false;
774
+ }
775
+ }
776
+ let ephemeralInstallId = "";
777
+ function resolveInstallId(now = Date.now()) {
778
+ if (telemetryDisabled()) return "";
779
+ try {
780
+ installIdentityPath();
781
+ } catch {
782
+ return "";
783
+ }
784
+ const stored = readStoredInstallIdentity();
785
+ if (stored && now - stored.rotatedAt < ROTATE_AFTER_MS) return stored.installId;
786
+ const rotated = {
787
+ version: 1,
788
+ installId: randomId(),
789
+ rotatedAt: now
790
+ };
791
+ if (writeStoredInstallIdentity(rotated)) {
792
+ ephemeralInstallId = "";
793
+ return rotated.installId;
794
+ }
795
+ if (!ephemeralInstallId) ephemeralInstallId = rotated.installId;
796
+ return ephemeralInstallId;
797
+ }
798
+ let processSessionId = "";
799
+ function session_identity_sessionId() {
800
+ if (telemetryDisabled()) return "";
801
+ if (!processSessionId) processSessionId = randomId();
802
+ return processSessionId;
803
+ }
804
+ function identityHeaders(tool) {
805
+ try {
806
+ if (telemetryDisabled()) return {};
807
+ const name = String(tool || "").trim().toLowerCase();
808
+ if (!/^[a-z0-9_]{1,64}$/.test(name)) return {};
809
+ const install = resolveInstallId();
810
+ const session = session_identity_sessionId();
811
+ if (!install || !session) return {};
812
+ return {
813
+ [INSTALL_HEADER]: install,
814
+ [SESSION_HEADER]: session,
815
+ [TOOL_HEADER]: name
801
816
  };
817
+ } catch {
818
+ return {};
802
819
  }
803
- if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
804
- return persistTokenResponse({
805
- apiBase: args.apiBase,
806
- data,
807
- provider: "github"
808
- });
809
820
  }
810
821
  function _define_property(obj, key, value) {
811
822
  if (key in obj) Object.defineProperty(obj, key, {
@@ -874,7 +885,8 @@ class RegistryAccessTokens {
874
885
  method: "POST",
875
886
  headers: {
876
887
  authorization: `Bearer ${token}`,
877
- "content-type": "application/json"
888
+ "content-type": "application/json",
889
+ ...identityHeaders("extension_registry_access")
878
890
  },
879
891
  body: JSON.stringify({
880
892
  workspaceSlug: ref.workspace,
@@ -1028,7 +1040,7 @@ async function fetchRegistryJson(url, fetchImpl = fetch, options) {
1028
1040
  };
1029
1041
  const grant = await tokens.get(ref, options?.api);
1030
1042
  if ("ok" !== grant.status) {
1031
- const detail = "no-credential" === grant.status ? "This project is private. Run extension_login for it, or set EXTENSION_DEV_TOKEN." : "public" === grant.status ? "The platform reports this project is public, but the registry refused the read." : grant.message;
1043
+ const detail = "no-credential" === grant.status ? "This project is private. Run extension_auth (action: login) for it, or set EXTENSION_DEV_TOKEN." : "public" === grant.status ? "The platform reports this project is public, but the registry refused the read." : grant.message;
1032
1044
  return {
1033
1045
  ok: false,
1034
1046
  status: res.status,
@@ -1134,7 +1146,7 @@ function detectPackageManager(projectPath) {
1134
1146
  }
1135
1147
  const create_schema = {
1136
1148
  name: "extension_create",
1137
- description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options. The scaffolder may initialize a git repository in the new project; the result's defaultsApplied block reports whether it did, along with every other decision made without being asked.",
1149
+ description: "Create a new browser extension project from a template in the extension.dev catalog. extension_templates lists what is available. The scaffolder may initialize a git repository in the new project; the result's defaultsApplied block reports whether it did, along with every other decision made without being asked.",
1138
1150
  inputSchema: {
1139
1151
  type: "object",
1140
1152
  properties: {
@@ -1144,12 +1156,12 @@ const create_schema = {
1144
1156
  },
1145
1157
  parentDir: {
1146
1158
  type: "string",
1147
- description: "Directory to create the project inside. Defaults to the MCP server process cwd (NOT the caller's cwd), which may not be where you expect; pass this explicitly when you care where the project lands. Aliases: parent, into."
1159
+ description: "Directory to create the project inside. Defaults to the MCP server process cwd, NOT the caller's cwd, so pass it whenever you care where the project lands. Aliases: parent, into."
1148
1160
  },
1149
1161
  template: {
1150
1162
  type: "string",
1151
1163
  default: "typescript",
1152
- description: "Template slug from the extension.dev template catalog (e.g. 'react', 'ai-claude', 'content-vue'). Use extension_list_templates to discover options."
1164
+ description: "Template slug from the extension.dev catalog (e.g. 'react', 'ai-claude', 'content-vue'). extension_templates discovers them."
1153
1165
  },
1154
1166
  install: {
1155
1167
  type: "boolean",
@@ -1248,7 +1260,7 @@ async function create_handler(args) {
1248
1260
  defaultsApplied: {
1249
1261
  parentDir: args.parentDir ? `${resolvedParent} (explicit)` : `${resolvedParent} (default: the MCP server process cwd, not yours; pass parentDir to choose)`,
1250
1262
  ...void 0 === args.template ? {
1251
- template: "typescript (default; run extension_list_templates to pick another, e.g. javascript for plain JS)"
1263
+ template: "typescript (default; call extension_templates to pick another, e.g. javascript for plain JS)"
1252
1264
  } : {},
1253
1265
  packageManager: `${packageManager} (auto-detected by the scaffolder, not asked)`,
1254
1266
  browser: "chrome (default: extension_dev and extension_build target chrome unless you pass browser)",
@@ -1452,12 +1464,99 @@ async function getTemplateBySlug(slug) {
1452
1464
  const meta = await fetchTemplatesMeta();
1453
1465
  return meta.templates.find((t)=>t.slug === slug);
1454
1466
  }
1455
- const list_templates_schema = {
1456
- name: "extension_list_templates",
1457
- description: "List available extension templates from the extension.dev template catalog. Filter by surface, framework, or tags. Returns structured metadata from templates-meta.json. Note: 'framework' is the UI framework only (react/vue/svelte/preact/vanilla) - it is not the language. TypeScript and JavaScript templates live under slugs (e.g. 'typescript', 'content-typescript'); shadcn is a React variant ('sidebar-shadcn') and provider AIs are tagged 'ai' ('ai-chatgpt', 'ai-claude'). Reach those with query/tags/slug, not framework.",
1467
+ async function searchTemplates(args) {
1468
+ const templates = await listTemplates(args);
1469
+ const results = templates.map((t)=>({
1470
+ slug: t.slug,
1471
+ description: t.description,
1472
+ uiFramework: t.uiFramework,
1473
+ frameworkLabel: t.uiFramework || "vanilla",
1474
+ surfaces: t.surfaces,
1475
+ tags: t.tags,
1476
+ difficulty: t.difficulty,
1477
+ featured: t.featured,
1478
+ useCases: t.useCases,
1479
+ repositoryUrl: t.repositoryUrl,
1480
+ downloads: t.downloads
1481
+ }));
1482
+ return JSON.stringify({
1483
+ count: results.length,
1484
+ templates: results
1485
+ });
1486
+ }
1487
+ async function readTemplateSource(args) {
1488
+ const template = await getTemplateBySlug(args.slug);
1489
+ if (!template) return JSON.stringify({
1490
+ error: `Template '${args.slug}' not found in the catalog`,
1491
+ hint: 'Use extension_templates with action: "list" to see available templates.'
1492
+ });
1493
+ const meta = {
1494
+ slug: template.slug,
1495
+ description: template.description,
1496
+ uiFramework: template.uiFramework || "vanilla",
1497
+ surfaces: template.surfaces,
1498
+ permissions: template.permissions,
1499
+ patternExplanation: template.patternExplanation,
1500
+ keyFiles: template.keyFiles,
1501
+ repositoryUrl: template.repositoryUrl
1502
+ };
1503
+ if (!args.files?.length) return JSON.stringify({
1504
+ ...meta,
1505
+ files: template.files.map((f)=>stripTemplatePathPrefix(template.slug, f)),
1506
+ hint: "Pass specific file paths in the files parameter to read their contents."
1507
+ });
1508
+ const fileContents = {};
1509
+ const errors = [];
1510
+ await Promise.all(args.files.map(async (filePath)=>{
1511
+ const urls = await templateFileUrls(args.slug, filePath);
1512
+ let lastStatus = 0;
1513
+ for (const url of urls)try {
1514
+ const response = await fetch(url);
1515
+ if (response.ok) {
1516
+ fileContents[filePath] = await response.text();
1517
+ return;
1518
+ }
1519
+ lastStatus = response.status;
1520
+ } catch {}
1521
+ errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
1522
+ }));
1523
+ return JSON.stringify({
1524
+ ...meta,
1525
+ fileContents,
1526
+ ...errors.length ? {
1527
+ errors
1528
+ } : {}
1529
+ });
1530
+ }
1531
+ const templates_schema = {
1532
+ name: "extension_templates",
1533
+ description: "Browse the extension.dev template catalog. action:'list' (default) searches and filters it and returns metadata per template; action:'source' reads one template's files by `slug`, for learning a pattern before building something similar. `framework` is the UI framework ONLY, never the language: TypeScript and JavaScript templates live under slugs ('typescript', 'content-typescript'), shadcn is a React variant ('sidebar-shadcn'), and provider AIs are tagged 'ai' ('ai-chatgpt', 'ai-claude'). Reach those with query/tags/slug.",
1458
1534
  inputSchema: {
1459
1535
  type: "object",
1460
1536
  properties: {
1537
+ action: {
1538
+ type: "string",
1539
+ enum: [
1540
+ "list",
1541
+ "source"
1542
+ ],
1543
+ default: "list"
1544
+ },
1545
+ slug: {
1546
+ type: "string",
1547
+ description: "source: which template to read (e.g. 'ai-claude', 'content-react'). Required for source."
1548
+ },
1549
+ files: {
1550
+ type: "array",
1551
+ items: {
1552
+ type: "string"
1553
+ },
1554
+ description: "source: paths to read (e.g. ['src/manifest.json']). Omit for the file listing."
1555
+ },
1556
+ query: {
1557
+ type: "string",
1558
+ description: "list: keyword search over slug, description, tags and useCases. Ranks by word matches, so a natural phrase works."
1559
+ },
1461
1560
  surface: {
1462
1561
  type: "string",
1463
1562
  enum: [
@@ -1466,7 +1565,7 @@ const list_templates_schema = {
1466
1565
  "newtab",
1467
1566
  "background"
1468
1567
  ],
1469
- description: "Filter by extension surface type. For a popup/action starter use the 'action' slug (query:'action'), not a surface filter."
1568
+ description: "list: filter by surface. For a popup/action starter use query:'action', not a surface."
1470
1569
  },
1471
1570
  framework: {
1472
1571
  type: "string",
@@ -1477,46 +1576,104 @@ const list_templates_schema = {
1477
1576
  "preact",
1478
1577
  ""
1479
1578
  ],
1480
- description: "Filter by UI framework only (empty string = vanilla JS). Not a language filter - for TypeScript/JavaScript use query or slug."
1579
+ description: "list: UI framework filter (empty string = vanilla JS)."
1481
1580
  },
1482
1581
  tags: {
1483
1582
  type: "array",
1484
1583
  items: {
1485
1584
  type: "string"
1486
1585
  },
1487
- description: "Filter by tags (e.g. ['ai', 'chat'])"
1586
+ description: "list: filter by tags, e.g. ['ai', 'chat']."
1488
1587
  },
1489
1588
  featured: {
1490
1589
  type: "boolean",
1491
- description: "Only show featured templates"
1492
- },
1493
- query: {
1494
- type: "string",
1495
- description: "Keyword search across slug, description, tags, and useCases. Ranks by how many query words match, so a natural phrase works; single keywords are fine too."
1590
+ description: "list: only featured templates."
1496
1591
  }
1497
- }
1592
+ },
1593
+ required: []
1498
1594
  }
1499
1595
  };
1500
- async function list_templates_handler(args) {
1501
- const templates = await listTemplates(args);
1502
- const results = templates.map((t)=>({
1503
- slug: t.slug,
1504
- description: t.description,
1505
- uiFramework: t.uiFramework,
1506
- frameworkLabel: t.uiFramework || "vanilla",
1507
- surfaces: t.surfaces,
1508
- tags: t.tags,
1509
- difficulty: t.difficulty,
1510
- featured: t.featured,
1511
- useCases: t.useCases,
1512
- repositoryUrl: t.repositoryUrl,
1513
- downloads: t.downloads
1514
- }));
1515
- return JSON.stringify({
1516
- count: results.length,
1517
- templates: results
1596
+ async function templates_handler(args) {
1597
+ if ((args.action ?? "list") === "source" || !args.action && args.slug) {
1598
+ if (!args.slug) return JSON.stringify({
1599
+ ok: false,
1600
+ error: "action 'source' needs a slug.",
1601
+ hint: 'Call extension_templates with action: "list" to find one.'
1602
+ });
1603
+ return readTemplateSource({
1604
+ slug: args.slug,
1605
+ files: args.files
1606
+ });
1607
+ }
1608
+ return searchTemplates({
1609
+ surface: args.surface,
1610
+ framework: args.framework,
1611
+ tags: args.tags,
1612
+ featured: args.featured,
1613
+ query: args.query
1518
1614
  });
1519
1615
  }
1616
+ const LAUNCHABLE_BROWSERS = [
1617
+ "chrome",
1618
+ "chromium",
1619
+ "edge",
1620
+ "brave",
1621
+ "opera",
1622
+ "vivaldi",
1623
+ "yandex",
1624
+ "firefox",
1625
+ "waterfox",
1626
+ "librewolf",
1627
+ "safari",
1628
+ "chromium-based",
1629
+ "gecko-based",
1630
+ "firefox-based",
1631
+ "webkit-based"
1632
+ ];
1633
+ const REAL_BROWSERS = [
1634
+ "chrome",
1635
+ "chromium",
1636
+ "edge",
1637
+ "brave",
1638
+ "opera",
1639
+ "vivaldi",
1640
+ "yandex",
1641
+ "firefox",
1642
+ "waterfox",
1643
+ "librewolf",
1644
+ "safari"
1645
+ ];
1646
+ const MANAGED_BROWSERS = [
1647
+ "chrome",
1648
+ "chromium",
1649
+ "edge",
1650
+ "firefox"
1651
+ ];
1652
+ const PROJECT_PATH = {
1653
+ type: "string",
1654
+ description: "Extension project root"
1655
+ };
1656
+ const SESSION_PROJECT_PATH = {
1657
+ type: "string",
1658
+ description: "Extension project root (needs a live dev session)"
1659
+ };
1660
+ const SESSION_BROWSER = {
1661
+ type: "string",
1662
+ description: "Session browser; defaults to this project's live session"
1663
+ };
1664
+ const CALL_TIMEOUT = {
1665
+ type: "number",
1666
+ description: "Command timeout in ms (default 5000)"
1667
+ };
1668
+ const API_BASE = {
1669
+ type: "string",
1670
+ description: "Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev)"
1671
+ };
1672
+ const LAUNCH_BROWSER = {
1673
+ type: "string",
1674
+ enum: LAUNCHABLE_BROWSERS,
1675
+ default: "chrome"
1676
+ };
1520
1677
  const PINNED_CLI_VERSION = String(package_namespaceObject.El.OP ?? "latest").replace(/^[\^~]/, "");
1521
1678
  function pinnedCliVersion() {
1522
1679
  const override = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
@@ -2006,17 +2163,17 @@ function materializeCarrier(projectPath, browser) {
2006
2163
  ...ignored ? {
2007
2164
  gitignored: ignored
2008
2165
  } : {},
2009
- note: "Live-preview carrier placed in ./extensions; Extension.js loads it as a companion beside your extension. Open https://inspect.extension.dev/?session=live in the dev browser (any http://localhost origin works too) to watch the session's real-lane chrome.* trace on the Trace tab. It is a debug companion, never part of a release: extension_stop and extension_build remove it again" + (ignored ? `, and ${ignored} was added to .gitignore.` : "."),
2166
+ note: "Live-preview carrier placed in ./extensions; Extension.js loads it as a companion beside your extension. Open https://preview.extension.dev/?session=live in the dev browser (any http://localhost origin works too) to watch the session's real-lane chrome.* trace on the Trace tab. It is a debug companion, never part of a release: extension_stop and extension_build remove it again" + (ignored ? `, and ${ignored} was added to .gitignore.` : "."),
2010
2167
  limitations: [
2011
2168
  "The trace shows calls a PAGE bridges to the carrier. Your extension's own chrome.* calls run directly in its contexts and never cross the carrier, so they do not appear.",
2012
2169
  "Bridged calls run under the CARRIER's identity, not your extension's. The preview assumes a single active guest and does not namespace per-extension state, so storage, action/badge state, messaging delivery, offscreen documents and relative script paths belong to the carrier. Rows affected are badged carrier-scoped in the Trace tab.",
2013
2170
  "Chromium-family only: Firefox has no externally_connectable channel for web pages."
2014
2171
  ],
2015
- graduation: "The carrier lane is the SHARED real lane: bridged calls run as the carrier, by design (see limitations). Your guest is already loaded as ITSELF in this same session, so for its own storage, identity, badge and messaging (the isolated real thing), drive the guest directly instead of the carrier bridge: extension_storage, extension_eval and extension_dom_inspect against this projectPath all operate on the guest as itself. Start (or replace) this session with allowControl: true (or allowEval: true) to unlock them. Use the carrier bridge for the shared real-lane TRACE; use the control verbs for the guest's OWN state.",
2172
+ graduation: "The carrier lane is the SHARED real lane: bridged calls run as the carrier, by design (see limitations). Your guest is already loaded as ITSELF in this same session, so for its own storage, identity, badge and messaging (the isolated real thing), drive the guest directly instead of the carrier bridge: extension_storage, extension_eval and extension_dom_snapshot against this projectPath all operate on the guest as itself. Start (or replace) this session with allowControl: true (or allowEval: true) to unlock them. Use the carrier bridge for the shared real-lane TRACE; use the control verbs for the guest's OWN state.",
2016
2173
  ...carrierId ? {
2017
2174
  bridgeProtocol: {
2018
2175
  carrierExtensionId: carrierId,
2019
- allowedOrigins: "https://inspect.extension.dev, https://intelligence.extension.dev, https://themes.extension.dev, http://localhost/*, http://127.0.0.1/*",
2176
+ allowedOrigins: "https://preview.extension.dev, https://code.extension.dev, https://themes.extension.dev, http://localhost/*, http://127.0.0.1/*",
2020
2177
  howTo: "From a page on an allowed origin, register your guest once with a 'session' message (it declares the permissions the carrier enforces), then send 'bridge' messages to run chrome.* for real; each one streams into the Trace tab. Use the EXACT dotted wire names the bridge dispatcher accepts: storage is storage.get/set/remove/clear with the AREA AS AN ARGUMENT, NOT storage.local.get.",
2021
2178
  example: [
2022
2179
  `const id = '${carrierId}'`,
@@ -2136,32 +2293,8 @@ const build_schema = {
2136
2293
  inputSchema: {
2137
2294
  type: "object",
2138
2295
  properties: {
2139
- projectPath: {
2140
- type: "string",
2141
- description: "Path to the extension project root"
2142
- },
2143
- browser: {
2144
- type: "string",
2145
- enum: [
2146
- "chrome",
2147
- "chromium",
2148
- "edge",
2149
- "brave",
2150
- "opera",
2151
- "vivaldi",
2152
- "yandex",
2153
- "firefox",
2154
- "waterfox",
2155
- "librewolf",
2156
- "safari",
2157
- "chromium-based",
2158
- "gecko-based",
2159
- "firefox-based",
2160
- "webkit-based"
2161
- ],
2162
- default: "chrome",
2163
- description: "Target browser"
2164
- },
2296
+ projectPath: PROJECT_PATH,
2297
+ browser: LAUNCH_BROWSER,
2165
2298
  zip: {
2166
2299
  type: "boolean",
2167
2300
  default: false,
@@ -2199,7 +2332,7 @@ const build_schema = {
2199
2332
  skipValidation: {
2200
2333
  type: "boolean",
2201
2334
  default: false,
2202
- description: "Build even when extension_manifest_validate reports build-blocking errors. The build normally refuses, because a manifest error means the bundle it produces is broken in ways the bundler itself does not report."
2335
+ description: "Build even when extension_manifest_validate reports build-blocking errors. The build normally refuses: a manifest error yields a broken bundle the bundler itself never flags."
2203
2336
  }
2204
2337
  },
2205
2338
  required: [
@@ -2421,22 +2554,19 @@ async function build_handler(args) {
2421
2554
  }
2422
2555
  const stop_schema = {
2423
2556
  name: "extension_stop",
2424
- description: "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched, and removes the live-preview carrier if extension_dev placed one. Counterpart to extension_dev/extension_start. Call it when you are done verifying so sessions do not accumulate.",
2557
+ description: "Stop a session that extension_dev or extension_start is running: terminates the server and the browser it launched, and removes the live-preview carrier if extension_dev placed one. Covers extension_start build:false too, which the registry still records as a preview session. Call it when you are done verifying so sessions do not accumulate.",
2425
2558
  inputSchema: {
2426
2559
  type: "object",
2427
2560
  properties: {
2428
- projectPath: {
2429
- type: "string",
2430
- description: "Path to the extension project root"
2431
- },
2561
+ projectPath: PROJECT_PATH,
2432
2562
  browser: {
2433
2563
  type: "string",
2434
- description: "Browser of the session to stop (matches the browser passed to extension_dev/extension_start). Defaults to the one live session for this project when omitted, instead of assuming chrome."
2564
+ description: "Browser of the session to stop. Defaults to the single live session for this project rather than assuming chrome."
2435
2565
  },
2436
2566
  all: {
2437
2567
  type: "boolean",
2438
2568
  default: false,
2439
- description: "Stop every known session, across projects and browsers. Discovers sessions from this server's registry AND the on-disk session markers earlier runs left, so it still finds sessions after an MCP restart or after a dev child exited out from under the registry. When true, projectPath/browser are ignored."
2569
+ description: "Stop every known session across projects and browsers, found from this server's registry AND the on-disk markers earlier runs left, so it still works after an MCP restart. projectPath/browser are then ignored."
2440
2570
  }
2441
2571
  },
2442
2572
  required: []
@@ -2588,7 +2718,7 @@ async function stop_handler(args) {
2588
2718
  const LAUNCH_FLAG_SCHEMA = {
2589
2719
  profile: {
2590
2720
  type: "string",
2591
- description: 'Browser profile path, or "false" to reuse the default user profile. Omit for a fresh throwaway profile.'
2721
+ description: 'Profile path, or "false" to reuse the real user profile. Omit for a throwaway one.'
2592
2722
  },
2593
2723
  startingUrl: {
2594
2724
  type: "string",
@@ -2596,26 +2726,26 @@ const LAUNCH_FLAG_SCHEMA = {
2596
2726
  },
2597
2727
  chromiumBinary: {
2598
2728
  type: "string",
2599
- description: "Path to a custom Chromium-based binary (overrides browser)"
2729
+ description: "Custom Chromium-based binary (overrides browser)"
2600
2730
  },
2601
2731
  geckoBinary: {
2602
2732
  type: "string",
2603
- description: "Path to a custom Gecko/Firefox binary (overrides browser)"
2733
+ description: "Custom Gecko/Firefox binary (overrides browser)"
2604
2734
  },
2605
2735
  host: {
2606
2736
  type: "string",
2607
- description: "Dev server bind host. Use 0.0.0.0 for Docker or devcontainers. Defaults to 127.0.0.1"
2737
+ description: "Bind host, default 127.0.0.1. Use 0.0.0.0 in Docker or devcontainers."
2608
2738
  },
2609
2739
  publicHost: {
2610
2740
  type: "string",
2611
- description: "Connectable host the browser dials for HMR and reload when it differs from the bind host"
2741
+ description: "Host the browser dials for HMR and reload when it differs from the bind host"
2612
2742
  },
2613
2743
  extensions: {
2614
2744
  type: "array",
2615
2745
  items: {
2616
2746
  type: "string"
2617
2747
  },
2618
- description: "Companion extension paths or store URLs to load alongside the project"
2748
+ description: "Extra extension paths or store URLs to load alongside the project"
2619
2749
  }
2620
2750
  };
2621
2751
  function launchFlagArgs(args) {
@@ -2631,35 +2761,12 @@ function launchFlagArgs(args) {
2631
2761
  }
2632
2762
  const dev_schema = {
2633
2763
  name: "extension_dev",
2634
- 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.",
2764
+ description: "Run the extension WHILE YOU EDIT IT: dev build, hot module replacement, and a browser with it loaded. The default answer to \"run my extension\". Only this tool can unlock the control channel that extension_storage/reload/open/dom_snapshot need (allowControl) and that extension_eval needs (allowEval). For the production build in a browser instead, use extension_start. Returns process info for extension_wait and extension_inspect.",
2635
2765
  inputSchema: {
2636
2766
  type: "object",
2637
2767
  properties: {
2638
- projectPath: {
2639
- type: "string",
2640
- description: "Path to the extension project root"
2641
- },
2642
- browser: {
2643
- type: "string",
2644
- enum: [
2645
- "chrome",
2646
- "chromium",
2647
- "edge",
2648
- "brave",
2649
- "opera",
2650
- "vivaldi",
2651
- "yandex",
2652
- "firefox",
2653
- "waterfox",
2654
- "librewolf",
2655
- "safari",
2656
- "chromium-based",
2657
- "gecko-based",
2658
- "firefox-based",
2659
- "webkit-based"
2660
- ],
2661
- default: "chrome"
2662
- },
2768
+ projectPath: PROJECT_PATH,
2769
+ browser: LAUNCH_BROWSER,
2663
2770
  port: {
2664
2771
  type: "number",
2665
2772
  description: "Dev server port (0 for auto-assign)"
@@ -2667,7 +2774,7 @@ const dev_schema = {
2667
2774
  noBrowser: {
2668
2775
  type: "boolean",
2669
2776
  default: false,
2670
- description: "Start dev server without launching browser"
2777
+ description: "Start the dev server without launching a browser"
2671
2778
  },
2672
2779
  polyfill: {
2673
2780
  type: "boolean",
@@ -2678,22 +2785,22 @@ const dev_schema = {
2678
2785
  replace: {
2679
2786
  type: "boolean",
2680
2787
  default: false,
2681
- description: "Stop any live session already running for this projectPath before starting; the result then reports it as replacedSession. Without it, extension_dev refuses to start over a live session instead of silently forking it (two sessions fight over the browser profile and the newer browser dies on the profile lock)."
2788
+ description: "Stop the live session for this projectPath first, reported as replacedSession. Without it a second call is refused rather than forking: two sessions fight over one profile and the newer browser dies on the lock."
2682
2789
  },
2683
2790
  allowControl: {
2684
2791
  type: "boolean",
2685
2792
  default: false,
2686
- description: "Enable the agent-bridge control channel so extension_storage/reload/open/dom_inspect work against this session"
2793
+ description: "Enable the agent-bridge control channel that extension_storage/reload/open/dom_snapshot need"
2687
2794
  },
2688
2795
  allowEval: {
2689
2796
  type: "boolean",
2690
2797
  default: false,
2691
- description: "Enable extension_eval (runs code in a context; writes a 0600 session token). Implies allowControl, so a single allowEval: true also unlocks storage/reload/open/dom_inspect. You do not need to pass both."
2798
+ description: "Enable extension_eval (runs code in a context; writes a 0600 session token). Implies allowControl, so you never need to pass both."
2692
2799
  },
2693
2800
  carrier: {
2694
2801
  type: "boolean",
2695
2802
  default: false,
2696
- description: "Load the bundled Extension.dev Live Preview carrier beside your extension (Chromium-family browsers only). It is placed in the project's ./extensions folder, which Extension.js auto-loads; allowlisted pages (inspect.extension.dev, localhost) can then pair with the session and stream its real-lane chrome.* trace. Writes extensions/extension-dev-live-preview/ into the project, gitignores it, and takes it back out on extension_stop or extension_build: it is a debug companion, never part of a release."
2803
+ description: "Load the bundled Live Preview carrier beside your extension (Chromium only) so allowlisted pages (preview.extension.dev, localhost) can pair with the session and stream its real-lane chrome.* trace. Written into the auto-loaded ./extensions folder, gitignored, and removed on extension_stop or extension_build: never part of a release."
2697
2804
  }
2698
2805
  },
2699
2806
  required: [
@@ -2806,11 +2913,11 @@ async function dev_handler(args) {
2806
2913
  hint: `A locked profile means another session's browser still holds it: call extension_stop with this projectPath to kill that session, then start extension_dev again. If the lock survives a crash, remove ${profileDir} manually before retrying.`
2807
2914
  });
2808
2915
  }
2809
- const controlVerbs = "storage, reload, open, dom_inspect";
2916
+ const controlVerbs = "storage, reload, open, dom_snapshot";
2810
2917
  const capabilities = {
2811
2918
  allowControl,
2812
2919
  allowEval: Boolean(args.allowEval),
2813
- unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, source_inspect, wait, doctor)"
2920
+ unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, inspect, wait, doctor)"
2814
2921
  };
2815
2922
  const boundPort = contractBoundPort(args.projectPath, browser, spawnedAt);
2816
2923
  if (null !== boundPort && boundPort !== args.port) registerSession({
@@ -2848,7 +2955,7 @@ async function dev_handler(args) {
2848
2955
  } : {}
2849
2956
  } : {},
2850
2957
  capabilities,
2851
- hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/dom_inspect/eval) need a live browser and will not work against this session. When you are done, call extension_stop to shut down the dev server." : "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_inspect need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser.",
2958
+ hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/dom_snapshot/eval) need a live browser and will not work against this session. When you are done, call extension_stop to shut down the dev server." : "Use extension_wait to check when the extension is fully loaded, then extension_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_snapshot need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser.",
2852
2959
  earlyOutput: cleanOutput.slice(0, 500),
2853
2960
  logPath
2854
2961
  });
@@ -2871,39 +2978,21 @@ function denoiseEarlyOutput(raw) {
2871
2978
  }
2872
2979
  const start_schema = {
2873
2980
  name: "extension_start",
2874
- description: "Build the extension for production and immediately preview it in a browser. Combines build + preview in one step. No hot reload.",
2981
+ description: "Run the PRODUCTION build in a browser: builds the project, then serves it and launches. No hot module replacement and no control channel, so your edits are not picked up and extension_eval/storage/reload/open/dom_snapshot cannot attach to this session. Use extension_dev while writing code; use this to check what actually ships. Pass build:false to launch on an existing dist/<browser> without rebuilding.",
2875
2982
  inputSchema: {
2876
2983
  type: "object",
2877
2984
  properties: {
2878
- projectPath: {
2879
- type: "string",
2880
- description: "Path to the extension project root"
2881
- },
2882
- browser: {
2883
- type: "string",
2884
- enum: [
2885
- "chrome",
2886
- "chromium",
2887
- "edge",
2888
- "brave",
2889
- "opera",
2890
- "vivaldi",
2891
- "yandex",
2892
- "firefox",
2893
- "waterfox",
2894
- "librewolf",
2895
- "safari",
2896
- "chromium-based",
2897
- "gecko-based",
2898
- "firefox-based",
2899
- "webkit-based"
2900
- ],
2901
- default: "chrome"
2985
+ projectPath: PROJECT_PATH,
2986
+ browser: LAUNCH_BROWSER,
2987
+ build: {
2988
+ type: "boolean",
2989
+ default: true,
2990
+ description: "Build before serving. false serves the existing dist/<browser> as-is and fails when there is none."
2902
2991
  },
2903
2992
  polyfill: {
2904
2993
  type: "boolean",
2905
2994
  default: true,
2906
- description: "Apply cross-browser polyfill"
2995
+ description: "Apply cross-browser polyfill (build only)"
2907
2996
  },
2908
2997
  port: {
2909
2998
  type: "number",
@@ -2912,7 +3001,7 @@ const start_schema = {
2912
3001
  noBrowser: {
2913
3002
  type: "boolean",
2914
3003
  default: false,
2915
- description: "Build and serve without launching a browser"
3004
+ description: "Serve without launching a browser"
2916
3005
  },
2917
3006
  ...LAUNCH_FLAG_SCHEMA
2918
3007
  },
@@ -2923,127 +3012,15 @@ const start_schema = {
2923
3012
  };
2924
3013
  async function start_handler(args) {
2925
3014
  const browser = args.browser ?? "chrome";
3015
+ const building = false !== args.build;
3016
+ const command = building ? "start" : "preview";
2926
3017
  const cliArgs = [
2927
- "start",
2928
- args.projectPath,
2929
- "--browser",
2930
- browser
2931
- ];
2932
- if (false === args.polyfill) cliArgs.push("--polyfill", "false");
2933
- if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
2934
- if (args.noBrowser) cliArgs.push("--no-browser");
2935
- cliArgs.push(...launchFlagArgs(args));
2936
- const spawnedAt = Date.now();
2937
- const spawned = spawnExtensionCli(cliArgs, {
2938
- projectDir: args.projectPath
2939
- });
2940
- const { child, logPath } = spawned;
2941
- const pid = child.pid;
2942
- registerSession({
2943
- pid,
2944
- browser,
2945
- projectPath: args.projectPath,
2946
- command: "start"
2947
- });
2948
- child.on("exit", ()=>removeSession(args.projectPath, browser));
2949
- await new Promise((resolve)=>setTimeout(resolve, 5000));
2950
- const earlyOutput = spawned.readOutput();
2951
- if (null !== child.exitCode || null !== child.signalCode) {
2952
- const code = child.exitCode;
2953
- const signal = child.signalCode;
2954
- return JSON.stringify({
2955
- ok: false,
2956
- status: "exited",
2957
- projectPath: args.projectPath,
2958
- browser,
2959
- pid,
2960
- exitCode: code,
2961
- signal,
2962
- error: `The preview server exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running.`,
2963
- output: earlyOutput.slice(0, 2000),
2964
- logPath,
2965
- hint: "Read `output` above for the cause: a failed production build, a port already in use, or a missing browser binary are the common ones. extension_build will surface a build error on its own."
2966
- });
2967
- }
2968
- const exitStamp = browserExitStamp(args.projectPath, browser, spawnedAt);
2969
- if (exitStamp) return JSON.stringify({
2970
- ok: false,
2971
- status: "browser-exited",
2972
- projectPath: args.projectPath,
2973
- browser,
2974
- pid,
2975
- ...exitStamp,
2976
- error: "The preview process is running but the browser it launched has exited (the extension may have been rejected or the browser crashed). The session cannot be driven.",
2977
- output: earlyOutput.slice(0, 2000),
2978
- logPath,
2979
- hint: "Read `output` above and extension_logs for the cause, then call extension_stop to clean up before retrying."
2980
- });
2981
- return JSON.stringify({
2982
- ok: true,
2983
- pid,
2984
- browser,
2985
- projectPath: args.projectPath,
2986
- status: "started",
2987
- hint: "Use extension_wait to check when the build and browser launch are complete. When you are done, call extension_stop to shut down the session.",
2988
- earlyOutput: earlyOutput.slice(0, 500),
2989
- logPath
2990
- });
2991
- }
2992
- const preview_schema = {
2993
- name: "extension_preview",
2994
- description: "Preview a production-built extension in a browser. Uses dist/ output directly. The extension must be built first with extension_build.",
2995
- inputSchema: {
2996
- type: "object",
2997
- properties: {
2998
- projectPath: {
2999
- type: "string",
3000
- description: "Path to the extension project root"
3001
- },
3002
- browser: {
3003
- type: "string",
3004
- enum: [
3005
- "chrome",
3006
- "chromium",
3007
- "edge",
3008
- "brave",
3009
- "opera",
3010
- "vivaldi",
3011
- "yandex",
3012
- "firefox",
3013
- "waterfox",
3014
- "librewolf",
3015
- "safari",
3016
- "chromium-based",
3017
- "gecko-based",
3018
- "firefox-based",
3019
- "webkit-based"
3020
- ],
3021
- default: "chrome"
3022
- },
3023
- port: {
3024
- type: "number",
3025
- description: "Server port (0 for auto-assign)"
3026
- },
3027
- noBrowser: {
3028
- type: "boolean",
3029
- default: false,
3030
- description: "Serve the preview without launching a browser"
3031
- },
3032
- ...LAUNCH_FLAG_SCHEMA
3033
- },
3034
- required: [
3035
- "projectPath"
3036
- ]
3037
- }
3038
- };
3039
- async function preview_handler(args) {
3040
- const browser = args.browser ?? "chrome";
3041
- const cliArgs = [
3042
- "preview",
3018
+ command,
3043
3019
  args.projectPath,
3044
3020
  "--browser",
3045
3021
  browser
3046
3022
  ];
3023
+ if (building && false === args.polyfill) cliArgs.push("--polyfill", "false");
3047
3024
  if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
3048
3025
  if (args.noBrowser) cliArgs.push("--no-browser");
3049
3026
  cliArgs.push(...launchFlagArgs(args));
@@ -3057,7 +3034,7 @@ async function preview_handler(args) {
3057
3034
  pid,
3058
3035
  browser,
3059
3036
  projectPath: args.projectPath,
3060
- command: "preview"
3037
+ command
3061
3038
  });
3062
3039
  child.on("exit", ()=>removeSession(args.projectPath, browser));
3063
3040
  await new Promise((resolve)=>setTimeout(resolve, 5000));
@@ -3073,10 +3050,10 @@ async function preview_handler(args) {
3073
3050
  pid,
3074
3051
  exitCode: code,
3075
3052
  signal,
3076
- error: `The preview process exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). Nothing is running.`,
3053
+ error: `The ${command} process exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running.`,
3077
3054
  output: earlyOutput.slice(0, 2000),
3078
3055
  logPath,
3079
- hint: "Read `output` above for the cause: a missing or broken dist/ (run extension_build first), or a missing browser binary are the common ones."
3056
+ hint: building ? "Read `output` above for the cause: a failed production build, a port already in use, or a missing browser binary are the common ones. extension_build will surface a build error on its own." : "Read `output` above for the cause: a missing or broken dist/ (run extension_build first, or drop build:false), or a missing browser binary are the common ones."
3080
3057
  });
3081
3058
  }
3082
3059
  const exitStamp = browserExitStamp(args.projectPath, browser, spawnedAt);
@@ -3087,7 +3064,7 @@ async function preview_handler(args) {
3087
3064
  browser,
3088
3065
  pid,
3089
3066
  ...exitStamp,
3090
- error: "The preview process is running but the browser it launched has exited (the extension may have been rejected or the browser crashed). The session cannot be driven.",
3067
+ error: `The ${command} process is running but the browser it launched has exited (the extension may have been rejected or the browser crashed). The session cannot be driven.`,
3091
3068
  output: earlyOutput.slice(0, 2000),
3092
3069
  logPath,
3093
3070
  hint: "Read `output` above and extension_logs for the cause, then call extension_stop to clean up before retrying."
@@ -3097,8 +3074,8 @@ async function preview_handler(args) {
3097
3074
  pid,
3098
3075
  browser,
3099
3076
  projectPath: args.projectPath,
3100
- status: "launched",
3101
- hint: "Call extension_stop when you are done to close the preview browser.",
3077
+ status: building ? "started" : "launched",
3078
+ hint: building ? "Use extension_wait to check when the build and browser launch are complete. When you are done, call extension_stop to shut down the session." : "Call extension_stop when you are done to close the preview browser.",
3102
3079
  earlyOutput: earlyOutput.slice(0, 500),
3103
3080
  logPath
3104
3081
  });
@@ -3716,7 +3693,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
3716
3693
  name: "NavigateFailed",
3717
3694
  message: `Navigation to ${url} did not produce a tab reporting that URL. The URL may not exist, or the browser refused the navigation (Firefox rejects privileged about:/chrome: URLs and other extensions' moz-extension: pages).`
3718
3695
  },
3719
- hint: "Confirm the URL, or discover open tabs with extension_dom_inspect listTabs: true. For an extension page, the path must match the BUILT manifest."
3696
+ hint: "Confirm the URL, or discover open tabs with extension_dom_snapshot listTabs: true. For an extension page, the path must match the BUILT manifest."
3720
3697
  });
3721
3698
  return JSON.stringify({
3722
3699
  ok: true,
@@ -3726,7 +3703,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
3726
3703
  url: settled.url,
3727
3704
  title: settled.title
3728
3705
  },
3729
- hint: "Inspect it with extension_dom_inspect or extension_eval using url or this numeric tab id (context: 'page'/'content')."
3706
+ hint: "Inspect it with extension_dom_snapshot or extension_eval using url or this numeric tab id (context: 'page'/'content')."
3730
3707
  });
3731
3708
  }
3732
3709
  async function resolveBridgeBaseUrl(projectPath, browser, timeout) {
@@ -3825,7 +3802,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
3825
3802
  name: "NavigateFailed",
3826
3803
  message: `Navigation to ${url} did not produce a live page target. ${isExtensionPage ? "The URL may not exist in the extension bundle, or Chrome refused the navigation." : "The page may have failed to load, or the browser refused the navigation."}`
3827
3804
  },
3828
- hint: isExtensionPage ? "Confirm the path exists in the built dist (extension_build / extension_inspect list entrypoints). For an extension page, the path must match the BUILT manifest, which may differ from your source layout." : "Confirm the URL loads in a normal browser and that the dev session's browser has network access. Nothing about your extension bundle is implicated in a failed http(s) navigation."
3805
+ hint: isExtensionPage ? "Confirm the path exists in the built dist (extension_build / extension_analyze list entrypoints). For an extension page, the path must match the BUILT manifest, which may differ from your source layout." : "Confirm the URL loads in a normal browser and that the dev session's browser has network access. Nothing about your extension bundle is implicated in a failed http(s) navigation."
3829
3806
  });
3830
3807
  }
3831
3808
  return JSON.stringify({
@@ -3845,7 +3822,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
3845
3822
  title: settled.title,
3846
3823
  url: settled.url
3847
3824
  },
3848
- hint: "Inspect it with extension_dom_inspect or extension_source_inspect using url (context: 'page'), they resolve the tab themselves. `target.targetId` is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. If you need a numeric tab id, call extension_dom_inspect with listTabs: true."
3825
+ hint: "Inspect it with extension_dom_snapshot or extension_inspect using url (context: 'page'), they resolve the tab themselves. `target.targetId` is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. If you need a numeric tab id, call extension_dom_snapshot with listTabs: true."
3849
3826
  });
3850
3827
  } catch (e) {
3851
3828
  return JSON.stringify({
@@ -4086,7 +4063,7 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
4086
4063
  popupBounds = await applyPopupBounds(projectPath, browser, parsed.target.targetId);
4087
4064
  if (popupBounds) parsed.renderedAsTab.popupBounds = popupBounds;
4088
4065
  }
4089
- parsed.hint = `Rendered the ${surface} document in a real tab, which is how you inspect a surface headlessly. ` + (popupBounds ? `The window was resized to the popup's content size (${popupBounds.width}x${popupBounds.height}${popupBounds.clamped ? ", clamped to Chrome's 25x25-800x600 popup bounds" : ""}), approximating real popup rendering. This resizes the WHOLE browser window for the session. It is the same page with the same extension APIs, but window.close() closes the tab. ` : "It is the same page with the same extension APIs, but it is NOT hosted in a popup window: no popup sizing, and window.close() closes the tab. ") + `Inspect it with extension_dom_inspect context: '${surface}' (include: ['html']), or extension_source_inspect with this url. ` + "Do NOT pass this extension-page url to extension_dom_inspect or extension_eval as a tab target: script injection cannot reach extension pages, only the surface context or CDP can.";
4066
+ parsed.hint = `Rendered the ${surface} document in a real tab, which is how you inspect a surface headlessly. ` + (popupBounds ? `The window was resized to the popup's content size (${popupBounds.width}x${popupBounds.height}${popupBounds.clamped ? ", clamped to Chrome's 25x25-800x600 popup bounds" : ""}), approximating real popup rendering. This resizes the WHOLE browser window for the session. It is the same page with the same extension APIs, but window.close() closes the tab. ` : "It is the same page with the same extension APIs, but it is NOT hosted in a popup window: no popup sizing, and window.close() closes the tab. ") + `Inspect it with extension_dom_snapshot context: '${surface}' (include: ['html']), or extension_inspect with this url. ` + "Do NOT pass this extension-page url to extension_dom_snapshot or extension_eval as a tab target: script injection cannot reach extension pages, only the surface context or CDP can.";
4090
4067
  return JSON.stringify(parsed);
4091
4068
  }
4092
4069
  } catch {}
@@ -4127,14 +4104,11 @@ async function confirmSurfaceTarget(projectPath, browser, surface, raw) {
4127
4104
  }
4128
4105
  const open_schema = {
4129
4106
  name: "extension_open",
4130
- description: "Open an extension surface or replay an event in a running session. 'popup'/'options'/'sidebar' open UI surfaces; 'newtab'/'history'/'bookmarks' open the extension's chrome_url_overrides page in a tab. '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 the dev session to be started with allowControl: true (extension_dev). Wraps `extension open`.",
4107
+ description: "Open an extension surface or replay an event in a running session. popup/options/sidebar open UI surfaces; newtab/history/bookmarks open the matching chrome_url_overrides page in a tab. 'action' triggers the toolbar action, opening its popup or replaying chrome.action.onClicked when there is none; 'command' replays a chrome.commands.onCommand 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 reports gesture:false and warns when activeTab is declared). Requires the session to have been started with allowControl: true (extension_dev).",
4131
4108
  inputSchema: {
4132
4109
  type: "object",
4133
4110
  properties: {
4134
- projectPath: {
4135
- type: "string",
4136
- description: "Path to the extension project root (must have an active dev session)"
4137
- },
4111
+ projectPath: SESSION_PROJECT_PATH,
4138
4112
  surface: {
4139
4113
  type: "string",
4140
4114
  enum: [
@@ -4147,7 +4121,7 @@ const open_schema = {
4147
4121
  "action",
4148
4122
  "command"
4149
4123
  ],
4150
- description: "Which surface to open or event to replay. 'newtab'/'history'/'bookmarks' open the matching chrome_url_overrides page. 'action' triggers the toolbar action; 'command' replays a keyboard-shortcut command (requires `name`)."
4124
+ description: "Which surface to open or event to replay."
4151
4125
  },
4152
4126
  name: {
4153
4127
  type: "string",
@@ -4155,21 +4129,15 @@ const open_schema = {
4155
4129
  },
4156
4130
  url: {
4157
4131
  type: "string",
4158
- description: "Navigate a real tab to this URL instead of opening a surface (Chromium via CDP; Firefox via the agent bridge, which needs allowEval: true). Use for content-script/webNavigation test pages, or the popup as a page: chrome-extension://<id>/popup.html. Alternative to `surface`."
4132
+ description: "Navigate a real tab here instead of opening a surface (Firefox needs allowEval: true). Use for content-script test pages, or a surface as a page: chrome-extension://<id>/popup.html."
4159
4133
  },
4160
4134
  asTab: {
4161
4135
  type: "boolean",
4162
4136
  default: false,
4163
- description: "For surface popup/options/sidebar: render the surface's document in a real tab (chrome-extension://<id>/<doc>) instead of opening a real popup window. This is how you inspect a surface HEADLESSLY, where no window exists to host a popup. Applied automatically as a fallback when a headless session refuses to open the surface. Same page and APIs, but no popup sizing and window.close() closes the tab."
4164
- },
4165
- browser: {
4166
- type: "string",
4167
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
4137
+ description: "popup/options/sidebar: render the surface's document in a real tab instead of a popup window. This is how you inspect a surface HEADLESSLY, and it is applied automatically when a headless session refuses to open one. Same page and APIs, but no popup sizing and window.close() closes the tab."
4168
4138
  },
4169
- timeout: {
4170
- type: "number",
4171
- description: "Command timeout in ms (default 5000)"
4172
- }
4139
+ browser: SESSION_BROWSER,
4140
+ timeout: CALL_TIMEOUT
4173
4141
  },
4174
4142
  required: [
4175
4143
  "projectPath"
@@ -4284,7 +4252,8 @@ async function publish(options = {}) {
4284
4252
  method: "POST",
4285
4253
  headers: {
4286
4254
  authorization: `Bearer ${token}`,
4287
- "content-type": "application/json"
4255
+ "content-type": "application/json",
4256
+ ...identityHeaders("extension_publish")
4288
4257
  },
4289
4258
  body: JSON.stringify(body)
4290
4259
  });
@@ -4360,7 +4329,7 @@ async function uploadPreview(options) {
4360
4329
  ok: false,
4361
4330
  error: {
4362
4331
  name: "PreviewAuthError",
4363
- message: "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
4332
+ message: "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
4364
4333
  }
4365
4334
  };
4366
4335
  const apiCheck = safeApiBase(resolveApiBase(options.api));
@@ -4414,7 +4383,8 @@ async function uploadPreview(options) {
4414
4383
  method: "POST",
4415
4384
  headers: {
4416
4385
  authorization: `Bearer ${token}`,
4417
- "content-type": "application/json"
4386
+ "content-type": "application/json",
4387
+ ...identityHeaders("extension_preview_web")
4418
4388
  },
4419
4389
  body: JSON.stringify({
4420
4390
  kind: "dist",
@@ -4573,23 +4543,13 @@ function recordSharedPreview(projectPath, entry) {
4573
4543
  } : {}
4574
4544
  };
4575
4545
  }
4576
- const DEFAULT_INSPECT_URL = "http://localhost:3106";
4577
4546
  const DEFAULT_PREVIEW_DEV_URL = "http://localhost:3110";
4578
- const SURFACES = {
4579
- preview: {
4580
- defaultOrigin: DEFAULT_PREVIEW_DEV_URL,
4581
- scheme: (encoded)=>`preview://build/${encoded}`,
4582
- fetchPath: "/__preview/fetch",
4583
- devCommand: "pnpm --filter preview.extension.dev dev",
4584
- label: "preview.extension.dev"
4585
- },
4586
- inspect: {
4587
- defaultOrigin: DEFAULT_INSPECT_URL,
4588
- scheme: (encoded)=>`inspect://path/${encoded}`,
4589
- fetchPath: "/__inspect/fetch",
4590
- devCommand: "pnpm --filter inspect.extension.dev dev",
4591
- label: "inspect.extension.dev"
4592
- }
4547
+ const SURFACE = {
4548
+ defaultOrigin: DEFAULT_PREVIEW_DEV_URL,
4549
+ scheme: (encoded)=>`preview://build/${encoded}`,
4550
+ fetchPath: "/__preview/fetch",
4551
+ devCommand: "pnpm --filter preview.extension.dev dev",
4552
+ label: "preview.extension.dev"
4593
4553
  };
4594
4554
  async function buildShare(projectPath, distDir, manifest, browser) {
4595
4555
  const result = await uploadPreview({
@@ -4606,7 +4566,7 @@ async function buildShare(projectPath, distDir, manifest, browser) {
4606
4566
  errorName: result.error.name,
4607
4567
  reason: result.error.message,
4608
4568
  ...isAuth ? {
4609
- loginHint: "Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
4569
+ loginHint: "Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
4610
4570
  } : {}
4611
4571
  };
4612
4572
  }
@@ -4674,89 +4634,49 @@ function detectSurfaces(manifest) {
4674
4634
  }
4675
4635
  const preview_web_schema = {
4676
4636
  name: "extension_preview_web",
4677
- description: "Preview an in-progress extension in the web emulator (no real browser). Builds the project (unless build:false), points preview.extension.dev at the dist directory over the dev-only preview://build scheme, and returns a deep link plus a loadability check against its dev server. preview is the author's front door: it renders YOUR build and carries the Emulated/Real lane toggle and the Trace tab. Pass surface:\"inspect\" to render in inspect.extension.dev instead (the evaluator's door, for fixture and forensic work). Pass share:true to UPLOAD the dist you just built and get back a public link (share.previewUrl) that renders those exact bytes for anyone who opens it, with no install and no dev server, and that also serves the whole build as a downloadable zip. Sharing needs a token scoped to an existing extension.dev workspace and project (extension_login or EXTENSION_DEV_TOKEN, valid up to 7 days), so a local folder with no project on the platform cannot share. Use share:true whenever the build has to reach someone who is not at this machine, since the plain deepLink only resolves locally. Shared links do not disappear when this response scrolls away: extension_shares lists every link this token has shared and revokes any of them.",
4637
+ description: "Preview an in-progress extension in the web emulator, with no real browser. Builds the project (unless build:false), points preview.extension.dev at dist/<browser> over the dev-only preview://build scheme, and returns a deep link plus a loadability check. This is the author's door for a LOCAL build: it renders your build and carries the Emulated/Real lane toggle and the Trace tab, but the deep link only resolves on this machine. To reach anyone who is not at this machine, pass share:true and hand out the public link it returns. extension_shares lists and revokes every link shared this way, so one does not vanish with this response.",
4678
4638
  inputSchema: {
4679
4639
  type: "object",
4680
4640
  properties: {
4681
- projectPath: {
4682
- type: "string",
4683
- description: "Path to the extension project root"
4684
- },
4641
+ projectPath: PROJECT_PATH,
4685
4642
  browser: {
4686
4643
  type: "string",
4687
- enum: [
4688
- "chrome",
4689
- "chromium",
4690
- "edge",
4691
- "brave",
4692
- "opera",
4693
- "vivaldi",
4694
- "yandex",
4695
- "firefox",
4696
- "waterfox",
4697
- "librewolf",
4698
- "safari"
4699
- ],
4644
+ enum: REAL_BROWSERS,
4700
4645
  default: "chrome",
4701
- description: "Which dist/<browser> output to preview. inspect renders it in the mocked-Chrome emulator regardless."
4646
+ description: "Which dist/<browser> output to preview. The emulator renders it as mocked Chrome either way."
4702
4647
  },
4703
4648
  build: {
4704
4649
  type: "boolean",
4705
4650
  default: true,
4706
- description: "Build the project before previewing. Set false to preview the existing dist/<browser> as-is."
4651
+ description: "Build first. false previews the existing dist/<browser> as-is."
4707
4652
  },
4708
4653
  distPath: {
4709
4654
  type: "string",
4710
- description: "Preview this built directory directly instead of resolving dist/<browser> under projectPath. Implies build:false."
4711
- },
4712
- surface: {
4713
- type: "string",
4714
- enum: [
4715
- "preview",
4716
- "inspect"
4717
- ],
4718
- default: "preview",
4719
- description: "Which front door renders the build. \"preview\" (default) is preview.extension.dev, the author's door: your own in-progress build, with the Emulated/Real lane toggle and the Trace tab. \"inspect\" is inspect.extension.dev, the evaluator's door."
4655
+ description: "Preview this built directory instead of dist/<browser> under projectPath. Implies build:false."
4720
4656
  },
4721
4657
  hostUrl: {
4722
4658
  type: "string",
4723
- description: "Origin of the running dev server for the chosen surface (defaults: http://localhost:3110 for preview, http://localhost:3106 for inspect)."
4724
- },
4725
- inspectUrl: {
4726
- type: "string",
4727
- description: "Deprecated alias for hostUrl, kept for callers written before the preview surface existed. Only consulted when surface is \"inspect\"."
4659
+ description: "Origin of the running preview.extension.dev dev server (default http://localhost:3110)."
4728
4660
  },
4729
4661
  probe: {
4730
4662
  type: "boolean",
4731
4663
  default: true,
4732
- description: "Confirm the surface can load the artifact by fetching its dev middleware before returning."
4664
+ description: "Fetch the surface's dev middleware first to confirm the artifact loads."
4733
4665
  },
4734
4666
  open: {
4735
4667
  type: "boolean",
4736
4668
  default: false,
4737
- description: "Open the deep link in a running dev session's browser (a new background tab, focus-safe) instead of only returning the link. Requires a live extension_dev/extension_preview session for the project."
4669
+ description: "Also open the deep link in a running session's browser, in a focus-safe background tab. Needs a live extension_dev/extension_start session."
4738
4670
  },
4739
4671
  openIn: {
4740
4672
  type: "string",
4741
- enum: [
4742
- "chrome",
4743
- "chromium",
4744
- "edge",
4745
- "brave",
4746
- "opera",
4747
- "vivaldi",
4748
- "yandex",
4749
- "firefox",
4750
- "waterfox",
4751
- "librewolf",
4752
- "safari"
4753
- ],
4754
- description: "Which running dev session's browser to open the preview in. Defaults to the `browser` value."
4673
+ enum: REAL_BROWSERS,
4674
+ description: "Which session's browser to open it in. Defaults to `browser`."
4755
4675
  },
4756
4676
  share: {
4757
4677
  type: "boolean",
4758
4678
  default: false,
4759
- description: "Upload the built dist and return a public link (share.previewUrl) that renders those exact bytes in the emulator for anyone who opens it: no install, no sign-in, no dev server. The link also serves the whole build as a downloadable zip (share.zipUrl), so sharing it hands over the built code. Needs a token scoped to an existing extension.dev workspace and project (extension_login or EXTENSION_DEV_TOKEN, valid up to 7 days); without one this returns a login hint and never fails the local preview. The link stays live until share.expiresAt, and DELETEing share.revokeUrl with the same token kills it sooner; a revoked link stays dead, and re-sharing the same build returns a new link. Because re-sharing never reproduces the old link, every successful share is also appended to .extension.dev/shared-previews.json in the project (gitignored) so the revoke handle survives losing this response, and extension_shares lists what is actually live on the platform and revokes any of it by id or by URL."
4679
+ description: "Upload the built dist and return a public link (share.previewUrl) that renders those exact bytes for anyone: no install, sign-in or dev server. It also serves the build as a zip (share.zipUrl), so sharing hands over the code. Needs a token scoped to an extension.dev project (extension_auth or EXTENSION_DEV_TOKEN); without one you get a login hint and the local preview still succeeds. Live until share.expiresAt; DELETE share.revokeUrl to kill it sooner. Revocation is permanent and re-sharing mints a different link, so each share is also appended to the project's gitignored .extension.dev/shared-previews.json."
4760
4680
  }
4761
4681
  },
4762
4682
  required: [
@@ -4766,9 +4686,7 @@ const preview_web_schema = {
4766
4686
  };
4767
4687
  async function preview_web_handler(args) {
4768
4688
  const browser = args.browser ?? "chrome";
4769
- const surfaceKey = "inspect" === args.surface ? "inspect" : "preview";
4770
- const surface = SURFACES[surfaceKey];
4771
- const hostBase = (args.hostUrl ?? ("inspect" === surfaceKey ? args.inspectUrl : void 0) ?? surface.defaultOrigin).replace(/\/+$/, "");
4689
+ const hostBase = (args.hostUrl ?? SURFACE.defaultOrigin).replace(/\/+$/, "");
4772
4690
  const shouldBuild = args.distPath ? false : false !== args.build;
4773
4691
  let buildResult = null;
4774
4692
  if (shouldBuild) {
@@ -4811,12 +4729,11 @@ async function preview_web_handler(args) {
4811
4729
  });
4812
4730
  }
4813
4731
  const encoded = Buffer.from(distDir).toString("base64url");
4814
- const internalUrl = surface.scheme(encoded);
4732
+ const internalUrl = SURFACE.scheme(encoded);
4815
4733
  const deepLink = `${hostBase}/?url=${encodeURIComponent(internalUrl)}`;
4816
4734
  const result = {
4817
4735
  ok: true,
4818
4736
  deepLink,
4819
- surface: surfaceKey,
4820
4737
  distDir,
4821
4738
  manifest: {
4822
4739
  name: manifest.name ?? node_path.basename(distDir),
@@ -4829,7 +4746,7 @@ async function preview_web_handler(args) {
4829
4746
  } : {
4830
4747
  built: false
4831
4748
  },
4832
- hint: `Open deepLink in a browser to see the extension render in ${surface.label}'s emulator. It must be running (${surface.devCommand}).${"preview" === surfaceKey ? " Once it renders, the Trace tab shows every chrome.* call it makes, and the lane toggle switches between the emulated backend and a real carrier-equipped browser." : ""}`
4749
+ hint: `Open deepLink in a browser to see the extension render in ${SURFACE.label}'s emulator. It must be running (${SURFACE.devCommand}). Once it renders, the Trace tab shows every chrome.* call it makes, and the lane toggle switches between the emulated backend and a real carrier-equipped browser.`
4833
4750
  };
4834
4751
  if (args.open) {
4835
4752
  const sessionBrowser = args.openIn ?? browser;
@@ -4849,7 +4766,7 @@ async function preview_web_handler(args) {
4849
4766
  }
4850
4767
  if (args.share) result.share = await buildShare(args.projectPath, distDir, manifest, browser);
4851
4768
  if (false === args.probe) return JSON.stringify(result);
4852
- const probeUrl = `${hostBase}${surface.fetchPath}?url=${encodeURIComponent(internalUrl)}`;
4769
+ const probeUrl = `${hostBase}${SURFACE.fetchPath}?url=${encodeURIComponent(internalUrl)}`;
4853
4770
  try {
4854
4771
  const res = await fetch(probeUrl, {
4855
4772
  headers: {
@@ -4864,7 +4781,7 @@ async function preview_web_handler(args) {
4864
4781
  probe: {
4865
4782
  status: res.status,
4866
4783
  contentType,
4867
- note: `${surface.label} answered but not with a preview payload. On the deployed host ${surface.fetchPath} does not exist (dev-only); run a local dev server (${surface.devCommand}) to use web preview.`
4784
+ note: `${SURFACE.label} answered but not with a preview payload. On the deployed host ${SURFACE.fetchPath} does not exist (dev-only); run a local dev server (${SURFACE.devCommand}) to use web preview.`
4868
4785
  }
4869
4786
  });
4870
4787
  const payload = await res.json();
@@ -4886,7 +4803,7 @@ async function preview_web_handler(args) {
4886
4803
  previewLoadable: false,
4887
4804
  probe: {
4888
4805
  error: err instanceof Error ? err.message : String(err),
4889
- note: `Could not reach ${surface.label} at ${hostBase}. Start it with '${surface.devCommand}', then open deepLink.`
4806
+ note: `Could not reach ${SURFACE.label} at ${hostBase}. Start it with '${SURFACE.devCommand}', then open deepLink.`
4890
4807
  }
4891
4808
  });
4892
4809
  }
@@ -4920,7 +4837,7 @@ function authError(name) {
4920
4837
  ok: false,
4921
4838
  error: {
4922
4839
  name,
4923
- message: "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
4840
+ message: "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
4924
4841
  }
4925
4842
  };
4926
4843
  }
@@ -4954,7 +4871,8 @@ async function listArtifacts(options = {}) {
4954
4871
  res = await doFetch(url.toString(), {
4955
4872
  headers: {
4956
4873
  authorization: `Bearer ${token}`,
4957
- accept: "application/json"
4874
+ accept: "application/json",
4875
+ ...identityHeaders("extension_shares")
4958
4876
  }
4959
4877
  });
4960
4878
  } catch (err) {
@@ -5009,7 +4927,8 @@ async function revokeArtifact(options) {
5009
4927
  method: "DELETE",
5010
4928
  headers: {
5011
4929
  authorization: `Bearer ${token}`,
5012
- accept: "application/json"
4930
+ accept: "application/json",
4931
+ ...identityHeaders("extension_shares")
5013
4932
  }
5014
4933
  });
5015
4934
  } catch (err) {
@@ -5050,10 +4969,10 @@ async function revokeArtifact(options) {
5050
4969
  }
5051
4970
  };
5052
4971
  }
5053
- const LOGIN_HINT = "Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).";
4972
+ const LOGIN_HINT = "Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).";
5054
4973
  const shares_schema = {
5055
4974
  name: "extension_shares",
5056
- description: "List and revoke the public preview links this token has shared (the links extension_preview_web share:true hands out). action:\"list\" (default) asks the platform for every artifact owned by the logged-in project and returns each one's artifactId, name, version, live/dead state, createdAt, expiresAt, revokedAt, size, and its previewUrl, zipUrl and revokeUrl, so a link you lost the response for is findable again. Every row also carries owner and sharedBy as the platform returned them, plus an attribution block: attribution.ownership is \"project\" when the owning workspace holds the share and any member can revoke it, \"personal\" when one person holds it alone and nobody else can, and \"unknown\" when the platform disclosed no owner. attribution.credit names the publisher for credit only and never decides access; it reads \"CLI token <id>\" when the token's issuer could not be resolved and \"not recorded\" for a share made before attribution existed, and neither is a person's name. action:\"revoke\" kills one by artifactId or by pasting any of its URLs; revocation is PERMANENT (the id is burned and re-sharing mints a different link), so it cannot be undone. Pass projectPath to reconcile the platform's answer with .extension.dev/shared-previews.json, the project's own append-only record: a share made on another machine shows up as remoteOnly, and a record with no live artifact behind it shows up under localOnly. Needs the same token as sharing (extension_login or EXTENSION_DEV_TOKEN); without one, listing still returns the local record with a login hint instead of failing. Read-only for the local file: this tool never rewrites shared-previews.json.",
4975
+ description: "List and revoke the public preview links this token has shared (what extension_preview_web share:true hands out). action:'list' (default) returns every artifact the logged-in project owns with its artifactId, name, version, live/dead state, createdAt, expiresAt, revokedAt, size, previewUrl, zipUrl and revokeUrl, so a link whose response you lost is findable again. Each row carries owner and sharedBy as the platform returned them plus an attribution block: attribution.ownership is 'project' when the workspace holds the share and any member can revoke it, 'personal' when one person holds it alone, 'unknown' when no owner was disclosed; attribution.credit names the publisher for credit only, never access, and reads 'CLI token <id>' or 'not recorded' when no person can be named. action:'revoke' kills one by artifactId or by pasting any of its URLs, and is PERMANENT. Pass projectPath to reconcile against the project's own append-only .extension.dev/shared-previews.json (read-only, never rewritten): a share made on another machine shows as remoteOnly, a record with no live artifact as localOnly. Needs the same token as sharing (extension_auth or EXTENSION_DEV_TOKEN); without one, listing still returns the local record with a login hint.",
5057
4976
  inputSchema: {
5058
4977
  type: "object",
5059
4978
  properties: {
@@ -5091,10 +5010,7 @@ const shares_schema = {
5091
5010
  type: "number",
5092
5011
  description: "How many shares to return, 1 to 200 (platform default 100). A cut list comes back with truncated:true."
5093
5012
  },
5094
- api: {
5095
- type: "string",
5096
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)."
5097
- }
5013
+ api: API_BASE
5098
5014
  },
5099
5015
  required: []
5100
5016
  }
@@ -5343,73 +5259,6 @@ async function shares_handler(args) {
5343
5259
  } : {}
5344
5260
  });
5345
5261
  }
5346
- const get_template_source_schema = {
5347
- name: "extension_get_template_source",
5348
- description: "Read source files from a template in the extension.dev template catalog. Use this to learn implementation patterns before building something similar.",
5349
- inputSchema: {
5350
- type: "object",
5351
- properties: {
5352
- slug: {
5353
- type: "string",
5354
- description: "Template slug (e.g. 'ai-claude', 'content-react')"
5355
- },
5356
- files: {
5357
- type: "array",
5358
- items: {
5359
- type: "string"
5360
- },
5361
- description: "Specific files to read (e.g. ['src/manifest.json', 'src/background.ts']). If omitted, returns the file listing from templates-meta.json."
5362
- }
5363
- },
5364
- required: [
5365
- "slug"
5366
- ]
5367
- }
5368
- };
5369
- async function get_template_source_handler(args) {
5370
- const template = await getTemplateBySlug(args.slug);
5371
- if (!template) return JSON.stringify({
5372
- error: `Template '${args.slug}' not found in the catalog`,
5373
- hint: "Use extension_list_templates to see available templates."
5374
- });
5375
- const meta = {
5376
- slug: template.slug,
5377
- description: template.description,
5378
- uiFramework: template.uiFramework || "vanilla",
5379
- surfaces: template.surfaces,
5380
- permissions: template.permissions,
5381
- patternExplanation: template.patternExplanation,
5382
- keyFiles: template.keyFiles,
5383
- repositoryUrl: template.repositoryUrl
5384
- };
5385
- if (!args.files?.length) return JSON.stringify({
5386
- ...meta,
5387
- files: template.files.map((f)=>stripTemplatePathPrefix(template.slug, f)),
5388
- hint: "Pass specific file paths in the files parameter to read their contents."
5389
- });
5390
- const fileContents = {};
5391
- const errors = [];
5392
- await Promise.all(args.files.map(async (filePath)=>{
5393
- const urls = await templateFileUrls(args.slug, filePath);
5394
- let lastStatus = 0;
5395
- for (const url of urls)try {
5396
- const response = await fetch(url);
5397
- if (response.ok) {
5398
- fileContents[filePath] = await response.text();
5399
- return;
5400
- }
5401
- lastStatus = response.status;
5402
- } catch {}
5403
- errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
5404
- }));
5405
- return JSON.stringify({
5406
- ...meta,
5407
- fileContents,
5408
- ...errors.length ? {
5409
- errors
5410
- } : {}
5411
- });
5412
- }
5413
5262
  const CHROME_DESKTOP_ONLY_KEYS = [
5414
5263
  "file_browser_handlers",
5415
5264
  "file_system_provider_capabilities",
@@ -6457,7 +6306,7 @@ function resolveChromeTheme(theme) {
6457
6306
  }
6458
6307
  const theme_verify_schema = {
6459
6308
  name: "extension_theme_verify",
6460
- description: "Verify a Chrome theme manifest before it ships. Settles the four-leg WYSIWYG contract (app-shows == manifest-says == chrome-paints, plus chrome-accepts) as far as is possible headless: it derives every color current Chrome would paint from the manifest (the transcribed Chromium resolver) and reports the divergence class of any problem - D1 fabrication, D3 parity gap, D4 acceptance gap (keys Chrome silently discards: dead legacy keys, incognito keys, unknown keys, out-of-range values). Verification is the product; this verb does not author or mutate a theme. The app-rendered leg [1] and the real-pixel paint leg [3] need a browser and are returned as needsAttended with a pointer to the assert:theme and install-parity harnesses, never reported as passed.",
6309
+ description: "Verify a Chrome theme manifest before it ships. Settles the four-leg WYSIWYG contract (app-shows == manifest-says == chrome-paints, plus chrome-accepts) as far as is possible headless: it derives every color current Chrome would paint from the manifest (the transcribed Chromium resolver) and classifies any problem as D1 fabrication, D3 parity gap, or D4 acceptance gap (keys Chrome silently discards: dead legacy, incognito, unknown, out-of-range). Verification only: it never authors or mutates a theme. The app-rendered and real-pixel legs need a browser and come back as needsAttended pointing at the assert:theme and install-parity harnesses, never as passed.",
6461
6310
  inputSchema: {
6462
6311
  type: "object",
6463
6312
  properties: {
@@ -6720,20 +6569,17 @@ async function theme_verify_handler(args) {
6720
6569
  attended
6721
6570
  });
6722
6571
  }
6723
- const inspect_schema = {
6724
- name: "extension_inspect",
6725
- description: "Inspect a built extension: file sizes, entry points, permissions used, and structure analysis. The extension must be built first.",
6572
+ const analyze_schema = {
6573
+ name: "extension_analyze",
6574
+ description: "Analyze a BUILT extension on disk: file sizes, declared entry points, permissions, bundle composition, and store-readiness checks. Static only, reads dist/<browser> from the filesystem and never touches a browser. The extension must be built first (extension_build). For a RUNNING extension's live DOM and console use extension_inspect.",
6726
6575
  inputSchema: {
6727
6576
  type: "object",
6728
6577
  properties: {
6729
- projectPath: {
6730
- type: "string",
6731
- description: "Path to the extension project root"
6732
- },
6578
+ projectPath: PROJECT_PATH,
6733
6579
  browser: {
6734
6580
  type: "string",
6735
6581
  default: "chrome",
6736
- description: "Browser build to inspect"
6582
+ description: "Browser build to analyze"
6737
6583
  },
6738
6584
  format: {
6739
6585
  type: "string",
@@ -6814,7 +6660,7 @@ function formatBytes(bytes) {
6814
6660
  if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
6815
6661
  return `${(bytes / 1048576).toFixed(1)} MB`;
6816
6662
  }
6817
- async function inspect_handler(args) {
6663
+ async function analyze_handler(args) {
6818
6664
  const browser = args.browser ?? "chrome";
6819
6665
  const distPath = node_path.resolve(args.projectPath, "dist", browser);
6820
6666
  if (!node_fs.existsSync(distPath)) return JSON.stringify({
@@ -7392,26 +7238,23 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
7392
7238
  if (notes.length) result.notes = notes;
7393
7239
  return JSON.stringify(result);
7394
7240
  }
7395
- const source_inspect_schema = {
7396
- name: "extension_source_inspect",
7397
- description: "Inspect a running extension's live state: full HTML (with shadow DOM), DOM structure, content script injection, console messages, and CSS selector queries. Chromium sessions ride Chrome DevTools Protocol. Firefox sessions are fully paired: summary/meta/html/dom_snapshot/extension_roots/probes ride the agent bridge (needs allowEval: true), console rides the RDP watcher replay (engine 4.0.15+), and deepDom walks closed shadow roots via a content-script eval (MV2 sessions with host permissions for the target url; Firefox MV3 background CSP blocks bridge evals entirely). Requires an active dev or start session.",
7241
+ const inspect_schema = {
7242
+ name: "extension_inspect",
7243
+ description: "DEEP inspection of a RUNNING extension over the browser's debugger protocol: full HTML including shadow DOM, DOM structure, content-script injection, console messages, and CSS selector queries (`probe`). The ONLY tool that pierces CLOSED shadow roots (`deepDom`), runs selector probes, and NAVIGATES a tab to `url` before reading it. It reads a web or override page and picks the first inspectable target (or the first whose url contains `url`); it cannot address an extension surface by name and takes no chrome.tabs id. To choose WHICH tab or which OPEN surface (popup/options/sidebar/devtools) to read, or to enumerate what is open, use extension_dom_snapshot. For a built extension's files and sizes on disk use extension_analyze. Chromium rides Chrome DevTools Protocol (needs the session's debug port, not allowControl). Firefox is fully paired: summary/meta/html/dom_snapshot/extension_roots/probes ride the agent bridge (needs allowEval: true), console rides the RDP watcher replay (engine 4.0.15+), and deepDom needs an MV2 session with host permissions for the target url (Firefox MV3 background CSP blocks bridge evals). Requires an active dev or start session.",
7398
7244
  inputSchema: {
7399
7245
  type: "object",
7400
7246
  properties: {
7401
- projectPath: {
7402
- type: "string",
7403
- description: "Path to the extension project root (must have an active dev session)"
7404
- },
7247
+ projectPath: SESSION_PROJECT_PATH,
7405
7248
  url: {
7406
7249
  type: "string",
7407
- description: "URL to inspect (navigates the browser tab to this URL first)"
7250
+ description: "URL to inspect; the tab is navigated there first"
7408
7251
  },
7409
7252
  probe: {
7410
7253
  type: "array",
7411
7254
  items: {
7412
7255
  type: "string"
7413
7256
  },
7414
- description: "CSS selectors to query, returns element counts and samples for each"
7257
+ description: "CSS selectors to query; returns counts and samples for each"
7415
7258
  },
7416
7259
  include: {
7417
7260
  type: "array",
@@ -7431,12 +7274,9 @@ const source_inspect_schema = {
7431
7274
  "meta",
7432
7275
  "console"
7433
7276
  ],
7434
- description: "What data to include in the response"
7435
- },
7436
- browser: {
7437
- type: "string",
7438
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
7277
+ description: "What to include"
7439
7278
  },
7279
+ browser: SESSION_BROWSER,
7440
7280
  maxBytes: {
7441
7281
  type: "number",
7442
7282
  default: 262144,
@@ -7445,7 +7285,7 @@ const source_inspect_schema = {
7445
7285
  deepDom: {
7446
7286
  type: "boolean",
7447
7287
  default: false,
7448
- description: "Pierce CLOSED shadow roots. The default path reads open shadow roots; closed ones need this escape hatch. Chromium: CDP DOM pierce. Firefox: a content-script walk via tabs.executeScript (MV2 sessions; the extension needs host permissions for the target url)."
7288
+ description: "Pierce CLOSED shadow roots; open ones are read anyway. Chromium: CDP DOM pierce. Firefox: a content-script walk via tabs.executeScript (MV2 only, needs host permissions for the target url)."
7449
7289
  }
7450
7290
  },
7451
7291
  required: [
@@ -7453,7 +7293,7 @@ const source_inspect_schema = {
7453
7293
  ]
7454
7294
  }
7455
7295
  };
7456
- async function source_inspect_handler(args) {
7296
+ async function inspect_handler(args) {
7457
7297
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
7458
7298
  const include = new Set(args.include ?? [
7459
7299
  "summary",
@@ -7565,18 +7405,12 @@ async function source_inspect_handler(args) {
7565
7405
  }
7566
7406
  const list_extensions_schema = {
7567
7407
  name: "extension_list_extensions",
7568
- description: "List the extensions in the running dev browser. Returns each extension's id, name, version, and (on Chromium) live contexts. The entry for THIS dev session's extension (the project being served) is flagged ownExtension: true, with name and version resolved from the session's ready contract even when the browser exposes no identity. On Chromium this rides the Chrome DevTools Protocol: entries are extensions with at least one live context (a dormant MV3 service worker with no open page may be absent until it wakes), and other extensions resolve via the read-only Extensions domain when available. On Firefox this rides the Remote Debugging Protocol root actor (listAddons, engine 4.0.15+): entries are INSTALLED add-ons regardless of live contexts, with temporarilyInstalled marking temporary loads, and carry no contexts. Either way other extensions' contexts are never attached to or evaluated in. Requires an active dev or start session.",
7408
+ description: "List the extensions in the running dev browser: id, name, version, and (on Chromium) live contexts. THIS session's own extension is flagged ownExtension: true, with name and version from the ready contract even when the browser exposes no identity. Chromium rides the Chrome DevTools Protocol, so an entry needs at least one live context (a dormant MV3 service worker may be absent until it wakes). Firefox rides the RDP root actor (listAddons, engine 4.0.15+), so entries are INSTALLED add-ons regardless of contexts, marked temporarilyInstalled where relevant, and carry none. Either way other extensions' contexts are never attached to or evaluated in. Requires an active dev or start session.",
7569
7409
  inputSchema: {
7570
7410
  type: "object",
7571
7411
  properties: {
7572
- projectPath: {
7573
- type: "string",
7574
- description: "Path to the extension project root (must have an active dev session)"
7575
- },
7576
- browser: {
7577
- type: "string",
7578
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
7579
- }
7412
+ projectPath: SESSION_PROJECT_PATH,
7413
+ browser: SESSION_BROWSER
7580
7414
  },
7581
7415
  required: [
7582
7416
  "projectPath"
@@ -7842,13 +7676,10 @@ const logs_schema_schema = {
7842
7676
  inputSchema: {
7843
7677
  type: "object",
7844
7678
  properties: {
7845
- projectPath: {
7846
- type: "string",
7847
- description: "Path to the extension project root (must have an active dev session)"
7848
- },
7679
+ projectPath: SESSION_PROJECT_PATH,
7849
7680
  browser: {
7850
7681
  type: "string",
7851
- description: "Which dist/extension-js/<browser>/ to read. Defaults to the active dev session's browser for this project (falls back to chromium)."
7682
+ description: "Which dist/extension-js/<browser>/ to read. Defaults to this project's live session, else chromium."
7852
7683
  },
7853
7684
  level: {
7854
7685
  type: "string",
@@ -7862,7 +7693,7 @@ const logs_schema_schema = {
7862
7693
  "all"
7863
7694
  ],
7864
7695
  default: "all",
7865
- description: "Minimum severity to include; selecting a level includes it plus everything more severe."
7696
+ description: "Minimum severity; a level includes everything more severe."
7866
7697
  },
7867
7698
  context: {
7868
7699
  type: "array",
@@ -7883,15 +7714,15 @@ const logs_schema_schema = {
7883
7714
  signalsOnly: {
7884
7715
  type: "boolean",
7885
7716
  default: false,
7886
- description: "Only structured dx.signal diagnostics (which carry code/status/remediation), skipping plain console lines."
7717
+ description: "Only structured dx.signal diagnostics (code/status/remediation), no plain console lines."
7887
7718
  },
7888
7719
  since: {
7889
7720
  type: "number",
7890
- description: "Only return events with seq greater than this (cursor for polling forward)."
7721
+ description: "Only events with seq greater than this; the cursor for polling forward."
7891
7722
  },
7892
7723
  url: {
7893
7724
  type: "string",
7894
- description: "Only events whose url/hostname matches (glob with * or plain substring), e.g. https://shop.example/*."
7725
+ description: "Only events whose url/hostname matches (glob or substring), e.g. https://shop.example/*."
7895
7726
  },
7896
7727
  tab: {
7897
7728
  type: "number",
@@ -7900,7 +7731,7 @@ const logs_schema_schema = {
7900
7731
  follow: {
7901
7732
  type: "boolean",
7902
7733
  default: false,
7903
- description: "Collect from the live control channel for a bounded window instead of reading the file. Use with followMs."
7734
+ description: "Collect from the live control channel for a bounded window instead of reading the file."
7904
7735
  },
7905
7736
  followMs: {
7906
7737
  type: "number",
@@ -7910,7 +7741,7 @@ const logs_schema_schema = {
7910
7741
  limit: {
7911
7742
  type: "number",
7912
7743
  default: 200,
7913
- description: "Maximum number of (most recent) events to return."
7744
+ description: "How many of the most recent events to return."
7914
7745
  }
7915
7746
  },
7916
7747
  required: [
@@ -8109,14 +7940,11 @@ async function logs_handler(args) {
8109
7940
  }
8110
7941
  const eval_schema = {
8111
7942
  name: "extension_eval",
8112
- description: "Evaluate an expression in a running extension context. Requires the dev session to be started with allowEval: true (extension_dev; writes a 0600 session token the CLI reads). Context default: on a Chromium session whose manifest is MV3 (the default template) the default is `page` (the active tab), because the MV3 background is a service worker whose CSP blocks eval, so a background default would fail on the most common path; on Firefox/MV2 sessions the default stays `background`. Pass `context: \"background\"` explicitly to target the worker anyway (on Chromium MV3 that returns the CSP explanation). Targeting for context content/page: pass `url` to pick the matching tab, or omit both `url` and `tab` to use the ACTIVE tab; a numeric `tab` id is only needed to disambiguate. Extension surfaces (popup/options/sidebar/devtools) and override pages (newtab/history/bookmarks) evaluate over the in-bundle relay and need NO tab id; the surface must be OPEN (extension_open first; a closed surface returns an explicit error). Use extension_dom_inspect with listTabs: true to enumerate {tabId,url,title}. Wraps `extension eval`.",
7943
+ description: "Evaluate an expression in a running extension context. Requires the session to have been started with allowEval: true (extension_dev; writes a 0600 session token). Context defaults to `background`, EXCEPT on a Chromium MV3 session (the default template) where it is `page`, the active tab, because the MV3 service worker CSP blocks eval; pass context:'background' to target the worker anyway and get that explanation back. For content/page, pass `url` to pick the tab or omit both `url` and `tab` for the ACTIVE tab; a numeric `tab` only disambiguates. Extension surfaces (popup/options/sidebar/devtools) and override pages evaluate over the in-bundle relay and need NO tab id, but must be OPEN (extension_open first; a closed one returns an explicit error). extension_dom_snapshot with listTabs: true enumerates {tabId,url,title}.",
8113
7944
  inputSchema: {
8114
7945
  type: "object",
8115
7946
  properties: {
8116
- projectPath: {
8117
- type: "string",
8118
- description: "Path to the extension project root (must have an active dev session)"
8119
- },
7947
+ projectPath: SESSION_PROJECT_PATH,
8120
7948
  expression: {
8121
7949
  type: "string",
8122
7950
  description: "JavaScript expression to evaluate in the target context"
@@ -8135,24 +7963,18 @@ const eval_schema = {
8135
7963
  "content",
8136
7964
  "page"
8137
7965
  ],
8138
- description: "Which extension surface to evaluate in. Default: `background`, EXCEPT on Chromium sessions whose manifest is MV3, where the default is `page` (the active tab) because the MV3 service worker CSP blocks eval; pass `context: \"background\"` explicitly to target the worker anyway."
7966
+ description: "Where to evaluate. Default background, except Chromium MV3 sessions default to page (the active tab)."
8139
7967
  },
8140
7968
  url: {
8141
7969
  type: "string",
8142
- description: "For content/page: selects the target tab by url (match pattern, then substring fallback). Preferred over `tab`. You do not need a numeric id."
7970
+ description: "content/page: pick the tab by url (match pattern, then substring). Preferred over `tab`."
8143
7971
  },
8144
7972
  tab: {
8145
7973
  type: "number",
8146
- description: "Numeric chrome.tabs id, for disambiguating when several tabs match. Optional: with neither `tab` nor `url`, content/page target the active tab."
8147
- },
8148
- browser: {
8149
- type: "string",
8150
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
7974
+ description: "Numeric chrome.tabs id, only to disambiguate when several tabs match."
8151
7975
  },
8152
- timeout: {
8153
- type: "number",
8154
- description: "Command timeout in ms (default 5000)"
8155
- }
7976
+ browser: SESSION_BROWSER,
7977
+ timeout: CALL_TIMEOUT
8156
7978
  },
8157
7979
  required: [
8158
7980
  "projectPath",
@@ -8207,7 +8029,7 @@ async function eval_handler(args) {
8207
8029
  if (parsed && "object" == typeof parsed) {
8208
8030
  parsed.defaultedContext = "page";
8209
8031
  parsed.contextNote = 'No context given: defaulted to "page" (the active tab) because this Chromium session\'s MV3 background is a service worker whose CSP blocks eval. Pass context: "background" explicitly to target the worker (works on Firefox/MV2 builds).';
8210
- if (false === parsed.ok && /cannot access|chrome-extension:\/\/|chrome:\/\//i.test(JSON.stringify(parsed.error ?? ""))) parsed.hint = "The active tab is a browser or extension page that eval cannot reach. Navigate the dev browser to a regular web page, or pass url (match pattern) or tab to pick one; extension_dom_inspect with listTabs: true lists open tabs.";
8032
+ if (false === parsed.ok && /cannot access|chrome-extension:\/\/|chrome:\/\//i.test(JSON.stringify(parsed.error ?? ""))) parsed.hint = "The active tab is a browser or extension page that eval cannot reach. Navigate the dev browser to a regular web page, or pass url (match pattern) or tab to pick one; extension_dom_snapshot with listTabs: true lists open tabs.";
8211
8033
  return JSON.stringify(parsed);
8212
8034
  }
8213
8035
  } catch {}
@@ -8215,14 +8037,11 @@ async function eval_handler(args) {
8215
8037
  }
8216
8038
  const storage_schema = {
8217
8039
  name: "extension_storage",
8218
- description: "Read or write chrome.storage in a running extension. Requires the dev session to be started with allowControl: true (extension_dev). Wraps `extension storage get|set`.",
8040
+ description: "Read or write chrome.storage in a running extension. Requires the session to have been started with allowControl: true (extension_dev).",
8219
8041
  inputSchema: {
8220
8042
  type: "object",
8221
8043
  properties: {
8222
- projectPath: {
8223
- type: "string",
8224
- description: "Path to the extension project root (must have an active dev session)"
8225
- },
8044
+ projectPath: SESSION_PROJECT_PATH,
8226
8045
  action: {
8227
8046
  type: "string",
8228
8047
  enum: [
@@ -8259,14 +8078,8 @@ const storage_schema = {
8259
8078
  ],
8260
8079
  default: "background"
8261
8080
  },
8262
- browser: {
8263
- type: "string",
8264
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
8265
- },
8266
- timeout: {
8267
- type: "number",
8268
- description: "Command timeout in ms (default 5000)"
8269
- }
8081
+ browser: SESSION_BROWSER,
8082
+ timeout: CALL_TIMEOUT
8270
8083
  },
8271
8084
  required: [
8272
8085
  "projectPath",
@@ -8307,14 +8120,11 @@ async function storage_handler(args) {
8307
8120
  }
8308
8121
  const reload_schema = {
8309
8122
  name: "extension_reload",
8310
- description: "Reload a running extension (background) or a tab. Requires the dev session to be started with allowControl: true (extension_dev). Wraps `extension reload`.",
8123
+ description: "Reload a running extension (background) or a tab. Requires the session to have been started with allowControl: true (extension_dev).",
8311
8124
  inputSchema: {
8312
8125
  type: "object",
8313
8126
  properties: {
8314
- projectPath: {
8315
- type: "string",
8316
- description: "Path to the extension project root (must have an active dev session)"
8317
- },
8127
+ projectPath: SESSION_PROJECT_PATH,
8318
8128
  context: {
8319
8129
  type: "string",
8320
8130
  enum: [
@@ -8328,14 +8138,8 @@ const reload_schema = {
8328
8138
  type: "number",
8329
8139
  description: "For content/page: a specific tab id"
8330
8140
  },
8331
- browser: {
8332
- type: "string",
8333
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
8334
- },
8335
- timeout: {
8336
- type: "number",
8337
- description: "Command timeout in ms (default 5000)"
8338
- }
8141
+ browser: SESSION_BROWSER,
8142
+ timeout: CALL_TIMEOUT
8339
8143
  },
8340
8144
  required: [
8341
8145
  "projectPath"
@@ -8353,7 +8157,7 @@ async function reload_handler(args) {
8353
8157
  })
8354
8158
  ], args.projectPath, args.timeout);
8355
8159
  }
8356
- const TARGET_ID_NOTE = "targetId is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_inspect with listTabs: true.";
8160
+ const TARGET_ID_NOTE = "targetId is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_snapshot with listTabs: true.";
8357
8161
  function filterPageTargets(raw) {
8358
8162
  return raw.filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
8359
8163
  targetId: String(t.id),
@@ -8371,38 +8175,35 @@ function matchTargetsByUrl(targets, needle) {
8371
8175
  if (byUrl.length > 0) return byUrl;
8372
8176
  return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
8373
8177
  }
8374
- const RDP_ACTOR_NOTE = "actor is an RDP tab descriptor actor id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_inspect with listTabs: true.";
8375
- const dom_inspect_schema = {
8376
- name: "extension_dom_inspect",
8377
- description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Target a tab by `tabUrl` (case-insensitive URL substring resolved against the browser's live page targets; zero or several matches return the candidates instead of guessing), by `url`, or by numeric `tab`. Discover what is open with listTargets: true (Chromium CDP targetIds; Firefox RDP tab actors) or listTabs: true (numeric chrome.tabs ids). Requires the dev session to be started with allowControl: true (extension_dev). For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
8178
+ const RDP_ACTOR_NOTE = "actor is an RDP tab descriptor actor id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_snapshot with listTabs: true.";
8179
+ const dom_snapshot_schema = {
8180
+ name: "extension_dom_snapshot",
8181
+ description: "Take a SHALLOW structured DOM snapshot of ONE CHOSEN surface through the agent bridge (CDP-free, localhost): element counts, extension roots, OPEN shadow roots, optional byte-capped HTML, optional recent console lines. This is the SURFACE PICKER: the only tool that reads an OPEN extension surface by name (`context`: popup/options/sidebar/devtools) or an override page, the only one that takes a numeric chrome.tabs id, and the only one that enumerates what is open (listTargets for CDP targetIds and RDP tab actors, listTabs for numeric tab ids). An ambiguous `tabUrl` returns the candidates instead of guessing. It does NOT pierce closed shadow roots, run selector probes, or navigate: for those, and for a deep read of an already-open web page, use extension_inspect. Requires the session to have been started with allowControl: true (extension_dev).",
8378
8182
  inputSchema: {
8379
8183
  type: "object",
8380
8184
  properties: {
8381
- projectPath: {
8382
- type: "string",
8383
- description: "Path to the extension project root (must have an active dev session)"
8384
- },
8185
+ projectPath: SESSION_PROJECT_PATH,
8385
8186
  tab: {
8386
8187
  type: "number",
8387
- description: "Numeric chrome.tabs id, for disambiguating when several tabs match. Optional: with neither `tab` nor `url`, content/page target the active tab."
8188
+ description: "Numeric chrome.tabs id, only to disambiguate when several tabs match. With neither `tab` nor `url`, content/page target the active tab."
8388
8189
  },
8389
8190
  url: {
8390
8191
  type: "string",
8391
- description: "For content/page: selects the target tab by url (match pattern, then substring fallback). Preferred over `tab`."
8192
+ description: "content/page: pick the tab by url (match pattern, then substring). Preferred over `tab`."
8392
8193
  },
8393
8194
  tabUrl: {
8394
8195
  type: "string",
8395
- description: "Target the tab whose URL contains this substring (case-insensitive; titles are checked only when no url matches). Resolved against the live browser BEFORE inspecting (Chromium: CDP page targets; Firefox: the agent bridge tab list): exactly one match proceeds; zero or several matches return the candidates so you can narrow, never a guess. Alternative to `url`."
8196
+ description: "Target the tab whose URL contains this substring (case-insensitive; titles checked only when no url matches). Resolved against the live browser first: exactly one match proceeds, zero or several return the candidates instead of a guess. Alternative to `url`."
8396
8197
  },
8397
8198
  listTargets: {
8398
8199
  type: "boolean",
8399
8200
  default: false,
8400
- description: "Enumerate the browser's live page targets and return, ignoring the other args. The discovery path for `tabUrl`. Chromium: CDP page targets as {targetId,url,title,type}. Firefox: RDP tab descriptors as {actor,url,title,type} (engine 4.0.15+). Neither id is a numeric chrome.tabs id (for those use listTabs)."
8201
+ description: "Enumerate live page targets and return, ignoring the other args. The discovery path for `tabUrl`. Chromium: {targetId,url,title,type}. Firefox: RDP tab descriptors {actor,url,title,type}. Neither id is a numeric chrome.tabs id; for those use listTabs."
8401
8202
  },
8402
8203
  listTabs: {
8403
8204
  type: "boolean",
8404
8205
  default: false,
8405
- description: "Enumerate open tabs as {tabId,url,title} and return, ignoring the other args. The discovery path when you need an explicit numeric tab id."
8206
+ description: "Enumerate open tabs as {tabId,url,title} and return, ignoring the other args. Use when you need a numeric tab id."
8406
8207
  },
8407
8208
  context: {
8408
8209
  type: "string",
@@ -8418,7 +8219,7 @@ const dom_inspect_schema = {
8418
8219
  "bookmarks"
8419
8220
  ],
8420
8221
  default: "content",
8421
- description: "content/page (targets `url`, else the active tab), an OPEN extension surface (popup/options/sidebar/devtools), or an override page (newtab/history/bookmarks)"
8222
+ description: "content/page targets `url`, else the active tab; the rest must already be OPEN"
8422
8223
  },
8423
8224
  include: {
8424
8225
  type: "array",
@@ -8443,16 +8244,10 @@ const dom_inspect_schema = {
8443
8244
  "number",
8444
8245
  "boolean"
8445
8246
  ],
8446
- description: "Also include recent console lines for the target (DOM + console in one call). A number is how many lines; true means 50."
8447
- },
8448
- browser: {
8449
- type: "string",
8450
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
8247
+ description: "Also include recent console lines. A number is how many; true means 50."
8451
8248
  },
8452
- timeout: {
8453
- type: "number",
8454
- description: "Command timeout in ms (default 5000)"
8455
- }
8249
+ browser: SESSION_BROWSER,
8250
+ timeout: CALL_TIMEOUT
8456
8251
  },
8457
8252
  required: [
8458
8253
  "projectPath"
@@ -8483,7 +8278,7 @@ async function cdpPortOrError(projectPath, browser, feature) {
8483
8278
  port: resolved.port
8484
8279
  };
8485
8280
  }
8486
- async function dom_inspect_handler(args) {
8281
+ async function dom_snapshot_handler(args) {
8487
8282
  const withConsole = true === args.withConsole ? 50 : false === args.withConsole ? void 0 : args.withConsole;
8488
8283
  if (args.listTargets) {
8489
8284
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
@@ -8663,7 +8458,7 @@ async function dom_inspect_handler(args) {
8663
8458
  }
8664
8459
  const publish_schema = {
8665
8460
  name: "extension_publish",
8666
- description: "Publish the project your stored token is scoped to (from extension_login, or EXTENSION_DEV_TOKEN) to extension.dev and return its shareable URL. The publish target is the token's project -- there is no projectPath, the local files are not uploaded. For a PUBLIC project the URL is the canonical public page and ttlHours does not apply; for a PRIVATE project it is a fresh time-limited share link (?share=) whose lifetime is ttlHours. Posts to the platform's CLI publish endpoint. Besides extension_login this is the only tool that talks to the hosted platform.",
8461
+ description: "Publish the project your stored token is scoped to (extension_auth, or EXTENSION_DEV_TOKEN) to extension.dev and return its shareable URL. This is what \"deploy\" or \"ship\" an extension usually means; extension_submit is the separate store-review path. The target is the token's project: there is no projectPath and no local file is uploaded. For a PUBLIC project the URL is the canonical public page and ttlHours does not apply; for a PRIVATE one it is a fresh time-limited share link (?share=) whose lifetime is ttlHours.",
8667
8462
  inputSchema: {
8668
8463
  type: "object",
8669
8464
  properties: {
@@ -8673,12 +8468,9 @@ const publish_schema = {
8673
8468
  },
8674
8469
  buildSha: {
8675
8470
  type: "string",
8676
- description: "Pin the share URL to a specific build sha (7-40 hex chars). The platform verifies the build exists in the project's build index and rejects an unknown sha, so the returned URL always points at a real build."
8471
+ description: "Pin the URL to a build sha (7-40 hex chars). An unknown sha is rejected, so the returned URL always points at a real build."
8677
8472
  },
8678
- api: {
8679
- type: "string",
8680
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
8681
- }
8473
+ api: API_BASE
8682
8474
  },
8683
8475
  required: []
8684
8476
  }
@@ -8694,7 +8486,7 @@ function fail(name, message) {
8694
8486
  }
8695
8487
  async function publish_handler(args) {
8696
8488
  const token = resolveToken();
8697
- if (!token) return fail("PublishAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
8489
+ if (!token) return fail("PublishAuthError", "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
8698
8490
  if (null != args.ttlHours) {
8699
8491
  const t = Number(args.ttlHours);
8700
8492
  if (!Number.isInteger(t) || t < 1 || t > 168) return fail("PublishBadRequest", "ttlHours must be an integer between 1 and 168.");
@@ -8740,7 +8532,7 @@ async function publish_handler(args) {
8740
8532
  }
8741
8533
  const release_promote_schema = {
8742
8534
  name: "extension_release_promote",
8743
- description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: uses your stored login (extension_login) or a release token in EXTENSION_DEV_TOKEN (mint and revoke it in the dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry). 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).",
8535
+ description: "Promote a built extension to a release channel (stable, preview, beta, ...) on extension.dev, headless. This WRITES: it is the only verb that changes what a channel points at. Auth-gated by your stored login (extension_auth) or a release token in EXTENSION_DEV_TOKEN, minted and revoked under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry. The project comes from the token. Use extension_release_status to find a valid buildId. Cutting a version-bump PR is not available headlessly (it writes to your source repo and needs an interactive login).",
8744
8536
  inputSchema: {
8745
8537
  type: "object",
8746
8538
  properties: {
@@ -8771,10 +8563,7 @@ const release_promote_schema = {
8771
8563
  type: "string",
8772
8564
  description: "Release notes markdown (optional)"
8773
8565
  },
8774
- api: {
8775
- type: "string",
8776
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
8777
- }
8566
+ api: API_BASE
8778
8567
  },
8779
8568
  required: [
8780
8569
  "buildId",
@@ -8793,7 +8582,7 @@ function release_promote_fail(name, message) {
8793
8582
  }
8794
8583
  async function release_promote_handler(args) {
8795
8584
  const token = resolveToken();
8796
- 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; tokens live at most 7 days, so CI must re-mint before expiry), or run extension_login.");
8585
+ 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; tokens live at most 7 days, so CI must re-mint before expiry), or run extension_auth (action: login).");
8797
8586
  const buildId = String(args.buildId || "").trim();
8798
8587
  const channel = String(args.channel || "").trim();
8799
8588
  if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
@@ -8836,7 +8625,7 @@ async function release_promote_handler(args) {
8836
8625
  const ref = resolveProjectRef();
8837
8626
  if (404 === res.status || "UNKNOWN_BUILD" === code) {
8838
8627
  enrich.buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
8839
- enrich.hint = "Run extension_release_list to see this project's channels, their promoted shas, and recent builds.";
8628
+ enrich.hint = "Run extension_release_status to see this project's channels, their promoted shas, and recent builds.";
8840
8629
  if (ref) {
8841
8630
  const channelsUrl = registryFileUrl(ref, "channels.json");
8842
8631
  const channelsRes = await fetchRegistryJson(channelsUrl, fetch, {
@@ -8879,28 +8668,6 @@ async function release_promote_handler(args) {
8879
8668
  } : data;
8880
8669
  return JSON.stringify(enriched);
8881
8670
  }
8882
- const release_list_schema = {
8883
- name: "extension_release_list",
8884
- description: "List the project's release channels (channel -> promoted build sha) and recent builds from the registry (registry.extension.land), so you can pick a valid buildSha for extension_release_promote, extension_deploy, or extension_publish. Read-only, no dispatch. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another project. Works for PRIVATE projects too when your stored login covers them. Returns the registry URLs it read, the console Builds page URL (needs a login), and a publicUrl per channel and build on the public viewer (no login needed for a public project).",
8885
- inputSchema: {
8886
- type: "object",
8887
- properties: {
8888
- workspace: {
8889
- type: "string",
8890
- description: "Workspace slug override (defaults to the stored login's workspace)."
8891
- },
8892
- project: {
8893
- type: "string",
8894
- description: "Project slug override (defaults to the stored login's project)."
8895
- },
8896
- api: {
8897
- type: "string",
8898
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL). Used to resolve link origins and, for a private project, to mint a short-lived read token."
8899
- }
8900
- },
8901
- required: []
8902
- }
8903
- };
8904
8671
  function release_list_fail(name, message, extra) {
8905
8672
  return JSON.stringify({
8906
8673
  ok: false,
@@ -8911,9 +8678,9 @@ function release_list_fail(name, message, extra) {
8911
8678
  ...extra ?? {}
8912
8679
  });
8913
8680
  }
8914
- async function release_list_handler(args) {
8681
+ async function readReleases(args) {
8915
8682
  const ref = resolveProjectRef(args);
8916
- if (!ref) return release_list_fail("ReleaseListInputError", "No project to list. Run extension_login (the stored login names the project), or pass workspace + project explicitly.");
8683
+ if (!ref) return release_list_fail("ReleaseListInputError", "No project to list. Run extension_auth (action: login), which names the project, or pass workspace + project explicitly.");
8917
8684
  const channelsUrl = registryFileUrl(ref, "channels.json");
8918
8685
  const metaUrl = registryFileUrl(ref, "meta.json");
8919
8686
  const buildsUrl = registryFileUrl(ref, "builds/index.json");
@@ -8932,7 +8699,7 @@ async function release_list_handler(args) {
8932
8699
  })
8933
8700
  ]);
8934
8701
  const buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
8935
- if (!channelsRes.ok && !metaRes.ok && !buildsRes.ok) return release_list_fail("ReleaseListNotFound", `No registry data for ${ref.workspace}/${ref.project} (${channelsUrl} returned ${channelsRes.status ?? "no response"}). The project may have no builds yet, or the workspace/project slugs may be wrong. If it is private, make sure extension_login covers this exact project (a token scoped elsewhere cannot read it). The console Builds page is the authoritative view: ${buildsPageUrl}`, {
8702
+ if (!channelsRes.ok && !metaRes.ok && !buildsRes.ok) return release_list_fail("ReleaseListNotFound", `No registry data for ${ref.workspace}/${ref.project} (${channelsUrl} returned ${channelsRes.status ?? "no response"}). The project may have no builds yet, or the workspace/project slugs may be wrong. If it is private, make sure extension_auth covers this exact project (a token scoped elsewhere cannot read it). The console Builds page is the authoritative view: ${buildsPageUrl}`, {
8936
8703
  workspace: ref.workspace,
8937
8704
  project: ref.project,
8938
8705
  registryUrl: channelsUrl,
@@ -8979,246 +8746,12 @@ async function release_list_handler(args) {
8979
8746
  if (!buildsRes.ok) result.buildsUnavailable = `builds/index.json unreadable: ${buildsRes.message}`;
8980
8747
  return JSON.stringify(result);
8981
8748
  }
8982
- function storeMdWarnings(browsers, cwd) {
8983
- const wantsFirefox = browsers.includes("firefox");
8984
- const wantsEdge = browsers.includes("edge");
8985
- if (!wantsFirefox && !wantsEdge) return [];
8986
- let content;
8987
- try {
8988
- content = node_fs.readFileSync(node_path.join(cwd, "STORE.md"), "utf8");
8989
- } catch {
8990
- return [
8991
- "No STORE.md found in the current working directory. Platform submissions read STORE.md from the project's source repository, so this may not apply here; make sure STORE.md exists there for Firefox reviewer notes and Edge certification notes. See the extension-dev skill's store-md reference."
8992
- ];
8993
- }
8994
- const hasField = (section, field)=>{
8995
- const parts = content.split(/^## +/m);
8996
- const match = parts.find((p)=>section.test(p.split("\n", 1)[0] ?? ""));
8997
- if (!match) return false;
8998
- const sub = match.split(/^### +/m).find((p)=>field.test(p.split("\n", 1)[0] ?? ""));
8999
- if (!sub) return false;
9000
- const body = sub.split("\n").slice(1).join("\n");
9001
- return body.replace(/<!--[\s\S]*?-->/g, "").trim().length > 0;
9002
- };
9003
- const warnings = [];
9004
- if (wantsFirefox && !hasField(/firefox|amo/i, /reviewer notes/i)) warnings.push("STORE.md has no Firefox reviewer notes; AMO reviews go faster with test credentials and steps.");
9005
- if (wantsEdge && !hasField(/edge/i, /certification notes/i)) warnings.push("STORE.md has no Edge certification notes; the certification team gets no testing guidance.");
9006
- return warnings;
9007
- }
9008
- const deploy_schema = {
9009
- name: "extension_deploy",
9010
- description: "Submit a built extension to the Chrome Web Store, Firefox AMO, Edge Add-ons, and/or the App Store (Safari) THROUGH extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI. DEFAULTS TO A DRY RUN (preflight - dispatches nothing): the platform side verifies auth, the project, that the build exists, and the store workflow, and this tool then adds the per-store verdict from each store's public credential-health record; trust the per-store rows in the result over the platform's bare preflight line, which does not check store health. Pass dryRun:false to actually submit, which is irreversible and enters store review. The target project is identified by your token (extension_login or a release token in EXTENSION_DEV_TOKEN; tokens live at most 7 days, so CI must re-mint from the console's Access tokens page). Store credentials are never tool arguments and local files are not uploaded. Pass browsers + buildSha (extension_release_list lists valid shas); after a real submission, extension_store_status reads the recorded outcome and review state. Posts to the platform's CLI store-submission endpoint.",
9011
- inputSchema: {
9012
- type: "object",
9013
- properties: {
9014
- browsers: {
9015
- type: "array",
9016
- items: {
9017
- type: "string",
9018
- enum: [
9019
- "chrome",
9020
- "firefox",
9021
- "edge",
9022
- "safari"
9023
- ]
9024
- },
9025
- description: "Stores to submit to."
9026
- },
9027
- buildSha: {
9028
- type: "string",
9029
- description: "The built commit SHA to submit. It must have a completed build in the project's build index; an unknown sha is rejected."
9030
- },
9031
- channel: {
9032
- type: "string",
9033
- description: "Release channel to submit from (default stable)."
9034
- },
9035
- version: {
9036
- type: "string",
9037
- description: "Version label for the submission record (optional)."
9038
- },
9039
- dryRun: {
9040
- type: "boolean",
9041
- default: true,
9042
- description: "Preflight only (verify auth, project, build, and store workflow). Pass false to actually dispatch the submission (irreversible, enters store review)."
9043
- },
9044
- api: {
9045
- type: "string",
9046
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
9047
- }
9048
- },
9049
- required: [
9050
- "browsers",
9051
- "buildSha"
9052
- ]
9053
- }
9054
- };
9055
- function deploy_fail(name, message) {
9056
- return JSON.stringify({
9057
- ok: false,
9058
- error: {
9059
- name,
9060
- message
9061
- }
9062
- });
9063
- }
9064
- async function deploy_handler(args) {
9065
- const token = resolveToken();
9066
- if (!token) return deploy_fail("DeployAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry).");
9067
- const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
9068
- if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
9069
- const buildSha = String(args.buildSha || "").trim();
9070
- if (!buildSha) return deploy_fail("DeployInputError", "buildSha is required (the built commit to submit).");
9071
- const apiCheck = safeApiBase(resolveApiBase(args.api));
9072
- if (!apiCheck.ok) return deploy_fail("DeployConfigError", apiCheck.message);
9073
- const url = `${apiCheck.base}/api/cli/stores/submit`;
9074
- const dryRun = false !== args.dryRun;
9075
- const body = {
9076
- browsers,
9077
- buildSha,
9078
- dryRun
9079
- };
9080
- if (args.channel) body.channel = String(args.channel).trim();
9081
- if (args.version) body.version = String(args.version).trim();
9082
- let res;
9083
- try {
9084
- res = await fetch(url, {
9085
- method: "POST",
9086
- headers: {
9087
- authorization: `Bearer ${token}`,
9088
- "content-type": "application/json"
9089
- },
9090
- body: JSON.stringify(body)
9091
- });
9092
- } catch (err) {
9093
- return deploy_fail("DeployNetworkError", `Could not reach ${url}: ${err?.message || err}`);
9094
- }
9095
- const text = await res.text();
9096
- let data;
9097
- try {
9098
- data = JSON.parse(text);
9099
- } catch {
9100
- data = {
9101
- message: text
9102
- };
9103
- }
9104
- if (!res.ok) return deploy_fail("DeployError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}`);
9105
- const warnings = Array.isArray(data?.warnings) ? [
9106
- ...data.warnings
9107
- ] : [];
9108
- warnings.push(...storeMdWarnings(browsers, process.cwd()));
9109
- const result = {
9110
- mode: "platform",
9111
- dryRun,
9112
- ...data
9113
- };
9114
- if (dryRun) {
9115
- const ref = resolveProjectRef();
9116
- const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
9117
- const storeModeNote = `Store publish mode (draft / skip-publish / live) is not readable with the CLI token, so it cannot be verified from here; check per-store settings at ${consoleStoresUrl}.`;
9118
- let health = null;
9119
- let healthUnreadable = null;
9120
- let channelRows = null;
9121
- if (ref) {
9122
- const [healthRes, channelsRes] = await Promise.all([
9123
- fetchRegistryJson(registryFileUrl(ref, "stores/health.json"), fetch, {
9124
- ref,
9125
- api: args.api
9126
- }),
9127
- fetchRegistryJson(registryFileUrl(ref, "channels.json"), fetch, {
9128
- ref,
9129
- api: args.api
9130
- })
9131
- ]);
9132
- if (healthRes.ok) {
9133
- const stores = healthRes.json?.stores;
9134
- health = stores && "object" == typeof stores ? stores : null;
9135
- if (!health) healthUnreadable = "stores/health.json had no stores map";
9136
- } else healthUnreadable = healthRes.message;
9137
- if (channelsRes.ok) channelRows = parseChannels(channelsRes.json);
9138
- } else healthUnreadable = "no stored workspace/project to look up (run extension_login)";
9139
- const preflight = browsers.map((browser)=>{
9140
- if (!health) return {
9141
- browser,
9142
- ok: false,
9143
- configured: "unknown",
9144
- publishMode: "unknown",
9145
- reason: `Store configuration could not be read (${healthUnreadable}); verify the ${browser} store in the console before submitting.`
9146
- };
9147
- const row = health[browser];
9148
- if (!row) return {
9149
- browser,
9150
- ok: false,
9151
- configured: false,
9152
- publishMode: "unknown",
9153
- reason: `No ${browser} store is configured on this project; a real submission for ${browser} would fail. Configure it at ${consoleStoresUrl}.`
9154
- };
9155
- if (true !== row.ok) return {
9156
- browser,
9157
- ok: false,
9158
- configured: false,
9159
- publishMode: "unknown",
9160
- reason: String(row.message || "").trim() || `The ${browser} store failed its last credential health check.`
9161
- };
9162
- return {
9163
- browser,
9164
- ok: true,
9165
- configured: true,
9166
- publishMode: "unknown"
9167
- };
9168
- });
9169
- const actionable = preflight.filter((p)=>p.ok).map((p)=>p.browser);
9170
- const blocked = preflight.filter((p)=>!p.ok);
9171
- const channelDefaulted = !String(args.channel || "").trim();
9172
- const resolvedChannel = String(data?.channel || "").trim() || (channelDefaulted ? "stable" : String(args.channel).trim());
9173
- if (channelRows) {
9174
- const exists = channelRows.some((r)=>r.channel === resolvedChannel || r.channel.endsWith(`-${resolvedChannel}`));
9175
- if (!exists) warnings.push(`Channel "${resolvedChannel}"${channelDefaulted ? " (the default)" : ""} does not exist in this project's channels.json (existing: ${channelRows.map((r)=>r.channel).join(", ") || "none"}), so a real submission from it has no promoted build to serve. Promote a build there first (extension_release_promote) or pass an existing channel.`);
9176
- }
9177
- const summaryParts = [];
9178
- if (actionable.length > 0) summaryParts.push(`Preflight passed for ${actionable.join(", ")}: the platform verified auth, the project, build ${data?.buildId ?? buildSha}, and the store workflow, and the store credentials passed their last health check.`);
9179
- for (const p of blocked)summaryParts.push(`${p.browser}: ${"unknown" === p.configured ? "cannot be verified" : "NOT actionable"} - ${p.reason}`);
9180
- summaryParts.push(storeModeNote);
9181
- result.ok = actionable.length > 0;
9182
- result.preflight = preflight;
9183
- result.channel = resolvedChannel;
9184
- result.channelDefaulted = channelDefaulted;
9185
- if (channelDefaulted) result.channelNote = `channel: ${resolvedChannel} (default)`;
9186
- result.consoleStoresUrl = consoleStoresUrl;
9187
- if ("string" == typeof data?.message) result.platformMessage = data.message;
9188
- result.message = summaryParts.join(" ");
9189
- }
9190
- if (!dryRun) result.statusNote = "Track this submission with extension_store_status: it reads the recorded outcome, per-store credential health, and review state from the public registry.";
9191
- if (warnings.length > 0) result.warnings = warnings;
9192
- return JSON.stringify(result);
9193
- }
9194
8749
  const KNOWN_STORES = [
9195
8750
  "chrome",
9196
8751
  "firefox",
9197
8752
  "edge",
9198
8753
  "safari"
9199
8754
  ];
9200
- const store_status_schema = {
9201
- name: "extension_store_status",
9202
- description: "Report the project's browser-store state after an extension_deploy submission: per store (chrome, firefox, edge, safari) whether it is configured, its latest credential health check, the last recorded submission (version, status, store URL, submitted-at), and the latest review status. Reads the project's public registry (registry.extension.land: stores/health.json, stores/status.json, stores/submissions.json) - read-only, dispatches nothing, no auth needed for public projects. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another public project. Registry state can lag the store dashboards by up to a polling interval.",
9203
- inputSchema: {
9204
- type: "object",
9205
- properties: {
9206
- workspace: {
9207
- type: "string",
9208
- description: "Workspace slug override (defaults to the stored login's workspace)."
9209
- },
9210
- project: {
9211
- type: "string",
9212
- description: "Project slug override (defaults to the stored login's project)."
9213
- },
9214
- api: {
9215
- type: "string",
9216
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL). Used to resolve link origins and, for a private project, to mint a short-lived read token."
9217
- }
9218
- },
9219
- required: []
9220
- }
9221
- };
9222
8755
  function isPlainObject(value) {
9223
8756
  return Boolean(value) && "object" == typeof value && !Array.isArray(value);
9224
8757
  }
@@ -9307,9 +8840,9 @@ function store_status_fail(name, message, extra) {
9307
8840
  ...extra ?? {}
9308
8841
  });
9309
8842
  }
9310
- async function store_status_handler(args) {
8843
+ async function readStores(args) {
9311
8844
  const ref = resolveProjectRef(args);
9312
- if (!ref) return store_status_fail("StoreStatusInputError", "No project to inspect. Run extension_login (the stored login names the project), or pass workspace + project explicitly.");
8845
+ if (!ref) return store_status_fail("StoreStatusInputError", "No project to inspect. Run extension_auth (action: login), which names the project, or pass workspace + project explicitly.");
9313
8846
  const healthUrl = registryFileUrl(ref, "stores/health.json");
9314
8847
  const statusUrl = registryFileUrl(ref, "stores/status.json");
9315
8848
  const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
@@ -9403,9 +8936,294 @@ async function store_status_handler(args) {
9403
8936
  consoleStoresUrl,
9404
8937
  message: `${summaryParts.join(". ")}. This is the registry's recorded state (submissions and the review poller write it); the store dashboards are authoritative and may be ahead of it.`
9405
8938
  };
9406
- if (!healthRes.ok) result.healthUnavailable = `stores/health.json unreadable: ${healthRes.message}`;
9407
- if (!statusRes.ok) result.statusUnavailable = `stores/status.json unreadable: ${statusRes.message}`;
9408
- if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
8939
+ if (!healthRes.ok) result.healthUnavailable = `stores/health.json unreadable: ${healthRes.message}`;
8940
+ if (!statusRes.ok) result.statusUnavailable = `stores/status.json unreadable: ${statusRes.message}`;
8941
+ if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
8942
+ return JSON.stringify(result);
8943
+ }
8944
+ const release_status_schema = {
8945
+ name: "extension_release_status",
8946
+ description: "Read where a project stands on extension.dev, from the public registry (registry.extension.land). Read-only: it dispatches nothing and promotes nothing. include:'releases' returns the release channels (channel -> promoted build sha), recent builds, and a public build-page URL for each, which is how you find a valid sha for extension_release_promote, extension_submit, or extension_publish. include:'stores' returns the per-store picture after an extension_submit (chrome, firefox, edge, safari): configured or not, the last credential health check, the last recorded submission, and the latest review status, read from stores/health.json, stores/status.json and stores/submissions.json. Both are included by default. Defaults to the logged-in project (extension_auth); pass workspace + project to read another. Private projects work when your stored login covers them. Registry state can lag the store dashboards by up to a polling interval.",
8947
+ inputSchema: {
8948
+ type: "object",
8949
+ properties: {
8950
+ include: {
8951
+ type: "array",
8952
+ items: {
8953
+ type: "string",
8954
+ enum: [
8955
+ "releases",
8956
+ "stores"
8957
+ ]
8958
+ },
8959
+ default: [
8960
+ "releases",
8961
+ "stores"
8962
+ ],
8963
+ description: "Which sections to read. Both by default."
8964
+ },
8965
+ workspace: {
8966
+ type: "string",
8967
+ description: "Workspace slug override (default: the stored login's)."
8968
+ },
8969
+ project: {
8970
+ type: "string",
8971
+ description: "Project slug override (default: the stored login's)."
8972
+ },
8973
+ api: API_BASE
8974
+ },
8975
+ required: []
8976
+ }
8977
+ };
8978
+ function parse(raw) {
8979
+ try {
8980
+ return JSON.parse(raw);
8981
+ } catch {
8982
+ return {
8983
+ ok: false,
8984
+ error: {
8985
+ name: "ParseError",
8986
+ message: raw
8987
+ }
8988
+ };
8989
+ }
8990
+ }
8991
+ async function release_status_handler(args) {
8992
+ const include = Array.isArray(args.include) && args.include.length ? args.include : [
8993
+ "releases",
8994
+ "stores"
8995
+ ];
8996
+ const scope = {
8997
+ workspace: args.workspace,
8998
+ project: args.project,
8999
+ api: args.api
9000
+ };
9001
+ const [releases, stores] = await Promise.all([
9002
+ include.includes("releases") ? readReleases(scope).then(parse) : null,
9003
+ include.includes("stores") ? readStores(scope).then(parse) : null
9004
+ ]);
9005
+ const sections = [
9006
+ releases,
9007
+ stores
9008
+ ].filter(Boolean);
9009
+ return JSON.stringify({
9010
+ ok: sections.some((section)=>true === section.ok),
9011
+ ...releases ? {
9012
+ releases
9013
+ } : {},
9014
+ ...stores ? {
9015
+ stores
9016
+ } : {}
9017
+ });
9018
+ }
9019
+ function storeMdWarnings(browsers, cwd) {
9020
+ const wantsFirefox = browsers.includes("firefox");
9021
+ const wantsEdge = browsers.includes("edge");
9022
+ if (!wantsFirefox && !wantsEdge) return [];
9023
+ let content;
9024
+ try {
9025
+ content = node_fs.readFileSync(node_path.join(cwd, "STORE.md"), "utf8");
9026
+ } catch {
9027
+ return [
9028
+ "No STORE.md found in the current working directory. Platform submissions read STORE.md from the project's source repository, so this may not apply here; make sure STORE.md exists there for Firefox reviewer notes and Edge certification notes. See the extension-dev skill's store-md reference."
9029
+ ];
9030
+ }
9031
+ const hasField = (section, field)=>{
9032
+ const parts = content.split(/^## +/m);
9033
+ const match = parts.find((p)=>section.test(p.split("\n", 1)[0] ?? ""));
9034
+ if (!match) return false;
9035
+ const sub = match.split(/^### +/m).find((p)=>field.test(p.split("\n", 1)[0] ?? ""));
9036
+ if (!sub) return false;
9037
+ const body = sub.split("\n").slice(1).join("\n");
9038
+ return body.replace(/<!--[\s\S]*?-->/g, "").trim().length > 0;
9039
+ };
9040
+ const warnings = [];
9041
+ if (wantsFirefox && !hasField(/firefox|amo/i, /reviewer notes/i)) warnings.push("STORE.md has no Firefox reviewer notes; AMO reviews go faster with test credentials and steps.");
9042
+ if (wantsEdge && !hasField(/edge/i, /certification notes/i)) warnings.push("STORE.md has no Edge certification notes; the certification team gets no testing guidance.");
9043
+ return warnings;
9044
+ }
9045
+ const submit_schema = {
9046
+ name: "extension_submit",
9047
+ description: "SUBMIT FOR REVIEW: send a built extension to the Chrome Web Store, Firefox AMO, Edge Add-ons and/or the App Store (Safari) THROUGH extension.dev, which holds your store credentials and dispatches from your project's mirror CI. Store review only. It does NOT push a build to the extension.dev platform and does NOT make a shareable link: that is extension_publish, which is what \"deploy\" or \"ship\" an extension almost always means. Reach for this only when the ask is explicitly a store submission. DEFAULTS TO A DRY RUN that dispatches nothing: the platform verifies auth, project, build and store workflow, and this tool adds each store's credential-health verdict; trust those per-store rows over the platform's bare preflight line, which does not check store health. dryRun:false actually submits, which is irreversible and enters store review. The project comes from your token (extension_auth or EXTENSION_DEV_TOKEN; tokens live at most 7 days, so CI must re-mint from the console's Access tokens page). Store credentials are never arguments and no local file is uploaded. extension_release_status lists valid shas and, after a real submission, reads the recorded outcome and review state.",
9048
+ inputSchema: {
9049
+ type: "object",
9050
+ properties: {
9051
+ browsers: {
9052
+ type: "array",
9053
+ items: {
9054
+ type: "string",
9055
+ enum: [
9056
+ "chrome",
9057
+ "firefox",
9058
+ "edge",
9059
+ "safari"
9060
+ ]
9061
+ },
9062
+ description: "Stores to submit to."
9063
+ },
9064
+ buildSha: {
9065
+ type: "string",
9066
+ description: "The built commit SHA to submit. It needs a completed build in the project's build index; an unknown sha is rejected."
9067
+ },
9068
+ channel: {
9069
+ type: "string",
9070
+ description: "Release channel to submit from (default stable)."
9071
+ },
9072
+ version: {
9073
+ type: "string",
9074
+ description: "Version label for the submission record (optional)."
9075
+ },
9076
+ dryRun: {
9077
+ type: "boolean",
9078
+ default: true,
9079
+ description: "Preflight only. Pass false to actually dispatch (irreversible, enters store review)."
9080
+ },
9081
+ api: API_BASE
9082
+ },
9083
+ required: [
9084
+ "browsers",
9085
+ "buildSha"
9086
+ ]
9087
+ }
9088
+ };
9089
+ function submit_fail(name, message) {
9090
+ return JSON.stringify({
9091
+ ok: false,
9092
+ error: {
9093
+ name,
9094
+ message
9095
+ }
9096
+ });
9097
+ }
9098
+ async function submit_handler(args) {
9099
+ const token = resolveToken();
9100
+ if (!token) return submit_fail("SubmitAuthError", "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry).");
9101
+ const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
9102
+ if (0 === browsers.length) return submit_fail("SubmitInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
9103
+ const buildSha = String(args.buildSha || "").trim();
9104
+ if (!buildSha) return submit_fail("SubmitInputError", "buildSha is required (the built commit to submit).");
9105
+ const apiCheck = safeApiBase(resolveApiBase(args.api));
9106
+ if (!apiCheck.ok) return submit_fail("SubmitConfigError", apiCheck.message);
9107
+ const url = `${apiCheck.base}/api/cli/stores/submit`;
9108
+ const dryRun = false !== args.dryRun;
9109
+ const body = {
9110
+ browsers,
9111
+ buildSha,
9112
+ dryRun
9113
+ };
9114
+ if (args.channel) body.channel = String(args.channel).trim();
9115
+ if (args.version) body.version = String(args.version).trim();
9116
+ let res;
9117
+ try {
9118
+ res = await fetch(url, {
9119
+ method: "POST",
9120
+ headers: {
9121
+ authorization: `Bearer ${token}`,
9122
+ "content-type": "application/json",
9123
+ ...identityHeaders("extension_submit")
9124
+ },
9125
+ body: JSON.stringify(body)
9126
+ });
9127
+ } catch (err) {
9128
+ return submit_fail("SubmitNetworkError", `Could not reach ${url}: ${err?.message || err}`);
9129
+ }
9130
+ const text = await res.text();
9131
+ let data;
9132
+ try {
9133
+ data = JSON.parse(text);
9134
+ } catch {
9135
+ data = {
9136
+ message: text
9137
+ };
9138
+ }
9139
+ if (!res.ok) return submit_fail("SubmitError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}`);
9140
+ const warnings = Array.isArray(data?.warnings) ? [
9141
+ ...data.warnings
9142
+ ] : [];
9143
+ warnings.push(...storeMdWarnings(browsers, process.cwd()));
9144
+ const result = {
9145
+ mode: "platform",
9146
+ dryRun,
9147
+ ...data
9148
+ };
9149
+ if (dryRun) {
9150
+ const ref = resolveProjectRef();
9151
+ const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
9152
+ const storeModeNote = `Store publish mode (draft / skip-publish / live) is not readable with the CLI token, so it cannot be verified from here; check per-store settings at ${consoleStoresUrl}.`;
9153
+ let health = null;
9154
+ let healthUnreadable = null;
9155
+ let channelRows = null;
9156
+ if (ref) {
9157
+ const [healthRes, channelsRes] = await Promise.all([
9158
+ fetchRegistryJson(registryFileUrl(ref, "stores/health.json"), fetch, {
9159
+ ref,
9160
+ api: args.api
9161
+ }),
9162
+ fetchRegistryJson(registryFileUrl(ref, "channels.json"), fetch, {
9163
+ ref,
9164
+ api: args.api
9165
+ })
9166
+ ]);
9167
+ if (healthRes.ok) {
9168
+ const stores = healthRes.json?.stores;
9169
+ health = stores && "object" == typeof stores ? stores : null;
9170
+ if (!health) healthUnreadable = "stores/health.json had no stores map";
9171
+ } else healthUnreadable = healthRes.message;
9172
+ if (channelsRes.ok) channelRows = parseChannels(channelsRes.json);
9173
+ } else healthUnreadable = "no stored workspace/project to look up (run extension_auth)";
9174
+ const preflight = browsers.map((browser)=>{
9175
+ if (!health) return {
9176
+ browser,
9177
+ ok: false,
9178
+ configured: "unknown",
9179
+ publishMode: "unknown",
9180
+ reason: `Store configuration could not be read (${healthUnreadable}); verify the ${browser} store in the console before submitting.`
9181
+ };
9182
+ const row = health[browser];
9183
+ if (!row) return {
9184
+ browser,
9185
+ ok: false,
9186
+ configured: false,
9187
+ publishMode: "unknown",
9188
+ reason: `No ${browser} store is configured on this project; a real submission for ${browser} would fail. Configure it at ${consoleStoresUrl}.`
9189
+ };
9190
+ if (true !== row.ok) return {
9191
+ browser,
9192
+ ok: false,
9193
+ configured: false,
9194
+ publishMode: "unknown",
9195
+ reason: String(row.message || "").trim() || `The ${browser} store failed its last credential health check.`
9196
+ };
9197
+ return {
9198
+ browser,
9199
+ ok: true,
9200
+ configured: true,
9201
+ publishMode: "unknown"
9202
+ };
9203
+ });
9204
+ const actionable = preflight.filter((p)=>p.ok).map((p)=>p.browser);
9205
+ const blocked = preflight.filter((p)=>!p.ok);
9206
+ const channelDefaulted = !String(args.channel || "").trim();
9207
+ const resolvedChannel = String(data?.channel || "").trim() || (channelDefaulted ? "stable" : String(args.channel).trim());
9208
+ if (channelRows) {
9209
+ const exists = channelRows.some((r)=>r.channel === resolvedChannel || r.channel.endsWith(`-${resolvedChannel}`));
9210
+ if (!exists) warnings.push(`Channel "${resolvedChannel}"${channelDefaulted ? " (the default)" : ""} does not exist in this project's channels.json (existing: ${channelRows.map((r)=>r.channel).join(", ") || "none"}), so a real submission from it has no promoted build to serve. Promote a build there first (extension_release_promote) or pass an existing channel.`);
9211
+ }
9212
+ const summaryParts = [];
9213
+ if (actionable.length > 0) summaryParts.push(`Preflight passed for ${actionable.join(", ")}: the platform verified auth, the project, build ${data?.buildId ?? buildSha}, and the store workflow, and the store credentials passed their last health check.`);
9214
+ for (const p of blocked)summaryParts.push(`${p.browser}: ${"unknown" === p.configured ? "cannot be verified" : "NOT actionable"} - ${p.reason}`);
9215
+ summaryParts.push(storeModeNote);
9216
+ result.ok = actionable.length > 0;
9217
+ result.preflight = preflight;
9218
+ result.channel = resolvedChannel;
9219
+ result.channelDefaulted = channelDefaulted;
9220
+ if (channelDefaulted) result.channelNote = `channel: ${resolvedChannel} (default)`;
9221
+ result.consoleStoresUrl = consoleStoresUrl;
9222
+ if ("string" == typeof data?.message) result.platformMessage = data.message;
9223
+ result.message = summaryParts.join(" ");
9224
+ }
9225
+ if (!dryRun) result.statusNote = "Track this submission with extension_release_status: it reads the recorded outcome, per-store credential health, and review state from the public registry.";
9226
+ if (warnings.length > 0) result.warnings = warnings;
9409
9227
  return JSON.stringify(result);
9410
9228
  }
9411
9229
  const ENGINE_COMPANION_IDS = new Set([
@@ -9475,7 +9293,7 @@ function doctor_readReadyContract(projectPath, browser) {
9475
9293
  }
9476
9294
  const doctor_schema = {
9477
9295
  name: "extension_doctor",
9478
- description: "Diagnose a dev session end-to-end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, and browser liveness. Returns one {check, status, detail, remediation?} entry per leg in dependency order, a 'skip' names the check that blocked it and is NOT a pass. Run this first when any act tool (storage/reload/eval/open) errors unexpectedly. Wraps `extension doctor`. Call with no projectPath for a pre-flight environment check (node, extension CLI, template cache) before any project exists.",
9296
+ description: "Diagnose a dev session end-to-end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, browser liveness. Returns one {check, status, detail, remediation?} per leg in dependency order; a 'skip' names the check that blocked it and is NOT a pass. Run this first when any act tool (storage/reload/eval/open) errors unexpectedly. Call with no projectPath for a pre-flight environment check (node, extension CLI, template cache) before any project exists.",
9479
9297
  inputSchema: {
9480
9298
  type: "object",
9481
9299
  properties: {
@@ -9516,7 +9334,7 @@ async function environmentPreflight() {
9516
9334
  checks.push({
9517
9335
  check: "template-cache",
9518
9336
  status: cacheExists ? "pass" : "warn",
9519
- detail: cacheExists ? `Template catalog cached at ${cacheFile}` : "Template catalog not cached yet (first extension_list_templates will fetch it)"
9337
+ detail: cacheExists ? `Template catalog cached at ${cacheFile}` : "Template catalog not cached yet (extension_templates will fetch it)"
9520
9338
  });
9521
9339
  const healthy = checks.every((c)=>"fail" !== c.status);
9522
9340
  return JSON.stringify({
@@ -9660,26 +9478,20 @@ function wait_isAlive(pid) {
9660
9478
  }
9661
9479
  const wait_schema = {
9662
9480
  name: "extension_wait",
9663
- description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status with these facts: compiled (the compiler finished), browserAttached (the engine's runtime executor connected), and for a browser session guestLoaded (the browser's OWN target list actually shows your extension). guestLoaded is the trustworthy load signal: it catches a silently rejected --load-extension that leaves ready.json stamped attached with empty logs (extension.js BUGS_TO_FIX §83); it is null when it could not be checked (no CDP port, e.g. a gecko session). Every result reports budgetMs (this call's wait budget) and elapsedMs; on status:'timeout' call again to keep waiting (polling resumes on the same contract). In a noBrowser (build-only) session it returns as soon as the compile lands instead of waiting for a browser that will never attach. Ports in the result come from the ready contract, so they always match what the dev server actually bound.",
9481
+ description: "Wait for a running dev or start session to be ready. Polls the ready.json contract and reports compiled (the compiler finished), browserAttached (the runtime executor connected), and guestLoaded (the browser's OWN target list shows your extension). guestLoaded is the trustworthy load signal: it catches a silently rejected --load-extension that leaves ready.json stamped attached with empty logs; it is null when it could not be checked (no CDP port, e.g. a gecko session). Every result reports budgetMs and elapsedMs; on status:'timeout' call again to keep waiting on the same contract. In a noBrowser session it returns as soon as the compile lands instead of waiting for a browser that will never attach. Ports come from the contract, so they match what the server actually bound.",
9664
9482
  inputSchema: {
9665
9483
  type: "object",
9666
9484
  properties: {
9667
- projectPath: {
9668
- type: "string",
9669
- description: "Path to the extension project root"
9670
- },
9671
- browser: {
9672
- type: "string",
9673
- description: "Browser to check readiness for. Defaults to the active dev session's browser for this project."
9674
- },
9485
+ projectPath: PROJECT_PATH,
9486
+ browser: SESSION_BROWSER,
9675
9487
  timeoutMs: {
9676
9488
  type: "number",
9677
9489
  default: DEFAULT_TIMEOUT_MS,
9678
- description: `Wait budget in milliseconds for this call. Default ${DEFAULT_TIMEOUT_MS}; clamped to ${MIN_TIMEOUT_MS}-${SAFE_CEILING_MS} (the ceiling keeps one call under the MCP client's 60s request timeout). On timeout the result reports elapsedMs plus what was observed (compiled, browserAttached); call again to keep waiting, polling resumes on the same contract.`
9490
+ description: `Wait budget for this call. Default ${DEFAULT_TIMEOUT_MS}, clamped to ${MIN_TIMEOUT_MS}-${SAFE_CEILING_MS} so one call stays under the client's 60s request timeout. On timeout, call again to keep waiting.`
9679
9491
  },
9680
9492
  timeout: {
9681
9493
  type: "number",
9682
- description: "Deprecated alias of timeoutMs, kept for callers that already pass it. timeoutMs wins when both are given."
9494
+ description: "Deprecated alias of timeoutMs, which wins when both are given."
9683
9495
  }
9684
9496
  },
9685
9497
  required: [
@@ -9718,7 +9530,7 @@ async function wait_handler(args) {
9718
9530
  buildOnly: true,
9719
9531
  compiled: true,
9720
9532
  browserAttached: false,
9721
- message: "Build-only session (noBrowser): the extension compiled and the dev server is live, but no browser was launched, so browserAttached will never become true. Do not call extension_wait again to wait for a browser. The control verbs (storage/reload/open/dom_inspect/eval) need a live browser and will not work against this session.",
9533
+ message: "Build-only session (noBrowser): the extension compiled and the dev server is live, but no browser was launched, so browserAttached will never become true. Do not call extension_wait again to wait for a browser. The control verbs (storage/reload/open/dom_snapshot/eval) need a live browser and will not work against this session.",
9722
9534
  command: contract.command,
9723
9535
  browser: contract.browser,
9724
9536
  port: contract.port,
@@ -9809,10 +9621,7 @@ const add_feature_schema = {
9809
9621
  inputSchema: {
9810
9622
  type: "object",
9811
9623
  properties: {
9812
- projectPath: {
9813
- type: "string",
9814
- description: "Path to the extension project root"
9815
- },
9624
+ projectPath: PROJECT_PATH,
9816
9625
  feature: {
9817
9626
  type: "string",
9818
9627
  enum: [
@@ -10066,78 +9875,6 @@ async function add_feature_handler(args) {
10066
9875
  hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
10067
9876
  });
10068
9877
  }
10069
- const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
10070
- const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
10071
- const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
10072
- const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
10073
- async function startDeviceCode(args) {
10074
- const doFetch = args.fetchImpl ?? fetch;
10075
- const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
10076
- method: "POST",
10077
- headers: {
10078
- accept: "application/json",
10079
- "content-type": "application/json"
10080
- },
10081
- body: JSON.stringify({
10082
- client_id: args.clientId,
10083
- scope: args.scope || "read:user"
10084
- })
10085
- });
10086
- const data = await res.json().catch(()=>({}));
10087
- if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
10088
- return {
10089
- deviceCode: String(data.device_code || ""),
10090
- userCode: String(data.user_code || ""),
10091
- verificationUri: String(data.verification_uri || "https://github.com/login/device"),
10092
- interval: Number(data.interval || 5),
10093
- expiresIn: Number(data.expires_in || 900)
10094
- };
10095
- }
10096
- async function pollForToken(args) {
10097
- const doFetch = args.fetchImpl ?? fetch;
10098
- const sleep = args.sleepImpl ?? defaultSleep;
10099
- let intervalMs = 1000 * Math.max(1, args.interval);
10100
- const deadline = Date.now() + Math.max(0, args.budgetMs);
10101
- while(Date.now() < deadline){
10102
- await sleep(intervalMs);
10103
- const res = await doFetch(GITHUB_TOKEN_URL, {
10104
- method: "POST",
10105
- headers: {
10106
- accept: "application/json",
10107
- "content-type": "application/json"
10108
- },
10109
- body: JSON.stringify({
10110
- client_id: args.clientId,
10111
- device_code: args.deviceCode,
10112
- grant_type: DEVICE_GRANT_TYPE
10113
- })
10114
- });
10115
- const data = await res.json().catch(()=>({}));
10116
- const token = String(data.access_token || "").trim();
10117
- if (token) return {
10118
- ok: true,
10119
- githubToken: token
10120
- };
10121
- const error = String(data.error || "");
10122
- if ("authorization_pending" === error) continue;
10123
- if ("slow_down" === error) {
10124
- intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
10125
- continue;
10126
- }
10127
- if ("expired_token" === error) return {
10128
- ok: false,
10129
- reason: "expired"
10130
- };
10131
- if ("access_denied" === error) return {
10132
- ok: false,
10133
- reason: "denied"
10134
- };
10135
- }
10136
- return {
10137
- ok: false,
10138
- reason: "pending"
10139
- };
10140
- }
10141
9878
  async function requestDeviceCode(args) {
10142
9879
  const doFetch = args.fetchImpl ?? fetch;
10143
9880
  const res = await doFetch(`${args.apiBase}${args.path}`, {
@@ -10201,8 +9938,7 @@ async function pollDeviceToken(args) {
10201
9938
  if (res.ok && data.token) {
10202
9939
  const creds = persistTokenResponse({
10203
9940
  apiBase: args.apiBase,
10204
- data,
10205
- provider: "extensiondev"
9941
+ data
10206
9942
  });
10207
9943
  return {
10208
9944
  ok: true,
@@ -10231,30 +9967,6 @@ async function pollDeviceToken(args) {
10231
9967
  await new Promise((r)=>setTimeout(r, 1000 * interval));
10232
9968
  }
10233
9969
  }
10234
- const login_schema = {
10235
- name: "extension_login",
10236
- description: "Authenticate to extension.dev 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. The server picks the flow: the extension.dev-gated device flow (you authorize at extension.dev/device; GitHub federation happens server-side, no GitHub token on this machine) or, as a fallback, the legacy GitHub device flow. Never returns the token. Minted tokens live at most 7 days (server-enforced), so CI pipelines must re-mint before expiry on the console's Access tokens page (project settings -> Access tokens). This is the only tool besides extension_publish that talks to the hosted platform.",
10237
- inputSchema: {
10238
- type: "object",
10239
- properties: {
10240
- project: {
10241
- type: "string",
10242
- description: "Target project as '<workspace>/<project>' (the token is scoped to it)"
10243
- },
10244
- deviceCode: {
10245
- type: "string",
10246
- description: "Resume token from a prior call's `deviceCode`; omit on the first call"
10247
- },
10248
- api: {
10249
- type: "string",
10250
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
10251
- }
10252
- },
10253
- required: [
10254
- "project"
10255
- ]
10256
- }
10257
- };
10258
9970
  const FIRST_CALL_BUDGET_MS = 8000;
10259
9971
  const RESUME_BUDGET_MS = 22000;
10260
9972
  function login_fail(name, message) {
@@ -10281,7 +9993,7 @@ function success(creds) {
10281
9993
  function login_pending(start) {
10282
9994
  const complete = String(start.verificationUriComplete || "").trim();
10283
9995
  const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
10284
- const message = hasCompleteLink ? `Open ${complete} and approve (code ${start.userCode} is pre-filled), then call extension_login again with this deviceCode (and the same project). If the page asks for a code, enter ${start.userCode} at ${start.verificationUri}.` : `Open ${start.verificationUri} and enter code ${start.userCode}, then call extension_login again with this deviceCode (and the same project).`;
9996
+ const message = hasCompleteLink ? `Open ${complete} and approve (code ${start.userCode} is pre-filled), then call extension_auth (action: login) again with this deviceCode and the same project. If the page asks for a code, enter ${start.userCode} at ${start.verificationUri}.` : `Open ${start.verificationUri} and enter code ${start.userCode}, then call extension_auth (action: login) again with this deviceCode and the same project.`;
10285
9997
  return JSON.stringify({
10286
9998
  ok: false,
10287
9999
  status: "authorization_pending",
@@ -10302,10 +10014,10 @@ function resumePending(deviceCode, verificationUri) {
10302
10014
  verificationUri,
10303
10015
  deviceCode,
10304
10016
  tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
10305
- message: `Still waiting for authorization. The one-click link and code from the previous response are still valid: open that link (or enter the code at ${verificationUri}), then call extension_login again with this same deviceCode (and the same project).`
10017
+ message: `Still waiting for authorization. The one-click link and code from the previous response are still valid: open that link (or enter the code at ${verificationUri}), then call extension_auth (action: login) again with this same deviceCode and the same project.`
10306
10018
  });
10307
10019
  }
10308
- async function login_handler(args) {
10020
+ async function loginToProject(args) {
10309
10021
  const project = String(args.project || "").trim();
10310
10022
  if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.");
10311
10023
  const apiBase = resolveApiBase(args.api);
@@ -10315,114 +10027,54 @@ async function login_handler(args) {
10315
10027
  } catch (err) {
10316
10028
  return login_fail("LoginConfigError", err?.message || "Could not load login config.");
10317
10029
  }
10318
- if ("extensiondev" === config.provider) {
10319
- if (args.deviceCode) {
10320
- const poll = await pollDeviceToken({
10321
- apiBase,
10322
- path: config.deviceTokenUrl,
10323
- project,
10324
- deviceCode: String(args.deviceCode),
10325
- interval: 5,
10326
- budgetMs: RESUME_BUDGET_MS
10327
- });
10328
- if (poll.ok) return success(poll.creds);
10329
- if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
10330
- if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
10331
- if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
10332
- return resumePending(String(args.deviceCode), config.verificationUri);
10333
- }
10334
- let start;
10335
- try {
10336
- start = await requestDeviceCode({
10337
- apiBase,
10338
- path: config.deviceCodeUrl,
10339
- project
10340
- });
10341
- } catch (err) {
10342
- return login_fail("LoginStartError", err?.message || "Could not start the device flow.");
10343
- }
10030
+ if (args.deviceCode) {
10344
10031
  const poll = await pollDeviceToken({
10345
10032
  apiBase,
10346
10033
  path: config.deviceTokenUrl,
10347
10034
  project,
10348
- deviceCode: start.deviceCode,
10349
- interval: start.interval,
10350
- budgetMs: FIRST_CALL_BUDGET_MS
10351
- });
10352
- if (poll.ok) return success(poll.creds);
10353
- if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
10354
- return login_pending({
10355
- deviceCode: start.deviceCode,
10356
- userCode: start.userCode,
10357
- verificationUri: start.verificationUri,
10358
- verificationUriComplete: start.verificationUriComplete
10359
- });
10360
- }
10361
- if (args.deviceCode) {
10362
- const poll = await pollForToken({
10363
- clientId: config.clientId,
10364
10035
  deviceCode: String(args.deviceCode),
10365
10036
  interval: 5,
10366
10037
  budgetMs: RESUME_BUDGET_MS
10367
10038
  });
10368
- if (!poll.ok) {
10369
- if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
10370
- if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
10371
- return resumePending(String(args.deviceCode), "https://github.com/login/device");
10372
- }
10373
- try {
10374
- const creds = await exchangeAndPersist({
10375
- apiBase,
10376
- githubToken: poll.githubToken,
10377
- project
10378
- });
10379
- return success(creds);
10380
- } catch (err) {
10381
- return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
10382
- }
10039
+ if (poll.ok) return success(poll.creds);
10040
+ if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_auth (action: login) again to restart.");
10041
+ if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
10042
+ if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
10043
+ return resumePending(String(args.deviceCode), config.verificationUri);
10383
10044
  }
10384
10045
  let start;
10385
10046
  try {
10386
- start = await startDeviceCode({
10387
- clientId: config.clientId,
10388
- scope: config.scope
10047
+ start = await requestDeviceCode({
10048
+ apiBase,
10049
+ path: config.deviceCodeUrl,
10050
+ project
10389
10051
  });
10390
10052
  } catch (err) {
10391
- return login_fail("LoginStartError", err?.message || "Could not start device flow.");
10053
+ return login_fail("LoginStartError", err?.message || "Could not start the device flow.");
10392
10054
  }
10393
- const poll = await pollForToken({
10394
- clientId: config.clientId,
10055
+ const poll = await pollDeviceToken({
10056
+ apiBase,
10057
+ path: config.deviceTokenUrl,
10058
+ project,
10395
10059
  deviceCode: start.deviceCode,
10396
10060
  interval: start.interval,
10397
10061
  budgetMs: FIRST_CALL_BUDGET_MS
10398
10062
  });
10399
- if (poll.ok) try {
10400
- const creds = await exchangeAndPersist({
10401
- apiBase,
10402
- githubToken: poll.githubToken,
10403
- project
10404
- });
10405
- return success(creds);
10406
- } catch (err) {
10407
- return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
10408
- }
10409
- if (!poll.ok && "denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
10410
- return login_pending(start);
10063
+ if (poll.ok) return success(poll.creds);
10064
+ if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
10065
+ return login_pending({
10066
+ deviceCode: start.deviceCode,
10067
+ userCode: start.userCode,
10068
+ verificationUri: start.verificationUri,
10069
+ verificationUriComplete: start.verificationUriComplete
10070
+ });
10411
10071
  }
10412
- const whoami_schema = {
10413
- name: "extension_whoami",
10414
- description: "Report the identity carried by the locally stored extension.dev token that extension_login minted (workspace/project scoped), plus its expiry, without revealing the token. The identity comes from that stored token alone; it does not change with the current working directory or whichever project folder you are in. The result records where the login was minted (apiRecordedAtLogin) without asserting a platform base URL: authenticated tools target their own api argument, EXTENSION_DEV_API_URL, or the production default. Returns logged-out status when no credentials are stored.",
10415
- inputSchema: {
10416
- type: "object",
10417
- properties: {}
10418
- }
10419
- };
10420
- async function whoami_handler() {
10072
+ async function readIdentity() {
10421
10073
  const creds = readCredentials();
10422
10074
  if (!creds) return JSON.stringify({
10423
10075
  ok: true,
10424
10076
  status: "logged-out",
10425
- message: "No stored credentials. Run extension_login to authenticate."
10077
+ message: "No stored credentials. Run extension_auth (action: login) to authenticate."
10426
10078
  });
10427
10079
  const now = Math.floor(Date.now() / 1000);
10428
10080
  const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
@@ -10431,7 +10083,7 @@ async function whoami_handler() {
10431
10083
  const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
10432
10084
  const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
10433
10085
  const messageParts = [];
10434
- messageParts.push(expired ? "The stored token has expired. Run extension_login to refresh it." : `Logged in as ${creds.workspaceSlug}/${creds.projectSlug}, per the token extension_login stored on this machine. That token is what scopes the identity: it does not follow the current working directory or project folder.`);
10086
+ messageParts.push(expired ? "The stored token has expired. Run extension_auth (action: login) to refresh it." : `Logged in as ${creds.workspaceSlug}/${creds.projectSlug}, per the token extension_auth stored on this machine. That token is what scopes the identity: it does not follow the current working directory or project folder.`);
10435
10087
  if (apiDiverges) messageParts.push(`This login was minted via ${recordedApi}, but authenticated tools do not read that recorded value: they target ${effectiveDefaultApi} unless given an api argument.`);
10436
10088
  if (envTokenSet) messageParts.push("EXTENSION_DEV_TOKEN is set and takes precedence over this stored login for authenticated tools; this report describes only the stored login.");
10437
10089
  return JSON.stringify({
@@ -10443,7 +10095,7 @@ async function whoami_handler() {
10443
10095
  apiRecordedAtLogin: recordedApi
10444
10096
  } : {},
10445
10097
  apiDefault: effectiveDefaultApi,
10446
- provider: creds.provider ?? "github",
10098
+ provider: creds.provider ?? "extensiondev",
10447
10099
  expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
10448
10100
  expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
10449
10101
  expired,
@@ -10454,213 +10106,61 @@ async function whoami_handler() {
10454
10106
  message: messageParts.join(" ")
10455
10107
  });
10456
10108
  }
10457
- const logout_schema = {
10458
- name: "extension_logout",
10459
- description: "Delete the locally stored extension.dev credentials. Does not revoke the token server-side (the response includes the dashboard URL where the token can be revoked); only removes it from this machine.",
10460
- inputSchema: {
10461
- type: "object",
10462
- properties: {}
10463
- }
10464
- };
10465
- async function logout_handler() {
10466
- const creds = readCredentials();
10467
- const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ? consoleProjectUrl({
10468
- workspace: creds.workspaceSlug,
10469
- project: creds.projectSlug
10470
- }, "settings/access-tokens") : null;
10471
- const result = clearCredentials();
10472
- return JSON.stringify({
10473
- ok: true,
10474
- cleared: result.cleared,
10475
- ...result.cleared && revokeUrl ? {
10476
- revokeUrl
10477
- } : {},
10478
- message: result.cleared ? revokeUrl ? `Local credentials removed. The token stays valid server-side until it expires; revoke it now at ${revokeUrl} (takes about a minute to propagate).` : "Local credentials removed. The token stays valid server-side until it expires; revoke it from the project's access-tokens page if needed." : "No stored credentials to remove."
10479
- });
10480
- }
10481
- const install_browser_schema = {
10482
- name: "extension_install_browser",
10483
- description: "Install a managed browser binary for extension testing. Useful in CI, Docker, or fresh environments where browsers are not pre-installed. This downloads ~580-625 MB in a single blocking call (30s+ on a fast link); on a slow network it can exceed a client's default request timeout, so allow a generous timeout when calling it.",
10484
- inputSchema: {
10485
- type: "object",
10486
- properties: {
10487
- browser: {
10488
- type: "string",
10489
- enum: [
10490
- "chrome",
10491
- "chromium",
10492
- "edge",
10493
- "firefox"
10494
- ],
10495
- description: "Browser to install"
10496
- }
10497
- },
10498
- required: [
10499
- "browser"
10500
- ]
10501
- }
10502
- };
10503
- async function install_browser_handler(args) {
10504
- const start = Date.now();
10505
- try {
10506
- await extensionInstall({
10507
- browser: args.browser
10508
- });
10509
- return JSON.stringify({
10510
- status: "installed",
10511
- browser: args.browser,
10512
- duration: Date.now() - start,
10513
- hint: `Browser "${args.browser}" is now available. Use extension_dev or extension_start with browser: "${args.browser}".`
10514
- });
10515
- } catch (err) {
10516
- return JSON.stringify({
10517
- status: "error",
10518
- browser: args.browser,
10519
- message: err instanceof Error ? err.message : String(err),
10520
- duration: Date.now() - start,
10521
- hint: "edge" === args.browser ? "Edge installation on Linux may require elevated privileges. Try using Chrome or Chromium instead." : "Check network connectivity and disk space. You can also install browsers manually."
10522
- });
10523
- }
10109
+ async function clearLocalCredentials() {
10110
+ const creds = readCredentials();
10111
+ const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ? consoleProjectUrl({
10112
+ workspace: creds.workspaceSlug,
10113
+ project: creds.projectSlug
10114
+ }, "settings/access-tokens") : null;
10115
+ const result = clearCredentials();
10116
+ return JSON.stringify({
10117
+ ok: true,
10118
+ cleared: result.cleared,
10119
+ ...result.cleared && revokeUrl ? {
10120
+ revokeUrl
10121
+ } : {},
10122
+ message: result.cleared ? revokeUrl ? `Local credentials removed. The token stays valid server-side until it expires; revoke it now at ${revokeUrl} (takes about a minute to propagate).` : "Local credentials removed. The token stays valid server-side until it expires; revoke it from the project's access-tokens page if needed." : "No stored credentials to remove."
10123
+ });
10524
10124
  }
10525
- const uninstall_browser_schema = {
10526
- name: "extension_uninstall_browser",
10527
- description: "Remove a managed browser binary from the Extension.js cache. Only touches the managed cache, never system-installed browsers.",
10125
+ const auth_schema = {
10126
+ name: "extension_auth",
10127
+ description: "Sign this machine in to extension.dev, report that login, or clear it. action:'status' (default) names the workspace/project the stored token is scoped to and when it expires, never the token itself; that identity comes from the stored token alone and does not change with the current working directory or whichever project folder you are in. action:'login' is two-phase: call with `project` to get a code plus a URL the user authorizes at extension.dev/device, then call again with the returned `deviceCode`. GitHub federation happens server-side, so no GitHub token lands on this machine. Minted tokens live at most 7 days (server-enforced), so CI must re-mint before expiry on the console's project settings -> Access tokens page. action:'logout' deletes the local credentials only; the token stays valid server-side until revoked at the URL the response returns.",
10528
10128
  inputSchema: {
10529
10129
  type: "object",
10530
10130
  properties: {
10531
- browser: {
10131
+ action: {
10532
10132
  type: "string",
10533
10133
  enum: [
10534
- "chrome",
10535
- "chromium",
10536
- "edge",
10537
- "firefox"
10134
+ "status",
10135
+ "login",
10136
+ "logout"
10538
10137
  ],
10539
- description: "Managed browser to remove"
10138
+ default: "status"
10540
10139
  },
10541
- all: {
10542
- type: "boolean",
10543
- default: false,
10544
- description: "Remove every managed browser binary"
10545
- }
10140
+ project: {
10141
+ type: "string",
10142
+ description: "login: target project as '<workspace>/<project>'; the token is scoped to it."
10143
+ },
10144
+ deviceCode: {
10145
+ type: "string",
10146
+ description: "login: resume token from the prior call's `deviceCode`; omit on the first call."
10147
+ },
10148
+ api: API_BASE
10546
10149
  },
10547
10150
  required: []
10548
10151
  }
10549
10152
  };
10550
- async function uninstall_browser_handler(args) {
10551
- const start = Date.now();
10552
- if (!args.browser && !args.all) return JSON.stringify({
10553
- status: "error",
10554
- message: "Provide a browser to remove, or set all: true."
10555
- });
10556
- try {
10557
- await extensionUninstall({
10558
- browser: args.browser,
10559
- all: args.all
10560
- });
10561
- return JSON.stringify({
10562
- status: "uninstalled",
10563
- target: args.all ? "all" : args.browser,
10564
- duration: Date.now() - start,
10565
- hint: "Use extension_list_browsers to confirm what remains in the managed cache."
10566
- });
10567
- } catch (err) {
10568
- return JSON.stringify({
10569
- status: "error",
10570
- target: args.all ? "all" : args.browser,
10571
- message: err instanceof Error ? err.message : String(err),
10572
- duration: Date.now() - start
10573
- });
10574
- }
10575
- }
10576
- const list_browsers_schema = {
10577
- name: "extension_list_browsers",
10578
- 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.",
10579
- inputSchema: {
10580
- type: "object",
10581
- properties: {}
10582
- }
10583
- };
10584
- const BROWSER_NAMES = [
10585
- "chrome",
10586
- "chromium",
10587
- "edge",
10588
- "firefox"
10589
- ];
10590
- function getDirSize(dir) {
10591
- let total = 0;
10592
- try {
10593
- for (const entry of readdirSync(dir, {
10594
- withFileTypes: true
10595
- })){
10596
- const full = join(dir, entry.name);
10597
- if (entry.isDirectory()) total += getDirSize(full);
10598
- else try {
10599
- total += statSync(full).size;
10600
- } catch {}
10601
- }
10602
- } catch {}
10603
- return total;
10604
- }
10605
- function list_browsers_formatBytes(bytes) {
10606
- if (bytes < 1024) return `${bytes} B`;
10607
- if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
10608
- return `${(bytes / 1048576).toFixed(1)} MB`;
10609
- }
10610
- async function list_browsers_handler() {
10611
- const cacheRoot = getManagedBrowsersCacheRoot();
10612
- const installed = [];
10613
- for (const browser of BROWSER_NAMES){
10614
- const browserDir = join(cacheRoot, browser);
10615
- if (existsSync(browserDir)) {
10616
- const size = getDirSize(browserDir);
10617
- installed.push({
10618
- browser,
10619
- path: browserDir,
10620
- size,
10621
- sizeFormatted: list_browsers_formatBytes(size),
10622
- engine: "firefox" === browser ? "gecko" : "chromium"
10623
- });
10624
- }
10625
- }
10626
- return JSON.stringify({
10627
- cacheRoot,
10628
- cacheExists: existsSync(cacheRoot),
10629
- installed,
10630
- availableToInstall: BROWSER_NAMES.filter((b)=>!installed.some((i)=>i.browser === b)),
10631
- hint: 0 === installed.length ? "No managed browsers found. Use extension_install_browser to install one, or use a system-installed browser." : `${installed.length} managed browser(s) found. Use extension_detect_browsers for a full system scan.`
10153
+ async function auth_handler(args) {
10154
+ const action = args.action ?? "status";
10155
+ if ("logout" === action) return clearLocalCredentials();
10156
+ if ("login" === action) return loginToProject({
10157
+ project: String(args.project ?? ""),
10158
+ deviceCode: args.deviceCode,
10159
+ api: args.api
10632
10160
  });
10161
+ return readIdentity();
10633
10162
  }
10634
10163
  const execFileAsync = promisify(execFile);
10635
- const detect_browsers_schema = {
10636
- name: "extension_detect_browsers",
10637
- description: "Detect which browsers are available for extension development. Checks both system-installed and managed browsers, returning paths and capabilities for each.",
10638
- inputSchema: {
10639
- type: "object",
10640
- properties: {
10641
- browsers: {
10642
- type: "array",
10643
- items: {
10644
- type: "string",
10645
- enum: [
10646
- "chrome",
10647
- "chromium",
10648
- "edge",
10649
- "brave",
10650
- "opera",
10651
- "vivaldi",
10652
- "yandex",
10653
- "firefox",
10654
- "waterfox",
10655
- "librewolf",
10656
- "safari"
10657
- ]
10658
- },
10659
- description: "Browsers to check. If omitted, checks all."
10660
- }
10661
- }
10662
- }
10663
- };
10664
10164
  const ALL_BROWSERS = [
10665
10165
  "chrome",
10666
10166
  "chromium",
@@ -10908,8 +10408,8 @@ async function getVersion(binaryPath, browser) {
10908
10408
  return null;
10909
10409
  }
10910
10410
  }
10911
- async function detect_browsers_handler(args) {
10912
- const browsersToCheck = args.browsers ?? [
10411
+ async function detectBrowsers(browsers) {
10412
+ const browsersToCheck = browsers ?? [
10913
10413
  ...ALL_BROWSERS
10914
10414
  ];
10915
10415
  const detected = [];
@@ -10951,8 +10451,161 @@ async function detect_browsers_handler(args) {
10951
10451
  available: available.map((d)=>d.browser),
10952
10452
  missing: missing.map((d)=>d.browser)
10953
10453
  },
10954
- 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."
10454
+ hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}.${missing.some((d)=>MANAGED_INSTALLABLE.has(d.browser)) ? ` Use extension_browsers with action: "install" to install ${missing.filter((d)=>MANAGED_INSTALLABLE.has(d.browser)).map((d)=>d.browser).join(", ")}.` : ""}` : "All requested browsers are available."
10455
+ });
10456
+ }
10457
+ const BROWSER_NAMES = [
10458
+ "chrome",
10459
+ "chromium",
10460
+ "edge",
10461
+ "firefox"
10462
+ ];
10463
+ function getDirSize(dir) {
10464
+ let total = 0;
10465
+ try {
10466
+ for (const entry of readdirSync(dir, {
10467
+ withFileTypes: true
10468
+ })){
10469
+ const full = join(dir, entry.name);
10470
+ if (entry.isDirectory()) total += getDirSize(full);
10471
+ else try {
10472
+ total += statSync(full).size;
10473
+ } catch {}
10474
+ }
10475
+ } catch {}
10476
+ return total;
10477
+ }
10478
+ function list_browsers_formatBytes(bytes) {
10479
+ if (bytes < 1024) return `${bytes} B`;
10480
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
10481
+ return `${(bytes / 1048576).toFixed(1)} MB`;
10482
+ }
10483
+ async function listManagedBrowsers() {
10484
+ const cacheRoot = getManagedBrowsersCacheRoot();
10485
+ const installed = [];
10486
+ for (const browser of BROWSER_NAMES){
10487
+ const browserDir = join(cacheRoot, browser);
10488
+ if (existsSync(browserDir)) {
10489
+ const size = getDirSize(browserDir);
10490
+ installed.push({
10491
+ browser,
10492
+ path: browserDir,
10493
+ size,
10494
+ sizeFormatted: list_browsers_formatBytes(size),
10495
+ engine: "firefox" === browser ? "gecko" : "chromium"
10496
+ });
10497
+ }
10498
+ }
10499
+ return JSON.stringify({
10500
+ cacheRoot,
10501
+ cacheExists: existsSync(cacheRoot),
10502
+ installed,
10503
+ availableToInstall: BROWSER_NAMES.filter((b)=>!installed.some((i)=>i.browser === b)),
10504
+ hint: 0 === installed.length ? "No managed browsers found. Use extension_browsers with action: \"install\" to install one, or use a system-installed browser." : `${installed.length} managed browser(s) found. Use extension_browsers with action: "detect" for a full system scan.`
10505
+ });
10506
+ }
10507
+ async function installManagedBrowser(browser) {
10508
+ const start = Date.now();
10509
+ try {
10510
+ await extensionInstall({
10511
+ browser
10512
+ });
10513
+ return JSON.stringify({
10514
+ status: "installed",
10515
+ browser,
10516
+ duration: Date.now() - start,
10517
+ hint: `Browser "${browser}" is now available. Use extension_dev or extension_start with browser: "${browser}".`
10518
+ });
10519
+ } catch (err) {
10520
+ return JSON.stringify({
10521
+ status: "error",
10522
+ browser,
10523
+ message: err instanceof Error ? err.message : String(err),
10524
+ duration: Date.now() - start,
10525
+ hint: "edge" === browser ? "Edge installation on Linux may require elevated privileges. Try using Chrome or Chromium instead." : "Check network connectivity and disk space. You can also install browsers manually."
10526
+ });
10527
+ }
10528
+ }
10529
+ async function uninstallManagedBrowser(args) {
10530
+ const start = Date.now();
10531
+ if (!args.browser && !args.all) return JSON.stringify({
10532
+ status: "error",
10533
+ message: "Provide a browser to remove, or set all: true."
10534
+ });
10535
+ try {
10536
+ await extensionUninstall({
10537
+ browser: args.browser,
10538
+ all: args.all
10539
+ });
10540
+ return JSON.stringify({
10541
+ status: "uninstalled",
10542
+ target: args.all ? "all" : args.browser,
10543
+ duration: Date.now() - start,
10544
+ hint: 'Use extension_browsers with action: "list" to confirm what remains in the managed cache.'
10545
+ });
10546
+ } catch (err) {
10547
+ return JSON.stringify({
10548
+ status: "error",
10549
+ target: args.all ? "all" : args.browser,
10550
+ message: err instanceof Error ? err.message : String(err),
10551
+ duration: Date.now() - start
10552
+ });
10553
+ }
10554
+ }
10555
+ const browsers_schema = {
10556
+ name: "extension_browsers",
10557
+ description: "Find, install, and remove the browsers extension tooling can launch. action:'detect' (default) scans BOTH system-installed and managed browsers and reports each one's binary path, version, engine, and debugger support. action:'list' reports only the managed cache this tool downloads into, with sizes on disk. action:'install' downloads a managed binary: ~580-625 MB in one blocking call, so allow a generous client timeout. action:'uninstall' removes managed binaries and never touches a system install.",
10558
+ inputSchema: {
10559
+ type: "object",
10560
+ properties: {
10561
+ action: {
10562
+ type: "string",
10563
+ enum: [
10564
+ "detect",
10565
+ "list",
10566
+ "install",
10567
+ "uninstall"
10568
+ ],
10569
+ default: "detect"
10570
+ },
10571
+ browsers: {
10572
+ type: "array",
10573
+ items: {
10574
+ type: "string",
10575
+ enum: REAL_BROWSERS
10576
+ },
10577
+ description: "detect: limit the scan to these. Omit to check all."
10578
+ },
10579
+ browser: {
10580
+ type: "string",
10581
+ enum: MANAGED_BROWSERS,
10582
+ description: "install/uninstall: which managed binary. Required for install."
10583
+ },
10584
+ all: {
10585
+ type: "boolean",
10586
+ default: false,
10587
+ description: "uninstall: remove every managed binary."
10588
+ }
10589
+ },
10590
+ required: []
10591
+ }
10592
+ };
10593
+ async function browsers_handler(args) {
10594
+ const action = args.action ?? "detect";
10595
+ if ("list" === action) return listManagedBrowsers();
10596
+ if ("install" === action) {
10597
+ if (!args.browser) return JSON.stringify({
10598
+ ok: false,
10599
+ status: "error",
10600
+ message: "action 'install' needs a browser: one of chrome, chromium, edge, firefox."
10601
+ });
10602
+ return installManagedBrowser(args.browser);
10603
+ }
10604
+ if ("uninstall" === action) return uninstallManagedBrowser({
10605
+ browser: args.browser,
10606
+ all: args.all
10955
10607
  });
10608
+ return detectBrowsers(args.browsers);
10956
10609
  }
10957
10610
  function typeOf(value) {
10958
10611
  if (Array.isArray(value)) return "array";
@@ -11120,40 +10773,32 @@ function inputValidationError(toolName, issues, inputSchema) {
11120
10773
  }
11121
10774
  const tools = [
11122
10775
  create_namespaceObject,
11123
- list_templates_namespaceObject,
10776
+ templates_namespaceObject,
11124
10777
  build_namespaceObject,
11125
10778
  dev_namespaceObject,
11126
10779
  start_namespaceObject,
11127
- preview_namespaceObject,
11128
10780
  preview_web_namespaceObject,
11129
10781
  shares_namespaceObject,
11130
10782
  stop_namespaceObject,
11131
- get_template_source_namespaceObject,
11132
10783
  manifest_validate_namespaceObject,
11133
10784
  theme_verify_namespaceObject,
10785
+ analyze_namespaceObject,
11134
10786
  inspect_namespaceObject,
11135
- source_inspect_namespaceObject,
11136
10787
  list_extensions_namespaceObject,
11137
10788
  logs_namespaceObject,
11138
10789
  eval_namespaceObject,
11139
10790
  storage_namespaceObject,
11140
10791
  reload_namespaceObject,
11141
10792
  open_namespaceObject,
11142
- dom_inspect_namespaceObject,
10793
+ dom_snapshot_namespaceObject,
11143
10794
  tools_publish_namespaceObject,
11144
- release_list_namespaceObject,
10795
+ release_status_namespaceObject,
11145
10796
  release_promote_namespaceObject,
11146
- deploy_namespaceObject,
11147
- store_status_namespaceObject,
10797
+ submit_namespaceObject,
11148
10798
  wait_namespaceObject,
11149
10799
  add_feature_namespaceObject,
11150
- login_namespaceObject,
11151
- whoami_namespaceObject,
11152
- logout_namespaceObject,
11153
- install_browser_namespaceObject,
11154
- uninstall_browser_namespaceObject,
11155
- list_browsers_namespaceObject,
11156
- detect_browsers_namespaceObject,
10800
+ auth_namespaceObject,
10801
+ browsers_namespaceObject,
11157
10802
  doctor_namespaceObject
11158
10803
  ];
11159
10804
  const toolMap = new Map();
@@ -11235,7 +10880,7 @@ async function runCli(cmd, args) {
11235
10880
  return i >= 0 ? args[i + 1] : void 0;
11236
10881
  };
11237
10882
  if ("whoami" === cmd) {
11238
- log(await whoami_handler());
10883
+ log(await readIdentity());
11239
10884
  return 0;
11240
10885
  }
11241
10886
  if ("release" === cmd) {
@@ -11267,7 +10912,7 @@ async function runCli(cmd, args) {
11267
10912
  return 1;
11268
10913
  }
11269
10914
  if ("logout" === cmd) {
11270
- log(await logout_handler());
10915
+ log(await clearLocalCredentials());
11271
10916
  return 0;
11272
10917
  }
11273
10918
  if ("login" === cmd) {
@@ -11279,55 +10924,28 @@ async function runCli(cmd, args) {
11279
10924
  const apiBase = resolveApiBase(flag("api"));
11280
10925
  try {
11281
10926
  const config = await fetchLoginConfig(apiBase);
11282
- if ("extensiondev" === config.provider) {
11283
- const start = await requestDeviceCode({
11284
- apiBase,
11285
- path: config.deviceCodeUrl,
11286
- project
11287
- });
11288
- log("");
11289
- log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
11290
- log("");
11291
- log(" Waiting for authorization...");
11292
- const poll = await pollDeviceToken({
11293
- apiBase,
11294
- path: config.deviceTokenUrl,
11295
- project,
11296
- deviceCode: start.deviceCode,
11297
- interval: start.interval,
11298
- budgetMs: 1000 * start.expiresIn
11299
- });
11300
- if (!poll.ok) {
11301
- log("denied" === poll.reason ? "Authorization was denied at extension.dev/device." : "expired" === poll.reason ? "The device code expired. Run login again." : "Timed out waiting for authorization. Run login again.");
11302
- return 1;
11303
- }
11304
- log(`Logged in to ${poll.creds.workspaceSlug}/${poll.creds.projectSlug}.`);
11305
- return 0;
11306
- }
11307
- const start = await startDeviceCode({
11308
- clientId: config.clientId,
11309
- scope: config.scope
10927
+ const start = await requestDeviceCode({
10928
+ apiBase,
10929
+ path: config.deviceCodeUrl,
10930
+ project
11310
10931
  });
11311
10932
  log("");
11312
10933
  log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
11313
10934
  log("");
11314
10935
  log(" Waiting for authorization...");
11315
- const poll = await pollForToken({
11316
- clientId: config.clientId,
10936
+ const poll = await pollDeviceToken({
10937
+ apiBase,
10938
+ path: config.deviceTokenUrl,
10939
+ project,
11317
10940
  deviceCode: start.deviceCode,
11318
10941
  interval: start.interval,
11319
10942
  budgetMs: 1000 * start.expiresIn
11320
10943
  });
11321
10944
  if (!poll.ok) {
11322
- log("denied" === poll.reason ? "Authorization was denied on GitHub." : "Timed out waiting for authorization. Run login again.");
10945
+ log("denied" === poll.reason ? "Authorization was denied at extension.dev/device." : "expired" === poll.reason ? "The device code expired. Run login again." : "Timed out waiting for authorization. Run login again.");
11323
10946
  return 1;
11324
10947
  }
11325
- const creds = await exchangeAndPersist({
11326
- apiBase,
11327
- githubToken: poll.githubToken,
11328
- project
11329
- });
11330
- log(`Logged in to ${creds.workspaceSlug}/${creds.projectSlug}.`);
10948
+ log(`Logged in to ${poll.creds.workspaceSlug}/${poll.creds.projectSlug}.`);
11331
10949
  return 0;
11332
10950
  } catch (err) {
11333
10951
  log(err instanceof Error ? err.message : String(err));