@extension.dev/mcp 7.0.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 (60) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +224 -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 +1026 -1186
  14. package/dist/src/lib/common-schema.d.ts +28 -0
  15. package/dist/src/lib/launch-flags.d.ts +6 -6
  16. package/dist/src/lib/session-identity.d.ts +16 -0
  17. package/dist/src/tools/add-feature.d.ts +2 -2
  18. package/dist/src/tools/analyze.d.ts +29 -0
  19. package/dist/src/tools/auth.d.ts +33 -0
  20. package/dist/src/tools/browsers.d.ts +39 -0
  21. package/dist/src/tools/build.d.ts +5 -6
  22. package/dist/src/tools/detect-browsers.d.ts +1 -20
  23. package/dist/src/tools/dev.d.ts +11 -11
  24. package/dist/src/tools/{dom-inspect.d.ts → dom-snapshot.d.ts} +6 -6
  25. package/dist/src/tools/eval.d.ts +6 -6
  26. package/dist/src/tools/get-template-source.d.ts +1 -22
  27. package/dist/src/tools/inspect.d.ts +33 -5
  28. package/dist/src/tools/install-browser.d.ts +1 -18
  29. package/dist/src/tools/list-browsers.d.ts +1 -9
  30. package/dist/src/tools/list-extensions.d.ts +4 -4
  31. package/dist/src/tools/list-templates.d.ts +1 -35
  32. package/dist/src/tools/login.d.ts +1 -23
  33. package/dist/src/tools/logout.d.ts +1 -9
  34. package/dist/src/tools/logs-schema.d.ts +2 -2
  35. package/dist/src/tools/open.d.ts +6 -6
  36. package/dist/src/tools/preview-web.d.ts +4 -4
  37. package/dist/src/tools/publish.d.ts +2 -2
  38. package/dist/src/tools/release-list.d.ts +1 -23
  39. package/dist/src/tools/release-promote.d.ts +2 -2
  40. package/dist/src/tools/release-status.d.ts +37 -0
  41. package/dist/src/tools/reload.d.ts +6 -6
  42. package/dist/src/tools/shares.d.ts +2 -2
  43. package/dist/src/tools/start.d.ts +16 -10
  44. package/dist/src/tools/stop.d.ts +2 -2
  45. package/dist/src/tools/storage.d.ts +6 -6
  46. package/dist/src/tools/store-status.d.ts +1 -23
  47. package/dist/src/tools/{deploy.d.ts → submit.d.ts} +4 -4
  48. package/dist/src/tools/{source-inspect.d.ts → templates.d.ts} +27 -23
  49. package/dist/src/tools/uninstall-browser.d.ts +1 -21
  50. package/dist/src/tools/wait.d.ts +4 -4
  51. package/dist/src/tools/whoami.d.ts +1 -9
  52. package/extensions/live-preview/chromium/action/index.js +1 -9
  53. package/extensions/live-preview/chromium/background/service_worker.js +3 -11
  54. package/extensions/live-preview/chromium/manifest.json +1 -1
  55. package/package.json +2 -2
  56. package/server.json +3 -3
  57. package/dist/src/__tests__/fixtures/ready-contract.d.ts +0 -7
  58. package/dist/src/__tests__/setup-session-dir.d.ts +0 -1
  59. package/dist/src/tools/preview.d.ts +0 -66
  60. /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":"7.0.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");
