@extension.dev/mcp 10.0.0 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.js CHANGED
@@ -6,8 +6,8 @@ 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 node_crypto, { createHash } from "node:crypto";
10
9
  import cross_spawn from "cross-spawn";
10
+ import node_crypto, { createHash } from "node:crypto";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { execFile, execFileSync } from "node:child_process";
13
13
  import ws_0 from "ws";
@@ -470,7 +470,7 @@ var inspect_namespaceObject = {};
470
470
  __webpack_require__.r(inspect_namespaceObject);
471
471
  __webpack_require__.d(inspect_namespaceObject, {
472
472
  handler: ()=>inspect_handler,
473
- schema: ()=>inspect_schema
473
+ schema: ()=>inspect_schema_schema
474
474
  });
475
475
  var list_extensions_namespaceObject = {};
476
476
  __webpack_require__.r(list_extensions_namespaceObject);
@@ -579,7 +579,208 @@ __webpack_require__.d(wait_namespaceObject, {
579
579
  handler: ()=>wait_handler,
580
580
  schema: ()=>wait_schema
581
581
  });
582
- var package_namespaceObject = JSON.parse('{"rE":"10.0.0","El":{"OP":"4.0.17"}}');
582
+ var package_namespaceObject = JSON.parse('{"rE":"10.1.0","El":{"OP":"4.0.17"}}');
583
+ const ENVELOPE_SCHEMA = 1;
584
+ const collectWarnings = (warnings)=>{
585
+ if (!warnings) return [];
586
+ const kept = [];
587
+ for (const warning of warnings){
588
+ if ("string" != typeof warning) continue;
589
+ const text = warning.trim();
590
+ if (text && !kept.includes(text)) kept.push(text);
591
+ }
592
+ return kept;
593
+ };
594
+ function envelopeObject(init) {
595
+ const frame = {
596
+ schema: ENVELOPE_SCHEMA,
597
+ ok: init.ok,
598
+ command: init.command,
599
+ status: init.status,
600
+ value: void 0 === init.value ? null : init.value,
601
+ error: init.error ?? null,
602
+ warnings: collectWarnings(init.warnings)
603
+ };
604
+ if (void 0 !== init.truncated) frame.truncated = init.truncated;
605
+ if ("string" == typeof init.hint && init.hint) frame.hint = init.hint;
606
+ return frame;
607
+ }
608
+ function envelope(init) {
609
+ return JSON.stringify(envelopeObject(init));
610
+ }
611
+ function sessionCommandSinceEnvelopeOwnsCommand(contract) {
612
+ return void 0 === contract.command ? {} : {
613
+ sessionCommand: contract.command
614
+ };
615
+ }
616
+ function isEnvelope(frame) {
617
+ return !!frame && "object" == typeof frame && frame.schema === ENVELOPE_SCHEMA;
618
+ }
619
+ const SPAWN_KILL_HEADROOM_MS = 5000;
620
+ const PINNED_CLI_VERSION = String(package_namespaceObject.El.OP ?? "latest").replace(/^[\^~]/, "");
621
+ function pinnedCliVersion() {
622
+ const override = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
623
+ return override || PINNED_CLI_VERSION;
624
+ }
625
+ function exactVersion(spec) {
626
+ return spec.trim().replace(/^[\^~]/, "").replace(/^v/, "");
627
+ }
628
+ function resolveExtensionInvocation(projectDir) {
629
+ if (projectDir) {
630
+ const bin = node_path.join(projectDir, "node_modules", ".bin", "win32" === process.platform ? "extension.cmd" : "extension");
631
+ try {
632
+ node_fs.accessSync(bin, node_fs.constants.X_OK);
633
+ return {
634
+ command: bin,
635
+ prefixArgs: []
636
+ };
637
+ } catch {}
638
+ }
639
+ return {
640
+ command: "npx",
641
+ prefixArgs: [
642
+ `extension@${pinnedCliVersion()}`
643
+ ]
644
+ };
645
+ }
646
+ function runExtensionCli(args, options) {
647
+ const { command, prefixArgs } = resolveExtensionInvocation(options?.cwd);
648
+ return new Promise((resolve)=>{
649
+ let ioDir;
650
+ let outFd;
651
+ let errFd;
652
+ try {
653
+ ioDir = node_fs.mkdtempSync(node_path.join(node_os.tmpdir(), "extension-mcp-io-"));
654
+ outFd = node_fs.openSync(node_path.join(ioDir, "stdout"), "a");
655
+ errFd = node_fs.openSync(node_path.join(ioDir, "stderr"), "a");
656
+ } catch (err) {
657
+ resolve({
658
+ code: 1,
659
+ stdout: "",
660
+ stderr: String(err)
661
+ });
662
+ return;
663
+ }
664
+ const readAll = (name)=>{
665
+ try {
666
+ return node_fs.readFileSync(node_path.join(ioDir, name), "utf8");
667
+ } catch {
668
+ return "";
669
+ }
670
+ };
671
+ const cleanup = ()=>{
672
+ for (const fd of [
673
+ outFd,
674
+ errFd
675
+ ])try {
676
+ node_fs.closeSync(fd);
677
+ } catch {}
678
+ try {
679
+ node_fs.rmSync(ioDir, {
680
+ recursive: true,
681
+ force: true
682
+ });
683
+ } catch {}
684
+ };
685
+ const child = cross_spawn(command, [
686
+ ...prefixArgs,
687
+ ...args
688
+ ], {
689
+ cwd: options?.cwd,
690
+ stdio: [
691
+ "ignore",
692
+ outFd,
693
+ errFd
694
+ ],
695
+ env: {
696
+ ...process.env,
697
+ FORCE_COLOR: "0",
698
+ NO_COLOR: "1"
699
+ },
700
+ timeout: (options?.timeoutMs ?? 30000) + SPAWN_KILL_HEADROOM_MS
701
+ });
702
+ child.on("close", (code)=>{
703
+ const stdout = readAll("stdout");
704
+ const stderr = readAll("stderr");
705
+ cleanup();
706
+ resolve({
707
+ code,
708
+ stdout,
709
+ stderr
710
+ });
711
+ });
712
+ child.on("error", (err)=>{
713
+ const stdout = readAll("stdout");
714
+ const stderr = readAll("stderr");
715
+ cleanup();
716
+ resolve({
717
+ code: 1,
718
+ stdout,
719
+ stderr: stderr || String(err)
720
+ });
721
+ });
722
+ });
723
+ }
724
+ function spawnExtensionCli(args, options) {
725
+ const { command, prefixArgs } = resolveExtensionInvocation(options?.projectDir ?? options?.cwd);
726
+ const logDir = node_fs.mkdtempSync(node_path.join(node_os.tmpdir(), "extension-mcp-"));
727
+ const logPath = node_path.join(logDir, "session.log");
728
+ const fd = node_fs.openSync(logPath, "a");
729
+ const child = cross_spawn(command, [
730
+ ...prefixArgs,
731
+ ...args
732
+ ], {
733
+ cwd: options?.cwd,
734
+ detached: true,
735
+ stdio: [
736
+ "ignore",
737
+ fd,
738
+ fd
739
+ ],
740
+ env: {
741
+ ...process.env,
742
+ FORCE_COLOR: "0",
743
+ NO_COLOR: "1"
744
+ }
745
+ });
746
+ node_fs.closeSync(fd);
747
+ let spawnFailure = null;
748
+ child.on("error", (err)=>{
749
+ spawnFailure = err;
750
+ });
751
+ child.unref();
752
+ return {
753
+ child,
754
+ logPath,
755
+ readOutput: ()=>{
756
+ try {
757
+ return node_fs.readFileSync(logPath, "utf8");
758
+ } catch {
759
+ return "";
760
+ }
761
+ },
762
+ spawnError: ()=>spawnFailure
763
+ };
764
+ }
765
+ async function spawnFailedEnvelope(command, spawned) {
766
+ const deadline = Date.now() + 500;
767
+ let cause = spawned.spawnError?.() ?? null;
768
+ while(!cause && Date.now() < deadline){
769
+ await new Promise((resolve)=>setTimeout(resolve, 25));
770
+ cause = spawned.spawnError?.() ?? null;
771
+ }
772
+ return envelope({
773
+ ok: false,
774
+ command,
775
+ status: "spawn-failed",
776
+ error: {
777
+ code: "E_CLI",
778
+ name: "CliError",
779
+ message: `The extension CLI process could not be spawned${cause ? ` (${cause.message})` : ""}. No session was started and nothing was registered.`
780
+ },
781
+ hint: "Neither a project-local node_modules/.bin/extension nor npx could be launched. Install the engine in the project (npm i -D extension) or put npx on the PATH the MCP server runs with, then call the tool again."
782
+ });
783
+ }
583
784
  function credentialsPath() {
584
785
  if ("win32" === process.platform) {
585
786
  const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
@@ -701,11 +902,17 @@ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
701
902
  function persistTokenResponse(args) {
702
903
  const token = String(args.data.token || "").trim();
703
904
  if (!token) throw new Error("Login returned no token.");
905
+ const workspaceSlug = String(args.data.workspaceSlug || "").trim();
906
+ const projectSlug = String(args.data.projectSlug || "").trim();
907
+ if (!workspaceSlug || !projectSlug) throw new Error(`Login for ${args.project} returned a token without a workspace/project scope; nothing was stored. Run extension_auth (action: login) again.`);
908
+ const [wantWorkspace = "", wantProject = ""] = args.project.split("/");
909
+ const matches = workspaceSlug.toLowerCase() === wantWorkspace.toLowerCase() && projectSlug.toLowerCase() === wantProject.toLowerCase();
910
+ if (!matches) throw new Error(`Login returned a token scoped to ${workspaceSlug}/${projectSlug}, not the requested ${args.project}; nothing was stored. Run extension_auth (action: login) again with the intended project.`);
704
911
  const creds = {
705
912
  version: 1,
706
913
  token,
707
- workspaceSlug: String(args.data.workspaceSlug || ""),
708
- projectSlug: String(args.data.projectSlug || ""),
914
+ workspaceSlug,
915
+ projectSlug,
709
916
  expiresAt: Number(args.data.expiresAt || 0),
710
917
  api: args.apiBase,
711
918
  provider: "extensiondev"
@@ -863,7 +1070,7 @@ class RegistryAccessTokens {
863
1070
  return result;
864
1071
  }
865
1072
  async mint(ref, apiHint) {
866
- const creds = readCredentials();
1073
+ const creds = readValidCredentials();
867
1074
  const token = String(process.env.EXTENSION_DEV_TOKEN || creds?.token || "").trim();
868
1075
  if (!token) return {
869
1076
  status: "no-credential"
@@ -1109,37 +1316,6 @@ function parseBuildIndex(json) {
1109
1316
  }
1110
1317
  return out;
1111
1318
  }
1112
- const ENVELOPE_SCHEMA = 1;
1113
- const collectWarnings = (warnings)=>{
1114
- if (!warnings) return [];
1115
- const kept = [];
1116
- for (const warning of warnings){
1117
- if ("string" != typeof warning) continue;
1118
- const text = warning.trim();
1119
- if (text && !kept.includes(text)) kept.push(text);
1120
- }
1121
- return kept;
1122
- };
1123
- function envelopeObject(init) {
1124
- const frame = {
1125
- schema: ENVELOPE_SCHEMA,
1126
- ok: init.ok,
1127
- command: init.command,
1128
- status: init.status,
1129
- value: void 0 === init.value ? null : init.value,
1130
- error: init.error ?? null,
1131
- warnings: collectWarnings(init.warnings)
1132
- };
1133
- if (void 0 !== init.truncated) frame.truncated = init.truncated;
1134
- if ("string" == typeof init.hint && init.hint) frame.hint = init.hint;
1135
- return frame;
1136
- }
1137
- function envelope(init) {
1138
- return JSON.stringify(envelopeObject(init));
1139
- }
1140
- function isEnvelope(frame) {
1141
- return !!frame && "object" == typeof frame && frame.schema === ENVELOPE_SCHEMA;
1142
- }
1143
1319
  const COMMAND = "extension_create";
1144
1320
  function scaffoldEnginePin(projectPath) {
1145
1321
  try {
@@ -1221,7 +1397,9 @@ async function create_handler(args) {
1221
1397
  const blob = logLines.join("\n").toLowerCase();
1222
1398
  return /timed out|timeout|etimedout|econnreset|rate limit|\b429\b|network|could not resolve host|terminal prompts disabled|authentication failed|early eof|rpc failed|remote end hung up/.test(blob);
1223
1399
  };
1400
+ const preExisting = node_fs.existsSync(projectInput);
1224
1401
  const cleanPartial = ()=>{
1402
+ if (preExisting) return;
1225
1403
  try {
1226
1404
  if (args.parentDir && node_fs.existsSync(projectInput)) node_fs.rmSync(projectInput, {
1227
1405
  recursive: true,
@@ -1297,7 +1475,7 @@ async function create_handler(args) {
1297
1475
  const addDev = (spec)=>"npm" === packageManager ? `npm i -D ${spec}` : "yarn" === packageManager ? `yarn add -D ${spec}` : `${packageManager} add -D ${spec}`;
1298
1476
  const pin = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
1299
1477
  const scaffoldPin = scaffoldEnginePin(result.projectPath);
1300
- const pinMatches = null !== scaffoldPin && "" !== pin && scaffoldPin.includes(pin);
1478
+ const pinMatches = null !== scaffoldPin && "" !== pin && exactVersion(scaffoldPin) === exactVersion(pin);
1301
1479
  const engineWarning = pin && "latest" !== pin && !pinMatches ? `The scaffold pins "extension": "${scaffoldPin ?? "unknown"}"; the project-local engine wins over EXTENSION_MCP_CLI_VERSION=${pin}. Run \`(cd ${result.projectPath} && ${addDev(`extension@${pin}`)})\` to match the pinned engine.` : void 0;
1302
1480
  const resolvedParent = args.parentDir ? node_path.resolve(args.parentDir) : process.cwd();
1303
1481
  const gitInit = node_fs.existsSync(node_path.join(result.projectPath, ".git"));
@@ -1432,9 +1610,13 @@ async function templateFileUrls(slug, relativePath) {
1432
1610
  urls.push(`${rawBaseForCommit(PINNED_COMMIT)}/examples/${slug}/${relative}`);
1433
1611
  return urls;
1434
1612
  }
1613
+ var templates_meta_snapshot_namespaceObject = JSON.parse('{"version":"2","sourceRepo":"extension-js/examples","generatorVersion":"2","commit":"336cfc03c39f9fa52f4ecffd351313241b5ca0a4","generatedAt":"2026-07-16T13:47:35.883Z","templates":[{"slug":"action","name":"action","title":"Action (Popup)","version":"1.0.0","manifest_version":3,"description":"Action popup example. Opens a toolbar popup with a simple page.","uiContext":["background"],"surfaces":["background"],"entrypoints":[],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["action","popup","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click the toolbar icon to open the action popup","Edit src/action/index.html and watch HMR update the popup"],"useCases":["Toolbar popup as the primary UI surface","Quick actions that don\'t need a full sidebar or page","Starting point for popup-based browser extensions"],"docsUrl":"https://extension.js.org/docs/implementation-guide/manifest-json","files":["extension.config.js","public/screenshot.png","src/action/index.html","src/action/scripts.js","src/action/styles.css","src/background.js","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/action/public/screenshot.png","icon":"examples/action/src/images/icon.png"},{"slug":"action-locales","name":"action-locales","title":"Action Locales","version":"1.0.0","manifest_version":3,"description":"Action popup example demonstrating i18n with _locales and message placeholders.","uiContext":["background"],"surfaces":["background"],"entrypoints":[],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/action/index.html","src/action/scripts.js","src/action/styles.css","src/background.js","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/action-locales/public/screenshot.png","icon":"examples/action-locales/src/images/icon.png"},{"slug":"ai-chatgpt","name":"ai-chatgpt","title":"ChatGPT AI Sidebar","version":"1.0.0","manifest_version":3,"description":"React sidebar with ChatGPT chat. Adds a side panel with a conversational interface powered by the OpenAI SDK.","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","chatgpt","openai","gpt","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your ChatGPT API key (from platform.openai.com)","Ask ChatGPT about the active tab — the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using ChatGPT","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware ChatGPT extensions"],"docsUrl":"https://platform.openai.com/docs/api-reference/chat","files":["extension.config.js","public/screenshot.png","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-chatgpt/public/screenshot.png","icon":"examples/ai-chatgpt/src/images/icon.png"},{"slug":"ai-claude","name":"ai-claude","title":"Claude AI Sidebar","version":"1.0.0","manifest_version":3,"description":"React sidebar with Claude AI chat. Adds a side panel with a conversational interface powered by the Anthropic SDK.","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","claude","anthropic","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your Claude API key (from console.anthropic.com)","Ask Claude about the active tab — the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using Claude","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware Claude extensions"],"docsUrl":"https://docs.anthropic.com/en/api/getting-started","files":["extension.config.js","public/screenshot.png","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-claude/public/screenshot.png","icon":"examples/ai-claude/src/images/icon.png"},{"slug":"ai-gemini","name":"ai-gemini","title":"Gemini AI Sidebar","version":"1.0.0","manifest_version":3,"description":"React sidebar with Gemini chat. Adds a side panel with a conversational interface powered by the Google Generative AI SDK.","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","gemini","google","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your Gemini API key (from aistudio.google.com)","Ask Gemini about the active tab — the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using Gemini","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware Gemini extensions"],"docsUrl":"https://ai.google.dev/gemini-api/docs","files":["extension.config.js","public/screenshot.png","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-gemini/public/screenshot.png","icon":"examples/ai-gemini/src/images/icon.png"},{"slug":"ai-perplexity","name":"ai-perplexity","title":"Perplexity AI Sidebar","version":"1.0.0","manifest_version":3,"description":"React sidebar with Perplexity chat. Adds a side panel with a conversational interface powered by the Perplexity API (OpenAI-compatible).","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","perplexity","search","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your Perplexity API key (from perplexity.ai/settings/api)","Ask Perplexity about the active tab — the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using Perplexity","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware Perplexity extensions"],"docsUrl":"https://docs.perplexity.ai/","files":["extension.config.js","public/screenshot.png","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-perplexity/public/screenshot.png","icon":"examples/ai-perplexity/src/images/icon.png"},{"slug":"content","name":"@extension-js/content","title":"Content scripts","version":"1.0.0","manifest_version":3,"description":"Content script example that injects a small badge into web pages to demonstrate DOM injection and styling in an extension.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/content/scripts.js","src/content/styles.css","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content/public/screenshot.png","icon":"examples/content/src/images/icon.png"},{"slug":"content-css-modules","name":"@extension-js/content-css-modules","title":"Content CSS Modules","version":"1.0.0","manifest_version":3,"description":"Content script example styled with CSS Modules. Injects a small badge and shows how to scope styles in content scripts.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/content/scripts.js","src/content/styles.module.css","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-css-modules/public/screenshot.png","icon":"examples/content-css-modules/src/images/icon.png"},{"slug":"content-custom-font","name":"content-custom-font","title":"Content Custom Font","version":"1.0.0","manifest_version":3,"description":"Content script example that loads custom web fonts. Demonstrates font files and web_accessible_resources setup.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/scripts.js"],"uiFramework":"","css":"css","configFiles":["postcss.config.js","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/content/scripts.js","src/content/styles.css","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-custom-font/public/screenshot.png","icon":"examples/content-custom-font/src/images/icon.png"},{"slug":"content-env","name":"content-env","title":"Content Env","version":"1.0.0","manifest_version":3,"description":"Content script example that reads environment variables (.env) and renders a small UI.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/scripts.ts"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-env/public/screenshot.png","icon":"examples/content-env/src/images/icon.png"},{"slug":"content-less","name":"@extension-js/content-less","title":"Content Less","version":"1.0.0","manifest_version":3,"description":"Content script example styled with Less. Injects a small badge and shows how to organize styles with Less in content scripts.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/scripts.js"],"uiFramework":"","css":"less","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"files":["extension.config.js","public/screenshot.png","src/background.js","src/content/scripts.js","src/content/styles.less","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-less/public/screenshot.png","icon":"examples/content-less/src/images/icon.png"},{"slug":"content-less-modules","name":"@extension-js/content-less-modules","title":"Content Less Modules","version":"1.0.0","manifest_version":3,"description":"Content script example using Less with CSS Modules. Injects a small badge and shows how to scope Less styles in content scripts.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/scripts.js"],"uiFramework":"","css":"less","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/content/scripts.js","src/content/styles.module.less","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-less-modules/public/screenshot.png","icon":"examples/content-less-modules/src/images/icon.png"},{"slug":"content-main-world","name":"content-main-world","title":"Main-world Content Script","version":"1.0.0","manifest_version":3,"description":"Main world content script with multiple groups. ISOLATED entries precede the MAIN world entry so bridge injection exercises canonical-index vs array-position resolution.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/isolated-one.js","content/isolated-two.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["content-scripts","main-world","isolated-world","javascript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any page and watch the main-world script run alongside isolated entries","Inspect window.postMessage bridges between isolated and main worlds"],"useCases":["Hook into page-level globals that isolated content scripts cannot reach","Bridge isolated and main-world execution contexts cleanly","Reference for multi-group content_scripts in MV3"],"docsUrl":"https://extension.js.org/docs/implementation-guide/content-scripts","files":["extension.config.js","public/screenshot.png","src/background.js","src/content/isolated-one.js","src/content/isolated-two.js","src/content/scripts.js","src/content/styles.css","src/content/utils/constants.js","src/content/utils/create-badge.js","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-main-world/public/screenshot.png","icon":"examples/content-main-world/src/images/icon.png"},{"slug":"content-multi-one-entry","name":"content-multi-one-entry","title":"Content Multi One Entry","version":"1.0.0","manifest_version":3,"description":"Content script example with four scripts listed in one entry. Each script injects a small element.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/script-top-left.js","content/script-top-right.js","content/script-bottom-left.js","content/script-bottom-right.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/content/script-bottom-left.js","src/content/script-bottom-right.js","src/content/script-top-left.js","src/content/script-top-right.js","src/content/styles.css","src/content/utils/constants.js","src/content/utils/create-badge.js","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-multi-one-entry/public/screenshot.png","icon":"examples/content-multi-one-entry/src/images/icon.png"},{"slug":"content-multi-three-entries","name":"content-multi-three-entries","title":"Multi-entry Content Scripts","version":"1.0.0","manifest_version":3,"description":"Content script example split across three entries (2 + 1 + 1). Each script injects a small element.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/script-top-left.js","content/script-top-right.js","content/script-bottom-left.js","content/script-bottom-right.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["content-scripts","multi-entry","javascript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any page — three separate content scripts inject small elements","Edit any of the entries in src/content/ to see HMR update independently"],"useCases":["Split content-script logic across multiple entries with different match rules","Target different page contexts with focused, independent bundles","Reference for the multi-entry content_scripts pattern in MV3"],"docsUrl":"https://extension.js.org/docs/implementation-guide/content-scripts","files":["extension.config.js","public/screenshot.png","src/background.js","src/content/script-bottom-left.js","src/content/script-bottom-right.js","src/content/script-top-left.js","src/content/script-top-right.js","src/content/styles.css","src/content/utils/constants.js","src/content/utils/create-badge.js","src/images/icon.png","src/manifest.json","src/screenshot.png"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-multi-three-entries/public/screenshot.png","icon":"examples/content-multi-three-entries/src/images/icon.png"},{"slug":"content-preact","name":"content-preact","title":"Content Preact","version":"1.0.0","manifest_version":3,"description":"Content script example rendered with Preact. Mounts a small UI inside a Shadow DOM on any page.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["./content/scripts.tsx"],"uiFramework":"preact","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/chromeWindow.png","src/images/icon.png","src/images/preact.png","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-preact/screenshot.png","icon":"examples/content-preact/src/images/icon.png"},{"slug":"content-react","name":"content-react","title":"Content React","version":"1.0.0","manifest_version":3,"description":"Content script example rendered with React. Mounts a small UI inside a Shadow DOM on any page.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["./content/scripts.tsx"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/chromeWindow.png","src/images/icon.png","src/images/react.png","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-react/screenshot.png","icon":"examples/content-react/src/images/icon.png"},{"slug":"content-sass","name":"@extension-js/content-sass","title":"Content Sass","version":"1.0.0","manifest_version":3,"description":"Content script example styled with Sass. Injects a small badge and shows how to organize styles with Sass in content scripts.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/scripts.js"],"uiFramework":"","css":"sass","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"files":["extension.config.js","public/screenshot.png","src/background.js","src/content/scripts.js","src/content/styles.scss","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-sass/public/screenshot.png","icon":"examples/content-sass/src/images/icon.png"},{"slug":"content-sass-modules","name":"@extension-js/content-sass-modules","title":"Content Sass Modules","version":"1.0.0","manifest_version":3,"description":"A browser extension content script example built with Extension.js using Sass CSS Modules. Demonstrates scoped styles for content scripts with Sass preprocessing.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/scripts.js"],"uiFramework":"","css":"sass","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"build":"extension build"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/content/scripts.js","src/content/styles.module.scss","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-sass-modules/public/screenshot.png","icon":"examples/content-sass-modules/src/images/icon.png"},{"slug":"content-svelte","name":"content-svelte","title":"Content Svelte","version":"1.0.0","manifest_version":3,"description":"Content script example rendered with Svelte. Mounts a small UI inside a Shadow DOM on any page.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["./content/scripts.ts"],"uiFramework":"svelte","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","src/background.ts","src/content/ContentApp.svelte","src/content/scripts.ts","src/content/styles.css","src/content/svelte.d.ts","src/images/chromeWindow.png","src/images/icon.png","src/images/svelte.png","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-svelte/screenshot.png","icon":"examples/content-svelte/src/images/icon.png"},{"slug":"content-typescript","name":"content-typescript","title":"Content TypeScript","version":"1.0.0","manifest_version":3,"description":"Content script example built with TypeScript. Mounts a small UI and uses typed modules.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/scripts.ts"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-typescript/public/screenshot.png","icon":"examples/content-typescript/src/images/icon.png"},{"slug":"content-vue","name":"content-vue","title":"Content Vue","version":"1.0.0","manifest_version":3,"description":"Content script example rendered with Vue. Mounts a small UI inside a Shadow DOM on any page.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["./content/scripts.ts"],"uiFramework":"vue","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","src/background.ts","src/content/ContentApp.vue","src/content/scripts.ts","src/content/shims-vue.d.ts","src/content/styles.css","src/images/chromeWindow.png","src/images/icon.png","src/images/logo.svg","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/images/vue.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-vue/screenshot.png","icon":"examples/content-vue/src/images/logo.svg"},{"slug":"init","name":"init","title":"Initial","version":"1.0.0","manifest_version":3,"description":"A basic browser extension example built with Extension.js. A starting point for developers learning current browser extension tooling.","uiContext":[],"surfaces":[],"entrypoints":[],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":false,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","src/images/icon.png","src/images/javascript.png","src/index.js","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/init/screenshot.png","icon":"examples/init/src/images/icon.png"},{"slug":"javascript","name":"javascript","title":"JavaScript","version":"1.0.0","manifest_version":3,"description":"JavaScript-based extension with a sidebar panel. Adds a sidebar with a simple page.","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/content/ContentApp.js","src/content/scripts.js","src/content/styles.css","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.js","src/sidebar/index.html","src/sidebar/scripts.js","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/javascript/screenshot.png","icon":"examples/javascript/src/images/icon.png"},{"slug":"new","name":"new","title":"Newtab","version":"1.0.0","manifest_version":3,"description":"New tab page example using plain JavaScript. Renders a simple page you can customize.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new/public/screenshot.png","icon":"examples/new/src/images/icon.png"},{"slug":"new-browser-flags","name":"new-browser-flags","title":"Newtab Browser Flags","version":"1.0.0","manifest_version":3,"description":"A browser extension example built with Extension.js demonstrating browser-specific features and flags. Shows how to handle browser differences across targets.","uiContext":["content","newtab","sidebar","background"],"surfaces":["content","newtab","sidebar","background"],"entrypoints":["newtab/index.html","sidebar/index.html","content/content.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"build":"extension build"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting","notifications"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"files":["extension.config.js","src/background.js","src/content/content.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/newtab.css","src/newtab/newtab.js","src/sidebar/index.html","src/sidebar/sidebar.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-browser-flags/screenshot.png","icon":"examples/new-browser-flags/src/images/icon.png"},{"slug":"new-config-eslint","name":"new-config-eslint","title":"Newtab Config ESLint","version":"1.0.0","manifest_version":3,"description":"New tab page example with ESLint configured. Shows how linting is set up in an extension project.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js","eslint.config.mjs"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-config-eslint/public/screenshot.png","icon":"examples/new-config-eslint/src/images/icon.png"},{"slug":"new-config-prettier","name":"new-config-prettier","title":"Newtab Config Prettier","version":"1.0.0","manifest_version":3,"description":"New tab page example with Prettier configured. Shows how formatting is set up in an extension project.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js",".prettierrc"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-config-prettier/public/screenshot.png","icon":"examples/new-config-prettier/src/images/icon.png"},{"slug":"new-config-stylelint","name":"new-config-stylelint","title":"Newtab Config Stylelint","version":"1.0.0","manifest_version":3,"description":"New tab page example with Stylelint configured. Shows how CSS linting is set up in an extension project.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"","css":"sass","configFiles":[".stylelintrc.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.scss"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-config-stylelint/public/screenshot.png","icon":"examples/new-config-stylelint/src/images/icon.png"},{"slug":"new-crypto","name":"new-crypto","title":"Newtab Crypto","version":"1.0.0","manifest_version":3,"description":"New tab page example demonstrating basic crypto operations inside an extension.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-crypto/public/screenshot.png","icon":"examples/new-crypto/src/images/icon.png"},{"slug":"new-env","name":"new-env","title":"Environment Variables","version":"1.0.0","manifest_version":3,"description":"New tab page example demonstrating environment variables (.env) in an extension project.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["env","configuration","new-tab","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Copy .env.example to .env and edit the values","Run npm run dev to launch the extension","Open a new tab — the page reads injected EXTENSION_PUBLIC_* values"],"useCases":["Inject build-time configuration without leaking secrets","Switch behavior per browser, mode, or deployment target","Reference for safe env-var usage across JS, JSON, and HTML"],"docsUrl":"https://extension.js.org/docs/features/environment-variables","files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-env/public/screenshot.png","icon":"examples/new-env/src/images/icon.png"},{"slug":"new-less","name":"new-less","title":"Newtab Less","version":"1.0.0","manifest_version":3,"description":"New tab page example styled with Less. Renders a simple page and organizes styles with Less.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"","css":"less","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.less"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-less/public/screenshot.png","icon":"examples/new-less/src/images/icon.png"},{"slug":"new-preact","name":"new-preact","title":"Newtab Preact","version":"1.0.0","manifest_version":3,"description":"New tab page example rendered with Preact. Loads a small Preact app you can extend.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"preact","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.tsx","src/newtab/index.html","src/newtab/scripts.tsx","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-preact/public/screenshot.png","icon":"examples/new-preact/src/images/icon.png"},{"slug":"new-react","name":"new-react","title":"Newtab React","version":"1.0.0","manifest_version":3,"description":"A browser extension new tab page example built with Extension.js and React. Demonstrates a React-based new tab page with a working build and preview flow.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"react","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.tsx","src/newtab/index.html","src/newtab/scripts.tsx","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-react/public/screenshot.png","icon":"examples/new-react/src/images/icon.png"},{"slug":"new-react-router","name":"new-react-router","title":"Newtab React Router","version":"1.0.0","manifest_version":3,"description":"New tab page example using React Router. Shows multiple routes inside the new tab app.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"react","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.tsx","src/newtab/index.html","src/newtab/scripts.tsx","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-react-router/public/screenshot.png","icon":"examples/new-react-router/src/images/icon.png"},{"slug":"new-sass","name":"new-sass","title":"Newtab Sass","version":"1.0.0","manifest_version":3,"description":"New tab page example styled with Sass. Renders a simple page and organizes styles with Sass.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"","css":"sass","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.scss"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-sass/public/screenshot.png","icon":"examples/new-sass/src/images/icon.png"},{"slug":"new-svelte","name":"new-svelte","title":"Newtab Svelte","version":"1.0.0","manifest_version":3,"description":"New tab page example rendered with Svelte. Loads a small Svelte app you can extend.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"svelte","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.svelte","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-svelte/public/screenshot.png","icon":"examples/new-svelte/src/images/icon.png"},{"slug":"new-typescript","name":"new-typescript","title":"Newtab TypeScript","version":"1.0.0","manifest_version":3,"description":"A browser extension new tab page example built with Extension.js and TypeScript. Demonstrates a type-safe new tab page with a working build and preview flow.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-typescript/public/screenshot.png","icon":"examples/new-typescript/src/images/icon.png"},{"slug":"new-vue","name":"new-vue","title":"Newtab Vue","version":"1.0.0","manifest_version":3,"description":"New tab page example rendered with Vue. Loads a small Vue app you can extend.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html"],"uiFramework":"vue","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.vue","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-vue/public/screenshot.png","icon":"examples/new-vue/src/images/icon.png"},{"slug":"playwright","name":"playwright","title":"Playwright E2E Sidebar","version":"1.0.0","manifest_version":3,"description":"Sidebar extension example demonstrating Playwright-driven E2E automation.","uiContext":["sidebar","background"],"surfaces":["sidebar","background"],"entrypoints":["sidebar/index.html"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["testing","playwright","e2e","sidebar","typescript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":4,"firstSteps":["Run npm install to set up Playwright deps","Run npm run dev to launch the extension","Run npm test to execute the Playwright spec against the loaded extension"],"useCases":["End-to-end testing for browser-extension surfaces (popup, sidebar, content)","CI-ready Playwright setup wired to Extension.js builds","Reference for automating extension flows in headed and headless modes"],"docsUrl":"https://extension.js.org/docs/workflows/playwright-e2e","files":["extension.config.js","public/screenshot.png","src/background.ts","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.ts","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/playwright/public/screenshot.png","icon":"examples/playwright/src/images/icon.png"},{"slug":"preact","name":"preact","title":"Preact","version":"1.0.0","manifest_version":3,"description":"Preact-based extension with a sidebar panel. Adds a sidebar with a simple page.","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.tsx"],"uiFramework":"preact","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"files":["extension.config.js","public/screenshot.png","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/icon.png","src/manifest.json","src/public/extension.svg","src/public/logo.svg","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/preact/public/screenshot.png","icon":"examples/preact/src/images/icon.png"},{"slug":"react","name":"react","title":"React","version":"1.0.0","manifest_version":3,"description":"React-based extension with a sidebar panel. Adds a sidebar with a simple page.","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.tsx"],"uiFramework":"react","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"files":["extension.config.js","public/screenshot.png","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/react/public/screenshot.png","icon":"examples/react/src/images/icon.png"},{"slug":"sidebar","name":"sidebar","title":"Sidebar page","version":"1.0.0","manifest_version":3,"description":"Sidebar panel example. Adds a sidebar to the browser with a simple page.","uiContext":["sidebar","background"],"surfaces":["sidebar","background"],"entrypoints":["sidebar/index.html"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/sidebar/index.html","src/sidebar/scripts.js","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar/public/screenshot.png","icon":"examples/sidebar/src/images/icon.png"},{"slug":"sidebar-antd","name":"sidebar-antd","title":"Sidebar Ant Design","version":"1.0.0","manifest_version":3,"description":"React sidebar using antd and @ant-design/x. Regression coverage for issue #445.","uiContext":["sidebar","background"],"surfaces":["sidebar","background"],"entrypoints":["sidebar/index.html"],"uiFramework":"react","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.jsx","src/sidebar/index.html","src/sidebar/scripts.jsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar-antd/public/screenshot.png","icon":"examples/sidebar-antd/src/images/icon.png"},{"slug":"sidebar-monorepo-turbopack","name":"sidebar-monorepo-turbopack","title":"Sidebar Turbopack","version":"1.0.0","manifest_version":3,"description":"Monorepo example with content script and sidebar.","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"dev":"extension dev packages/extension","build":"extension build packages/extension"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"files":["extension.config.js","packages/analytics/src/index.js","packages/extension/src/background.js","packages/extension/src/content/scripts.js","packages/extension/src/content/styles.css","packages/extension/src/images/icon.png","packages/extension/src/manifest.json","packages/extension/src/sidebar/SidebarApp.js","packages/extension/src/sidebar/index.html","packages/extension/src/sidebar/scripts.js","packages/extension/src/sidebar/styles.css","public/screenshot.png"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar-monorepo-turbopack/public/screenshot.png","icon":null},{"slug":"sidebar-shadcn","name":"sidebar-shadcn","title":"Sidebar shadcn","version":"1.0.0","manifest_version":3,"description":"React sidebar example using shadcn/ui components. Adds a sidebar panel with a simple React page.","uiContext":["sidebar","background"],"surfaces":["sidebar","background"],"entrypoints":["sidebar/index.html"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"files":["extension.config.js","public/screenshot.png","src/background.ts","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/label.tsx","src/components/ui/switch.tsx","src/images/icon.png","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar-shadcn/public/screenshot.png","icon":"examples/sidebar-shadcn/src/images/icon.png"},{"slug":"special-folders-pages","name":"special-folders-pages","title":"Special Folders: pages/","version":"1.0.0","manifest_version":3,"description":"Opens a welcome page on extension load, showcasing the pages/ folder.","uiContext":["background"],"surfaces":["background"],"entrypoints":[],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["special-folders","pages","welcome-page","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","A welcome page from src/pages/ opens automatically on install","Add another HTML file under src/pages/ — it ships as a separate page"],"useCases":["Onboarding, help, and welcome pages that ship with the extension","Standalone HTML routes without manifest plumbing","Reference for Extension.js\'s pages/ special folder convention"],"docsUrl":"https://extension.js.org/docs/features/special-folders","files":["extension.config.js","public/css/file.css","public/html/file.html","public/js/file.js","public/screenshot.png","src/background.js","src/images/icon.png","src/images/javascript.png","src/manifest.json","src/sandbox/index.html","src/sandbox/scripts.js","src/sandbox/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/special-folders-pages/public/screenshot.png","icon":"examples/special-folders-pages/src/images/icon.png"},{"slug":"special-folders-scripts","name":"special-folders-scripts","title":"Special Folders Scripts","version":"1.0.0","manifest_version":3,"description":"Demonstrates scripts/ folder organization and how to run standalone scripts via the extension.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"files":["extension.config.js","public/screenshot.png","src/background.js","src/content/scripts.js","src/content/styles.css","src/images/icon.png","src/images/javascript.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/special-folders-scripts/public/screenshot.png","icon":"examples/special-folders-scripts/src/images/icon.png"},{"slug":"svelte","name":"svelte","title":"Svelte","version":"1.0.0","manifest_version":3,"description":"Svelte-based extension with a sidebar panel. Adds a sidebar with a simple page.","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.ts"],"uiFramework":"svelte","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"files":["extension.config.js","public/screenshot.png","src/background.ts","src/content/ContentApp.svelte","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/manifest.json","src/public/extension.svg","src/sidebar/SidebarApp.svelte","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/svelte/public/screenshot.png","icon":"examples/svelte/src/images/icon.png"},{"slug":"transformers-js","name":"transformers-js","title":"Transformers.js","version":"1.0.0","manifest_version":3,"description":"Transformers.js demo with a sidebar and a content script: classify the active page or your selection on-device via WebGPU/WASM.","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"files":["extension.config.js","public/screenshot.png","src/background.js","src/constants.js","src/content/scripts.js","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.js","src/sidebar/index.html","src/sidebar/sakura.css","src/sidebar/scripts.js","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/transformers-js/public/screenshot.png","icon":"examples/transformers-js/src/images/icon.png"},{"slug":"typescript","name":"typescript","title":"TypeScript","version":"1.0.0","manifest_version":3,"description":"TypeScript-based extension with a sidebar panel. Adds a sidebar with a simple page.","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.ts"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"files":["extension.config.js","public/screenshot.png","src/background.ts","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/content/types.d.ts","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.ts","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/typescript/screenshot.png","icon":"examples/typescript/src/images/icon.png"},{"slug":"vue","name":"vue","title":"Vue","version":"1.0.0","manifest_version":3,"description":"Vue.js-based extension with a sidebar panel. Adds a sidebar with a simple page.","uiContext":["content","sidebar","background"],"surfaces":["content","sidebar","background"],"entrypoints":["sidebar/index.html","content/scripts.ts"],"uiFramework":"vue","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"files":["extension.config.js","public/screenshot.png","src/background.ts","src/content/ContentApp.vue","src/content/scripts.ts","src/content/styles.css","src/content/types.d.ts","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.vue","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/vue/public/screenshot.png","icon":"examples/vue/src/images/icon.png"}]}');
1435
1614
  const CACHE_DIR = node_path.join(node_os.homedir(), ".cache", "extension-js");
1436
1615
  const CACHE_FILE = node_path.join(CACHE_DIR, "templates-meta.json");
1437
1616
  const CACHE_TTL_MS = 3600000;
1617
+ function isUsableMeta(data) {
1618
+ return "object" == typeof data && null !== data && Array.isArray(data.templates) && data.templates.length > 0;
1619
+ }
1438
1620
  function isCacheValid() {
1439
1621
  try {
1440
1622
  const stat = node_fs.statSync(CACHE_FILE);
@@ -1443,10 +1625,18 @@ function isCacheValid() {
1443
1625
  return false;
1444
1626
  }
1445
1627
  }
1628
+ function readCachedMeta() {
1629
+ try {
1630
+ const cached = JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
1631
+ return isUsableMeta(cached) ? cached : null;
1632
+ } catch {
1633
+ return null;
1634
+ }
1635
+ }
1446
1636
  async function fetchTemplatesMeta() {
1447
1637
  if (isCacheValid()) {
1448
- const cached = JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
1449
- return cached;
1638
+ const cached = readCachedMeta();
1639
+ if (cached) return cached;
1450
1640
  }
1451
1641
  let lastStatus = 0;
1452
1642
  for (const url of (await templateMetaUrls()))try {
@@ -1456,13 +1646,18 @@ async function fetchTemplatesMeta() {
1456
1646
  continue;
1457
1647
  }
1458
1648
  const data = await response.json();
1649
+ if (!isUsableMeta(data)) continue;
1459
1650
  node_fs.mkdirSync(CACHE_DIR, {
1460
1651
  recursive: true
1461
1652
  });
1462
- node_fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
1653
+ const tmpFile = `${CACHE_FILE}.${process.pid}.tmp`;
1654
+ node_fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2));
1655
+ node_fs.renameSync(tmpFile, CACHE_FILE);
1463
1656
  return data;
1464
1657
  } catch {}
1465
- if (node_fs.existsSync(CACHE_FILE)) return JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
1658
+ const stale = readCachedMeta();
1659
+ if (stale) return stale;
1660
+ if (isUsableMeta(templates_meta_snapshot_namespaceObject)) return templates_meta_snapshot_namespaceObject;
1466
1661
  throw new Error(`Failed to fetch templates-meta.json (${lastStatus || "network error"}).`);
1467
1662
  }
1468
1663
  async function listTemplates(filters) {
@@ -1762,144 +1957,8 @@ const LAUNCH_BROWSER = {
1762
1957
  enum: LAUNCHABLE_BROWSERS,
1763
1958
  default: "chrome"
1764
1959
  };
1765
- const PINNED_CLI_VERSION = String(package_namespaceObject.El.OP ?? "latest").replace(/^[\^~]/, "");
1766
- function pinnedCliVersion() {
1767
- const override = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
1768
- return override || PINNED_CLI_VERSION;
1769
- }
1770
- function resolveExtensionInvocation(projectDir) {
1771
- if (projectDir) {
1772
- const bin = node_path.join(projectDir, "node_modules", ".bin", "win32" === process.platform ? "extension.cmd" : "extension");
1773
- try {
1774
- node_fs.accessSync(bin, node_fs.constants.X_OK);
1775
- return {
1776
- command: bin,
1777
- prefixArgs: []
1778
- };
1779
- } catch {}
1780
- }
1781
- return {
1782
- command: "npx",
1783
- prefixArgs: [
1784
- `extension@${pinnedCliVersion()}`
1785
- ]
1786
- };
1787
- }
1788
- function runExtensionCli(args, options) {
1789
- const { command, prefixArgs } = resolveExtensionInvocation(options?.cwd);
1790
- return new Promise((resolve)=>{
1791
- let ioDir;
1792
- let outFd;
1793
- let errFd;
1794
- try {
1795
- ioDir = node_fs.mkdtempSync(node_path.join(node_os.tmpdir(), "extension-mcp-io-"));
1796
- outFd = node_fs.openSync(node_path.join(ioDir, "stdout"), "a");
1797
- errFd = node_fs.openSync(node_path.join(ioDir, "stderr"), "a");
1798
- } catch (err) {
1799
- resolve({
1800
- code: 1,
1801
- stdout: "",
1802
- stderr: String(err)
1803
- });
1804
- return;
1805
- }
1806
- const readAll = (name)=>{
1807
- try {
1808
- return node_fs.readFileSync(node_path.join(ioDir, name), "utf8");
1809
- } catch {
1810
- return "";
1811
- }
1812
- };
1813
- const cleanup = ()=>{
1814
- for (const fd of [
1815
- outFd,
1816
- errFd
1817
- ])try {
1818
- node_fs.closeSync(fd);
1819
- } catch {}
1820
- try {
1821
- node_fs.rmSync(ioDir, {
1822
- recursive: true,
1823
- force: true
1824
- });
1825
- } catch {}
1826
- };
1827
- const child = cross_spawn(command, [
1828
- ...prefixArgs,
1829
- ...args
1830
- ], {
1831
- cwd: options?.cwd,
1832
- stdio: [
1833
- "ignore",
1834
- outFd,
1835
- errFd
1836
- ],
1837
- env: {
1838
- ...process.env,
1839
- FORCE_COLOR: "0",
1840
- NO_COLOR: "1"
1841
- },
1842
- timeout: options?.timeoutMs ?? 30000
1843
- });
1844
- child.on("close", (code)=>{
1845
- const stdout = readAll("stdout");
1846
- const stderr = readAll("stderr");
1847
- cleanup();
1848
- resolve({
1849
- code,
1850
- stdout,
1851
- stderr
1852
- });
1853
- });
1854
- child.on("error", (err)=>{
1855
- const stdout = readAll("stdout");
1856
- const stderr = readAll("stderr");
1857
- cleanup();
1858
- resolve({
1859
- code: 1,
1860
- stdout,
1861
- stderr: stderr || String(err)
1862
- });
1863
- });
1864
- });
1865
- }
1866
- function spawnExtensionCli(args, options) {
1867
- const { command, prefixArgs } = resolveExtensionInvocation(options?.projectDir ?? options?.cwd);
1868
- const logDir = node_fs.mkdtempSync(node_path.join(node_os.tmpdir(), "extension-mcp-"));
1869
- const logPath = node_path.join(logDir, "session.log");
1870
- const fd = node_fs.openSync(logPath, "a");
1871
- const child = cross_spawn(command, [
1872
- ...prefixArgs,
1873
- ...args
1874
- ], {
1875
- cwd: options?.cwd,
1876
- detached: true,
1877
- stdio: [
1878
- "ignore",
1879
- fd,
1880
- fd
1881
- ],
1882
- env: {
1883
- ...process.env,
1884
- FORCE_COLOR: "0",
1885
- NO_COLOR: "1"
1886
- }
1887
- });
1888
- node_fs.closeSync(fd);
1889
- child.unref();
1890
- return {
1891
- child,
1892
- logPath,
1893
- readOutput: ()=>{
1894
- try {
1895
- return node_fs.readFileSync(logPath, "utf8");
1896
- } catch {
1897
- return "";
1898
- }
1899
- }
1900
- };
1901
- }
1902
1960
  const sessions = new Map();
1961
+ const registrationStamps = new Map();
1903
1962
  function sessionKey(projectPath, browser) {
1904
1963
  return `${node_path.resolve(projectPath)}::${browser}`;
1905
1964
  }
@@ -1910,9 +1969,14 @@ function markerPath(projectPath, browser) {
1910
1969
  const digest = node_crypto.createHash("sha1").update(sessionKey(projectPath, browser)).digest("hex").slice(0, 16);
1911
1970
  return node_path.join(markerDir(), `${digest}.json`);
1912
1971
  }
1913
- function removeSessionMarker(projectPath, browser) {
1972
+ function removeSessionMarker(projectPath, browser, pid) {
1973
+ const file = markerPath(projectPath, browser);
1974
+ if (void 0 !== pid) try {
1975
+ const parsed = JSON.parse(node_fs.readFileSync(file, "utf8"));
1976
+ if ("number" == typeof parsed?.pid && parsed.pid !== pid) return;
1977
+ } catch {}
1914
1978
  try {
1915
- node_fs.rmSync(markerPath(projectPath, browser), {
1979
+ node_fs.rmSync(file, {
1916
1980
  force: true
1917
1981
  });
1918
1982
  } catch {}
@@ -1931,8 +1995,7 @@ function listSessionMarkers() {
1931
1995
  } catch {}
1932
1996
  return out;
1933
1997
  }
1934
- function registerSession(info) {
1935
- sessions.set(sessionKey(info.projectPath, info.browser), info);
1998
+ function writeMarkerBestEffort(info, registeredAtMs) {
1936
1999
  try {
1937
2000
  node_fs.mkdirSync(markerDir(), {
1938
2001
  recursive: true
@@ -1940,12 +2003,34 @@ function registerSession(info) {
1940
2003
  node_fs.writeFileSync(markerPath(info.projectPath, info.browser), JSON.stringify({
1941
2004
  ...info,
1942
2005
  projectPath: node_path.resolve(info.projectPath),
1943
- registeredAt: new Date().toISOString()
2006
+ registeredAt: new Date(registeredAtMs).toISOString()
1944
2007
  }));
1945
2008
  } catch {}
1946
2009
  }
1947
- function getSession(projectPath, browser) {
1948
- return sessions.get(sessionKey(projectPath, browser));
2010
+ function registerSession(info) {
2011
+ const key = sessionKey(info.projectPath, info.browser);
2012
+ const prior = registrationStamps.get(key);
2013
+ const at = prior && prior.pid === info.pid ? prior.at : Date.now();
2014
+ registrationStamps.set(key, {
2015
+ pid: info.pid,
2016
+ at
2017
+ });
2018
+ sessions.set(key, info);
2019
+ writeMarkerBestEffort(info, at);
2020
+ }
2021
+ function sessionSinceMs(projectPath, browser) {
2022
+ const stamp = registrationStamps.get(sessionKey(projectPath, browser));
2023
+ if (stamp) return stamp.at;
2024
+ const resolved = node_path.resolve(projectPath);
2025
+ for (const marker of listSessionMarkers()){
2026
+ if (node_path.resolve(marker.projectPath) !== resolved || marker.browser !== browser) continue;
2027
+ const registeredAt = marker.registeredAt;
2028
+ if ("string" == typeof registeredAt) {
2029
+ const parsed = Date.parse(registeredAt);
2030
+ if (Number.isFinite(parsed)) return parsed;
2031
+ }
2032
+ }
2033
+ return null;
1949
2034
  }
1950
2035
  function findSessionInfo(projectPath, browser) {
1951
2036
  const inMemory = sessions.get(sessionKey(projectPath, browser));
@@ -1953,8 +2038,11 @@ function findSessionInfo(projectPath, browser) {
1953
2038
  const resolved = node_path.resolve(projectPath);
1954
2039
  for (const marker of listSessionMarkers())if (node_path.resolve(marker.projectPath) === resolved && marker.browser === browser) return marker;
1955
2040
  }
1956
- function removeSession(projectPath, browser) {
1957
- sessions.delete(sessionKey(projectPath, browser));
2041
+ function removeSession(projectPath, browser, pid) {
2042
+ const key = sessionKey(projectPath, browser);
2043
+ if (void 0 !== pid && sessions.get(key)?.pid !== pid) return;
2044
+ sessions.delete(key);
2045
+ registrationStamps.delete(key);
1958
2046
  }
1959
2047
  function listSessions() {
1960
2048
  return Array.from(sessions.values());
@@ -2015,6 +2103,17 @@ function liveProjectSessions(projectPath) {
2015
2103
  source: "contract"
2016
2104
  });
2017
2105
  }
2106
+ for (const marker of listSessionMarkers()){
2107
+ if (node_path.resolve(marker.projectPath) === resolved) {
2108
+ if (!out.has(marker.browser)) {
2109
+ if ("number" == typeof marker.pid && pidAlive(marker.pid)) out.set(marker.browser, {
2110
+ browser: marker.browser,
2111
+ pid: marker.pid,
2112
+ source: "marker"
2113
+ });
2114
+ }
2115
+ }
2116
+ }
2018
2117
  return [
2019
2118
  ...out.values()
2020
2119
  ];
@@ -2641,7 +2740,7 @@ async function build_handler(args) {
2641
2740
  hint: "Check that the project has a valid src/manifest.json and its dependencies are installed (extension_dev auto-installs; build does not)."
2642
2741
  });
2643
2742
  }
2644
- const NOISE = [
2743
+ const MERGED_FD_CHATTER = [
2645
2744
  /^npm warn Unknown project config/i,
2646
2745
  /This will stop working in the next major version of npm/i,
2647
2746
  /^npm warn config/i,
@@ -2655,7 +2754,7 @@ const NOISE = [
2655
2754
  /Successfully compiled asm\.js/i
2656
2755
  ];
2657
2756
  function denoiseCliLog(raw) {
2658
- return raw.split("\n").filter((line)=>!NOISE.some((re)=>re.test(line.trim()))).join("\n").replace(/\n{2,}/g, "\n").trimStart();
2757
+ return raw.split("\n").filter((line)=>!MERGED_FD_CHATTER.some((re)=>re.test(line.trim()))).join("\n").replace(/\n{2,}/g, "\n").trimStart();
2659
2758
  }
2660
2759
  function legacyCompileScrape(cleanOutput) {
2661
2760
  return /compiled with errors|✖✖✖|ERROR in |Module not found|NOT FOUND/i.test(cleanOutput);
@@ -2664,7 +2763,7 @@ function legacyProfileLockScrape(cleanOutput) {
2664
2763
  return /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(cleanOutput);
2665
2764
  }
2666
2765
  const LEGACY_FIDELITY_WARNING = "This session ran a CLI that does not stamp the machine contract, so the boot verdict was read from the dev server's output instead of its ready contract. Diagnostics are less precise: compile errors come back as a text tail rather than a list. Upgrade the project's extension dependency to get the precise verdict.";
2667
- const PROFILE_LOCKED_CODE = "profile_locked";
2766
+ const READY_STAMP_PROFILE_LOCKED_CODE = "profile_locked";
2668
2767
  function readyContractPath(projectPath, browser) {
2669
2768
  return node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
2670
2769
  }
@@ -2702,7 +2801,7 @@ function contractVerdict(reading, noBrowser) {
2702
2801
  const { contract, fresh } = reading;
2703
2802
  if (!fresh) return null;
2704
2803
  if ("error" !== contract.status) return null;
2705
- if (!noBrowser && contract.code === PROFILE_LOCKED_CODE) return {
2804
+ if (!noBrowser && contract.code === READY_STAMP_PROFILE_LOCKED_CODE) return {
2706
2805
  kind: "profile-locked",
2707
2806
  owner: profileLockOwner(contract),
2708
2807
  message: "string" == typeof contract.message ? contract.message : void 0,
@@ -2737,7 +2836,7 @@ async function pollBootVerdict(projectPath, browser, options) {
2737
2836
  signal: child.signalCode
2738
2837
  },
2739
2838
  machineContract: speaksMachineContract(contractSeen?.contract),
2740
- output: denoiseCliLog(readOutput()),
2839
+ evidenceTail: denoiseCliLog(readOutput()),
2741
2840
  warnings: []
2742
2841
  };
2743
2842
  contractSeen = readContract(projectPath, browser, since) ?? contractSeen;
@@ -2746,7 +2845,7 @@ async function pollBootVerdict(projectPath, browser, options) {
2746
2845
  if (verdict) return {
2747
2846
  verdict,
2748
2847
  machineContract: speaksMachineContract(contractSeen.contract),
2749
- output: denoiseCliLog(readOutput()),
2848
+ evidenceTail: denoiseCliLog(readOutput()),
2750
2849
  warnings: []
2751
2850
  };
2752
2851
  }
@@ -2754,43 +2853,43 @@ async function pollBootVerdict(projectPath, browser, options) {
2754
2853
  await new Promise((resolve)=>setTimeout(resolve, Math.min(intervalMs, Math.max(deadline - Date.now(), 1))));
2755
2854
  }
2756
2855
  const machineContract = speaksMachineContract(contractSeen?.contract);
2757
- const output = denoiseCliLog(readOutput());
2856
+ const evidenceTail = denoiseCliLog(readOutput());
2758
2857
  if (machineContract) return {
2759
2858
  verdict: {
2760
2859
  kind: "alive"
2761
2860
  },
2762
2861
  machineContract,
2763
- output,
2862
+ evidenceTail,
2764
2863
  warnings: []
2765
2864
  };
2766
- if (legacyCompileScrape(output)) return {
2865
+ if (legacyCompileScrape(evidenceTail)) return {
2767
2866
  verdict: {
2768
2867
  kind: "compile-failed",
2769
2868
  compileErrors: []
2770
2869
  },
2771
2870
  machineContract,
2772
- output,
2871
+ evidenceTail,
2773
2872
  warnings: [
2774
2873
  LEGACY_FIDELITY_WARNING
2775
2874
  ]
2776
2875
  };
2777
- if (!noBrowser && legacyProfileLockScrape(output)) return {
2876
+ if (!noBrowser && legacyProfileLockScrape(evidenceTail)) return {
2778
2877
  verdict: {
2779
2878
  kind: "profile-locked",
2780
2879
  owner: null
2781
2880
  },
2782
2881
  machineContract,
2783
- output,
2882
+ evidenceTail,
2784
2883
  warnings: [
2785
2884
  LEGACY_FIDELITY_WARNING
2786
2885
  ]
2787
2886
  };
2788
2887
  return {
2789
2888
  verdict: {
2790
- kind: "alive"
2889
+ kind: "silent-within-budget"
2791
2890
  },
2792
2891
  machineContract,
2793
- output,
2892
+ evidenceTail,
2794
2893
  warnings: []
2795
2894
  };
2796
2895
  }
@@ -2833,16 +2932,54 @@ function pgrepPids(pattern) {
2833
2932
  return [];
2834
2933
  }
2835
2934
  }
2935
+ function escapeRegex(text) {
2936
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2937
+ }
2938
+ function projectPathForms(projectPath) {
2939
+ const forms = new Set([
2940
+ projectPath,
2941
+ node_path.resolve(projectPath)
2942
+ ]);
2943
+ try {
2944
+ forms.add(node_fs.realpathSync(projectPath));
2945
+ } catch {}
2946
+ return [
2947
+ ...forms
2948
+ ];
2949
+ }
2950
+ const PLAUSIBLE_SESSION_BINARY = /chrom|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari|node|electron|extension/i;
2951
+ function processCommand(pid) {
2952
+ try {
2953
+ const cmdline = node_fs.readFileSync(`/proc/${pid}/cmdline`, "utf8");
2954
+ const argv0 = cmdline.split("\0")[0];
2955
+ if (argv0) return node_path.basename(argv0);
2956
+ } catch {}
2957
+ try {
2958
+ return execFileSync("ps", [
2959
+ "-o",
2960
+ "comm=",
2961
+ "-p",
2962
+ String(pid)
2963
+ ], {
2964
+ encoding: "utf8"
2965
+ }).trim();
2966
+ } catch {
2967
+ return "";
2968
+ }
2969
+ }
2836
2970
  function sessionProcessPids(projectPath) {
2837
- const resolved = node_path.resolve(projectPath);
2838
2971
  const pids = new Set();
2839
- for (const marker of [
2840
- `extension dev ${resolved}`,
2841
- node_path.join(resolved, "dist")
2842
- ])for (const pid of pgrepPids(marker))pids.add(pid);
2972
+ for (const form of projectPathForms(projectPath)){
2973
+ const escaped = escapeRegex(form);
2974
+ const patterns = [
2975
+ `extension[^ ]* (dev|start|preview) ${escaped}`,
2976
+ `${escaped}${escapeRegex(node_path.sep)}dist${escapeRegex(node_path.sep)}extension-profile-`
2977
+ ];
2978
+ for (const pattern of patterns)for (const pid of pgrepPids(pattern))pids.add(pid);
2979
+ }
2843
2980
  return [
2844
2981
  ...pids
2845
- ];
2982
+ ].filter((pid)=>PLAUSIBLE_SESSION_BINARY.test(processCommand(pid)));
2846
2983
  }
2847
2984
  function reapSessionProcesses(projectPath) {
2848
2985
  const pids = sessionProcessPids(projectPath);
@@ -2885,7 +3022,7 @@ function pidFromReadyContract(projectPath, browser) {
2885
3022
  }
2886
3023
  }
2887
3024
  async function stopOne(projectPath, browser) {
2888
- const session = getSession(projectPath, browser);
3025
+ const session = findSessionInfo(projectPath, browser);
2889
3026
  const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
2890
3027
  if (null == pid) {
2891
3028
  const reaped = reapSessionProcesses(projectPath);
@@ -3127,6 +3264,7 @@ async function dev_handler(args) {
3127
3264
  projectDir: args.projectPath
3128
3265
  });
3129
3266
  const { child, logPath } = spawned;
3267
+ if (void 0 === child.pid) return spawnFailedEnvelope(dev_schema.name, spawned);
3130
3268
  const pid = child.pid;
3131
3269
  registerSession({
3132
3270
  pid,
@@ -3136,7 +3274,10 @@ async function dev_handler(args) {
3136
3274
  command: "dev",
3137
3275
  noBrowser: Boolean(args.noBrowser)
3138
3276
  });
3139
- child.on("exit", ()=>removeSession(args.projectPath, browser));
3277
+ child.on("exit", ()=>{
3278
+ removeSession(args.projectPath, browser, pid);
3279
+ removeSessionMarker(args.projectPath, browser, pid);
3280
+ });
3140
3281
  const boot = await pollBootVerdict(args.projectPath, browser, {
3141
3282
  child,
3142
3283
  readOutput: spawned.readOutput,
@@ -3144,7 +3285,7 @@ async function dev_handler(args) {
3144
3285
  since: spawnedAt,
3145
3286
  noBrowser: Boolean(args.noBrowser)
3146
3287
  });
3147
- const cleanOutput = boot.output;
3288
+ const cleanOutput = boot.evidenceTail;
3148
3289
  const session = {
3149
3290
  projectPath: args.projectPath,
3150
3291
  browser,
@@ -3337,6 +3478,7 @@ async function start_handler(args) {
3337
3478
  projectDir: args.projectPath
3338
3479
  });
3339
3480
  const { child, logPath } = spawned;
3481
+ if (void 0 === child.pid) return spawnFailedEnvelope(start_schema.name, spawned);
3340
3482
  const pid = child.pid;
3341
3483
  registerSession({
3342
3484
  pid,
@@ -3344,7 +3486,10 @@ async function start_handler(args) {
3344
3486
  projectPath: args.projectPath,
3345
3487
  command
3346
3488
  });
3347
- child.on("exit", ()=>removeSession(args.projectPath, browser));
3489
+ child.on("exit", ()=>{
3490
+ removeSession(args.projectPath, browser, pid);
3491
+ removeSessionMarker(args.projectPath, browser, pid);
3492
+ });
3348
3493
  const boot = await pollBootVerdict(args.projectPath, browser, {
3349
3494
  child,
3350
3495
  readOutput: spawned.readOutput,
@@ -3352,7 +3497,7 @@ async function start_handler(args) {
3352
3497
  since: spawnedAt,
3353
3498
  noBrowser: Boolean(args.noBrowser)
3354
3499
  });
3355
- const cleanOutput = boot.output;
3500
+ const cleanOutput = boot.evidenceTail;
3356
3501
  const session = {
3357
3502
  projectPath: args.projectPath,
3358
3503
  browser,
@@ -3436,15 +3581,18 @@ async function start_handler(args) {
3436
3581
  function toMcpSpeak(text) {
3437
3582
  return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
3438
3583
  }
3439
- const CONTROL_CODES = new Set([
3584
+ const CONTROL_CHANNEL_DOWN_CODES = new Set([
3440
3585
  "E_CONTROL_DENIED",
3441
3586
  "E_CONTROL_UNAVAILABLE",
3442
3587
  "E_NO_CONTROL_CHANNEL",
3443
3588
  "E_NOT_ATTACHED",
3444
3589
  "E_SESSION_NOT_FOUND"
3445
3590
  ]);
3591
+ function legacyControlChannelScrape(message) {
3592
+ return /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
3593
+ }
3446
3594
  function withSessionContext(message, projectPath, code) {
3447
- const isControlError = code ? CONTROL_CODES.has(code) : /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
3595
+ const isControlError = code ? CONTROL_CHANNEL_DOWN_CODES.has(code) : legacyControlChannelScrape(message);
3448
3596
  if (!isControlError) return message;
3449
3597
  const dead = deadReadySession(projectPath);
3450
3598
  if (dead) return `${message}\nLikely cause: the dev server has exited, ${dead.browser} ready.json still says ready but its pid ${dead.pid} is dead. Restart with extension_dev (this is not an allowControl problem); extension_doctor confirms.`;
@@ -3459,14 +3607,17 @@ function translateFrame(frame, projectPath) {
3459
3607
  if ("string" == typeof frame.hint) frame.hint = toMcpSpeak(frame.hint);
3460
3608
  return frame;
3461
3609
  }
3462
- const VERB_TOOL = {
3610
+ const TOOL_OWNING_VERB = {
3463
3611
  eval: "extension_eval",
3464
3612
  inspect: "extension_dom_snapshot",
3465
3613
  open: "extension_open",
3466
3614
  reload: "extension_reload",
3467
3615
  storage: "extension_storage"
3468
3616
  };
3469
- const LEGACY_CODES = {
3617
+ function commandNamePerD6(verb, tool) {
3618
+ return tool ?? TOOL_OWNING_VERB[verb] ?? "extension_act";
3619
+ }
3620
+ const LEGACY_NAME_TO_CODE = {
3470
3621
  BadRequest: "E_BAD_REQUEST",
3471
3622
  CliError: "E_CLI",
3472
3623
  NoSession: "E_NO_SESSION"
@@ -3481,7 +3632,7 @@ function wrapLegacyFrame(frame, command) {
3481
3632
  status: true === ok ? "ok" : "failed",
3482
3633
  value: void 0 === value ? null : value,
3483
3634
  error: true === ok ? null : {
3484
- code: "string" == typeof raw.code && raw.code ? raw.code : name && LEGACY_CODES[name] || "E_CLI",
3635
+ code: "string" == typeof raw.code && raw.code ? raw.code : name && LEGACY_NAME_TO_CODE[name] || "E_CLI",
3485
3636
  message: "string" == typeof raw.message ? raw.message : "command failed",
3486
3637
  ...name ? {
3487
3638
  name
@@ -3508,12 +3659,19 @@ function wrapLegacyFrame(frame, command) {
3508
3659
  };
3509
3660
  }
3510
3661
  async function runActVerb(args, projectPath, timeoutMs, tool) {
3511
- const command = tool ?? VERB_TOOL[args[0]] ?? "extension_act";
3512
- const { code, stdout, stderr } = await runExtensionCli([
3662
+ const command = commandNamePerD6(args[0], tool);
3663
+ const separatorAt = args.indexOf("--");
3664
+ const cliArgs = -1 === separatorAt ? [
3513
3665
  ...args,
3514
3666
  "--output",
3515
3667
  "json"
3516
- ], {
3668
+ ] : [
3669
+ ...args.slice(0, separatorAt),
3670
+ "--output",
3671
+ "json",
3672
+ ...args.slice(separatorAt)
3673
+ ];
3674
+ const { code, stdout, stderr } = await runExtensionCli(cliArgs, {
3517
3675
  cwd: projectPath,
3518
3676
  timeoutMs
3519
3677
  });
@@ -3547,16 +3705,19 @@ function addWarning(frame, text) {
3547
3705
  function actFrameJson(frame) {
3548
3706
  return JSON.stringify(frame);
3549
3707
  }
3550
- function patchValue(frame, patch) {
3551
- const current = frame.value;
3552
- if (current && "object" == typeof current && !Array.isArray(current)) return void Object.assign(current, patch);
3553
- frame.value = null == current ? {
3708
+ function patchKeepingScalarResult(current, patch) {
3709
+ return null == current ? {
3554
3710
  ...patch
3555
3711
  } : {
3556
3712
  result: current,
3557
3713
  ...patch
3558
3714
  };
3559
3715
  }
3716
+ function patchValue(frame, patch) {
3717
+ const current = frame.value;
3718
+ if (current && "object" == typeof current && !Array.isArray(current)) return void Object.assign(current, patch);
3719
+ frame.value = patchKeepingScalarResult(current, patch);
3720
+ }
3560
3721
  function commonFlags(args) {
3561
3722
  const flags = [];
3562
3723
  if (args.context) flags.push("--context", args.context);
@@ -4043,6 +4204,63 @@ function isCdpEndpoint(port) {
4043
4204
  });
4044
4205
  });
4045
4206
  }
4207
+ const ENGINE_COMPANION_IDS = new Set([
4208
+ "kgdaecdpfkikjncaalnmmnjjfpofkcbl",
4209
+ CARRIER_EXTENSION_ID
4210
+ ]);
4211
+ const EXTENSION_URL = /^chrome-extension:\/\/([a-p]{32})\//i;
4212
+ async function verifyGuestLoaded(projectPath, browser, options) {
4213
+ let cdpPort;
4214
+ try {
4215
+ const resolved = await resolveCdpPort(projectPath, browser, {
4216
+ waitMs: options?.waitMs ?? 0
4217
+ });
4218
+ if (!resolved) return {
4219
+ checked: false,
4220
+ loaded: false,
4221
+ guestTargets: [],
4222
+ guestIds: [],
4223
+ reason: "No CDP port in the session's ready contract, so the browser's target list could not be queried (a headless Chromium session still exposes one; a gecko/Firefox session does not)."
4224
+ };
4225
+ cdpPort = resolved.port;
4226
+ const timeoutMs = options?.timeoutMs ?? 3000;
4227
+ const targets = await Promise.race([
4228
+ CDPClient.discoverTargets(cdpPort),
4229
+ new Promise((_, reject)=>setTimeout(()=>reject(new Error(`CDP /json timed out after ${timeoutMs}ms`)), timeoutMs))
4230
+ ]);
4231
+ const guestTargets = [];
4232
+ for (const t of targets){
4233
+ const match = EXTENSION_URL.exec(String(t.url ?? ""));
4234
+ if (!match) continue;
4235
+ const id = match[1].toLowerCase();
4236
+ if (!ENGINE_COMPANION_IDS.has(id)) guestTargets.push({
4237
+ id,
4238
+ type: String(t.type),
4239
+ url: String(t.url)
4240
+ });
4241
+ }
4242
+ const guestIds = [
4243
+ ...new Set(guestTargets.map((t)=>t.id))
4244
+ ];
4245
+ return {
4246
+ checked: true,
4247
+ loaded: guestTargets.length > 0,
4248
+ guestTargets,
4249
+ guestIds,
4250
+ cdpPort,
4251
+ reason: guestTargets.length > 0 ? `The browser lists ${guestIds.length} extension target${1 === guestIds.length ? "" : "s"} that are not the engine companion (${guestIds.join(", ")}), so the guest is loaded.` : "The browser's target list has no chrome-extension:// target other than the engine's devtools companion. On Chrome this is the signature of a silently rejected --load-extension (extension.js BUGS_TO_FIX §83): the CLI and ready.json cannot see it."
4252
+ };
4253
+ } catch (err) {
4254
+ return {
4255
+ checked: false,
4256
+ loaded: false,
4257
+ guestTargets: [],
4258
+ guestIds: [],
4259
+ cdpPort,
4260
+ reason: `Could not query the browser's target list${cdpPort ? ` on CDP port ${cdpPort}` : ""}: ${err?.message ?? String(err)}.`
4261
+ };
4262
+ }
4263
+ }
4046
4264
  async function listBridgeTabs(projectPath, browser, timeout, tool = "extension_dom_snapshot") {
4047
4265
  const raw = await runActVerb([
4048
4266
  "inspect",
@@ -4303,9 +4521,16 @@ function unpackedExtensionId(distPath) {
4303
4521
  }
4304
4522
  async function resolveExtensionId(projectPath, browser) {
4305
4523
  const distPath = readDistPath(projectPath, browser);
4306
- const computed = distPath ? unpackedExtensionId(distPath) : null;
4524
+ const computedIds = [];
4525
+ if (distPath) {
4526
+ computedIds.push(unpackedExtensionId(distPath));
4527
+ try {
4528
+ const real = node_fs.realpathSync(distPath);
4529
+ if (real !== distPath) computedIds.push(unpackedExtensionId(real));
4530
+ } catch {}
4531
+ }
4307
4532
  const resolved = await resolveCdpPort(projectPath, browser);
4308
- if (!resolved) return computed;
4533
+ if (!resolved) return computedIds[0] ?? null;
4309
4534
  const ids = new Set();
4310
4535
  try {
4311
4536
  for (const t of (await CDPClient.discoverTargets(resolved.port))){
@@ -4315,8 +4540,12 @@ async function resolveExtensionId(projectPath, browser) {
4315
4540
  if (id) ids.add(id);
4316
4541
  }
4317
4542
  } catch {}
4318
- if (computed && ids.has(computed)) return computed;
4319
- if (computed) return computed;
4543
+ for (const id of computedIds)if (ids.has(id)) return id;
4544
+ if (ids.size > 0) {
4545
+ const guest = await verifyGuestLoaded(projectPath, browser);
4546
+ if (guest.checked && 1 === guest.guestIds.length) return guest.guestIds[0];
4547
+ }
4548
+ if (computedIds.length > 0) return computedIds[0];
4320
4549
  return 1 === ids.size ? [
4321
4550
  ...ids
4322
4551
  ][0] : null;
@@ -5610,13 +5839,15 @@ function localIndex(entries) {
5610
5839
  }
5611
5840
  return index;
5612
5841
  }
5613
- function localOnlyStatus(entry, truncated, now) {
5842
+ function localOnlyStatus(entry, truncated, liveFiltered, now) {
5614
5843
  if (truncated) return "unknown: the platform list was cut short, so this share may simply be past the returned window.";
5615
5844
  const expiresAt = entry.expiresAt ? Date.parse(entry.expiresAt) : NaN;
5616
5845
  if (Number.isFinite(expiresAt) && expiresAt <= now) return "expired: its own expiresAt has passed, and the platform no longer lists it.";
5846
+ if (liveFiltered) return 'not in this live-only listing: the platform was asked for live shares only, so this one may be expired or revoked rather than not owned. Rerun with status:"all" to tell.';
5617
5847
  return "not owned by this token: expired and pruned, revoked long ago, or shared while logged in to a different project.";
5618
5848
  }
5619
5849
  async function listShares(args) {
5850
+ const liveFiltered = "live" === args.status;
5620
5851
  const local = args.projectPath ? readSharedPreviews(args.projectPath) : void 0;
5621
5852
  const localRecord = local ? {
5622
5853
  path: local.path,
@@ -5630,7 +5861,7 @@ async function listShares(args) {
5630
5861
  note: "Pass projectPath to reconcile this list against the project's own share record."
5631
5862
  };
5632
5863
  const listing = await listArtifacts({
5633
- liveOnly: "live" === args.status,
5864
+ liveOnly: liveFiltered,
5634
5865
  ...null != args.limit ? {
5635
5866
  limit: args.limit
5636
5867
  } : {},
@@ -5692,7 +5923,7 @@ async function listShares(args) {
5692
5923
  });
5693
5924
  const localOnly = (local?.entries ?? []).filter((entry)=>!seen.has(entry.artifactId)).map((entry)=>({
5694
5925
  ...entry,
5695
- status: localOnlyStatus(entry, listing.data.truncated, now)
5926
+ status: localOnlyStatus(entry, listing.data.truncated, liveFiltered, now)
5696
5927
  }));
5697
5928
  const liveCount = shares.filter((share)=>share.live).length;
5698
5929
  const ownership = {
@@ -5725,7 +5956,7 @@ async function listShares(args) {
5725
5956
  } : {},
5726
5957
  localRecord
5727
5958
  },
5728
- hint: `${liveCount} of ${shares.length} listed shares still resolve${local ? `; ${localOnly.length} local record ${1 === localOnly.length ? "entry has" : "entries have"} no artifact behind them` : ""}. Revoke one with action:"revoke" and its artifactId or any of its URLs.`,
5959
+ hint: `${liveCount} of ${shares.length} listed shares still resolve${local ? `; ${localOnly.length} local record ${1 === localOnly.length ? "entry is" : "entries are"} ${liveFiltered ? 'not in this live-only listing (possibly dead rather than not owned; rerun with status:"all" to tell)' : "without an artifact behind them"}` : ""}. Revoke one with action:"revoke" and its artifactId or any of its URLs.`,
5729
5960
  warnings: [
5730
5961
  "previewUrl and zipUrl are null for a share that is no longer live, because a revoked or expired link cannot resolve for anyone. revokeUrl stays on every row. Revocation is permanent: a revoked id is burned and re-sharing the same build mints a different link.",
5731
5962
  "attribution.ownership says who the share belongs to and therefore who may revoke it: project means the owning workspace holds it and any member can pull it back, personal means one person holds it alone. attribution.credit names the publisher and is attribution only, granting and restricting nothing. A credit of \"CLI token ...\" means the platform could not resolve which human minted that token, and a credit of \"not recorded\" means it never knew; neither is a name, and neither should be reported as one.",
@@ -5780,6 +6011,23 @@ async function revokeShare(args) {
5780
6011
  ]
5781
6012
  });
5782
6013
  }
6014
+ if (true !== result.data.revoked) return envelope({
6015
+ ok: true,
6016
+ command: "extension_shares",
6017
+ status: "revoke-unconfirmed",
6018
+ value: {
6019
+ action: "revoke",
6020
+ artifactId: ref,
6021
+ revoked: false,
6022
+ ...result.data.revokedAt ? {
6023
+ revokedAt: result.data.revokedAt
6024
+ } : {}
6025
+ },
6026
+ warnings: [
6027
+ `The platform accepted the request but did not confirm the revocation (revoked came back false), so do not treat this link as dead yet. Run action:"list" to see whether ${ref} still resolves, and retry the revoke if it does.`,
6028
+ recordNote
6029
+ ]
6030
+ });
5783
6031
  return envelope({
5784
6032
  ok: true,
5785
6033
  command: "extension_shares",
@@ -6234,8 +6482,9 @@ async function manifest_validate_handler(args) {
6234
6482
  }
6235
6483
  if (isFirefox) {
6236
6484
  const contentScripts = effective.content_scripts;
6237
- if (contentScripts) {
6238
- for (const cs of contentScripts)if ("MAIN" === cs.world || "MAIN" === cs["world"]) issues.push('content_scripts.world: "MAIN" is Chromium-only. Use "chromium:world": "MAIN" and provide a Firefox fallback.');
6485
+ if (contentScripts?.some((cs)=>"MAIN" === cs.world)) {
6486
+ const note = 'content_scripts.world: "MAIN" needs Firefox 128 or later (earlier Firefox runs the script in the isolated world). If you depend on it, set browser_specific_settings.gecko.strict_min_version to "128.0".';
6487
+ if (!result.warnings.includes(note)) result.warnings.push(note);
6239
6488
  }
6240
6489
  if (chromiumManifest.side_panel && !effective.sidebar_action) issues.push("Chromium side_panel declared but no firefox:sidebar_action. Firefox uses sidebar_action for sidebars.");
6241
6490
  const bg = effective.background;
@@ -7636,7 +7885,62 @@ async function rdpCollectConsoleMessages(port, options) {
7636
7885
  return messages;
7637
7886
  });
7638
7887
  }
7639
- const TOOL = "extension_inspect";
7888
+ const inspect_schema_schema = {
7889
+ name: "extension_inspect",
7890
+ description: "Inspect a running extension deeply over the browser's debugger protocol: full HTML including shadow DOM, DOM structure, content-script injection, console messages, and CSS selector queries through `probe`. This is 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. Use extension_dom_snapshot to choose which tab or which open surface (popup, options, sidebar, devtools) to read, or to enumerate what is open. Use extension_analyze for a built extension's files and sizes on disk. Chromium rides the Chrome DevTools Protocol and needs the session's debug port, not allowControl. Firefox is fully paired: summary, meta, html, dom_snapshot, extension_roots and probes ride the agent bridge and need allowEval:true, console rides the RDP watcher replay on engine 4.0.15 and later, and deepDom needs an MV2 session with host permissions for the target url, because the Firefox MV3 background CSP blocks bridge evals. This requires an active dev or start session.",
7891
+ inputSchema: {
7892
+ type: "object",
7893
+ properties: {
7894
+ projectPath: SESSION_PROJECT_PATH,
7895
+ url: {
7896
+ type: "string",
7897
+ description: "URL to inspect; the tab is navigated there first"
7898
+ },
7899
+ probe: {
7900
+ type: "array",
7901
+ items: {
7902
+ type: "string"
7903
+ },
7904
+ description: "CSS selectors to query; returns counts and samples for each"
7905
+ },
7906
+ include: {
7907
+ type: "array",
7908
+ items: {
7909
+ type: "string",
7910
+ enum: [
7911
+ "html",
7912
+ "summary",
7913
+ "meta",
7914
+ "dom_snapshot",
7915
+ "console",
7916
+ "extension_roots"
7917
+ ]
7918
+ },
7919
+ default: [
7920
+ "summary",
7921
+ "meta",
7922
+ "console"
7923
+ ],
7924
+ description: "What to include"
7925
+ },
7926
+ browser: SESSION_BROWSER,
7927
+ maxBytes: {
7928
+ type: "number",
7929
+ default: 262144,
7930
+ description: "Truncate HTML output at this byte count (0 = unlimited)"
7931
+ },
7932
+ deepDom: {
7933
+ type: "boolean",
7934
+ default: false,
7935
+ 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)."
7936
+ }
7937
+ },
7938
+ required: [
7939
+ "projectPath"
7940
+ ]
7941
+ }
7942
+ };
7943
+ const TOOL = inspect_schema_schema.name;
7640
7944
  function buildBridgeInspectExpression(opts) {
7641
7945
  const parts = [
7642
7946
  "const out = {};"
@@ -7754,11 +8058,11 @@ async function collectGeckoConsole(args, browser, urlFilter, result, notes) {
7754
8058
  async function inspectViaBridge(args, browser, include, maxBytes) {
7755
8059
  const notes = [];
7756
8060
  if (args.url) {
7757
- const listed = await listBridgeTabs(args.projectPath, browser, args.timeout, "extension_inspect");
8061
+ const listed = await listBridgeTabs(args.projectPath, browser, args.timeout, TOOL);
7758
8062
  if ("error" in listed) return listed.error;
7759
8063
  const already = listed.tabs.some((t)=>t.url.includes(args.url));
7760
8064
  if (!already) {
7761
- const nav = await navigateToUrlViaBridge(args.projectPath, browser, args.url, args.timeout, "extension_inspect");
8065
+ const nav = await navigateToUrlViaBridge(args.projectPath, browser, args.url, args.timeout, TOOL);
7762
8066
  try {
7763
8067
  if (JSON.parse(nav)?.ok !== true) return nav;
7764
8068
  } catch {
@@ -7874,61 +8178,6 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
7874
8178
  ]
7875
8179
  });
7876
8180
  }
7877
- const inspect_schema = {
7878
- name: "extension_inspect",
7879
- description: "Inspect a running extension deeply over the browser's debugger protocol: full HTML including shadow DOM, DOM structure, content-script injection, console messages, and CSS selector queries through `probe`. This is 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. Use extension_dom_snapshot to choose which tab or which open surface (popup, options, sidebar, devtools) to read, or to enumerate what is open. Use extension_analyze for a built extension's files and sizes on disk. Chromium rides the Chrome DevTools Protocol and needs the session's debug port, not allowControl. Firefox is fully paired: summary, meta, html, dom_snapshot, extension_roots and probes ride the agent bridge and need allowEval:true, console rides the RDP watcher replay on engine 4.0.15 and later, and deepDom needs an MV2 session with host permissions for the target url, because the Firefox MV3 background CSP blocks bridge evals. This requires an active dev or start session.",
7880
- inputSchema: {
7881
- type: "object",
7882
- properties: {
7883
- projectPath: SESSION_PROJECT_PATH,
7884
- url: {
7885
- type: "string",
7886
- description: "URL to inspect; the tab is navigated there first"
7887
- },
7888
- probe: {
7889
- type: "array",
7890
- items: {
7891
- type: "string"
7892
- },
7893
- description: "CSS selectors to query; returns counts and samples for each"
7894
- },
7895
- include: {
7896
- type: "array",
7897
- items: {
7898
- type: "string",
7899
- enum: [
7900
- "html",
7901
- "summary",
7902
- "meta",
7903
- "dom_snapshot",
7904
- "console",
7905
- "extension_roots"
7906
- ]
7907
- },
7908
- default: [
7909
- "summary",
7910
- "meta",
7911
- "console"
7912
- ],
7913
- description: "What to include"
7914
- },
7915
- browser: SESSION_BROWSER,
7916
- maxBytes: {
7917
- type: "number",
7918
- default: 262144,
7919
- description: "Truncate HTML output at this byte count (0 = unlimited)"
7920
- },
7921
- deepDom: {
7922
- type: "boolean",
7923
- default: false,
7924
- 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)."
7925
- }
7926
- },
7927
- required: [
7928
- "projectPath"
7929
- ]
7930
- }
7931
- };
7932
8181
  async function inspect_handler(args) {
7933
8182
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
7934
8183
  const include = new Set(args.include ?? [
@@ -7941,7 +8190,7 @@ async function inspect_handler(args) {
7941
8190
  const resolved = await resolveCdpPort(args.projectPath, browser);
7942
8191
  if (!resolved) return envelope({
7943
8192
  ok: false,
7944
- command: inspect_schema.name,
8193
+ command: inspect_schema_schema.name,
7945
8194
  status: "no-session",
7946
8195
  error: {
7947
8196
  code: "E_NO_SESSION",
@@ -7965,7 +8214,7 @@ async function inspect_handler(args) {
7965
8214
  const chromeOnly = allTargets.some((t)=>"page" === t.type && t.url.startsWith("chrome://"));
7966
8215
  return envelope({
7967
8216
  ok: false,
7968
- command: inspect_schema.name,
8217
+ command: inspect_schema_schema.name,
7969
8218
  status: "no-inspectable-target",
7970
8219
  error: {
7971
8220
  code: "E_NO_TARGET",
@@ -7981,7 +8230,8 @@ async function inspect_handler(args) {
7981
8230
  }
7982
8231
  });
7983
8232
  }
7984
- const target = args.url ? pageTargets.find((t)=>t.url.includes(args.url)) ?? pageTargets[0] : pageTargets[0];
8233
+ const isExtensionSurface = (u)=>u.startsWith("chrome-extension://") || u.startsWith("moz-extension://");
8234
+ const target = args.url ? pageTargets.find((t)=>t.url.includes(args.url)) ?? pageTargets.find((t)=>!isExtensionSurface(t.url)) ?? pageTargets[0] : pageTargets[0];
7985
8235
  const browserWsUrl = await CDPClient.discoverBrowserWsUrl(cdpPort);
7986
8236
  await cdp.connect(browserWsUrl);
7987
8237
  const sessionId = await cdp.attachToTarget(target.id);
@@ -8046,7 +8296,7 @@ async function inspect_handler(args) {
8046
8296
  delete result.probeWarning;
8047
8297
  return envelope({
8048
8298
  ok: true,
8049
- command: inspect_schema.name,
8299
+ command: inspect_schema_schema.name,
8050
8300
  status: "inspected",
8051
8301
  value: result,
8052
8302
  warnings: [
@@ -8056,7 +8306,7 @@ async function inspect_handler(args) {
8056
8306
  } catch (err) {
8057
8307
  return envelope({
8058
8308
  ok: false,
8059
- command: inspect_schema.name,
8309
+ command: inspect_schema_schema.name,
8060
8310
  status: "cdp-failed",
8061
8311
  error: {
8062
8312
  code: "E_CDP",
@@ -8373,7 +8623,9 @@ function makeFilter(args) {
8373
8623
  if ("header" === event.type) return false;
8374
8624
  if (args.signalsOnly && "dx.signal" !== event.eventType) return false;
8375
8625
  if (contexts && !contexts.has(event.context)) return false;
8376
- if ("all" !== minLevel && "off" !== minLevel) {
8626
+ if ("off" === minLevel) {
8627
+ if ("dx.signal" !== event.eventType) return false;
8628
+ } else if ("all" !== minLevel) {
8377
8629
  if (levelRank(event.level) > levelRank(minLevel)) return false;
8378
8630
  }
8379
8631
  if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
@@ -8418,7 +8670,10 @@ const logs_schema_schema = {
8418
8670
  "sidebar",
8419
8671
  "popup",
8420
8672
  "options",
8421
- "devtools"
8673
+ "devtools",
8674
+ "newtab",
8675
+ "history",
8676
+ "bookmarks"
8422
8677
  ]
8423
8678
  },
8424
8679
  description: "Restrict to these contexts. Omit for all."
@@ -8482,11 +8737,11 @@ function readReadyContract(projectPath, browser) {
8482
8737
  function capRecent(events, limit) {
8483
8738
  if (events.length <= limit) return {
8484
8739
  events,
8485
- truncated: false
8740
+ windowTruncated: false
8486
8741
  };
8487
8742
  return {
8488
8743
  events: events.slice(events.length - limit),
8489
- truncated: true
8744
+ windowTruncated: true
8490
8745
  };
8491
8746
  }
8492
8747
  function emptyReason(projectPath, browser) {
@@ -8506,9 +8761,9 @@ function emptyReason(projectPath, browser) {
8506
8761
  return `ready.json reports ready but its dev-server pid ${contract.pid} is dead: the session exited. Logs stop at the moment it died. Restart with extension_dev; extension_doctor will confirm.`;
8507
8762
  }
8508
8763
  }
8509
- function summarize(events, source, browser, runId, limit, dropped, projectPath, staleNote) {
8764
+ function summarize(events, source, browser, runId, limit, dropped, projectPath, staleNote, streamNote) {
8510
8765
  const matched = events.length;
8511
- const { events: out, truncated } = capRecent(events, limit);
8766
+ const { events: out, windowTruncated } = capRecent(events, limit);
8512
8767
  const lastSeq = out.length ? out.reduce((m, e)=>"number" == typeof e.seq && e.seq > m ? e.seq : m, -1) : -1;
8513
8768
  const reason = 0 === matched && projectPath ? emptyReason(projectPath, browser) : void 0;
8514
8769
  const stale = Boolean(staleNote) && matched > 0;
@@ -8522,14 +8777,15 @@ function summarize(events, source, browser, runId, limit, dropped, projectPath,
8522
8777
  runId: runId || void 0,
8523
8778
  matched,
8524
8779
  count: out.length,
8525
- truncated,
8780
+ windowTruncated,
8526
8781
  dropped: dropped || void 0,
8527
8782
  nextSince: lastSeq >= 0 ? lastSeq : void 0,
8528
8783
  events: out
8529
8784
  },
8530
8785
  warnings: [
8531
8786
  reason ?? null,
8532
- stale ? staleNote ?? null : null
8787
+ stale ? staleNote ?? null : null,
8788
+ streamNote ?? null
8533
8789
  ]
8534
8790
  });
8535
8791
  }
@@ -8621,6 +8877,7 @@ async function readFromStream(args, browser, limit) {
8621
8877
  }));
8622
8878
  return;
8623
8879
  }
8880
+ let streamNote;
8624
8881
  const finish = ()=>{
8625
8882
  if (settled) return;
8626
8883
  settled = true;
@@ -8628,7 +8885,7 @@ async function readFromStream(args, browser, limit) {
8628
8885
  try {
8629
8886
  socket.close();
8630
8887
  } catch {}
8631
- resolve(summarize(events, "stream", browser, runId, limit, dropped, args.projectPath));
8888
+ resolve(summarize(events, "stream", browser, runId, limit, dropped, args.projectPath, void 0, streamNote));
8632
8889
  };
8633
8890
  const timer = setTimeout(finish, followMs);
8634
8891
  socket.on("open", ()=>{
@@ -8655,8 +8912,16 @@ async function readFromStream(args, browser, limit) {
8655
8912
  });
8656
8913
  socket.on("error", ()=>{
8657
8914
  if (settled) return;
8915
+ if (events.length > 0 || dropped > 0) {
8916
+ streamNote = `The control channel at ${url} errored before the follow window ended, so this is a partial read. The dev session may have stopped or the control port changed; re-check with extension_wait.`;
8917
+ finish();
8918
+ return;
8919
+ }
8658
8920
  settled = true;
8659
8921
  clearTimeout(timer);
8922
+ try {
8923
+ socket.close();
8924
+ } catch {}
8660
8925
  resolve(envelope({
8661
8926
  ok: false,
8662
8927
  command: logs_TOOL,
@@ -8748,13 +9013,14 @@ async function eval_handler(args) {
8748
9013
  const context = defaulted ? "page" : args.context;
8749
9014
  const raw = await runActVerb([
8750
9015
  "eval",
8751
- args.expression,
8752
- args.projectPath,
8753
9016
  ...commonFlags({
8754
9017
  ...args,
8755
9018
  context,
8756
9019
  browser
8757
- })
9020
+ }),
9021
+ "--",
9022
+ args.expression,
9023
+ args.projectPath
8758
9024
  ], args.projectPath, args.timeout, eval_schema.name);
8759
9025
  if ("content" === args.context) try {
8760
9026
  const parsed = JSON.parse(raw);
@@ -8927,7 +9193,7 @@ function matchTargetsByUrl(targets, needle) {
8927
9193
  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.";
8928
9194
  const dom_snapshot_schema = {
8929
9195
  name: "extension_dom_snapshot",
8930
- description: "Take a shallow structured DOM snapshot of one chosen surface through the agent bridge (no CDP, localhost only): element counts, extension roots, open shadow roots, optional byte-capped HTML, and 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: use extension_inspect for those, and for a deep read of an already-open web page. Start the session with allowControl:true (extension_dev).",
9196
+ description: "Take a shallow structured DOM snapshot of one chosen surface through the agent bridge (localhost only; the snapshot itself needs no CDP, but listTargets and `tabUrl` resolution ask the browser directly and need the session's debug port: CDP page targets on Chromium, RDP tab descriptors on Firefox): element counts, extension roots, open shadow roots, optional byte-capped HTML, and 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: use extension_inspect for those, and for a deep read of an already-open web page. Start the session with allowControl:true (extension_dev).",
8931
9197
  inputSchema: {
8932
9198
  type: "object",
8933
9199
  properties: {
@@ -9330,19 +9596,34 @@ async function publish_handler(args) {
9330
9596
  });
9331
9597
  if (buildsRes.ok) {
9332
9598
  const items = parseBuildIndex(buildsRes.json);
9333
- const pinned = args.buildSha ? items.find((item)=>{
9334
- const short = String(args.buildSha).slice(0, 7).toLowerCase();
9335
- return item.sha.toLowerCase() === short || String(item.commit ?? "").toLowerCase().startsWith(short);
9336
- }) : void 0;
9337
- const newestSuccess = items.filter((item)=>"success" === item.status).sort((a, b)=>String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")))[0];
9338
- const served = pinned ?? newestSuccess;
9339
- if (served) {
9340
- if (null == data.buildSha) data.buildSha = served.sha;
9341
- if (null == data.builtAt && served.timestamp) data.builtAt = served.timestamp;
9342
- if (null == data.version && served.version) data.version = served.version;
9343
- if (null == data.channel && served.channel) data.channel = served.channel;
9344
- data.registryUrl = buildsUrl;
9345
- if (!pinned && null == args.buildSha) buildNote = "buildSha/builtAt/version describe the newest successful build in the project's registry index, which is what the share link serves. Pin buildSha to serve a specific build.";
9599
+ if (args.buildSha) {
9600
+ const pin = String(args.buildSha).toLowerCase();
9601
+ const pinned = items.find((item)=>{
9602
+ const sha = item.sha.toLowerCase();
9603
+ const commit = String(item.commit ?? "").toLowerCase();
9604
+ return sha.startsWith(pin) || pin.startsWith(sha) || "" !== commit && (commit.startsWith(pin) || pin.startsWith(commit));
9605
+ });
9606
+ if (pinned) {
9607
+ if (null == data.buildSha) data.buildSha = pinned.sha;
9608
+ if (null == data.builtAt && pinned.timestamp) data.builtAt = pinned.timestamp;
9609
+ if (null == data.version && pinned.version) data.version = pinned.version;
9610
+ if (null == data.channel && pinned.channel) data.channel = pinned.channel;
9611
+ data.registryUrl = buildsUrl;
9612
+ } else {
9613
+ if (null == data.buildSha) data.buildSha = args.buildSha;
9614
+ data.registryUrl = buildsUrl;
9615
+ buildNote = `buildSha ${args.buildSha} is pinned but was not found in the project's registry build index, so builtAt/version/channel are not filled in from another build. The URL still serves the pinned build the platform accepted.`;
9616
+ }
9617
+ } else {
9618
+ const newestSuccess = items.filter((item)=>"success" === item.status).sort((a, b)=>String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")))[0];
9619
+ if (newestSuccess) {
9620
+ if (null == data.buildSha) data.buildSha = newestSuccess.sha;
9621
+ if (null == data.builtAt && newestSuccess.timestamp) data.builtAt = newestSuccess.timestamp;
9622
+ if (null == data.version && newestSuccess.version) data.version = newestSuccess.version;
9623
+ if (null == data.channel && newestSuccess.channel) data.channel = newestSuccess.channel;
9624
+ data.registryUrl = buildsUrl;
9625
+ buildNote = "buildSha/builtAt/version describe the newest successful build in the project's registry index, which is what the share link serves. Pin buildSha to serve a specific build.";
9626
+ }
9346
9627
  }
9347
9628
  }
9348
9629
  }
@@ -9946,6 +10227,10 @@ const submit_schema = {
9946
10227
  default: true,
9947
10228
  description: "Preflight only. Pass false to actually dispatch (irreversible, enters store review)."
9948
10229
  },
10230
+ projectPath: {
10231
+ type: "string",
10232
+ description: "Path to the extension project root, read only for the local STORE.md advisory check. Nothing local is uploaded; without it the check falls back to the server's working directory."
10233
+ },
9949
10234
  api: API_BASE
9950
10235
  },
9951
10236
  required: [
@@ -10011,7 +10296,7 @@ async function submit_handler(args) {
10011
10296
  const warnings = Array.isArray(data?.warnings) ? [
10012
10297
  ...data.warnings
10013
10298
  ] : [];
10014
- warnings.push(...storeMdWarnings(browsers, process.cwd()));
10299
+ warnings.push(...storeMdWarnings(browsers, String(args.projectPath || "").trim() || process.cwd()));
10015
10300
  const result = {
10016
10301
  mode: "platform",
10017
10302
  dryRun,
@@ -10020,7 +10305,8 @@ async function submit_handler(args) {
10020
10305
  delete result.ok;
10021
10306
  delete result.warnings;
10022
10307
  delete result.message;
10023
- let ok = data?.ok !== false;
10308
+ const platformOk = data?.ok !== false;
10309
+ let ok = platformOk;
10024
10310
  let message = "string" == typeof data?.message ? data.message : "";
10025
10311
  let channelNote = null;
10026
10312
  let statusNote = null;
@@ -10080,7 +10366,8 @@ async function submit_handler(args) {
10080
10366
  };
10081
10367
  });
10082
10368
  const actionable = preflight.filter((p)=>p.ok).map((p)=>p.browser);
10083
- const blocked = preflight.filter((p)=>!p.ok);
10369
+ const blocked = preflight.filter((p)=>!p.ok && "unknown" !== p.configured);
10370
+ const unverified = preflight.filter((p)=>"unknown" === p.configured);
10084
10371
  const channelDefaulted = !String(args.channel || "").trim();
10085
10372
  const resolvedChannel = String(data?.channel || "").trim() || (channelDefaulted ? "stable" : String(args.channel).trim());
10086
10373
  if (channelRows) {
@@ -10088,10 +10375,12 @@ async function submit_handler(args) {
10088
10375
  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.`);
10089
10376
  }
10090
10377
  const summaryParts = [];
10091
- 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.`);
10092
- for (const p of blocked)summaryParts.push(`${p.browser}: ${"unknown" === p.configured ? "cannot be verified" : "NOT actionable"} - ${p.reason}`);
10378
+ if (!platformOk) summaryParts.push(`Preflight FAILED on the platform${"string" == typeof data?.message && data.message ? `: ${data.message}` : ""}. The per-store rows below are advisory and do not override that verdict.`);
10379
+ if (actionable.length > 0) summaryParts.push(platformOk ? `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.` : `${actionable.join(", ")}: the store credentials passed their last health check, but the platform failure above still blocks submission.`);
10380
+ for (const p of blocked)summaryParts.push(`${p.browser}: NOT actionable - ${p.reason}`);
10381
+ for (const p of unverified)summaryParts.push(`${p.browser}: cannot be verified - ${p.reason}${platformOk ? " The platform preflight itself passed, so this is advisory only." : ""}`);
10093
10382
  summaryParts.push(storeModeNote);
10094
- ok = actionable.length > 0;
10383
+ ok = platformOk && (0 === blocked.length || actionable.length > 0);
10095
10384
  result.preflight = preflight;
10096
10385
  result.channel = resolvedChannel;
10097
10386
  result.channelDefaulted = channelDefaulted;
@@ -10114,63 +10403,6 @@ async function submit_handler(args) {
10114
10403
  ]
10115
10404
  });
10116
10405
  }
10117
- const ENGINE_COMPANION_IDS = new Set([
10118
- "kgdaecdpfkikjncaalnmmnjjfpofkcbl",
10119
- CARRIER_EXTENSION_ID
10120
- ]);
10121
- const EXTENSION_URL = /^chrome-extension:\/\/([a-p]{32})\//i;
10122
- async function verifyGuestLoaded(projectPath, browser, options) {
10123
- let cdpPort;
10124
- try {
10125
- const resolved = await resolveCdpPort(projectPath, browser, {
10126
- waitMs: options?.waitMs ?? 0
10127
- });
10128
- if (!resolved) return {
10129
- checked: false,
10130
- loaded: false,
10131
- guestTargets: [],
10132
- guestIds: [],
10133
- reason: "No CDP port in the session's ready contract, so the browser's target list could not be queried (a headless Chromium session still exposes one; a gecko/Firefox session does not)."
10134
- };
10135
- cdpPort = resolved.port;
10136
- const timeoutMs = options?.timeoutMs ?? 3000;
10137
- const targets = await Promise.race([
10138
- CDPClient.discoverTargets(cdpPort),
10139
- new Promise((_, reject)=>setTimeout(()=>reject(new Error(`CDP /json timed out after ${timeoutMs}ms`)), timeoutMs))
10140
- ]);
10141
- const guestTargets = [];
10142
- for (const t of targets){
10143
- const match = EXTENSION_URL.exec(String(t.url ?? ""));
10144
- if (!match) continue;
10145
- const id = match[1].toLowerCase();
10146
- if (!ENGINE_COMPANION_IDS.has(id)) guestTargets.push({
10147
- id,
10148
- type: String(t.type),
10149
- url: String(t.url)
10150
- });
10151
- }
10152
- const guestIds = [
10153
- ...new Set(guestTargets.map((t)=>t.id))
10154
- ];
10155
- return {
10156
- checked: true,
10157
- loaded: guestTargets.length > 0,
10158
- guestTargets,
10159
- guestIds,
10160
- cdpPort,
10161
- reason: guestTargets.length > 0 ? `The browser lists ${guestIds.length} extension target${1 === guestIds.length ? "" : "s"} that are not the engine companion (${guestIds.join(", ")}), so the guest is loaded.` : "The browser's target list has no chrome-extension:// target other than the engine's devtools companion. On Chrome this is the signature of a silently rejected --load-extension (extension.js BUGS_TO_FIX §83): the CLI and ready.json cannot see it."
10162
- };
10163
- } catch (err) {
10164
- return {
10165
- checked: false,
10166
- loaded: false,
10167
- guestTargets: [],
10168
- guestIds: [],
10169
- cdpPort,
10170
- reason: `Could not query the browser's target list${cdpPort ? ` on CDP port ${cdpPort}` : ""}: ${err?.message ?? String(err)}.`
10171
- };
10172
- }
10173
- }
10174
10406
  function doctor_readReadyContract(projectPath, browser) {
10175
10407
  try {
10176
10408
  const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
@@ -10279,6 +10511,9 @@ function projectEngineVersion(projectPath) {
10279
10511
  return null;
10280
10512
  }
10281
10513
  }
10514
+ function capabilityProbeChecks(parsed) {
10515
+ return isEnvelope(parsed) ? parsed.value?.checks : parsed;
10516
+ }
10282
10517
  async function doctor_handler(args) {
10283
10518
  if (!args.projectPath) return environmentPreflight();
10284
10519
  const projectPath = args.projectPath;
@@ -10296,7 +10531,7 @@ async function doctor_handler(args) {
10296
10531
  const out = stdout.trim();
10297
10532
  try {
10298
10533
  const parsed = JSON.parse(out);
10299
- const checks = isEnvelope(parsed) ? parsed.value?.checks : parsed;
10534
+ const checks = capabilityProbeChecks(parsed);
10300
10535
  if (!Array.isArray(checks)) throw new Error("not a check array");
10301
10536
  for (const check of checks){
10302
10537
  if ("string" == typeof check.detail) check.detail = toMcpSpeak(check.detail);
@@ -10328,12 +10563,12 @@ async function doctor_handler(args) {
10328
10563
  }
10329
10564
  const engineVersion = projectEngineVersion(projectPath);
10330
10565
  if (engineVersion) {
10331
- const pin = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
10332
- const mismatch = "" !== pin && "latest" !== pin && !engineVersion.includes(pin);
10566
+ const pin = pinnedCliVersion();
10567
+ const mismatch = "" !== pin && "latest" !== pin && exactVersion(engineVersion) !== exactVersion(pin);
10333
10568
  checks.push({
10334
10569
  check: "project-engine",
10335
10570
  status: mismatch ? "warn" : "pass",
10336
- detail: `project-local extension@${engineVersion}${mismatch ? `, but EXTENSION_MCP_CLI_VERSION=${pin}; the dev loop uses the project bin, not the pin` : ""}`,
10571
+ detail: `project-local extension@${engineVersion}${mismatch ? `, but the MCP pins extension@${pin}; the dev loop uses the project bin, not the pin` : ""}`,
10337
10572
  ...mismatch ? {
10338
10573
  remediation: `Run \`(cd ${projectPath} && npm i -D extension@${pin})\` to match the pinned engine.`
10339
10574
  } : {}
@@ -10369,6 +10604,7 @@ async function doctor_handler(args) {
10369
10604
  const SAFE_CEILING_MS = 50000;
10370
10605
  const DEFAULT_TIMEOUT_MS = 45000;
10371
10606
  const MIN_TIMEOUT_MS = 1000;
10607
+ const CONTRACT_FRESHNESS_SLACK_MS = 2000;
10372
10608
  function wait_isAlive(pid) {
10373
10609
  try {
10374
10610
  process.kill(pid, 0);
@@ -10407,14 +10643,24 @@ async function wait_handler(args) {
10407
10643
  const clamped = requested > SAFE_CEILING_MS;
10408
10644
  const readyPath = node_path.resolve(args.projectPath, "dist", "extension-js", browser, "ready.json");
10409
10645
  const buildOnly = findSessionInfo(args.projectPath, browser)?.noBrowser === true;
10646
+ const since = sessionSinceMs(args.projectPath, browser);
10410
10647
  const start = Date.now();
10411
10648
  const pollInterval = 1000;
10412
10649
  let sawCompiledButUnattached = false;
10413
10650
  let lastContractStatus = null;
10651
+ let staleContractNote = null;
10414
10652
  while(Date.now() - start < budgetMs){
10415
10653
  try {
10654
+ const stat = node_fs.statSync(readyPath);
10416
10655
  const raw = node_fs.readFileSync(readyPath, "utf8");
10417
10656
  const contract = JSON.parse(raw);
10657
+ const stampedBeforeSession = null !== since && stat.mtimeMs < since - CONTRACT_FRESHNESS_SLACK_MS;
10658
+ const deadOrphanStamp = null === since && "ready" !== contract.status && "number" == typeof contract.pid && !wait_isAlive(contract.pid);
10659
+ if (stampedBeforeSession || deadOrphanStamp) {
10660
+ staleContractNote = stampedBeforeSession ? `A ready.json contract stamped before this session started (status: ${contract.status}) was ignored; it describes the previous run, not this one.` : `A ready.json contract whose dev-server pid ${contract.pid} is dead (status: ${contract.status}) was ignored; it describes a session that already exited.`;
10661
+ await new Promise((resolve)=>setTimeout(resolve, pollInterval));
10662
+ continue;
10663
+ }
10418
10664
  lastContractStatus = contract.status;
10419
10665
  if ("ready" === contract.status) {
10420
10666
  if ("number" == typeof contract.pid && !wait_isAlive(contract.pid)) return envelope({
@@ -10441,7 +10687,7 @@ async function wait_handler(args) {
10441
10687
  buildOnly: true,
10442
10688
  compiled: true,
10443
10689
  browserAttached: false,
10444
- sessionCommand: contract.command,
10690
+ ...sessionCommandSinceEnvelopeOwnsCommand(contract),
10445
10691
  browser: contract.browser,
10446
10692
  port: contract.port,
10447
10693
  pid: contract.pid,
@@ -10477,7 +10723,7 @@ async function wait_handler(args) {
10477
10723
  } : {
10478
10724
  guestLoadNote: guestCheck.reason
10479
10725
  },
10480
- sessionCommand: contract.command,
10726
+ ...sessionCommandSinceEnvelopeOwnsCommand(contract),
10481
10727
  browser: contract.browser,
10482
10728
  port: contract.port,
10483
10729
  pid: contract.pid,
@@ -10546,7 +10792,8 @@ async function wait_handler(args) {
10546
10792
  elapsedMs: Date.now() - start
10547
10793
  },
10548
10794
  warnings: [
10549
- clamped ? `requested ${requested}ms was clamped to ${SAFE_CEILING_MS}ms to stay under the MCP client request timeout` : null
10795
+ clamped ? `requested ${requested}ms was clamped to ${SAFE_CEILING_MS}ms to stay under the MCP client request timeout` : null,
10796
+ staleContractNote
10550
10797
  ],
10551
10798
  hint: "Still building, call extension_wait again to keep waiting (it resumes polling the same contract). If it never readies, check the dev process with extension_doctor."
10552
10799
  });
@@ -10896,15 +11143,22 @@ async function pollDeviceToken(args) {
10896
11143
  message: text
10897
11144
  };
10898
11145
  }
10899
- if (res.ok && data.token) {
11146
+ if (res.ok && data.token) try {
10900
11147
  const creds = persistTokenResponse({
10901
11148
  apiBase: args.apiBase,
11149
+ project: args.project,
10902
11150
  data
10903
11151
  });
10904
11152
  return {
10905
11153
  ok: true,
10906
11154
  creds
10907
11155
  };
11156
+ } catch (err) {
11157
+ return {
11158
+ ok: false,
11159
+ reason: "error",
11160
+ message: err?.message ? String(err.message) : String(err)
11161
+ };
10908
11162
  }
10909
11163
  const error = String(data.error || "");
10910
11164
  if ("access_denied" === error) return {
@@ -10921,6 +11175,11 @@ async function pollDeviceToken(args) {
10921
11175
  reason: "error",
10922
11176
  message: String(data.message || error)
10923
11177
  };
11178
+ else if (!error && !res.ok) return {
11179
+ ok: false,
11180
+ reason: "error",
11181
+ message: `Device token poll failed (${res.status}): ${String(data.message || text || "no response body").slice(0, 200)}`
11182
+ };
10924
11183
  if (Date.now() + 1000 * interval >= deadline) return {
10925
11184
  ok: false,
10926
11185
  reason: "pending"
@@ -11011,7 +11270,9 @@ function resumePending(deviceCode, verificationUri) {
11011
11270
  async function loginToProject(args) {
11012
11271
  const project = String(args.project || "").trim();
11013
11272
  if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.", "bad-request", "E_BAD_REQUEST");
11014
- const apiBase = resolveApiBase(args.api);
11273
+ const apiCheck = safeApiBase(resolveApiBase(args.api));
11274
+ if (!apiCheck.ok) return login_fail("LoginConfigError", apiCheck.message, "login-failed", "E_AUTH_FAILED");
11275
+ const apiBase = apiCheck.base;
11015
11276
  let config;
11016
11277
  try {
11017
11278
  config = await fetchLoginConfig(apiBase);
@@ -11052,7 +11313,9 @@ async function loginToProject(args) {
11052
11313
  budgetMs: FIRST_CALL_BUDGET_MS
11053
11314
  });
11054
11315
  if (poll.ok) return success(poll.creds);
11316
+ if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_auth (action: login) again to restart.", "login-expired", "E_AUTH_EXPIRED");
11055
11317
  if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.", "login-denied", "E_AUTH_DENIED");
11318
+ if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.", "login-failed", "E_AUTH_FAILED");
11056
11319
  return login_pending({
11057
11320
  deviceCode: start.deviceCode,
11058
11321
  userCode: start.userCode,
@@ -11076,7 +11339,7 @@ async function readIdentity() {
11076
11339
  const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
11077
11340
  const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
11078
11341
  const identityNote = 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.`;
11079
- const apiDivergesNote = apiDiverges ? `This login was minted via ${recordedApi}, but authenticated tools do not read that recorded value: they target ${effectiveDefaultApi} unless given an api argument.` : null;
11342
+ const apiDivergesNote = apiDiverges ? `This login was minted via ${recordedApi}: access grants for private registry reads use that recorded base when no api argument is given, while other authenticated tools target ${effectiveDefaultApi} unless given one.` : null;
11080
11343
  const envTokenNote = envTokenSet ? "EXTENSION_DEV_TOKEN is set and takes precedence over this stored login for authenticated tools; this report describes only the stored login." : null;
11081
11344
  const message = [
11082
11345
  identityNote,
@@ -11984,7 +12247,12 @@ async function runCli(cmd, args) {
11984
12247
  log("Usage: extension-mcp login --project <workspace>/<project> [--api <url>]");
11985
12248
  return 1;
11986
12249
  }
11987
- const apiBase = resolveApiBase(flag("api"));
12250
+ const apiCheck = safeApiBase(resolveApiBase(flag("api")));
12251
+ if (!apiCheck.ok) {
12252
+ log(apiCheck.message);
12253
+ return 1;
12254
+ }
12255
+ const apiBase = apiCheck.base;
11988
12256
  try {
11989
12257
  const config = await fetchLoginConfig(apiBase);
11990
12258
  const start = await requestDeviceCode({
@@ -11992,8 +12260,12 @@ async function runCli(cmd, args) {
11992
12260
  path: config.deviceCodeUrl,
11993
12261
  project
11994
12262
  });
12263
+ const completeLink = String(start.verificationUriComplete || "").trim();
11995
12264
  log("");
11996
- log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
12265
+ if (completeLink && completeLink !== start.verificationUri) {
12266
+ log(` Open ${completeLink} to approve (code ${start.userCode} is pre-filled).`);
12267
+ log(` If the page asks for a code, enter ${start.userCode} at ${start.verificationUri}.`);
12268
+ } else log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
11997
12269
  log("");
11998
12270
  log(" Waiting for authorization...");
11999
12271
  const poll = await pollDeviceToken({
@@ -12005,7 +12277,7 @@ async function runCli(cmd, args) {
12005
12277
  budgetMs: 1000 * start.expiresIn
12006
12278
  });
12007
12279
  if (!poll.ok) {
12008
- log("denied" === poll.reason ? "Authorization was denied at extension.dev/device." : "expired" === poll.reason ? "The device code expired. Run login again." : "Timed out waiting for authorization. Run login again.");
12280
+ log("denied" === poll.reason ? "Authorization was denied at extension.dev/device." : "expired" === poll.reason ? "The device code expired. Run login again." : "error" === poll.reason ? poll.message || "Device login failed. Run login again." : "Timed out waiting for authorization. Run login again.");
12009
12281
  return 1;
12010
12282
  }
12011
12283
  log(`Logged in to ${poll.creds.workspaceSlug}/${poll.creds.projectSlug}.`);