@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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +60 -0
- package/README.md +5 -5
- package/bin/extension-mcp.js +24 -2
- package/claude/ARCHITECTURE.md +1 -1
- package/claude/CLAUDE.md +5 -5
- package/claude/rules/cross-browser.md +4 -2
- package/claude/rules/mcp-tools.md +5 -5
- package/dist/module.js +672 -400
- package/dist/src/lib/boot-verdict.d.ts +3 -9
- package/dist/src/lib/envelope.d.ts +6 -0
- package/dist/src/lib/exec.d.ts +4 -0
- package/dist/src/lib/login-flow.d.ts +1 -0
- package/dist/src/lib/process-manager.d.ts +3 -2
- package/dist/src/lib/session-browser.d.ts +1 -1
- package/dist/src/tools/inspect-schema.d.ts +48 -0
- package/dist/src/tools/inspect.d.ts +2 -48
- package/dist/src/tools/submit.d.ts +5 -0
- package/package.json +2 -4
- package/server.json +2 -2
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: ()=>
|
|
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.
|
|
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
|
|
708
|
-
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
1948
|
-
|
|
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
|
-
|
|
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
|
|
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)=>!
|
|
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
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2856
|
+
const evidenceTail = denoiseCliLog(readOutput());
|
|
2758
2857
|
if (machineContract) return {
|
|
2759
2858
|
verdict: {
|
|
2760
2859
|
kind: "alive"
|
|
2761
2860
|
},
|
|
2762
2861
|
machineContract,
|
|
2763
|
-
|
|
2862
|
+
evidenceTail,
|
|
2764
2863
|
warnings: []
|
|
2765
2864
|
};
|
|
2766
|
-
if (legacyCompileScrape(
|
|
2865
|
+
if (legacyCompileScrape(evidenceTail)) return {
|
|
2767
2866
|
verdict: {
|
|
2768
2867
|
kind: "compile-failed",
|
|
2769
2868
|
compileErrors: []
|
|
2770
2869
|
},
|
|
2771
2870
|
machineContract,
|
|
2772
|
-
|
|
2871
|
+
evidenceTail,
|
|
2773
2872
|
warnings: [
|
|
2774
2873
|
LEGACY_FIDELITY_WARNING
|
|
2775
2874
|
]
|
|
2776
2875
|
};
|
|
2777
|
-
if (!noBrowser && legacyProfileLockScrape(
|
|
2876
|
+
if (!noBrowser && legacyProfileLockScrape(evidenceTail)) return {
|
|
2778
2877
|
verdict: {
|
|
2779
2878
|
kind: "profile-locked",
|
|
2780
2879
|
owner: null
|
|
2781
2880
|
},
|
|
2782
2881
|
machineContract,
|
|
2783
|
-
|
|
2882
|
+
evidenceTail,
|
|
2784
2883
|
warnings: [
|
|
2785
2884
|
LEGACY_FIDELITY_WARNING
|
|
2786
2885
|
]
|
|
2787
2886
|
};
|
|
2788
2887
|
return {
|
|
2789
2888
|
verdict: {
|
|
2790
|
-
kind: "
|
|
2889
|
+
kind: "silent-within-budget"
|
|
2791
2890
|
},
|
|
2792
2891
|
machineContract,
|
|
2793
|
-
|
|
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
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
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 =
|
|
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", ()=>
|
|
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.
|
|
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", ()=>
|
|
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.
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
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 =
|
|
3512
|
-
const
|
|
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
|
|
3551
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4319
|
-
if (
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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 ("
|
|
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
|
-
|
|
8740
|
+
windowTruncated: false
|
|
8486
8741
|
};
|
|
8487
8742
|
return {
|
|
8488
8743
|
events: events.slice(events.length - limit),
|
|
8489
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
9334
|
-
const
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
if (
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
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
|
-
|
|
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 (
|
|
10092
|
-
|
|
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 =
|
|
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 =
|
|
10332
|
-
const mismatch = "" !== pin && "latest" !== 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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}
|
|
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
|
|
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
|
-
|
|
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}.`);
|