@@ -763,6 +713,111 @@ function persistTokenResponse(args) {
763
713
  writeCredentials(creds);
764
714
  return creds;
765
715
  }
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() {
743
+ try {
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
+ };
757
+ } catch {
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
816
+ };
817
+ } catch {
818
+ return {};
819
+ }
820
+ }
766
821
  function _define_property(obj, key, value) {
767
822
  if (key in obj) Object.defineProperty(obj, key, {
768
823
  value: value,
@@ -830,7 +885,8 @@ class RegistryAccessTokens {
830
885
  method: "POST",
831
886
  headers: {
832
887
  authorization: `Bearer ${token}`,
833
- "content-type": "application/json"
888
+ "content-type": "application/json",
889
+ ...identityHeaders("extension_registry_access")
834
890
  },
835
891
  body: JSON.stringify({
836
892
  workspaceSlug: ref.workspace,
@@ -984,7 +1040,7 @@ async function fetchRegistryJson(url, fetchImpl = fetch, options) {
984
1040
  };
985
1041
  const grant = await tokens.get(ref, options?.api);
986
1042
  if ("ok" !== grant.status) {
987
- 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;
988
1044
  return {
989
1045
  ok: false,
990
1046
  status: res.status,
@@ -1090,7 +1146,7 @@ function detectPackageManager(projectPath) {
1090
1146
  }
1091
1147
  const create_schema = {
1092
1148
  name: "extension_create",
1093
- 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.",
1094
1150
  inputSchema: {
1095
1151
  type: "object",
1096
1152
  properties: {
@@ -1100,12 +1156,12 @@ const create_schema = {
1100
1156
  },
1101
1157
  parentDir: {
1102
1158
  type: "string",
1103
- 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."
1104
1160
  },
1105
1161
  template: {
1106
1162
  type: "string",
1107
1163
  default: "typescript",
1108
- 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."
1109
1165
  },
1110
1166
  install: {
1111
1167
  type: "boolean",
@@ -1204,7 +1260,7 @@ async function create_handler(args) {
1204
1260
  defaultsApplied: {
1205
1261
  parentDir: args.parentDir ? `${resolvedParent} (explicit)` : `${resolvedParent} (default: the MCP server process cwd, not yours; pass parentDir to choose)`,
1206
1262
  ...void 0 === args.template ? {
1207
- 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)"
1208
1264
  } : {},
1209
1265
  packageManager: `${packageManager} (auto-detected by the scaffolder, not asked)`,
1210
1266
  browser: "chrome (default: extension_dev and extension_build target chrome unless you pass browser)",
@@ -1408,12 +1464,99 @@ async function getTemplateBySlug(slug) {
1408
1464
  const meta = await fetchTemplatesMeta();
1409
1465
  return meta.templates.find((t)=>t.slug === slug);
1410
1466
  }
1411
- const list_templates_schema = {
1412
- name: "extension_list_templates",
1413
- 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.",
1414
1534
  inputSchema: {
1415
1535
  type: "object",
1416
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
+ },
1417
1560
  surface: {
1418
1561
  type: "string",
1419
1562
  enum: [
@@ -1422,7 +1565,7 @@ const list_templates_schema = {
1422
1565
  "newtab",
1423
1566
  "background"
1424
1567
  ],
1425
- 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."
1426
1569
  },
1427
1570
  framework: {
1428
1571
  type: "string",
@@ -1433,46 +1576,104 @@ const list_templates_schema = {
1433
1576
  "preact",
1434
1577
  ""
1435
1578
  ],
1436
- 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)."
1437
1580
  },
1438
1581
  tags: {
1439
1582
  type: "array",
1440
1583
  items: {
1441
1584
  type: "string"
1442
1585
  },
1443
- description: "Filter by tags (e.g. ['ai', 'chat'])"
1586
+ description: "list: filter by tags, e.g. ['ai', 'chat']."
1444
1587
  },
1445
1588
  featured: {
1446
1589
  type: "boolean",
1447
- description: "Only show featured templates"
1448
- },
1449
- query: {
1450
- type: "string",
1451
- 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."
1452
1591
  }
1453
- }
1592
+ },
1593
+ required: []
1454
1594
  }
1455
1595
  };
1456
- async function list_templates_handler(args) {
1457
- const templates = await listTemplates(args);
1458
- const results = templates.map((t)=>({
1459
- slug: t.slug,
1460
- description: t.description,
1461
- uiFramework: t.uiFramework,
1462
- frameworkLabel: t.uiFramework || "vanilla",
1463
- surfaces: t.surfaces,
1464
- tags: t.tags,
1465
- difficulty: t.difficulty,
1466
- featured: t.featured,
1467
- useCases: t.useCases,
1468
- repositoryUrl: t.repositoryUrl,
1469
- downloads: t.downloads
1470
- }));
1471
- return JSON.stringify({
1472
- count: results.length,
1473
- 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
1474
1614
  });
1475
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
+ };
1476
1677
  const PINNED_CLI_VERSION = String(package_namespaceObject.El.OP ?? "latest").replace(/^[\^~]/, "");
1477
1678
  function pinnedCliVersion() {
1478
1679
  const override = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
@@ -1968,11 +2169,11 @@ function materializeCarrier(projectPath, browser) {
1968
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.",
1969
2170
  "Chromium-family only: Firefox has no externally_connectable channel for web pages."
1970
2171
  ],
1971
- 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.",
1972
2173
  ...carrierId ? {
1973
2174
  bridgeProtocol: {
1974
2175
  carrierExtensionId: carrierId,
1975
- allowedOrigins: "https://preview.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/*",
1976
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.",
1977
2178
  example: [
1978
2179
  `const id = '${carrierId}'`,
@@ -2092,32 +2293,8 @@ const build_schema = {
2092
2293
  inputSchema: {
2093
2294
  type: "object",
2094
2295
  properties: {
2095
- projectPath: {
2096
- type: "string",
2097
- description: "Path to the extension project root"
2098
- },
2099
- browser: {
2100
- type: "string",
2101
- enum: [
2102
- "chrome",
2103
- "chromium",
2104
- "edge",
2105
- "brave",
2106
- "opera",
2107
- "vivaldi",
2108
- "yandex",
2109
- "firefox",
2110
- "waterfox",
2111
- "librewolf",
2112
- "safari",
2113
- "chromium-based",
2114
- "gecko-based",
2115
- "firefox-based",
2116
- "webkit-based"
2117
- ],
2118
- default: "chrome",
2119
- description: "Target browser"
2120
- },
2296
+ projectPath: PROJECT_PATH,
2297
+ browser: LAUNCH_BROWSER,
2121
2298
  zip: {
2122
2299
  type: "boolean",
2123
2300
  default: false,
@@ -2155,7 +2332,7 @@ const build_schema = {
2155
2332
  skipValidation: {
2156
2333
  type: "boolean",
2157
2334
  default: false,
2158
- 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."
2159
2336
  }
2160
2337
  },
2161
2338
  required: [
@@ -2377,22 +2554,19 @@ async function build_handler(args) {
2377
2554
  }
2378
2555
  const stop_schema = {
2379
2556
  name: "extension_stop",
2380
- 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.",
2381
2558
  inputSchema: {
2382
2559
  type: "object",
2383
2560
  properties: {
2384
- projectPath: {
2385
- type: "string",
2386
- description: "Path to the extension project root"
2387
- },
2561
+ projectPath: PROJECT_PATH,
2388
2562
  browser: {
2389
2563
  type: "string",
2390
- 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."
2391
2565
  },
2392
2566
  all: {
2393
2567
  type: "boolean",
2394
2568
  default: false,
2395
- 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."
2396
2570
  }
2397
2571
  },
2398
2572
  required: []
@@ -2544,7 +2718,7 @@ async function stop_handler(args) {
2544
2718
  const LAUNCH_FLAG_SCHEMA = {
2545
2719
  profile: {
2546
2720
  type: "string",
2547
- 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.'
2548
2722
  },
2549
2723
  startingUrl: {
2550
2724
  type: "string",
@@ -2552,26 +2726,26 @@ const LAUNCH_FLAG_SCHEMA = {
2552
2726
  },
2553
2727
  chromiumBinary: {
2554
2728
  type: "string",
2555
- description: "Path to a custom Chromium-based binary (overrides browser)"
2729
+ description: "Custom Chromium-based binary (overrides browser)"
2556
2730
  },
2557
2731
  geckoBinary: {
2558
2732
  type: "string",
2559
- description: "Path to a custom Gecko/Firefox binary (overrides browser)"
2733
+ description: "Custom Gecko/Firefox binary (overrides browser)"
2560
2734
  },
2561
2735
  host: {
2562
2736
  type: "string",
2563
- 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."
2564
2738
  },
2565
2739
  publicHost: {
2566
2740
  type: "string",
2567
- 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"
2568
2742
  },
2569
2743
  extensions: {
2570
2744
  type: "array",
2571
2745
  items: {
2572
2746
  type: "string"
2573
2747
  },
2574
- 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"
2575
2749
  }
2576
2750
  };
2577
2751
  function launchFlagArgs(args) {
@@ -2587,35 +2761,12 @@ function launchFlagArgs(args) {
2587
2761
  }
2588
2762
  const dev_schema = {
2589
2763
  name: "extension_dev",
2590
- 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.",
2591
2765
  inputSchema: {
2592
2766
  type: "object",
2593
2767
  properties: {
2594
- projectPath: {
2595
- type: "string",
2596
- description: "Path to the extension project root"
2597
- },
2598
- browser: {
2599
- type: "string",
2600
- enum: [
2601
- "chrome",
2602
- "chromium",
2603
- "edge",
2604
- "brave",
2605
- "opera",
2606
- "vivaldi",
2607
- "yandex",
2608
- "firefox",
2609
- "waterfox",
2610
- "librewolf",
2611
- "safari",
2612
- "chromium-based",
2613
- "gecko-based",
2614
- "firefox-based",
2615
- "webkit-based"
2616
- ],
2617
- default: "chrome"
2618
- },
2768
+ projectPath: PROJECT_PATH,
2769
+ browser: LAUNCH_BROWSER,
2619
2770
  port: {
2620
2771
  type: "number",
2621
2772
  description: "Dev server port (0 for auto-assign)"
@@ -2623,7 +2774,7 @@ const dev_schema = {
2623
2774
  noBrowser: {
2624
2775
  type: "boolean",
2625
2776
  default: false,
2626
- description: "Start dev server without launching browser"
2777
+ description: "Start the dev server without launching a browser"
2627
2778
  },
2628
2779
  polyfill: {
2629
2780
  type: "boolean",
@@ -2634,22 +2785,22 @@ const dev_schema = {
2634
2785
  replace: {
2635
2786
  type: "boolean",
2636
2787
  default: false,
2637
- 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."
2638
2789
  },
2639
2790
  allowControl: {
2640
2791
  type: "boolean",
2641
2792
  default: false,
2642
- 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"
2643
2794
  },
2644
2795
  allowEval: {
2645
2796
  type: "boolean",
2646
2797
  default: false,
2647
- 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."
2648
2799
  },
2649
2800
  carrier: {
2650
2801
  type: "boolean",
2651
2802
  default: false,
2652
- 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 (preview.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."
2653
2804
  }
2654
2805
  },
2655
2806
  required: [
@@ -2762,11 +2913,11 @@ async function dev_handler(args) {
2762
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.`
2763
2914
  });
2764
2915
  }
2765
- const controlVerbs = "storage, reload, open, dom_inspect";
2916
+ const controlVerbs = "storage, reload, open, dom_snapshot";
2766
2917
  const capabilities = {
2767
2918
  allowControl,
2768
2919
  allowEval: Boolean(args.allowEval),
2769
- 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)"
2770
2921
  };
2771
2922
  const boundPort = contractBoundPort(args.projectPath, browser, spawnedAt);
2772
2923
  if (null !== boundPort && boundPort !== args.port) registerSession({
@@ -2804,7 +2955,7 @@ async function dev_handler(args) {
2804
2955
  } : {}
2805
2956
  } : {},
2806
2957
  capabilities,
2807
- 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.",
2808
2959
  earlyOutput: cleanOutput.slice(0, 500),
2809
2960
  logPath
2810
2961
  });
@@ -2827,39 +2978,21 @@ function denoiseEarlyOutput(raw) {
2827
2978
  }
2828
2979
  const start_schema = {
2829
2980
  name: "extension_start",
2830
- 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.",
2831
2982
  inputSchema: {
2832
2983
  type: "object",
2833
2984
  properties: {
2834
- projectPath: {
2835
- type: "string",
2836
- description: "Path to the extension project root"
2837
- },
2838
- browser: {
2839
- type: "string",
2840
- enum: [
2841
- "chrome",
2842
- "chromium",
2843
- "edge",
2844
- "brave",
2845
- "opera",
2846
- "vivaldi",
2847
- "yandex",
2848
- "firefox",
2849
- "waterfox",
2850
- "librewolf",
2851
- "safari",
2852
- "chromium-based",
2853
- "gecko-based",
2854
- "firefox-based",
2855
- "webkit-based"
2856
- ],
2857
- 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."
2858
2991
  },
2859
2992
  polyfill: {
2860
2993
  type: "boolean",
2861
2994
  default: true,
2862
- description: "Apply cross-browser polyfill"
2995
+ description: "Apply cross-browser polyfill (build only)"
2863
2996
  },
2864
2997
  port: {
2865
2998
  type: "number",
@@ -2868,7 +3001,7 @@ const start_schema = {
2868
3001
  noBrowser: {
2869
3002
  type: "boolean",
2870
3003
  default: false,
2871
- description: "Build and serve without launching a browser"
3004
+ description: "Serve without launching a browser"
2872
3005
  },
2873
3006
  ...LAUNCH_FLAG_SCHEMA
2874
3007
  },
@@ -2879,13 +3012,15 @@ const start_schema = {
2879
3012
  };
2880
3013
  async function start_handler(args) {
2881
3014
  const browser = args.browser ?? "chrome";
3015
+ const building = false !== args.build;
3016
+ const command = building ? "start" : "preview";
2882
3017
  const cliArgs = [
2883
- "start",
3018
+ command,
2884
3019
  args.projectPath,
2885
3020
  "--browser",
2886
3021
  browser
2887
3022
  ];
2888
- if (false === args.polyfill) cliArgs.push("--polyfill", "false");
3023
+ if (building && false === args.polyfill) cliArgs.push("--polyfill", "false");
2889
3024
  if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
2890
3025
  if (args.noBrowser) cliArgs.push("--no-browser");
2891
3026
  cliArgs.push(...launchFlagArgs(args));
@@ -2899,7 +3034,7 @@ async function start_handler(args) {
2899
3034
  pid,
2900
3035
  browser,
2901
3036
  projectPath: args.projectPath,
2902
- command: "start"
3037
+ command
2903
3038
  });
2904
3039
  child.on("exit", ()=>removeSession(args.projectPath, browser));
2905
3040
  await new Promise((resolve)=>setTimeout(resolve, 5000));
@@ -2915,10 +3050,10 @@ async function start_handler(args) {
2915
3050
  pid,
2916
3051
  exitCode: code,
2917
3052
  signal,
2918
- error: `The preview server exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running.`,
3053
+ error: `The ${command} process exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running.`,
2919
3054
  output: earlyOutput.slice(0, 2000),
2920
3055
  logPath,
2921
- 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."
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."
2922
3057
  });
2923
3058
  }
2924
3059
  const exitStamp = browserExitStamp(args.projectPath, browser, spawnedAt);
@@ -2929,7 +3064,7 @@ async function start_handler(args) {
2929
3064
  browser,
2930
3065
  pid,
2931
3066
  ...exitStamp,
2932
- 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.`,
2933
3068
  output: earlyOutput.slice(0, 2000),
2934
3069
  logPath,
2935
3070
  hint: "Read `output` above and extension_logs for the cause, then call extension_stop to clean up before retrying."
@@ -2939,122 +3074,8 @@ async function start_handler(args) {
2939
3074
  pid,
2940
3075
  browser,
2941
3076
  projectPath: args.projectPath,
2942
- status: "started",
2943
- 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.",
2944
- earlyOutput: earlyOutput.slice(0, 500),
2945
- logPath
2946
- });
2947
- }
2948
- const preview_schema = {
2949
- name: "extension_preview",
2950
- description: "Preview a production-built extension in a browser. Uses dist/ output directly. The extension must be built first with extension_build.",
2951
- inputSchema: {
2952
- type: "object",
2953
- properties: {
2954
- projectPath: {
2955
- type: "string",
2956
- description: "Path to the extension project root"
2957
- },
2958
- browser: {
2959
- type: "string",
2960
- enum: [
2961
- "chrome",
2962
- "chromium",
2963
- "edge",
2964
- "brave",
2965
- "opera",
2966
- "vivaldi",
2967
- "yandex",
2968
- "firefox",
2969
- "waterfox",
2970
- "librewolf",
2971
- "safari",
2972
- "chromium-based",
2973
- "gecko-based",
2974
- "firefox-based",
2975
- "webkit-based"
2976
- ],
2977
- default: "chrome"
2978
- },
2979
- port: {
2980
- type: "number",
2981
- description: "Server port (0 for auto-assign)"
2982
- },
2983
- noBrowser: {
2984
- type: "boolean",
2985
- default: false,
2986
- description: "Serve the preview without launching a browser"
2987
- },
2988
- ...LAUNCH_FLAG_SCHEMA
2989
- },
2990
- required: [
2991
- "projectPath"
2992
- ]
2993
- }
2994
- };
2995
- async function preview_handler(args) {
2996
- const browser = args.browser ?? "chrome";
2997
- const cliArgs = [
2998
- "preview",
2999
- args.projectPath,
3000
- "--browser",
3001
- browser
3002
- ];
3003
- if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
3004
- if (args.noBrowser) cliArgs.push("--no-browser");
3005
- cliArgs.push(...launchFlagArgs(args));
3006
- const spawnedAt = Date.now();
3007
- const spawned = spawnExtensionCli(cliArgs, {
3008
- projectDir: args.projectPath
3009
- });
3010
- const { child, logPath } = spawned;
3011
- const pid = child.pid;
3012
- registerSession({
3013
- pid,
3014
- browser,
3015
- projectPath: args.projectPath,
3016
- command: "preview"
3017
- });
3018
- child.on("exit", ()=>removeSession(args.projectPath, browser));
3019
- await new Promise((resolve)=>setTimeout(resolve, 5000));
3020
- const earlyOutput = spawned.readOutput();
3021
- if (null !== child.exitCode || null !== child.signalCode) {
3022
- const code = child.exitCode;
3023
- const signal = child.signalCode;
3024
- return JSON.stringify({
3025
- ok: false,
3026
- status: "exited",
3027
- projectPath: args.projectPath,
3028
- browser,
3029
- pid,
3030
- exitCode: code,
3031
- signal,
3032
- error: `The preview process exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). Nothing is running.`,
3033
- output: earlyOutput.slice(0, 2000),
3034
- logPath,
3035
- 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."
3036
- });
3037
- }
3038
- const exitStamp = browserExitStamp(args.projectPath, browser, spawnedAt);
3039
- if (exitStamp) return JSON.stringify({
3040
- ok: false,
3041
- status: "browser-exited",
3042
- projectPath: args.projectPath,
3043
- browser,
3044
- pid,
3045
- ...exitStamp,
3046
- 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.",
3047
- output: earlyOutput.slice(0, 2000),
3048
- logPath,
3049
- hint: "Read `output` above and extension_logs for the cause, then call extension_stop to clean up before retrying."
3050
- });
3051
- return JSON.stringify({
3052
- ok: true,
3053
- pid,
3054
- browser,
3055
- projectPath: args.projectPath,
3056
- status: "launched",
3057
- 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.",
3058
3079
  earlyOutput: earlyOutput.slice(0, 500),
3059
3080
  logPath
3060
3081
  });
@@ -3672,7 +3693,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
3672
3693
  name: "NavigateFailed",
3673
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).`
3674
3695
  },
3675
- 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."
3676
3697
  });
3677
3698
  return JSON.stringify({
3678
3699
  ok: true,
@@ -3682,7 +3703,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
3682
3703
  url: settled.url,
3683
3704
  title: settled.title
3684
3705
  },
3685
- 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')."
3686
3707
  });
3687
3708
  }
3688
3709
  async function resolveBridgeBaseUrl(projectPath, browser, timeout) {
@@ -3781,7 +3802,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
3781
3802
  name: "NavigateFailed",
3782
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."}`
3783
3804
  },
3784
- 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."
3785
3806
  });
3786
3807
  }
3787
3808
  return JSON.stringify({
@@ -3801,7 +3822,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
3801
3822
  title: settled.title,
3802
3823
  url: settled.url
3803
3824
  },
3804
- 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."
3805
3826
  });
3806
3827
  } catch (e) {
3807
3828
  return JSON.stringify({
@@ -4042,7 +4063,7 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
4042
4063
  popupBounds = await applyPopupBounds(projectPath, browser, parsed.target.targetId);
4043
4064
  if (popupBounds) parsed.renderedAsTab.popupBounds = popupBounds;
4044
4065
  }
4045
- 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.";
4046
4067
  return JSON.stringify(parsed);
4047
4068
  }
4048
4069
  } catch {}
@@ -4083,14 +4104,11 @@ async function confirmSurfaceTarget(projectPath, browser, surface, raw) {
4083
4104
  }
4084
4105
  const open_schema = {
4085
4106
  name: "extension_open",
4086
- 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).",
4087
4108
  inputSchema: {
4088
4109
  type: "object",
4089
4110
  properties: {
4090
- projectPath: {
4091
- type: "string",
4092
- description: "Path to the extension project root (must have an active dev session)"
4093
- },
4111
+ projectPath: SESSION_PROJECT_PATH,
4094
4112
  surface: {
4095
4113
  type: "string",
4096
4114
  enum: [
@@ -4103,7 +4121,7 @@ const open_schema = {
4103
4121
  "action",
4104
4122
  "command"
4105
4123
  ],
4106
- 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."
4107
4125
  },
4108
4126
  name: {
4109
4127
  type: "string",
@@ -4111,21 +4129,15 @@ const open_schema = {
4111
4129
  },
4112
4130
  url: {
4113
4131
  type: "string",
4114
- 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."
4115
4133
  },
4116
4134
  asTab: {
4117
4135
  type: "boolean",
4118
4136
  default: false,
4119
- 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."
4120
- },
4121
- browser: {
4122
- type: "string",
4123
- 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."
4124
4138
  },
4125
- timeout: {
4126
- type: "number",
4127
- description: "Command timeout in ms (default 5000)"
4128
- }
4139
+ browser: SESSION_BROWSER,
4140
+ timeout: CALL_TIMEOUT
4129
4141
  },
4130
4142
  required: [
4131
4143
  "projectPath"
@@ -4240,7 +4252,8 @@ async function publish(options = {}) {
4240
4252
  method: "POST",
4241
4253
  headers: {
4242
4254
  authorization: `Bearer ${token}`,
4243
- "content-type": "application/json"
4255
+ "content-type": "application/json",
4256
+ ...identityHeaders("extension_publish")
4244
4257
  },
4245
4258
  body: JSON.stringify(body)
4246
4259
  });
@@ -4316,7 +4329,7 @@ async function uploadPreview(options) {
4316
4329
  ok: false,
4317
4330
  error: {
4318
4331
  name: "PreviewAuthError",
4319
- 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)."
4320
4333
  }
4321
4334
  };
4322
4335
  const apiCheck = safeApiBase(resolveApiBase(options.api));
@@ -4370,7 +4383,8 @@ async function uploadPreview(options) {
4370
4383
  method: "POST",
4371
4384
  headers: {
4372
4385
  authorization: `Bearer ${token}`,
4373
- "content-type": "application/json"
4386
+ "content-type": "application/json",
4387
+ ...identityHeaders("extension_preview_web")
4374
4388
  },
4375
4389
  body: JSON.stringify({
4376
4390
  kind: "dist",
@@ -4552,7 +4566,7 @@ async function buildShare(projectPath, distDir, manifest, browser) {
4552
4566
  errorName: result.error.name,
4553
4567
  reason: result.error.message,
4554
4568
  ...isAuth ? {
4555
- 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)."
4556
4570
  } : {}
4557
4571
  };
4558
4572
  }
@@ -4620,40 +4634,25 @@ function detectSurfaces(manifest) {
4620
4634
  }
4621
4635
  const preview_web_schema = {
4622
4636
  name: "extension_preview_web",
4623
- 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.extension.dev is the author's door for a local build: it renders YOUR build and carries the Emulated/Real lane toggle and the Trace tab. 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.",
4624
4638
  inputSchema: {
4625
4639
  type: "object",
4626
4640
  properties: {
4627
- projectPath: {
4628
- type: "string",
4629
- description: "Path to the extension project root"
4630
- },
4641
+ projectPath: PROJECT_PATH,
4631
4642
  browser: {
4632
4643
  type: "string",
4633
- enum: [
4634
- "chrome",
4635
- "chromium",
4636
- "edge",
4637
- "brave",
4638
- "opera",
4639
- "vivaldi",
4640
- "yandex",
4641
- "firefox",
4642
- "waterfox",
4643
- "librewolf",
4644
- "safari"
4645
- ],
4644
+ enum: REAL_BROWSERS,
4646
4645
  default: "chrome",
4647
- description: "Which dist/<browser> output to preview. The emulator renders it as mocked Chrome regardless."
4646
+ description: "Which dist/<browser> output to preview. The emulator renders it as mocked Chrome either way."
4648
4647
  },
4649
4648
  build: {
4650
4649
  type: "boolean",
4651
4650
  default: true,
4652
- 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."
4653
4652
  },
4654
4653
  distPath: {
4655
4654
  type: "string",
4656
- description: "Preview this built directory directly instead of resolving dist/<browser> under projectPath. Implies build:false."
4655
+ description: "Preview this built directory instead of dist/<browser> under projectPath. Implies build:false."
4657
4656
  },
4658
4657
  hostUrl: {
4659
4658
  type: "string",
@@ -4662,34 +4661,22 @@ const preview_web_schema = {
4662
4661
  probe: {
4663
4662
  type: "boolean",
4664
4663
  default: true,
4665
- 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."
4666
4665
  },
4667
4666
  open: {
4668
4667
  type: "boolean",
4669
4668
  default: false,
4670
- 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."
4671
4670
  },
4672
4671
  openIn: {
4673
4672
  type: "string",
4674
- enum: [
4675
- "chrome",
4676
- "chromium",
4677
- "edge",
4678
- "brave",
4679
- "opera",
4680
- "vivaldi",
4681
- "yandex",
4682
- "firefox",
4683
- "waterfox",
4684
- "librewolf",
4685
- "safari"
4686
- ],
4687
- 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`."
4688
4675
  },
4689
4676
  share: {
4690
4677
  type: "boolean",
4691
4678
  default: false,
4692
- 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."
4693
4680
  }
4694
4681
  },
4695
4682
  required: [
@@ -4850,7 +4837,7 @@ function authError(name) {
4850
4837
  ok: false,
4851
4838
  error: {
4852
4839
  name,
4853
- 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)."
4854
4841
  }
4855
4842
  };
4856
4843
  }
@@ -4884,7 +4871,8 @@ async function listArtifacts(options = {}) {
4884
4871
  res = await doFetch(url.toString(), {
4885
4872
  headers: {
4886
4873
  authorization: `Bearer ${token}`,
4887
- accept: "application/json"
4874
+ accept: "application/json",
4875
+ ...identityHeaders("extension_shares")
4888
4876
  }
4889
4877
  });
4890
4878
  } catch (err) {
@@ -4939,7 +4927,8 @@ async function revokeArtifact(options) {
4939
4927
  method: "DELETE",
4940
4928
  headers: {
4941
4929
  authorization: `Bearer ${token}`,
4942
- accept: "application/json"
4930
+ accept: "application/json",
4931
+ ...identityHeaders("extension_shares")
4943
4932
  }
4944
4933
  });
4945
4934
  } catch (err) {
@@ -4980,10 +4969,10 @@ async function revokeArtifact(options) {
4980
4969
  }
4981
4970
  };
4982
4971
  }
4983
- 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).";
4984
4973
  const shares_schema = {
4985
4974
  name: "extension_shares",
4986
- 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.",
4987
4976
  inputSchema: {
4988
4977
  type: "object",
4989
4978
  properties: {
@@ -5021,10 +5010,7 @@ const shares_schema = {
5021
5010
  type: "number",
5022
5011
  description: "How many shares to return, 1 to 200 (platform default 100). A cut list comes back with truncated:true."
5023
5012
  },
5024
- api: {
5025
- type: "string",
5026
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)."
5027
- }
5013
+ api: API_BASE
5028
5014
  },
5029
5015
  required: []
5030
5016
  }
@@ -5273,73 +5259,6 @@ async function shares_handler(args) {
5273
5259
  } : {}
5274
5260
  });
5275
5261
  }
5276
- const get_template_source_schema = {
5277
- name: "extension_get_template_source",
5278
- description: "Read source files from a template in the extension.dev template catalog. Use this to learn implementation patterns before building something similar.",
5279
- inputSchema: {
5280
- type: "object",
5281
- properties: {
5282
- slug: {
5283
- type: "string",
5284
- description: "Template slug (e.g. 'ai-claude', 'content-react')"
5285
- },
5286
- files: {
5287
- type: "array",
5288
- items: {
5289
- type: "string"
5290
- },
5291
- description: "Specific files to read (e.g. ['src/manifest.json', 'src/background.ts']). If omitted, returns the file listing from templates-meta.json."
5292
- }
5293
- },
5294
- required: [
5295
- "slug"
5296
- ]
5297
- }
5298
- };
5299
- async function get_template_source_handler(args) {
5300
- const template = await getTemplateBySlug(args.slug);
5301
- if (!template) return JSON.stringify({
5302
- error: `Template '${args.slug}' not found in the catalog`,
5303
- hint: "Use extension_list_templates to see available templates."
5304
- });
5305
- const meta = {
5306
- slug: template.slug,
5307
- description: template.description,
5308
- uiFramework: template.uiFramework || "vanilla",
5309
- surfaces: template.surfaces,
5310
- permissions: template.permissions,
5311
- patternExplanation: template.patternExplanation,
5312
- keyFiles: template.keyFiles,
5313
- repositoryUrl: template.repositoryUrl
5314
- };
5315
- if (!args.files?.length) return JSON.stringify({
5316
- ...meta,
5317
- files: template.files.map((f)=>stripTemplatePathPrefix(template.slug, f)),
5318
- hint: "Pass specific file paths in the files parameter to read their contents."
5319
- });
5320
- const fileContents = {};
5321
- const errors = [];
5322
- await Promise.all(args.files.map(async (filePath)=>{
5323
- const urls = await templateFileUrls(args.slug, filePath);
5324
- let lastStatus = 0;
5325
- for (const url of urls)try {
5326
- const response = await fetch(url);
5327
- if (response.ok) {
5328
- fileContents[filePath] = await response.text();
5329
- return;
5330
- }
5331
- lastStatus = response.status;
5332
- } catch {}
5333
- errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
5334
- }));
5335
- return JSON.stringify({
5336
- ...meta,
5337
- fileContents,
5338
- ...errors.length ? {
5339
- errors
5340
- } : {}
5341
- });
5342
- }
5343
5262
  const CHROME_DESKTOP_ONLY_KEYS = [
5344
5263
  "file_browser_handlers",
5345
5264
  "file_system_provider_capabilities",
@@ -6387,7 +6306,7 @@ function resolveChromeTheme(theme) {
6387
6306
  }
6388
6307
  const theme_verify_schema = {
6389
6308
  name: "extension_theme_verify",
6390
- 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.",
6391
6310
  inputSchema: {
6392
6311
  type: "object",
6393
6312
  properties: {
@@ -6650,20 +6569,17 @@ async function theme_verify_handler(args) {
6650
6569
  attended
6651
6570
  });
6652
6571
  }
6653
- const inspect_schema = {
6654
- name: "extension_inspect",
6655
- 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.",
6656
6575
  inputSchema: {
6657
6576
  type: "object",
6658
6577
  properties: {
6659
- projectPath: {
6660
- type: "string",
6661
- description: "Path to the extension project root"
6662
- },
6578
+ projectPath: PROJECT_PATH,
6663
6579
  browser: {
6664
6580
  type: "string",
6665
6581
  default: "chrome",
6666
- description: "Browser build to inspect"
6582
+ description: "Browser build to analyze"
6667
6583
  },
6668
6584
  format: {
6669
6585
  type: "string",
@@ -6744,7 +6660,7 @@ function formatBytes(bytes) {
6744
6660
  if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
6745
6661
  return `${(bytes / 1048576).toFixed(1)} MB`;
6746
6662
  }
6747
- async function inspect_handler(args) {
6663
+ async function analyze_handler(args) {
6748
6664
  const browser = args.browser ?? "chrome";
6749
6665
  const distPath = node_path.resolve(args.projectPath, "dist", browser);
6750
6666
  if (!node_fs.existsSync(distPath)) return JSON.stringify({
@@ -7322,26 +7238,23 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
7322
7238
  if (notes.length) result.notes = notes;
7323
7239
  return JSON.stringify(result);
7324
7240
  }
7325
- const source_inspect_schema = {
7326
- name: "extension_source_inspect",
7327
- 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.",
7328
7244
  inputSchema: {
7329
7245
  type: "object",
7330
7246
  properties: {
7331
- projectPath: {
7332
- type: "string",
7333
- description: "Path to the extension project root (must have an active dev session)"
7334
- },
7247
+ projectPath: SESSION_PROJECT_PATH,
7335
7248
  url: {
7336
7249
  type: "string",
7337
- description: "URL to inspect (navigates the browser tab to this URL first)"
7250
+ description: "URL to inspect; the tab is navigated there first"
7338
7251
  },
7339
7252
  probe: {
7340
7253
  type: "array",
7341
7254
  items: {
7342
7255
  type: "string"
7343
7256
  },
7344
- description: "CSS selectors to query, returns element counts and samples for each"
7257
+ description: "CSS selectors to query; returns counts and samples for each"
7345
7258
  },
7346
7259
  include: {
7347
7260
  type: "array",
@@ -7361,12 +7274,9 @@ const source_inspect_schema = {
7361
7274
  "meta",
7362
7275
  "console"
7363
7276
  ],
7364
- description: "What data to include in the response"
7365
- },
7366
- browser: {
7367
- type: "string",
7368
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
7277
+ description: "What to include"
7369
7278
  },
7279
+ browser: SESSION_BROWSER,
7370
7280
  maxBytes: {
7371
7281
  type: "number",
7372
7282
  default: 262144,
@@ -7375,7 +7285,7 @@ const source_inspect_schema = {
7375
7285
  deepDom: {
7376
7286
  type: "boolean",
7377
7287
  default: false,
7378
- 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)."
7379
7289
  }
7380
7290
  },
7381
7291
  required: [
@@ -7383,7 +7293,7 @@ const source_inspect_schema = {
7383
7293
  ]
7384
7294
  }
7385
7295
  };
7386
- async function source_inspect_handler(args) {
7296
+ async function inspect_handler(args) {
7387
7297
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
7388
7298
  const include = new Set(args.include ?? [
7389
7299
  "summary",
@@ -7495,18 +7405,12 @@ async function source_inspect_handler(args) {
7495
7405
  }
7496
7406
  const list_extensions_schema = {
7497
7407
  name: "extension_list_extensions",
7498
- 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.",
7499
7409
  inputSchema: {
7500
7410
  type: "object",
7501
7411
  properties: {
7502
- projectPath: {
7503
- type: "string",
7504
- description: "Path to the extension project root (must have an active dev session)"
7505
- },
7506
- browser: {
7507
- type: "string",
7508
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
7509
- }
7412
+ projectPath: SESSION_PROJECT_PATH,
7413
+ browser: SESSION_BROWSER
7510
7414
  },
7511
7415
  required: [
7512
7416
  "projectPath"
@@ -7772,13 +7676,10 @@ const logs_schema_schema = {
7772
7676
  inputSchema: {
7773
7677
  type: "object",
7774
7678
  properties: {
7775
- projectPath: {
7776
- type: "string",
7777
- description: "Path to the extension project root (must have an active dev session)"
7778
- },
7679
+ projectPath: SESSION_PROJECT_PATH,
7779
7680
  browser: {
7780
7681
  type: "string",
7781
- 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."
7782
7683
  },
7783
7684
  level: {
7784
7685
  type: "string",
@@ -7792,7 +7693,7 @@ const logs_schema_schema = {
7792
7693
  "all"
7793
7694
  ],
7794
7695
  default: "all",
7795
- description: "Minimum severity to include; selecting a level includes it plus everything more severe."
7696
+ description: "Minimum severity; a level includes everything more severe."
7796
7697
  },
7797
7698
  context: {
7798
7699
  type: "array",
@@ -7813,15 +7714,15 @@ const logs_schema_schema = {
7813
7714
  signalsOnly: {
7814
7715
  type: "boolean",
7815
7716
  default: false,
7816
- 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."
7817
7718
  },
7818
7719
  since: {
7819
7720
  type: "number",
7820
- 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."
7821
7722
  },
7822
7723
  url: {
7823
7724
  type: "string",
7824
- 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/*."
7825
7726
  },
7826
7727
  tab: {
7827
7728
  type: "number",
@@ -7830,7 +7731,7 @@ const logs_schema_schema = {
7830
7731
  follow: {
7831
7732
  type: "boolean",
7832
7733
  default: false,
7833
- 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."
7834
7735
  },
7835
7736
  followMs: {
7836
7737
  type: "number",
@@ -7840,7 +7741,7 @@ const logs_schema_schema = {
7840
7741
  limit: {
7841
7742
  type: "number",
7842
7743
  default: 200,
7843
- description: "Maximum number of (most recent) events to return."
7744
+ description: "How many of the most recent events to return."
7844
7745
  }
7845
7746
  },
7846
7747
  required: [
@@ -8039,14 +7940,11 @@ async function logs_handler(args) {
8039
7940
  }
8040
7941
  const eval_schema = {
8041
7942
  name: "extension_eval",
8042
- 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}.",
8043
7944
  inputSchema: {
8044
7945
  type: "object",
8045
7946
  properties: {
8046
- projectPath: {
8047
- type: "string",
8048
- description: "Path to the extension project root (must have an active dev session)"
8049
- },
7947
+ projectPath: SESSION_PROJECT_PATH,
8050
7948
  expression: {
8051
7949
  type: "string",
8052
7950
  description: "JavaScript expression to evaluate in the target context"
@@ -8065,24 +7963,18 @@ const eval_schema = {
8065
7963
  "content",
8066
7964
  "page"
8067
7965
  ],
8068
- 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)."
8069
7967
  },
8070
7968
  url: {
8071
7969
  type: "string",
8072
- 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`."
8073
7971
  },
8074
7972
  tab: {
8075
7973
  type: "number",
8076
- description: "Numeric chrome.tabs id, for disambiguating when several tabs match. Optional: with neither `tab` nor `url`, content/page target the active tab."
8077
- },
8078
- browser: {
8079
- type: "string",
8080
- 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."
8081
7975
  },
8082
- timeout: {
8083
- type: "number",
8084
- description: "Command timeout in ms (default 5000)"
8085
- }
7976
+ browser: SESSION_BROWSER,
7977
+ timeout: CALL_TIMEOUT
8086
7978
  },
8087
7979
  required: [
8088
7980
  "projectPath",
@@ -8137,7 +8029,7 @@ async function eval_handler(args) {
8137
8029
  if (parsed && "object" == typeof parsed) {
8138
8030
  parsed.defaultedContext = "page";
8139
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).';
8140
- 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.";
8141
8033
  return JSON.stringify(parsed);
8142
8034
  }
8143
8035
  } catch {}
@@ -8145,14 +8037,11 @@ async function eval_handler(args) {
8145
8037
  }
8146
8038
  const storage_schema = {
8147
8039
  name: "extension_storage",
8148
- 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).",
8149
8041
  inputSchema: {
8150
8042
  type: "object",
8151
8043
  properties: {
8152
- projectPath: {
8153
- type: "string",
8154
- description: "Path to the extension project root (must have an active dev session)"
8155
- },
8044
+ projectPath: SESSION_PROJECT_PATH,
8156
8045
  action: {
8157
8046
  type: "string",
8158
8047
  enum: [
@@ -8189,14 +8078,8 @@ const storage_schema = {
8189
8078
  ],
8190
8079
  default: "background"
8191
8080
  },
8192
- browser: {
8193
- type: "string",
8194
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
8195
- },
8196
- timeout: {
8197
- type: "number",
8198
- description: "Command timeout in ms (default 5000)"
8199
- }
8081
+ browser: SESSION_BROWSER,
8082
+ timeout: CALL_TIMEOUT
8200
8083
  },
8201
8084
  required: [
8202
8085
  "projectPath",
@@ -8237,14 +8120,11 @@ async function storage_handler(args) {
8237
8120
  }
8238
8121
  const reload_schema = {
8239
8122
  name: "extension_reload",
8240
- 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).",
8241
8124
  inputSchema: {
8242
8125
  type: "object",
8243
8126
  properties: {
8244
- projectPath: {
8245
- type: "string",
8246
- description: "Path to the extension project root (must have an active dev session)"
8247
- },
8127
+ projectPath: SESSION_PROJECT_PATH,
8248
8128
  context: {
8249
8129
  type: "string",
8250
8130
  enum: [
@@ -8258,14 +8138,8 @@ const reload_schema = {
8258
8138
  type: "number",
8259
8139
  description: "For content/page: a specific tab id"
8260
8140
  },
8261
- browser: {
8262
- type: "string",
8263
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
8264
- },
8265
- timeout: {
8266
- type: "number",
8267
- description: "Command timeout in ms (default 5000)"
8268
- }
8141
+ browser: SESSION_BROWSER,
8142
+ timeout: CALL_TIMEOUT
8269
8143
  },
8270
8144
  required: [
8271
8145
  "projectPath"
@@ -8283,7 +8157,7 @@ async function reload_handler(args) {
8283
8157
  })
8284
8158
  ], args.projectPath, args.timeout);
8285
8159
  }
8286
- 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.";
8287
8161
  function filterPageTargets(raw) {
8288
8162
  return raw.filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
8289
8163
  targetId: String(t.id),
@@ -8301,38 +8175,35 @@ function matchTargetsByUrl(targets, needle) {
8301
8175
  if (byUrl.length > 0) return byUrl;
8302
8176
  return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
8303
8177
  }
8304
- 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.";
8305
- const dom_inspect_schema = {
8306
- name: "extension_dom_inspect",
8307
- 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).",
8308
8182
  inputSchema: {
8309
8183
  type: "object",
8310
8184
  properties: {
8311
- projectPath: {
8312
- type: "string",
8313
- description: "Path to the extension project root (must have an active dev session)"
8314
- },
8185
+ projectPath: SESSION_PROJECT_PATH,
8315
8186
  tab: {
8316
8187
  type: "number",
8317
- 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."
8318
8189
  },
8319
8190
  url: {
8320
8191
  type: "string",
8321
- 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`."
8322
8193
  },
8323
8194
  tabUrl: {
8324
8195
  type: "string",
8325
- 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`."
8326
8197
  },
8327
8198
  listTargets: {
8328
8199
  type: "boolean",
8329
8200
  default: false,
8330
- 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."
8331
8202
  },
8332
8203
  listTabs: {
8333
8204
  type: "boolean",
8334
8205
  default: false,
8335
- 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."
8336
8207
  },
8337
8208
  context: {
8338
8209
  type: "string",
@@ -8348,7 +8219,7 @@ const dom_inspect_schema = {
8348
8219
  "bookmarks"
8349
8220
  ],
8350
8221
  default: "content",
8351
- 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"
8352
8223
  },
8353
8224
  include: {
8354
8225
  type: "array",
@@ -8373,16 +8244,10 @@ const dom_inspect_schema = {
8373
8244
  "number",
8374
8245
  "boolean"
8375
8246
  ],
8376
- description: "Also include recent console lines for the target (DOM + console in one call). A number is how many lines; true means 50."
8247
+ description: "Also include recent console lines. A number is how many; true means 50."
8377
8248
  },
8378
- browser: {
8379
- type: "string",
8380
- description: "Browser session to target. Defaults to the active dev session's browser for this project."
8381
- },
8382
- timeout: {
8383
- type: "number",
8384
- description: "Command timeout in ms (default 5000)"
8385
- }
8249
+ browser: SESSION_BROWSER,
8250
+ timeout: CALL_TIMEOUT
8386
8251
  },
8387
8252
  required: [
8388
8253
  "projectPath"
@@ -8413,7 +8278,7 @@ async function cdpPortOrError(projectPath, browser, feature) {
8413
8278
  port: resolved.port
8414
8279
  };
8415
8280
  }
8416
- async function dom_inspect_handler(args) {
8281
+ async function dom_snapshot_handler(args) {
8417
8282
  const withConsole = true === args.withConsole ? 50 : false === args.withConsole ? void 0 : args.withConsole;
8418
8283
  if (args.listTargets) {
8419
8284
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
@@ -8593,7 +8458,7 @@ async function dom_inspect_handler(args) {
8593
8458
  }
8594
8459
  const publish_schema = {
8595
8460
  name: "extension_publish",
8596
- 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.",
8597
8462
  inputSchema: {
8598
8463
  type: "object",
8599
8464
  properties: {
@@ -8603,12 +8468,9 @@ const publish_schema = {
8603
8468
  },
8604
8469
  buildSha: {
8605
8470
  type: "string",
8606
- 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."
8607
8472
  },
8608
- api: {
8609
- type: "string",
8610
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
8611
- }
8473
+ api: API_BASE
8612
8474
  },
8613
8475
  required: []
8614
8476
  }
@@ -8624,7 +8486,7 @@ function fail(name, message) {
8624
8486
  }
8625
8487
  async function publish_handler(args) {
8626
8488
  const token = resolveToken();
8627
- 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).");
8628
8490
  if (null != args.ttlHours) {
8629
8491
  const t = Number(args.ttlHours);
8630
8492
  if (!Number.isInteger(t) || t < 1 || t > 168) return fail("PublishBadRequest", "ttlHours must be an integer between 1 and 168.");
@@ -8670,7 +8532,7 @@ async function publish_handler(args) {
8670
8532
  }
8671
8533
  const release_promote_schema = {
8672
8534
  name: "extension_release_promote",
8673
- 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).",
8674
8536
  inputSchema: {
8675
8537
  type: "object",
8676
8538
  properties: {
@@ -8701,10 +8563,7 @@ const release_promote_schema = {
8701
8563
  type: "string",
8702
8564
  description: "Release notes markdown (optional)"
8703
8565
  },
8704
- api: {
8705
- type: "string",
8706
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
8707
- }
8566
+ api: API_BASE
8708
8567
  },
8709
8568
  required: [
8710
8569
  "buildId",
@@ -8723,7 +8582,7 @@ function release_promote_fail(name, message) {
8723
8582
  }
8724
8583
  async function release_promote_handler(args) {
8725
8584
  const token = resolveToken();
8726
- 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).");
8727
8586
  const buildId = String(args.buildId || "").trim();
8728
8587
  const channel = String(args.channel || "").trim();
8729
8588
  if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
@@ -8766,7 +8625,7 @@ async function release_promote_handler(args) {
8766
8625
  const ref = resolveProjectRef();
8767
8626
  if (404 === res.status || "UNKNOWN_BUILD" === code) {
8768
8627
  enrich.buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
8769
- 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.";
8770
8629
  if (ref) {
8771
8630
  const channelsUrl = registryFileUrl(ref, "channels.json");
8772
8631
  const channelsRes = await fetchRegistryJson(channelsUrl, fetch, {
@@ -8809,28 +8668,6 @@ async function release_promote_handler(args) {
8809
8668
  } : data;
8810
8669
  return JSON.stringify(enriched);
8811
8670
  }
8812
- const release_list_schema = {
8813
- name: "extension_release_list",
8814
- 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).",
8815
- inputSchema: {
8816
- type: "object",
8817
- properties: {
8818
- workspace: {
8819
- type: "string",
8820
- description: "Workspace slug override (defaults to the stored login's workspace)."
8821
- },
8822
- project: {
8823
- type: "string",
8824
- description: "Project slug override (defaults to the stored login's project)."
8825
- },
8826
- api: {
8827
- type: "string",
8828
- 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."
8829
- }
8830
- },
8831
- required: []
8832
- }
8833
- };
8834
8671
  function release_list_fail(name, message, extra) {
8835
8672
  return JSON.stringify({
8836
8673
  ok: false,
@@ -8841,9 +8678,9 @@ function release_list_fail(name, message, extra) {
8841
8678
  ...extra ?? {}
8842
8679
  });
8843
8680
  }
8844
- async function release_list_handler(args) {
8681
+ async function readReleases(args) {
8845
8682
  const ref = resolveProjectRef(args);
8846
- 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.");
8847
8684
  const channelsUrl = registryFileUrl(ref, "channels.json");
8848
8685
  const metaUrl = registryFileUrl(ref, "meta.json");
8849
8686
  const buildsUrl = registryFileUrl(ref, "builds/index.json");
@@ -8862,7 +8699,7 @@ async function release_list_handler(args) {
8862
8699
  })
8863
8700
  ]);
8864
8701
  const buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
8865
- 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}`, {
8866
8703
  workspace: ref.workspace,
8867
8704
  project: ref.project,
8868
8705
  registryUrl: channelsUrl,
@@ -8909,246 +8746,12 @@ async function release_list_handler(args) {
8909
8746
  if (!buildsRes.ok) result.buildsUnavailable = `builds/index.json unreadable: ${buildsRes.message}`;
8910
8747
  return JSON.stringify(result);
8911
8748
  }
8912
- function storeMdWarnings(browsers, cwd) {
8913
- const wantsFirefox = browsers.includes("firefox");
8914
- const wantsEdge = browsers.includes("edge");
8915
- if (!wantsFirefox && !wantsEdge) return [];
8916
- let content;
8917
- try {
8918
- content = node_fs.readFileSync(node_path.join(cwd, "STORE.md"), "utf8");
8919
- } catch {
8920
- return [
8921
- "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."
8922
- ];
8923
- }
8924
- const hasField = (section, field)=>{
8925
- const parts = content.split(/^## +/m);
8926
- const match = parts.find((p)=>section.test(p.split("\n", 1)[0] ?? ""));
8927
- if (!match) return false;
8928
- const sub = match.split(/^### +/m).find((p)=>field.test(p.split("\n", 1)[0] ?? ""));
8929
- if (!sub) return false;
8930
- const body = sub.split("\n").slice(1).join("\n");
8931
- return body.replace(/<!--[\s\S]*?-->/g, "").trim().length > 0;
8932
- };
8933
- const warnings = [];
8934
- 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.");
8935
- if (wantsEdge && !hasField(/edge/i, /certification notes/i)) warnings.push("STORE.md has no Edge certification notes; the certification team gets no testing guidance.");
8936
- return warnings;
8937
- }
8938
- const deploy_schema = {
8939
- name: "extension_deploy",
8940
- 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.",
8941
- inputSchema: {
8942
- type: "object",
8943
- properties: {
8944
- browsers: {
8945
- type: "array",
8946
- items: {
8947
- type: "string",
8948
- enum: [
8949
- "chrome",
8950
- "firefox",
8951
- "edge",
8952
- "safari"
8953
- ]
8954
- },
8955
- description: "Stores to submit to."
8956
- },
8957
- buildSha: {
8958
- type: "string",
8959
- description: "The built commit SHA to submit. It must have a completed build in the project's build index; an unknown sha is rejected."
8960
- },
8961
- channel: {
8962
- type: "string",
8963
- description: "Release channel to submit from (default stable)."
8964
- },
8965
- version: {
8966
- type: "string",
8967
- description: "Version label for the submission record (optional)."
8968
- },
8969
- dryRun: {
8970
- type: "boolean",
8971
- default: true,
8972
- description: "Preflight only (verify auth, project, build, and store workflow). Pass false to actually dispatch the submission (irreversible, enters store review)."
8973
- },
8974
- api: {
8975
- type: "string",
8976
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
8977
- }
8978
- },
8979
- required: [
8980
- "browsers",
8981
- "buildSha"
8982
- ]
8983
- }
8984
- };
8985
- function deploy_fail(name, message) {
8986
- return JSON.stringify({
8987
- ok: false,
8988
- error: {
8989
- name,
8990
- message
8991
- }
8992
- });
8993
- }
8994
- async function deploy_handler(args) {
8995
- const token = resolveToken();
8996
- 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).");
8997
- const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
8998
- if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
8999
- const buildSha = String(args.buildSha || "").trim();
9000
- if (!buildSha) return deploy_fail("DeployInputError", "buildSha is required (the built commit to submit).");
9001
- const apiCheck = safeApiBase(resolveApiBase(args.api));
9002
- if (!apiCheck.ok) return deploy_fail("DeployConfigError", apiCheck.message);
9003
- const url = `${apiCheck.base}/api/cli/stores/submit`;
9004
- const dryRun = false !== args.dryRun;
9005
- const body = {
9006
- browsers,
9007
- buildSha,
9008
- dryRun
9009
- };
9010
- if (args.channel) body.channel = String(args.channel).trim();
9011
- if (args.version) body.version = String(args.version).trim();
9012
- let res;
9013
- try {
9014
- res = await fetch(url, {
9015
- method: "POST",
9016
- headers: {
9017
- authorization: `Bearer ${token}`,
9018
- "content-type": "application/json"
9019
- },
9020
- body: JSON.stringify(body)
9021
- });
9022
- } catch (err) {
9023
- return deploy_fail("DeployNetworkError", `Could not reach ${url}: ${err?.message || err}`);
9024
- }
9025
- const text = await res.text();
9026
- let data;
9027
- try {
9028
- data = JSON.parse(text);
9029
- } catch {
9030
- data = {
9031
- message: text
9032
- };
9033
- }
9034
- if (!res.ok) return deploy_fail("DeployError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}`);
9035
- const warnings = Array.isArray(data?.warnings) ? [
9036
- ...data.warnings
9037
- ] : [];
9038
- warnings.push(...storeMdWarnings(browsers, process.cwd()));
9039
- const result = {
9040
- mode: "platform",
9041
- dryRun,
9042
- ...data
9043
- };
9044
- if (dryRun) {
9045
- const ref = resolveProjectRef();
9046
- const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
9047
- 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}.`;
9048
- let health = null;
9049
- let healthUnreadable = null;
9050
- let channelRows = null;
9051
- if (ref) {
9052
- const [healthRes, channelsRes] = await Promise.all([
9053
- fetchRegistryJson(registryFileUrl(ref, "stores/health.json"), fetch, {
9054
- ref,
9055
- api: args.api
9056
- }),
9057
- fetchRegistryJson(registryFileUrl(ref, "channels.json"), fetch, {
9058
- ref,
9059
- api: args.api
9060
- })
9061
- ]);
9062
- if (healthRes.ok) {
9063
- const stores = healthRes.json?.stores;
9064
- health = stores && "object" == typeof stores ? stores : null;
9065
- if (!health) healthUnreadable = "stores/health.json had no stores map";
9066
- } else healthUnreadable = healthRes.message;
9067
- if (channelsRes.ok) channelRows = parseChannels(channelsRes.json);
9068
- } else healthUnreadable = "no stored workspace/project to look up (run extension_login)";
9069
- const preflight = browsers.map((browser)=>{
9070
- if (!health) return {
9071
- browser,
9072
- ok: false,
9073
- configured: "unknown",
9074
- publishMode: "unknown",
9075
- reason: `Store configuration could not be read (${healthUnreadable}); verify the ${browser} store in the console before submitting.`
9076
- };
9077
- const row = health[browser];
9078
- if (!row) return {
9079
- browser,
9080
- ok: false,
9081
- configured: false,
9082
- publishMode: "unknown",
9083
- reason: `No ${browser} store is configured on this project; a real submission for ${browser} would fail. Configure it at ${consoleStoresUrl}.`
9084
- };
9085
- if (true !== row.ok) return {
9086
- browser,
9087
- ok: false,
9088
- configured: false,
9089
- publishMode: "unknown",
9090
- reason: String(row.message || "").trim() || `The ${browser} store failed its last credential health check.`
9091
- };
9092
- return {
9093
- browser,
9094
- ok: true,
9095
- configured: true,
9096
- publishMode: "unknown"
9097
- };
9098
- });
9099
- const actionable = preflight.filter((p)=>p.ok).map((p)=>p.browser);
9100
- const blocked = preflight.filter((p)=>!p.ok);
9101
- const channelDefaulted = !String(args.channel || "").trim();
9102
- const resolvedChannel = String(data?.channel || "").trim() || (channelDefaulted ? "stable" : String(args.channel).trim());
9103
- if (channelRows) {
9104
- const exists = channelRows.some((r)=>r.channel === resolvedChannel || r.channel.endsWith(`-${resolvedChannel}`));
9105
- 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.`);
9106
- }
9107
- const summaryParts = [];
9108
- 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.`);
9109
- for (const p of blocked)summaryParts.push(`${p.browser}: ${"unknown" === p.configured ? "cannot be verified" : "NOT actionable"} - ${p.reason}`);
9110
- summaryParts.push(storeModeNote);
9111
- result.ok = actionable.length > 0;
9112
- result.preflight = preflight;
9113
- result.channel = resolvedChannel;
9114
- result.channelDefaulted = channelDefaulted;
9115
- if (channelDefaulted) result.channelNote = `channel: ${resolvedChannel} (default)`;
9116
- result.consoleStoresUrl = consoleStoresUrl;
9117
- if ("string" == typeof data?.message) result.platformMessage = data.message;
9118
- result.message = summaryParts.join(" ");
9119
- }
9120
- 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.";
9121
- if (warnings.length > 0) result.warnings = warnings;
9122
- return JSON.stringify(result);
9123
- }
9124
8749
  const KNOWN_STORES = [
9125
8750
  "chrome",
9126
8751
  "firefox",
9127
8752
  "edge",
9128
8753
  "safari"
9129
8754
  ];
9130
- const store_status_schema = {
9131
- name: "extension_store_status",
9132
- 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.",
9133
- inputSchema: {
9134
- type: "object",
9135
- properties: {
9136
- workspace: {
9137
- type: "string",
9138
- description: "Workspace slug override (defaults to the stored login's workspace)."
9139
- },
9140
- project: {
9141
- type: "string",
9142
- description: "Project slug override (defaults to the stored login's project)."
9143
- },
9144
- api: {
9145
- type: "string",
9146
- 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."
9147
- }
9148
- },
9149
- required: []
9150
- }
9151
- };
9152
8755
  function isPlainObject(value) {
9153
8756
  return Boolean(value) && "object" == typeof value && !Array.isArray(value);
9154
8757
  }
@@ -9237,9 +8840,9 @@ function store_status_fail(name, message, extra) {
9237
8840
  ...extra ?? {}
9238
8841
  });
9239
8842
  }
9240
- async function store_status_handler(args) {
8843
+ async function readStores(args) {
9241
8844
  const ref = resolveProjectRef(args);
9242
- 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.");
9243
8846
  const healthUrl = registryFileUrl(ref, "stores/health.json");
9244
8847
  const statusUrl = registryFileUrl(ref, "stores/status.json");
9245
8848
  const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
@@ -9315,27 +8918,312 @@ async function store_status_handler(args) {
9315
8918
  return tail.length > 0 ? `${head}; ${tail.join("; ")}` : head;
9316
8919
  });
9317
8920
  const result = {
9318
- ok: true,
9319
- workspace: ref.workspace,
9320
- project: ref.project,
9321
- stores: rows,
9322
- ...status.lastSubmission ? {
9323
- lastSubmission: status.lastSubmission
9324
- } : {},
9325
- ...status.lastPollAt ? {
9326
- lastPollAt: status.lastPollAt
9327
- } : {},
9328
- registryUrls: {
9329
- health: healthUrl,
9330
- status: statusUrl,
9331
- submissions: submissionsUrl
9332
- },
9333
- consoleStoresUrl,
9334
- 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.`
8921
+ ok: true,
8922
+ workspace: ref.workspace,
8923
+ project: ref.project,
8924
+ stores: rows,
8925
+ ...status.lastSubmission ? {
8926
+ lastSubmission: status.lastSubmission
8927
+ } : {},
8928
+ ...status.lastPollAt ? {
8929
+ lastPollAt: status.lastPollAt
8930
+ } : {},
8931
+ registryUrls: {
8932
+ health: healthUrl,
8933
+ status: statusUrl,
8934
+ submissions: submissionsUrl
8935
+ },
8936
+ consoleStoresUrl,
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.`
8938
+ };
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
9335
9148
  };
9336
- if (!healthRes.ok) result.healthUnavailable = `stores/health.json unreadable: ${healthRes.message}`;
9337
- if (!statusRes.ok) result.statusUnavailable = `stores/status.json unreadable: ${statusRes.message}`;
9338
- if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
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;
9339
9227
  return JSON.stringify(result);
9340
9228
  }
9341
9229
  const ENGINE_COMPANION_IDS = new Set([
@@ -9405,7 +9293,7 @@ function doctor_readReadyContract(projectPath, browser) {
9405
9293
  }
9406
9294
  const doctor_schema = {
9407
9295
  name: "extension_doctor",
9408
- 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.",
9409
9297
  inputSchema: {
9410
9298
  type: "object",
9411
9299
  properties: {
@@ -9446,7 +9334,7 @@ async function environmentPreflight() {
9446
9334
  checks.push({
9447
9335
  check: "template-cache",
9448
9336
  status: cacheExists ? "pass" : "warn",
9449
- 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)"
9450
9338
  });
9451
9339
  const healthy = checks.every((c)=>"fail" !== c.status);
9452
9340
  return JSON.stringify({
@@ -9590,26 +9478,20 @@ function wait_isAlive(pid) {
9590
9478
  }
9591
9479
  const wait_schema = {
9592
9480
  name: "extension_wait",
9593
- 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.",
9594
9482
  inputSchema: {
9595
9483
  type: "object",
9596
9484
  properties: {
9597
- projectPath: {
9598
- type: "string",
9599
- description: "Path to the extension project root"
9600
- },
9601
- browser: {
9602
- type: "string",
9603
- description: "Browser to check readiness for. Defaults to the active dev session's browser for this project."
9604
- },
9485
+ projectPath: PROJECT_PATH,
9486
+ browser: SESSION_BROWSER,
9605
9487
  timeoutMs: {
9606
9488
  type: "number",
9607
9489
  default: DEFAULT_TIMEOUT_MS,
9608
- 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.`
9609
9491
  },
9610
9492
  timeout: {
9611
9493
  type: "number",
9612
- 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."
9613
9495
  }
9614
9496
  },
9615
9497
  required: [
@@ -9648,7 +9530,7 @@ async function wait_handler(args) {
9648
9530
  buildOnly: true,
9649
9531
  compiled: true,
9650
9532
  browserAttached: false,
9651
- 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.",
9652
9534
  command: contract.command,
9653
9535
  browser: contract.browser,
9654
9536
  port: contract.port,
@@ -9739,10 +9621,7 @@ const add_feature_schema = {
9739
9621
  inputSchema: {
9740
9622
  type: "object",
9741
9623
  properties: {
9742
- projectPath: {
9743
- type: "string",
9744
- description: "Path to the extension project root"
9745
- },
9624
+ projectPath: PROJECT_PATH,
9746
9625
  feature: {
9747
9626
  type: "string",
9748
9627
  enum: [
@@ -10088,30 +9967,6 @@ async function pollDeviceToken(args) {
10088
9967
  await new Promise((r)=>setTimeout(r, 1000 * interval));
10089
9968
  }
10090
9969
  }
10091
- const login_schema = {
10092
- name: "extension_login",
10093
- 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. You authorize at extension.dev/device; GitHub federation happens server-side, so no GitHub token ever lands on this machine. 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.",
10094
- inputSchema: {
10095
- type: "object",
10096
- properties: {
10097
- project: {
10098
- type: "string",
10099
- description: "Target project as '<workspace>/<project>' (the token is scoped to it)"
10100
- },
10101
- deviceCode: {
10102
- type: "string",
10103
- description: "Resume token from a prior call's `deviceCode`; omit on the first call"
10104
- },
10105
- api: {
10106
- type: "string",
10107
- description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
10108
- }
10109
- },
10110
- required: [
10111
- "project"
10112
- ]
10113
- }
10114
- };
10115
9970
  const FIRST_CALL_BUDGET_MS = 8000;
10116
9971
  const RESUME_BUDGET_MS = 22000;
10117
9972
  function login_fail(name, message) {
@@ -10138,7 +9993,7 @@ function success(creds) {
10138
9993
  function login_pending(start) {
10139
9994
  const complete = String(start.verificationUriComplete || "").trim();
10140
9995
  const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
10141
- 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.`;
10142
9997
  return JSON.stringify({
10143
9998
  ok: false,
10144
9999
  status: "authorization_pending",
@@ -10159,10 +10014,10 @@ function resumePending(deviceCode, verificationUri) {
10159
10014
  verificationUri,
10160
10015
  deviceCode,
10161
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).",
10162
- 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.`
10163
10018
  });
10164
10019
  }
10165
- async function login_handler(args) {
10020
+ async function loginToProject(args) {
10166
10021
  const project = String(args.project || "").trim();
10167
10022
  if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.");
10168
10023
  const apiBase = resolveApiBase(args.api);
@@ -10182,7 +10037,7 @@ async function login_handler(args) {
10182
10037
  budgetMs: RESUME_BUDGET_MS
10183
10038
  });
10184
10039
  if (poll.ok) return success(poll.creds);
10185
- if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
10040
+ if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_auth (action: login) again to restart.");
10186
10041
  if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
10187
10042
  if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
10188
10043
  return resumePending(String(args.deviceCode), config.verificationUri);
@@ -10214,20 +10069,12 @@ async function login_handler(args) {
10214
10069
  verificationUriComplete: start.verificationUriComplete
10215
10070
  });
10216
10071
  }
10217
- const whoami_schema = {
10218
- name: "extension_whoami",
10219
- 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.",
10220
- inputSchema: {
10221
- type: "object",
10222
- properties: {}
10223
- }
10224
- };
10225
- async function whoami_handler() {
10072
+ async function readIdentity() {
10226
10073
  const creds = readCredentials();
10227
10074
  if (!creds) return JSON.stringify({
10228
10075
  ok: true,
10229
10076
  status: "logged-out",
10230
- message: "No stored credentials. Run extension_login to authenticate."
10077
+ message: "No stored credentials. Run extension_auth (action: login) to authenticate."
10231
10078
  });
10232
10079
  const now = Math.floor(Date.now() / 1000);
10233
10080
  const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
@@ -10236,7 +10083,7 @@ async function whoami_handler() {
10236
10083
  const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
10237
10084
  const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
10238
10085
  const messageParts = [];
10239
- 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.`);
10240
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.`);
10241
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.");
10242
10089
  return JSON.stringify({
@@ -10245,227 +10092,75 @@ async function whoami_handler() {
10245
10092
  workspaceSlug: creds.workspaceSlug,
10246
10093
  projectSlug: creds.projectSlug,
10247
10094
  ...recordedApi ? {
10248
- apiRecordedAtLogin: recordedApi
10249
- } : {},
10250
- apiDefault: effectiveDefaultApi,
10251
- provider: creds.provider ?? "extensiondev",
10252
- expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
10253
- expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
10254
- expired,
10255
- ...envTokenSet ? {
10256
- envTokenOverride: true
10257
- } : {},
10258
- tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
10259
- message: messageParts.join(" ")
10260
- });
10261
- }
10262
- const logout_schema = {
10263
- name: "extension_logout",
10264
- 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.",
10265
- inputSchema: {
10266
- type: "object",
10267
- properties: {}
10268
- }
10269
- };
10270
- async function logout_handler() {
10271
- const creds = readCredentials();
10272
- const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ? consoleProjectUrl({
10273
- workspace: creds.workspaceSlug,
10274
- project: creds.projectSlug
10275
- }, "settings/access-tokens") : null;
10276
- const result = clearCredentials();
10277
- return JSON.stringify({
10278
- ok: true,
10279
- cleared: result.cleared,
10280
- ...result.cleared && revokeUrl ? {
10281
- revokeUrl
10282
- } : {},
10283
- 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."
10284
- });
10285
- }
10286
- const install_browser_schema = {
10287
- name: "extension_install_browser",
10288
- 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.",
10289
- inputSchema: {
10290
- type: "object",
10291
- properties: {
10292
- browser: {
10293
- type: "string",
10294
- enum: [
10295
- "chrome",
10296
- "chromium",
10297
- "edge",
10298
- "firefox"
10299
- ],
10300
- description: "Browser to install"
10301
- }
10302
- },
10303
- required: [
10304
- "browser"
10305
- ]
10306
- }
10307
- };
10308
- async function install_browser_handler(args) {
10309
- const start = Date.now();
10310
- try {
10311
- await extensionInstall({
10312
- browser: args.browser
10313
- });
10314
- return JSON.stringify({
10315
- status: "installed",
10316
- browser: args.browser,
10317
- duration: Date.now() - start,
10318
- hint: `Browser "${args.browser}" is now available. Use extension_dev or extension_start with browser: "${args.browser}".`
10319
- });
10320
- } catch (err) {
10321
- return JSON.stringify({
10322
- status: "error",
10323
- browser: args.browser,
10324
- message: err instanceof Error ? err.message : String(err),
10325
- duration: Date.now() - start,
10326
- 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."
10327
- });
10328
- }
10095
+ apiRecordedAtLogin: recordedApi
10096
+ } : {},
10097
+ apiDefault: effectiveDefaultApi,
10098
+ provider: creds.provider ?? "extensiondev",
10099
+ expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
10100
+ expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
10101
+ expired,
10102
+ ...envTokenSet ? {
10103
+ envTokenOverride: true
10104
+ } : {},
10105
+ tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
10106
+ message: messageParts.join(" ")
10107
+ });
10108
+ }
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
+ });
10329
10124
  }
10330
- const uninstall_browser_schema = {
10331
- name: "extension_uninstall_browser",
10332
- 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.",
10333
10128
  inputSchema: {
10334
10129
  type: "object",
10335
10130
  properties: {
10336
- browser: {
10131
+ action: {
10337
10132
  type: "string",
10338
10133
  enum: [
10339
- "chrome",
10340
- "chromium",
10341
- "edge",
10342
- "firefox"
10134
+ "status",
10135
+ "login",
10136
+ "logout"
10343
10137
  ],
10344
- description: "Managed browser to remove"
10138
+ default: "status"
10345
10139
  },
10346
- all: {
10347
- type: "boolean",
10348
- default: false,
10349
- description: "Remove every managed browser binary"
10350
- }
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
10351
10149
  },
10352
10150
  required: []
10353
10151
  }
10354
10152
  };
10355
- async function uninstall_browser_handler(args) {
10356
- const start = Date.now();
10357
- if (!args.browser && !args.all) return JSON.stringify({
10358
- status: "error",
10359
- message: "Provide a browser to remove, or set all: true."
10360
- });
10361
- try {
10362
- await extensionUninstall({
10363
- browser: args.browser,
10364
- all: args.all
10365
- });
10366
- return JSON.stringify({
10367
- status: "uninstalled",
10368
- target: args.all ? "all" : args.browser,
10369
- duration: Date.now() - start,
10370
- hint: "Use extension_list_browsers to confirm what remains in the managed cache."
10371
- });
10372
- } catch (err) {
10373
- return JSON.stringify({
10374
- status: "error",
10375
- target: args.all ? "all" : args.browser,
10376
- message: err instanceof Error ? err.message : String(err),
10377
- duration: Date.now() - start
10378
- });
10379
- }
10380
- }
10381
- const list_browsers_schema = {
10382
- name: "extension_list_browsers",
10383
- 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.",
10384
- inputSchema: {
10385
- type: "object",
10386
- properties: {}
10387
- }
10388
- };
10389
- const BROWSER_NAMES = [
10390
- "chrome",
10391
- "chromium",
10392
- "edge",
10393
- "firefox"
10394
- ];
10395
- function getDirSize(dir) {
10396
- let total = 0;
10397
- try {
10398
- for (const entry of readdirSync(dir, {
10399
- withFileTypes: true
10400
- })){
10401
- const full = join(dir, entry.name);
10402
- if (entry.isDirectory()) total += getDirSize(full);
10403
- else try {
10404
- total += statSync(full).size;
10405
- } catch {}
10406
- }
10407
- } catch {}
10408
- return total;
10409
- }
10410
- function list_browsers_formatBytes(bytes) {
10411
- if (bytes < 1024) return `${bytes} B`;
10412
- if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
10413
- return `${(bytes / 1048576).toFixed(1)} MB`;
10414
- }
10415
- async function list_browsers_handler() {
10416
- const cacheRoot = getManagedBrowsersCacheRoot();
10417
- const installed = [];
10418
- for (const browser of BROWSER_NAMES){
10419
- const browserDir = join(cacheRoot, browser);
10420
- if (existsSync(browserDir)) {
10421
- const size = getDirSize(browserDir);
10422
- installed.push({
10423
- browser,
10424
- path: browserDir,
10425
- size,
10426
- sizeFormatted: list_browsers_formatBytes(size),
10427
- engine: "firefox" === browser ? "gecko" : "chromium"
10428
- });
10429
- }
10430
- }
10431
- return JSON.stringify({
10432
- cacheRoot,
10433
- cacheExists: existsSync(cacheRoot),
10434
- installed,
10435
- availableToInstall: BROWSER_NAMES.filter((b)=>!installed.some((i)=>i.browser === b)),
10436
- 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
10437
10160
  });
10161
+ return readIdentity();
10438
10162
  }
10439
10163
  const execFileAsync = promisify(execFile);
10440
- const detect_browsers_schema = {
10441
- name: "extension_detect_browsers",
10442
- description: "Detect which browsers are available for extension development. Checks both system-installed and managed browsers, returning paths and capabilities for each.",
10443
- inputSchema: {
10444
- type: "object",
10445
- properties: {
10446
- browsers: {
10447
- type: "array",
10448
- items: {
10449
- type: "string",
10450
- enum: [
10451
- "chrome",
10452
- "chromium",
10453
- "edge",
10454
- "brave",
10455
- "opera",
10456
- "vivaldi",
10457
- "yandex",
10458
- "firefox",
10459
- "waterfox",
10460
- "librewolf",
10461
- "safari"
10462
- ]
10463
- },
10464
- description: "Browsers to check. If omitted, checks all."
10465
- }
10466
- }
10467
- }
10468
- };
10469
10164
  const ALL_BROWSERS = [
10470
10165
  "chrome",
10471
10166
  "chromium",
@@ -10713,8 +10408,8 @@ async function getVersion(binaryPath, browser) {
10713
10408
  return null;
10714
10409
  }
10715
10410
  }
10716
- async function detect_browsers_handler(args) {
10717
- const browsersToCheck = args.browsers ?? [
10411
+ async function detectBrowsers(browsers) {
10412
+ const browsersToCheck = browsers ?? [
10718
10413
  ...ALL_BROWSERS
10719
10414
  ];
10720
10415
  const detected = [];
@@ -10756,8 +10451,161 @@ async function detect_browsers_handler(args) {
10756
10451
  available: available.map((d)=>d.browser),
10757
10452
  missing: missing.map((d)=>d.browser)
10758
10453
  },
10759
- 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
10760
10607
  });
10608
+ return detectBrowsers(args.browsers);
10761
10609
  }
10762
10610
  function typeOf(value) {
10763
10611
  if (Array.isArray(value)) return "array";
@@ -10925,40 +10773,32 @@ function inputValidationError(toolName, issues, inputSchema) {
10925
10773
  }
10926
10774
  const tools = [
10927
10775
  create_namespaceObject,
10928
- list_templates_namespaceObject,
10776
+ templates_namespaceObject,
10929
10777
  build_namespaceObject,
10930
10778
  dev_namespaceObject,
10931
10779
  start_namespaceObject,
10932
- preview_namespaceObject,
10933
10780
  preview_web_namespaceObject,
10934
10781
  shares_namespaceObject,
10935
10782
  stop_namespaceObject,
10936
- get_template_source_namespaceObject,
10937
10783
  manifest_validate_namespaceObject,
10938
10784
  theme_verify_namespaceObject,
10785
+ analyze_namespaceObject,
10939
10786
  inspect_namespaceObject,
10940
- source_inspect_namespaceObject,
10941
10787
  list_extensions_namespaceObject,
10942
10788
  logs_namespaceObject,
10943
10789
  eval_namespaceObject,
10944
10790
  storage_namespaceObject,
10945
10791
  reload_namespaceObject,
10946
10792
  open_namespaceObject,
10947
- dom_inspect_namespaceObject,
10793
+ dom_snapshot_namespaceObject,
10948
10794
  tools_publish_namespaceObject,
10949
- release_list_namespaceObject,
10795
+ release_status_namespaceObject,
10950
10796
  release_promote_namespaceObject,
10951
- deploy_namespaceObject,
10952
- store_status_namespaceObject,
10797
+ submit_namespaceObject,
10953
10798
  wait_namespaceObject,
10954
10799
  add_feature_namespaceObject,
10955
- login_namespaceObject,
10956
- whoami_namespaceObject,
10957
- logout_namespaceObject,
10958
- install_browser_namespaceObject,
10959
- uninstall_browser_namespaceObject,
10960
- list_browsers_namespaceObject,
10961
- detect_browsers_namespaceObject,
10800
+ auth_namespaceObject,
10801
+ browsers_namespaceObject,
10962
10802
  doctor_namespaceObject
10963
10803
  ];
10964
10804
  const toolMap = new Map();
@@ -11040,7 +10880,7 @@ async function runCli(cmd, args) {
11040
10880
  return i >= 0 ? args[i + 1] : void 0;
11041
10881
  };
11042
10882
  if ("whoami" === cmd) {
11043
- log(await whoami_handler());
10883
+ log(await readIdentity());
11044
10884
  return 0;
11045
10885
  }
11046
10886
  if ("release" === cmd) {
@@ -11072,7 +10912,7 @@ async function runCli(cmd, args) {
11072
10912
  return 1;
11073
10913
  }
11074
10914
  if ("logout" === cmd) {
11075
- log(await logout_handler());
10915
+ log(await clearLocalCredentials());
11076
10916
  return 0;
11077
10917
  }
11078
10918
  if ("login" === cmd) {