@extension.dev/mcp 5.3.1 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +89 -0
- package/dist/module.js +632 -207
- package/dist/src/lib/cdp-targets.d.ts +15 -0
- package/dist/src/lib/process-manager.d.ts +1 -0
- package/dist/src/lib/session-browser.d.ts +1 -0
- package/dist/src/lib/types.d.ts +6 -1
- package/dist/src/tools/dom-inspect.d.ts +11 -0
- package/dist/src/tools/eval.d.ts +1 -1
- package/dist/src/tools/wait.d.ts +6 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/module.js
CHANGED
|
@@ -68,6 +68,7 @@ var eval_namespaceObject = {};
|
|
|
68
68
|
__webpack_require__.r(eval_namespaceObject);
|
|
69
69
|
__webpack_require__.d(eval_namespaceObject, {
|
|
70
70
|
handler: ()=>eval_handler,
|
|
71
|
+
resolveDefaultEvalContext: ()=>resolveDefaultEvalContext,
|
|
71
72
|
schema: ()=>eval_schema
|
|
72
73
|
});
|
|
73
74
|
var get_template_source_namespaceObject = {};
|
|
@@ -210,7 +211,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
210
211
|
handler: ()=>whoami_handler,
|
|
211
212
|
schema: ()=>whoami_schema
|
|
212
213
|
});
|
|
213
|
-
var package_namespaceObject = JSON.parse('{"rE":"5.
|
|
214
|
+
var package_namespaceObject = JSON.parse('{"rE":"5.5.0","El":{"OP":"^4.0.13"}}');
|
|
214
215
|
function scaffoldEnginePin(projectPath) {
|
|
215
216
|
try {
|
|
216
217
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -623,6 +624,185 @@ function spawnExtensionCli(args, options) {
|
|
|
623
624
|
}
|
|
624
625
|
};
|
|
625
626
|
}
|
|
627
|
+
const sessions = new Map();
|
|
628
|
+
function sessionKey(projectPath, browser) {
|
|
629
|
+
return `${node_path.resolve(projectPath)}::${browser}`;
|
|
630
|
+
}
|
|
631
|
+
function markerDir() {
|
|
632
|
+
return process.env.EXTENSION_MCP_SESSION_DIR || node_path.join(node_os.tmpdir(), "extension-dev-mcp-sessions");
|
|
633
|
+
}
|
|
634
|
+
function markerPath(projectPath, browser) {
|
|
635
|
+
const digest = node_crypto.createHash("sha1").update(sessionKey(projectPath, browser)).digest("hex").slice(0, 16);
|
|
636
|
+
return node_path.join(markerDir(), `${digest}.json`);
|
|
637
|
+
}
|
|
638
|
+
function removeSessionMarker(projectPath, browser) {
|
|
639
|
+
try {
|
|
640
|
+
node_fs.rmSync(markerPath(projectPath, browser), {
|
|
641
|
+
force: true
|
|
642
|
+
});
|
|
643
|
+
} catch {}
|
|
644
|
+
}
|
|
645
|
+
function listSessionMarkers() {
|
|
646
|
+
let files;
|
|
647
|
+
try {
|
|
648
|
+
files = node_fs.readdirSync(markerDir());
|
|
649
|
+
} catch {
|
|
650
|
+
return [];
|
|
651
|
+
}
|
|
652
|
+
const out = [];
|
|
653
|
+
for (const file of files)if (file.endsWith(".json")) try {
|
|
654
|
+
const parsed = JSON.parse(node_fs.readFileSync(node_path.join(markerDir(), file), "utf8"));
|
|
655
|
+
if ("string" == typeof parsed?.projectPath && "string" == typeof parsed?.browser) out.push(parsed);
|
|
656
|
+
} catch {}
|
|
657
|
+
return out;
|
|
658
|
+
}
|
|
659
|
+
function registerSession(info) {
|
|
660
|
+
sessions.set(sessionKey(info.projectPath, info.browser), info);
|
|
661
|
+
try {
|
|
662
|
+
node_fs.mkdirSync(markerDir(), {
|
|
663
|
+
recursive: true
|
|
664
|
+
});
|
|
665
|
+
node_fs.writeFileSync(markerPath(info.projectPath, info.browser), JSON.stringify({
|
|
666
|
+
...info,
|
|
667
|
+
projectPath: node_path.resolve(info.projectPath),
|
|
668
|
+
registeredAt: new Date().toISOString()
|
|
669
|
+
}));
|
|
670
|
+
} catch {}
|
|
671
|
+
}
|
|
672
|
+
function getSession(projectPath, browser) {
|
|
673
|
+
return sessions.get(sessionKey(projectPath, browser));
|
|
674
|
+
}
|
|
675
|
+
function findSessionInfo(projectPath, browser) {
|
|
676
|
+
const inMemory = sessions.get(sessionKey(projectPath, browser));
|
|
677
|
+
if (inMemory) return inMemory;
|
|
678
|
+
const resolved = node_path.resolve(projectPath);
|
|
679
|
+
for (const marker of listSessionMarkers())if (node_path.resolve(marker.projectPath) === resolved && marker.browser === browser) return marker;
|
|
680
|
+
}
|
|
681
|
+
function removeSession(projectPath, browser) {
|
|
682
|
+
sessions.delete(sessionKey(projectPath, browser));
|
|
683
|
+
}
|
|
684
|
+
function listSessions() {
|
|
685
|
+
return Array.from(sessions.values());
|
|
686
|
+
}
|
|
687
|
+
function contractSightings(projectPath) {
|
|
688
|
+
const root = node_path.resolve(projectPath, "dist", "extension-js");
|
|
689
|
+
let dirs;
|
|
690
|
+
try {
|
|
691
|
+
dirs = node_fs.readdirSync(root);
|
|
692
|
+
} catch {
|
|
693
|
+
return [];
|
|
694
|
+
}
|
|
695
|
+
const sightings = [];
|
|
696
|
+
for (const dir of dirs){
|
|
697
|
+
const readyPath = node_path.join(root, dir, "ready.json");
|
|
698
|
+
try {
|
|
699
|
+
const stat = node_fs.statSync(readyPath);
|
|
700
|
+
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
701
|
+
if (contract?.status !== "ready") continue;
|
|
702
|
+
sightings.push({
|
|
703
|
+
browser: dir,
|
|
704
|
+
mtimeMs: stat.mtimeMs,
|
|
705
|
+
pid: "number" == typeof contract.pid ? contract.pid : void 0
|
|
706
|
+
});
|
|
707
|
+
} catch {}
|
|
708
|
+
}
|
|
709
|
+
return sightings;
|
|
710
|
+
}
|
|
711
|
+
function pidAlive(pid) {
|
|
712
|
+
try {
|
|
713
|
+
process.kill(pid, 0);
|
|
714
|
+
return true;
|
|
715
|
+
} catch {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function knownSessionBrowsers(projectPath) {
|
|
720
|
+
const resolved = node_path.resolve(projectPath);
|
|
721
|
+
const browsers = [];
|
|
722
|
+
for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) browsers.push(session.browser);
|
|
723
|
+
for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
|
|
724
|
+
return Array.from(new Set(browsers));
|
|
725
|
+
}
|
|
726
|
+
function liveProjectSessions(projectPath) {
|
|
727
|
+
const resolved = node_path.resolve(projectPath);
|
|
728
|
+
const out = new Map();
|
|
729
|
+
for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) {
|
|
730
|
+
if (pidAlive(session.pid)) out.set(session.browser, {
|
|
731
|
+
browser: session.browser,
|
|
732
|
+
pid: session.pid,
|
|
733
|
+
source: "registry"
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
for (const sighting of contractSightings(projectPath))if (void 0 !== sighting.pid && pidAlive(sighting.pid)) {
|
|
737
|
+
if (!out.has(sighting.browser)) out.set(sighting.browser, {
|
|
738
|
+
browser: sighting.browser,
|
|
739
|
+
pid: sighting.pid,
|
|
740
|
+
source: "contract"
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
return [
|
|
744
|
+
...out.values()
|
|
745
|
+
];
|
|
746
|
+
}
|
|
747
|
+
function deadReadySession(projectPath) {
|
|
748
|
+
for (const sighting of contractSightings(projectPath))if (void 0 !== sighting.pid && !pidAlive(sighting.pid)) return {
|
|
749
|
+
browser: sighting.browser,
|
|
750
|
+
pid: sighting.pid
|
|
751
|
+
};
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
function browserExitStamp(projectPath, browser, since) {
|
|
755
|
+
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
756
|
+
try {
|
|
757
|
+
const stat = node_fs.statSync(readyPath);
|
|
758
|
+
if (stat.mtimeMs < since) return null;
|
|
759
|
+
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
760
|
+
const exited = contract?.code === "browser_exited" || contract?.browserExitCode !== void 0 || contract?.browserExitedAt !== void 0;
|
|
761
|
+
if (contract?.status === "error" && exited) return {
|
|
762
|
+
code: contract.code,
|
|
763
|
+
browserExitCode: contract.browserExitCode ?? null,
|
|
764
|
+
browserExitedAt: contract.browserExitedAt
|
|
765
|
+
};
|
|
766
|
+
} catch {}
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
function contractBoundPort(projectPath, browser, since) {
|
|
770
|
+
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
771
|
+
try {
|
|
772
|
+
const stat = node_fs.statSync(readyPath);
|
|
773
|
+
if (stat.mtimeMs < since) return null;
|
|
774
|
+
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
775
|
+
return "number" == typeof contract?.port && Number.isFinite(contract.port) ? contract.port : null;
|
|
776
|
+
} catch {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function resolveSessionBrowser(projectPath, explicit, fallback = "chrome") {
|
|
781
|
+
if (explicit) return {
|
|
782
|
+
browser: explicit,
|
|
783
|
+
source: "explicit"
|
|
784
|
+
};
|
|
785
|
+
const resolved = node_path.resolve(projectPath);
|
|
786
|
+
const mine = listSessions().filter((s)=>node_path.resolve(s.projectPath) === resolved);
|
|
787
|
+
if (mine.length > 0) return {
|
|
788
|
+
browser: mine[mine.length - 1].browser,
|
|
789
|
+
source: "session"
|
|
790
|
+
};
|
|
791
|
+
const sightings = contractSightings(projectPath).sort((a, b)=>b.mtimeMs - a.mtimeMs);
|
|
792
|
+
const live = sightings.filter((s)=>void 0 === s.pid || pidAlive(s.pid));
|
|
793
|
+
if (live.length > 0) return {
|
|
794
|
+
browser: live[0].browser,
|
|
795
|
+
source: "contract"
|
|
796
|
+
};
|
|
797
|
+
if (sightings.length > 0) return {
|
|
798
|
+
browser: sightings[0].browser,
|
|
799
|
+
source: "stale"
|
|
800
|
+
};
|
|
801
|
+
return {
|
|
802
|
+
browser: fallback,
|
|
803
|
+
source: "fallback"
|
|
804
|
+
};
|
|
805
|
+
}
|
|
626
806
|
function readBuildSummary(projectPath, browser, since) {
|
|
627
807
|
const file = node_path.resolve(projectPath, "dist", "extension-js", browser, "build-summary.json");
|
|
628
808
|
try {
|
|
@@ -678,6 +858,46 @@ function builtEntrypoints(distDir) {
|
|
|
678
858
|
});
|
|
679
859
|
return out;
|
|
680
860
|
}
|
|
861
|
+
function engineSanitize(input) {
|
|
862
|
+
return input.toLowerCase().replace(/[^a-z0-9 ]/gi, "").trim().replace(/\s+/g, "-");
|
|
863
|
+
}
|
|
864
|
+
function newestZip(dir, since, match) {
|
|
865
|
+
try {
|
|
866
|
+
const fresh = node_fs.readdirSync(dir).filter((name)=>name.endsWith(".zip") && (!match || match(name))).map((name)=>{
|
|
867
|
+
const full = node_path.join(dir, name);
|
|
868
|
+
return {
|
|
869
|
+
full,
|
|
870
|
+
mtimeMs: node_fs.statSync(full).mtimeMs
|
|
871
|
+
};
|
|
872
|
+
}).filter((entry)=>entry.mtimeMs >= since).sort((a, b)=>b.mtimeMs - a.mtimeMs);
|
|
873
|
+
return fresh[0]?.full ?? null;
|
|
874
|
+
} catch {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function engineZipBase(distDir, projectPath) {
|
|
879
|
+
let manifest = {};
|
|
880
|
+
try {
|
|
881
|
+
manifest = JSON.parse(node_fs.readFileSync(node_path.join(distDir, "manifest.json"), "utf8"));
|
|
882
|
+
} catch {}
|
|
883
|
+
const rawName = "string" != typeof manifest.name || /^__MSG_.+__$/.test(manifest.name) ? node_path.basename(node_path.resolve(projectPath)) : manifest.name;
|
|
884
|
+
const version = "string" == typeof manifest.version && manifest.version ? manifest.version : "0.0.0";
|
|
885
|
+
return `${engineSanitize(rawName)}-${version}`;
|
|
886
|
+
}
|
|
887
|
+
function locateDistZip(projectPath, browser, zipFilename, since) {
|
|
888
|
+
const distDir = node_path.resolve(projectPath, "dist", browser);
|
|
889
|
+
const base = zipFilename ? engineSanitize(zipFilename) : engineZipBase(distDir, projectPath);
|
|
890
|
+
const expected = node_path.join(distDir, `${base}.zip`);
|
|
891
|
+
if (node_fs.existsSync(expected)) return expected;
|
|
892
|
+
return newestZip(distDir, since);
|
|
893
|
+
}
|
|
894
|
+
function locateSourceZip(projectPath, browser, since) {
|
|
895
|
+
const distDir = node_path.resolve(projectPath, "dist", browser);
|
|
896
|
+
const distRoot = node_path.resolve(projectPath, "dist");
|
|
897
|
+
const expected = node_path.join(distRoot, `${engineZipBase(distDir, projectPath)}-source.zip`);
|
|
898
|
+
if (node_fs.existsSync(expected)) return expected;
|
|
899
|
+
return newestZip(distRoot, since, (name)=>name.endsWith("-source.zip"));
|
|
900
|
+
}
|
|
681
901
|
const build_schema = {
|
|
682
902
|
name: "extension_build",
|
|
683
903
|
description: "Build a browser extension for production. Outputs to dist/<browser>/. Optionally creates .zip for store submission.",
|
|
@@ -816,6 +1036,8 @@ async function build_handler(args) {
|
|
|
816
1036
|
duration: Date.now() - start,
|
|
817
1037
|
hint: "Fix the errors above, then build again. Run extension_manifest_validate for the full report. To build anyway (for example to inspect the broken output), pass skipValidation: true."
|
|
818
1038
|
});
|
|
1039
|
+
const clobberedSessions = liveProjectSessions(args.projectPath).filter((session)=>session.browser === browser);
|
|
1040
|
+
const warnings = clobberedSessions.map((session)=>`A live dev session (pid ${session.pid}) is running on this project for ${browser}, and this build wrote over its dist/${browser} output. The dev browser may now serve the production artifact instead of the dev build until the next recompile. Run extension_stop, or let dev recompile on the next source change, to resolve it.`);
|
|
819
1041
|
const cliArgs = [
|
|
820
1042
|
"build",
|
|
821
1043
|
args.projectPath,
|
|
@@ -858,6 +1080,9 @@ async function build_handler(args) {
|
|
|
858
1080
|
manifestWarnings: preflight.warnings
|
|
859
1081
|
} : {},
|
|
860
1082
|
...buildWarnings,
|
|
1083
|
+
...warnings.length ? {
|
|
1084
|
+
warnings
|
|
1085
|
+
} : {},
|
|
861
1086
|
duration,
|
|
862
1087
|
output: lastLines(out, 12),
|
|
863
1088
|
hint: "The bundler exited 0 but did not emit these files. Check that the manifest paths match what the build produces, and that nothing references a file outside the source tree."
|
|
@@ -884,7 +1109,26 @@ async function build_handler(args) {
|
|
|
884
1109
|
productionDivergence: divergence
|
|
885
1110
|
} : {};
|
|
886
1111
|
})(),
|
|
1112
|
+
...warnings.length ? {
|
|
1113
|
+
warnings
|
|
1114
|
+
} : {},
|
|
887
1115
|
zip: args.zip ?? false,
|
|
1116
|
+
...args.zip ? (()=>{
|
|
1117
|
+
const zipPath = locateDistZip(args.projectPath, browser, args.zipFilename, start);
|
|
1118
|
+
return zipPath ? {
|
|
1119
|
+
zipPath
|
|
1120
|
+
} : {
|
|
1121
|
+
zipPathNote: `zip: true was requested and the build succeeded, but no .zip file could be located in dist/${browser}. The engine may not have packaged it; check the build output below.`
|
|
1122
|
+
};
|
|
1123
|
+
})() : {},
|
|
1124
|
+
...args.zipSource ? (()=>{
|
|
1125
|
+
const zipSourcePath = locateSourceZip(args.projectPath, browser, start);
|
|
1126
|
+
return zipSourcePath ? {
|
|
1127
|
+
zipSourcePath
|
|
1128
|
+
} : {
|
|
1129
|
+
zipSourcePathNote: "zipSource: true was requested and the build succeeded, but no *-source.zip file could be located in dist/. The engine may not have packaged it; check the build output below."
|
|
1130
|
+
};
|
|
1131
|
+
})() : {},
|
|
888
1132
|
duration,
|
|
889
1133
|
output: lastLines(out, 12)
|
|
890
1134
|
});
|
|
@@ -894,172 +1138,13 @@ async function build_handler(args) {
|
|
|
894
1138
|
success: false,
|
|
895
1139
|
browser,
|
|
896
1140
|
error: message.slice(0, 1200),
|
|
1141
|
+
...warnings.length ? {
|
|
1142
|
+
warnings
|
|
1143
|
+
} : {},
|
|
897
1144
|
duration,
|
|
898
1145
|
hint: "Check that the project has a valid src/manifest.json and its dependencies are installed (extension_dev auto-installs; build does not)."
|
|
899
1146
|
});
|
|
900
1147
|
}
|
|
901
|
-
const sessions = new Map();
|
|
902
|
-
function sessionKey(projectPath, browser) {
|
|
903
|
-
return `${node_path.resolve(projectPath)}::${browser}`;
|
|
904
|
-
}
|
|
905
|
-
function markerDir() {
|
|
906
|
-
return process.env.EXTENSION_MCP_SESSION_DIR || node_path.join(node_os.tmpdir(), "extension-dev-mcp-sessions");
|
|
907
|
-
}
|
|
908
|
-
function markerPath(projectPath, browser) {
|
|
909
|
-
const digest = node_crypto.createHash("sha1").update(sessionKey(projectPath, browser)).digest("hex").slice(0, 16);
|
|
910
|
-
return node_path.join(markerDir(), `${digest}.json`);
|
|
911
|
-
}
|
|
912
|
-
function removeSessionMarker(projectPath, browser) {
|
|
913
|
-
try {
|
|
914
|
-
node_fs.rmSync(markerPath(projectPath, browser), {
|
|
915
|
-
force: true
|
|
916
|
-
});
|
|
917
|
-
} catch {}
|
|
918
|
-
}
|
|
919
|
-
function listSessionMarkers() {
|
|
920
|
-
let files;
|
|
921
|
-
try {
|
|
922
|
-
files = node_fs.readdirSync(markerDir());
|
|
923
|
-
} catch {
|
|
924
|
-
return [];
|
|
925
|
-
}
|
|
926
|
-
const out = [];
|
|
927
|
-
for (const file of files)if (file.endsWith(".json")) try {
|
|
928
|
-
const parsed = JSON.parse(node_fs.readFileSync(node_path.join(markerDir(), file), "utf8"));
|
|
929
|
-
if ("string" == typeof parsed?.projectPath && "string" == typeof parsed?.browser) out.push(parsed);
|
|
930
|
-
} catch {}
|
|
931
|
-
return out;
|
|
932
|
-
}
|
|
933
|
-
function registerSession(info) {
|
|
934
|
-
sessions.set(sessionKey(info.projectPath, info.browser), info);
|
|
935
|
-
try {
|
|
936
|
-
node_fs.mkdirSync(markerDir(), {
|
|
937
|
-
recursive: true
|
|
938
|
-
});
|
|
939
|
-
node_fs.writeFileSync(markerPath(info.projectPath, info.browser), JSON.stringify({
|
|
940
|
-
...info,
|
|
941
|
-
projectPath: node_path.resolve(info.projectPath),
|
|
942
|
-
registeredAt: new Date().toISOString()
|
|
943
|
-
}));
|
|
944
|
-
} catch {}
|
|
945
|
-
}
|
|
946
|
-
function getSession(projectPath, browser) {
|
|
947
|
-
return sessions.get(sessionKey(projectPath, browser));
|
|
948
|
-
}
|
|
949
|
-
function removeSession(projectPath, browser) {
|
|
950
|
-
sessions.delete(sessionKey(projectPath, browser));
|
|
951
|
-
}
|
|
952
|
-
function listSessions() {
|
|
953
|
-
return Array.from(sessions.values());
|
|
954
|
-
}
|
|
955
|
-
function contractSightings(projectPath) {
|
|
956
|
-
const root = node_path.resolve(projectPath, "dist", "extension-js");
|
|
957
|
-
let dirs;
|
|
958
|
-
try {
|
|
959
|
-
dirs = node_fs.readdirSync(root);
|
|
960
|
-
} catch {
|
|
961
|
-
return [];
|
|
962
|
-
}
|
|
963
|
-
const sightings = [];
|
|
964
|
-
for (const dir of dirs){
|
|
965
|
-
const readyPath = node_path.join(root, dir, "ready.json");
|
|
966
|
-
try {
|
|
967
|
-
const stat = node_fs.statSync(readyPath);
|
|
968
|
-
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
969
|
-
if (contract?.status !== "ready") continue;
|
|
970
|
-
sightings.push({
|
|
971
|
-
browser: dir,
|
|
972
|
-
mtimeMs: stat.mtimeMs,
|
|
973
|
-
pid: "number" == typeof contract.pid ? contract.pid : void 0
|
|
974
|
-
});
|
|
975
|
-
} catch {}
|
|
976
|
-
}
|
|
977
|
-
return sightings;
|
|
978
|
-
}
|
|
979
|
-
function pidAlive(pid) {
|
|
980
|
-
try {
|
|
981
|
-
process.kill(pid, 0);
|
|
982
|
-
return true;
|
|
983
|
-
} catch {
|
|
984
|
-
return false;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
function knownSessionBrowsers(projectPath) {
|
|
988
|
-
const resolved = node_path.resolve(projectPath);
|
|
989
|
-
const browsers = [];
|
|
990
|
-
for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) browsers.push(session.browser);
|
|
991
|
-
for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
|
|
992
|
-
return Array.from(new Set(browsers));
|
|
993
|
-
}
|
|
994
|
-
function liveProjectSessions(projectPath) {
|
|
995
|
-
const resolved = node_path.resolve(projectPath);
|
|
996
|
-
const out = new Map();
|
|
997
|
-
for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) {
|
|
998
|
-
if (pidAlive(session.pid)) out.set(session.browser, {
|
|
999
|
-
browser: session.browser,
|
|
1000
|
-
pid: session.pid,
|
|
1001
|
-
source: "registry"
|
|
1002
|
-
});
|
|
1003
|
-
}
|
|
1004
|
-
for (const sighting of contractSightings(projectPath))if (void 0 !== sighting.pid && pidAlive(sighting.pid)) {
|
|
1005
|
-
if (!out.has(sighting.browser)) out.set(sighting.browser, {
|
|
1006
|
-
browser: sighting.browser,
|
|
1007
|
-
pid: sighting.pid,
|
|
1008
|
-
source: "contract"
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
return [
|
|
1012
|
-
...out.values()
|
|
1013
|
-
];
|
|
1014
|
-
}
|
|
1015
|
-
function deadReadySession(projectPath) {
|
|
1016
|
-
for (const sighting of contractSightings(projectPath))if (void 0 !== sighting.pid && !pidAlive(sighting.pid)) return {
|
|
1017
|
-
browser: sighting.browser,
|
|
1018
|
-
pid: sighting.pid
|
|
1019
|
-
};
|
|
1020
|
-
return null;
|
|
1021
|
-
}
|
|
1022
|
-
function browserExitStamp(projectPath, browser, since) {
|
|
1023
|
-
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1024
|
-
try {
|
|
1025
|
-
const stat = node_fs.statSync(readyPath);
|
|
1026
|
-
if (stat.mtimeMs < since) return null;
|
|
1027
|
-
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1028
|
-
const exited = contract?.code === "browser_exited" || contract?.browserExitCode !== void 0 || contract?.browserExitedAt !== void 0;
|
|
1029
|
-
if (contract?.status === "error" && exited) return {
|
|
1030
|
-
code: contract.code,
|
|
1031
|
-
browserExitCode: contract.browserExitCode ?? null,
|
|
1032
|
-
browserExitedAt: contract.browserExitedAt
|
|
1033
|
-
};
|
|
1034
|
-
} catch {}
|
|
1035
|
-
return null;
|
|
1036
|
-
}
|
|
1037
|
-
function resolveSessionBrowser(projectPath, explicit, fallback = "chrome") {
|
|
1038
|
-
if (explicit) return {
|
|
1039
|
-
browser: explicit,
|
|
1040
|
-
source: "explicit"
|
|
1041
|
-
};
|
|
1042
|
-
const resolved = node_path.resolve(projectPath);
|
|
1043
|
-
const mine = listSessions().filter((s)=>node_path.resolve(s.projectPath) === resolved);
|
|
1044
|
-
if (mine.length > 0) return {
|
|
1045
|
-
browser: mine[mine.length - 1].browser,
|
|
1046
|
-
source: "session"
|
|
1047
|
-
};
|
|
1048
|
-
const sightings = contractSightings(projectPath).sort((a, b)=>b.mtimeMs - a.mtimeMs);
|
|
1049
|
-
const live = sightings.filter((s)=>void 0 === s.pid || pidAlive(s.pid));
|
|
1050
|
-
if (live.length > 0) return {
|
|
1051
|
-
browser: live[0].browser,
|
|
1052
|
-
source: "contract"
|
|
1053
|
-
};
|
|
1054
|
-
if (sightings.length > 0) return {
|
|
1055
|
-
browser: sightings[0].browser,
|
|
1056
|
-
source: "stale"
|
|
1057
|
-
};
|
|
1058
|
-
return {
|
|
1059
|
-
browser: fallback,
|
|
1060
|
-
source: "fallback"
|
|
1061
|
-
};
|
|
1062
|
-
}
|
|
1063
1148
|
const stop_schema = {
|
|
1064
1149
|
name: "extension_stop",
|
|
1065
1150
|
description: "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched. Counterpart to extension_dev/extension_start. Call it when you are done verifying so sessions do not accumulate.",
|
|
@@ -1380,7 +1465,8 @@ async function dev_handler(args) {
|
|
|
1380
1465
|
browser,
|
|
1381
1466
|
port: args.port,
|
|
1382
1467
|
projectPath: args.projectPath,
|
|
1383
|
-
command: "dev"
|
|
1468
|
+
command: "dev",
|
|
1469
|
+
noBrowser: Boolean(args.noBrowser)
|
|
1384
1470
|
});
|
|
1385
1471
|
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
1386
1472
|
await new Promise((resolve)=>setTimeout(resolve, 3000));
|
|
@@ -1437,11 +1523,30 @@ async function dev_handler(args) {
|
|
|
1437
1523
|
allowEval: Boolean(args.allowEval),
|
|
1438
1524
|
unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, source_inspect, wait, doctor)"
|
|
1439
1525
|
};
|
|
1526
|
+
const boundPort = contractBoundPort(args.projectPath, browser, spawnedAt);
|
|
1527
|
+
if (null !== boundPort && boundPort !== args.port) registerSession({
|
|
1528
|
+
pid,
|
|
1529
|
+
browser,
|
|
1530
|
+
port: boundPort,
|
|
1531
|
+
projectPath: args.projectPath,
|
|
1532
|
+
command: "dev",
|
|
1533
|
+
noBrowser: Boolean(args.noBrowser)
|
|
1534
|
+
});
|
|
1535
|
+
const portReport = null !== boundPort ? {
|
|
1536
|
+
port: boundPort,
|
|
1537
|
+
...void 0 !== args.port && args.port !== boundPort ? {
|
|
1538
|
+
requestedPort: args.port,
|
|
1539
|
+
portNote: `Requested port ${args.port} was not available; the dev server bound ${boundPort} (read from the engine's ready.json contract, the same source extension_wait reports).`
|
|
1540
|
+
} : {}
|
|
1541
|
+
} : {
|
|
1542
|
+
requestedPort: args.port ?? 8080,
|
|
1543
|
+
portNote: "The engine has not stamped its ready.json contract yet, so the bound port is not known at response time (a taken port makes the server bind the next free one). extension_wait reports the bound port from that contract once it lands; requestedPort above is only what was asked for."
|
|
1544
|
+
};
|
|
1440
1545
|
return JSON.stringify({
|
|
1441
1546
|
ok: true,
|
|
1442
1547
|
pid,
|
|
1443
1548
|
browser,
|
|
1444
|
-
|
|
1549
|
+
...portReport,
|
|
1445
1550
|
projectPath: args.projectPath,
|
|
1446
1551
|
status: "started",
|
|
1447
1552
|
...replaced.length > 0 ? {
|
|
@@ -1451,7 +1556,7 @@ async function dev_handler(args) {
|
|
|
1451
1556
|
} : {}
|
|
1452
1557
|
} : {},
|
|
1453
1558
|
capabilities,
|
|
1454
|
-
hint: "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_inspect need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser.",
|
|
1559
|
+
hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/dom_inspect/eval) need a live browser and will not work against this session. When you are done, call extension_stop to shut down the dev server." : "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_inspect need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser.",
|
|
1455
1560
|
earlyOutput: denoiseEarlyOutput(earlyOutput).slice(0, 500),
|
|
1456
1561
|
logPath
|
|
1457
1562
|
});
|
|
@@ -1463,7 +1568,10 @@ function denoiseEarlyOutput(raw) {
|
|
|
1463
1568
|
/^npm warn config/i,
|
|
1464
1569
|
/V8: .*Invalid asm\.js/i,
|
|
1465
1570
|
/^\(node:\d+\) V8:/i,
|
|
1466
|
-
/Use `node --trace-warnings/i
|
|
1571
|
+
/Use `node --trace-warnings/i,
|
|
1572
|
+
/Invalid asm\.js:/i,
|
|
1573
|
+
/Linking failure in asm\.js/i,
|
|
1574
|
+
/Successfully compiled asm\.js/i
|
|
1467
1575
|
];
|
|
1468
1576
|
return raw.split("\n").filter((line)=>!NOISE.some((re)=>re.test(line.trim()))).join("\n").replace(/\n{2,}/g, "\n").trimStart();
|
|
1469
1577
|
}
|
|
@@ -2307,6 +2415,9 @@ function walkDir(dir, base = "") {
|
|
|
2307
2415
|
else if ([
|
|
2308
2416
|
".map"
|
|
2309
2417
|
].includes(ext)) type = "sourcemap";
|
|
2418
|
+
else if ([
|
|
2419
|
+
".zip"
|
|
2420
|
+
].includes(ext)) type = "archive";
|
|
2310
2421
|
entries.push({
|
|
2311
2422
|
path: rel,
|
|
2312
2423
|
size: stat.size,
|
|
@@ -2347,7 +2458,8 @@ async function inspect_handler(args) {
|
|
|
2347
2458
|
}
|
|
2348
2459
|
const sourcemapSize = byType.sourcemap?.size ?? 0;
|
|
2349
2460
|
const buildType = sourcemapSize > 0 ? "development" : "production";
|
|
2350
|
-
const
|
|
2461
|
+
const archiveSize = byType.archive?.size ?? 0;
|
|
2462
|
+
const shippableSize = totalSize - sourcemapSize - archiveSize;
|
|
2351
2463
|
const sizeByPath = new Map(files.map((f)=>[
|
|
2352
2464
|
f.path,
|
|
2353
2465
|
f.size
|
|
@@ -2377,7 +2489,7 @@ async function inspect_handler(args) {
|
|
|
2377
2489
|
});
|
|
2378
2490
|
const PROMO_RE = /(screenshot|promo|marquee|tile|banner|preview)[-_.]?\d*\.(png|jpe?g|webp|gif)$/i;
|
|
2379
2491
|
const sizeWarnings = [];
|
|
2380
|
-
for (const f of files)if ("sourcemap" !== f.type) {
|
|
2492
|
+
for (const f of files)if ("sourcemap" !== f.type && "archive" !== f.type) {
|
|
2381
2493
|
if (PROMO_RE.test(f.path)) sizeWarnings.push(`${f.path} (${formatBytes(f.size)}) looks like a store-listing promo image shipped inside the extension package, move it out of the bundled sources so it does not inflate the store zip.`);
|
|
2382
2494
|
else if ("image" === f.type && !f.path.includes("icon") && f.size > 51200 && shippableSize > 0 && f.size / shippableSize > 0.25) sizeWarnings.push(`${f.path} (${formatBytes(f.size)}) is ${Math.round(f.size / shippableSize * 100)}% of the shipped bundle, unusually large for a shipped asset.`);
|
|
2383
2495
|
}
|
|
@@ -2397,6 +2509,9 @@ async function inspect_handler(args) {
|
|
|
2397
2509
|
..."development" === buildType ? {
|
|
2398
2510
|
note: `This dist contains ${formatBytes(sourcemapSize)} of sourcemaps and looks like a dev build; run extension_build for production sizes. shippableSize excludes sourcemaps.`
|
|
2399
2511
|
} : {},
|
|
2512
|
+
...archiveSize > 0 ? {
|
|
2513
|
+
archiveNote: `This dist contains ${formatBytes(archiveSize)} of .zip archive(s) (store packaging output, written into dist by zip builds). shippableSize excludes them so the packaged copy does not double-count the files it contains.`
|
|
2514
|
+
} : {},
|
|
2400
2515
|
manifest: {
|
|
2401
2516
|
name: manifest.name,
|
|
2402
2517
|
version: manifest.version,
|
|
@@ -2429,7 +2544,7 @@ async function inspect_handler(args) {
|
|
|
2429
2544
|
has128Icon: "string" == typeof manifest.icons?.["128"],
|
|
2430
2545
|
noSourceMaps: !files.some((f)=>"sourcemap" === f.type),
|
|
2431
2546
|
noPromoAssets: !files.some((f)=>PROMO_RE.test(f.path)),
|
|
2432
|
-
under10MB: totalSize < 10485760
|
|
2547
|
+
under10MB: totalSize - archiveSize < 10485760
|
|
2433
2548
|
}
|
|
2434
2549
|
};
|
|
2435
2550
|
return JSON.stringify(result);
|
|
@@ -3065,7 +3180,7 @@ async function source_inspect_handler(args) {
|
|
|
3065
3180
|
}
|
|
3066
3181
|
const list_extensions_schema = {
|
|
3067
3182
|
name: "extension_list_extensions",
|
|
3068
|
-
description: "List the extensions with a live context in the running dev browser via Chrome DevTools Protocol. Returns each extension's id, name, version, and live contexts (service worker, page).
|
|
3183
|
+
description: "List the extensions with a live context in the running dev browser via Chrome DevTools Protocol. Returns each extension's id, name, version, and live contexts (service worker, page). The entry for THIS dev session's extension (the project being served) is flagged ownExtension: true, with name and version resolved from the session's ready contract even when the browser exposes no identity. Other extensions resolve via the read-only Extensions domain when available; their contexts are never attached to or evaluated in, so an id that the domain does not describe stays id-only with a note saying why. A dormant MV3 service worker with no open page may be absent until it wakes. Chromium only (Firefox uses RDP, not yet supported). Requires an active dev or start session.",
|
|
3069
3184
|
inputSchema: {
|
|
3070
3185
|
type: "object",
|
|
3071
3186
|
properties: {
|
|
@@ -3083,6 +3198,47 @@ const list_extensions_schema = {
|
|
|
3083
3198
|
]
|
|
3084
3199
|
}
|
|
3085
3200
|
};
|
|
3201
|
+
function unpackedExtensionId(distPath) {
|
|
3202
|
+
const digest = node_crypto.createHash("sha256").update(distPath).digest();
|
|
3203
|
+
let id = "";
|
|
3204
|
+
for(let i = 0; i < 16; i++){
|
|
3205
|
+
id += String.fromCharCode(97 + (digest[i] >> 4));
|
|
3206
|
+
id += String.fromCharCode(97 + (0x0f & digest[i]));
|
|
3207
|
+
}
|
|
3208
|
+
return id;
|
|
3209
|
+
}
|
|
3210
|
+
function readOwnIdentity(projectPath, browser) {
|
|
3211
|
+
let contract;
|
|
3212
|
+
try {
|
|
3213
|
+
const file = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
3214
|
+
contract = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
3215
|
+
} catch {
|
|
3216
|
+
return null;
|
|
3217
|
+
}
|
|
3218
|
+
const distPath = "string" == typeof contract?.distPath ? contract.distPath : null;
|
|
3219
|
+
const ids = [];
|
|
3220
|
+
if (distPath) {
|
|
3221
|
+
ids.push(unpackedExtensionId(distPath));
|
|
3222
|
+
try {
|
|
3223
|
+
const real = node_fs.realpathSync(distPath);
|
|
3224
|
+
if (real !== distPath) ids.push(unpackedExtensionId(real));
|
|
3225
|
+
} catch {}
|
|
3226
|
+
}
|
|
3227
|
+
let name = "string" == typeof contract?.extensionName ? contract.extensionName : void 0;
|
|
3228
|
+
let version = "string" == typeof contract?.extensionVersion ? contract.extensionVersion : void 0;
|
|
3229
|
+
if ((!name || !version) && distPath) try {
|
|
3230
|
+
const manifest = JSON.parse(node_fs.readFileSync(node_path.join(distPath, "manifest.json"), "utf8"));
|
|
3231
|
+
if (!name && "string" == typeof manifest?.name && !manifest.name.startsWith("__MSG_")) name = manifest.name;
|
|
3232
|
+
if (!version && "string" == typeof manifest?.version) version = manifest.version;
|
|
3233
|
+
} catch {}
|
|
3234
|
+
if (0 === ids.length && !name) return null;
|
|
3235
|
+
return {
|
|
3236
|
+
ids,
|
|
3237
|
+
name,
|
|
3238
|
+
version
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
const UNRESOLVED_NOTE = "Identity unresolved: the browser's Extensions CDP domain returned nothing for this id, and other extensions' contexts are never attached to or evaluated in to read a manifest.";
|
|
3086
3242
|
async function list_extensions_handler(args) {
|
|
3087
3243
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
3088
3244
|
if (!isChromiumFamily(browser)) return JSON.stringify({
|
|
@@ -3123,6 +3279,7 @@ async function list_extensions_handler(args) {
|
|
|
3123
3279
|
});
|
|
3124
3280
|
byId.set(id, list);
|
|
3125
3281
|
}
|
|
3282
|
+
const own = readOwnIdentity(args.projectPath, browser);
|
|
3126
3283
|
const extensions = [];
|
|
3127
3284
|
for (const [id, ctxTargets] of byId){
|
|
3128
3285
|
const entry = {
|
|
@@ -3143,15 +3300,33 @@ async function list_extensions_handler(args) {
|
|
|
3143
3300
|
entry.source = "extensions-domain";
|
|
3144
3301
|
}
|
|
3145
3302
|
} catch {}
|
|
3303
|
+
if (own?.ids.includes(id)) {
|
|
3304
|
+
entry.ownExtension = true;
|
|
3305
|
+
if (void 0 === entry.name && void 0 !== own.name) {
|
|
3306
|
+
entry.name = own.name;
|
|
3307
|
+
if (void 0 !== own.version) entry.version = own.version;
|
|
3308
|
+
entry.source = "session-contract";
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3146
3311
|
extensions.push(entry);
|
|
3147
3312
|
}
|
|
3148
|
-
|
|
3313
|
+
if (own?.name && !extensions.some((e)=>e.ownExtension)) {
|
|
3314
|
+
const byName = extensions.filter((e)=>e.name === own.name);
|
|
3315
|
+
if (1 === byName.length) byName[0].ownExtension = true;
|
|
3316
|
+
}
|
|
3317
|
+
for (const entry of extensions)if (void 0 === entry.name) entry.note = UNRESOLVED_NOTE;
|
|
3318
|
+
extensions.sort((a, b)=>{
|
|
3319
|
+
if ((a.ownExtension ?? false) !== (b.ownExtension ?? false)) return a.ownExtension ? -1 : 1;
|
|
3320
|
+
return (a.name ?? a.id).localeCompare(b.name ?? b.id);
|
|
3321
|
+
});
|
|
3322
|
+
const ownEntry = extensions.find((e)=>e.ownExtension);
|
|
3149
3323
|
return JSON.stringify({
|
|
3150
3324
|
cdpPort,
|
|
3151
3325
|
browser,
|
|
3152
3326
|
count: extensions.length,
|
|
3327
|
+
ownExtensionId: ownEntry?.id ?? null,
|
|
3153
3328
|
extensions,
|
|
3154
|
-
note: "Lists extensions that currently have at least one live context (service worker or open page). An MV3 service worker that has gone dormant with no open page may be absent until it wakes.
|
|
3329
|
+
note: "Lists extensions that currently have at least one live context (service worker or open page). An MV3 service worker that has gone dormant with no open page may be absent until it wakes. ownExtension marks the extension this dev session serves, identified from the session's ready contract. Other identity is read read-only via the Extensions domain; other extensions' contexts are never attached to or evaluated in."
|
|
3155
3330
|
});
|
|
3156
3331
|
} catch (error) {
|
|
3157
3332
|
return JSON.stringify({
|
|
@@ -3483,7 +3658,7 @@ async function logs_handler(args) {
|
|
|
3483
3658
|
return readFromFile(args, browser, limit);
|
|
3484
3659
|
}
|
|
3485
3660
|
function toMcpSpeak(text) {
|
|
3486
|
-
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(/--context[= ](
|
|
3661
|
+
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`");
|
|
3487
3662
|
}
|
|
3488
3663
|
function withSessionContext(message, projectPath) {
|
|
3489
3664
|
const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
|
|
@@ -3536,7 +3711,7 @@ function commonFlags(args) {
|
|
|
3536
3711
|
}
|
|
3537
3712
|
const eval_schema = {
|
|
3538
3713
|
name: "extension_eval",
|
|
3539
|
-
description: "Evaluate an expression in a running extension context. Requires the dev session to be started with allowEval: true (extension_dev; writes a 0600 session token the CLI reads). Targeting for context content/page: pass `url` to pick the matching tab, or omit both `url` and `tab` to use the ACTIVE tab; a numeric `tab` id is only needed to disambiguate. Extension surfaces (popup/options/sidebar/devtools) and override pages (newtab/history/bookmarks) evaluate over the in-bundle relay and need NO tab id; the surface must be OPEN (extension_open first; a closed surface returns an explicit error).
|
|
3714
|
+
description: "Evaluate an expression in a running extension context. Requires the dev session to be started with allowEval: true (extension_dev; writes a 0600 session token the CLI reads). Context default: on a Chromium session whose manifest is MV3 (the default template) the default is `page` (the active tab), because the MV3 background is a service worker whose CSP blocks eval, so a background default would fail on the most common path; on Firefox/MV2 sessions the default stays `background`. Pass `context: \"background\"` explicitly to target the worker anyway (on Chromium MV3 that returns the CSP explanation). Targeting for context content/page: pass `url` to pick the matching tab, or omit both `url` and `tab` to use the ACTIVE tab; a numeric `tab` id is only needed to disambiguate. Extension surfaces (popup/options/sidebar/devtools) and override pages (newtab/history/bookmarks) evaluate over the in-bundle relay and need NO tab id; the surface must be OPEN (extension_open first; a closed surface returns an explicit error). Use extension_dom_inspect with listTabs: true to enumerate {tabId,url,title}. Wraps `extension eval`.",
|
|
3540
3715
|
inputSchema: {
|
|
3541
3716
|
type: "object",
|
|
3542
3717
|
properties: {
|
|
@@ -3562,8 +3737,7 @@ const eval_schema = {
|
|
|
3562
3737
|
"content",
|
|
3563
3738
|
"page"
|
|
3564
3739
|
],
|
|
3565
|
-
default: "background"
|
|
3566
|
-
description: "Which extension surface to evaluate in"
|
|
3740
|
+
description: "Which extension surface to evaluate in. Default: `background`, EXCEPT on Chromium sessions whose manifest is MV3, where the default is `page` (the active tab) because the MV3 service worker CSP blocks eval; pass `context: \"background\"` explicitly to target the worker anyway."
|
|
3567
3741
|
},
|
|
3568
3742
|
url: {
|
|
3569
3743
|
type: "string",
|
|
@@ -3588,14 +3762,38 @@ const eval_schema = {
|
|
|
3588
3762
|
]
|
|
3589
3763
|
}
|
|
3590
3764
|
};
|
|
3765
|
+
function resolveDefaultEvalContext(projectPath, browser) {
|
|
3766
|
+
if (!isChromiumFamily(browser)) return "background";
|
|
3767
|
+
const candidates = [
|
|
3768
|
+
node_path.join(projectPath, "dist", browser, "manifest.json"),
|
|
3769
|
+
node_path.join(projectPath, "dist", "manifest.json"),
|
|
3770
|
+
node_path.join(projectPath, "src", "manifest.json"),
|
|
3771
|
+
node_path.join(projectPath, "manifest.json")
|
|
3772
|
+
];
|
|
3773
|
+
for (const file of candidates){
|
|
3774
|
+
let manifest;
|
|
3775
|
+
try {
|
|
3776
|
+
manifest = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
3777
|
+
} catch {
|
|
3778
|
+
continue;
|
|
3779
|
+
}
|
|
3780
|
+
const version = manifest["chromium:manifest_version"] ?? manifest.manifest_version;
|
|
3781
|
+
if (3 === version) return "page";
|
|
3782
|
+
if (2 === version) break;
|
|
3783
|
+
}
|
|
3784
|
+
return "background";
|
|
3785
|
+
}
|
|
3591
3786
|
async function eval_handler(args) {
|
|
3592
3787
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
3788
|
+
const defaulted = !args.context && "page" === resolveDefaultEvalContext(args.projectPath, browser);
|
|
3789
|
+
const context = defaulted ? "page" : args.context;
|
|
3593
3790
|
const raw = await runActVerb([
|
|
3594
3791
|
"eval",
|
|
3595
3792
|
args.expression,
|
|
3596
3793
|
args.projectPath,
|
|
3597
3794
|
...commonFlags({
|
|
3598
3795
|
...args,
|
|
3796
|
+
context,
|
|
3599
3797
|
browser
|
|
3600
3798
|
})
|
|
3601
3799
|
], args.projectPath, args.timeout);
|
|
@@ -3606,6 +3804,15 @@ async function eval_handler(args) {
|
|
|
3606
3804
|
return JSON.stringify(parsed);
|
|
3607
3805
|
}
|
|
3608
3806
|
} catch {}
|
|
3807
|
+
if (defaulted) try {
|
|
3808
|
+
const parsed = JSON.parse(raw);
|
|
3809
|
+
if (parsed && "object" == typeof parsed) {
|
|
3810
|
+
parsed.defaultedContext = "page";
|
|
3811
|
+
parsed.contextNote = 'No context given: defaulted to "page" (the active tab) because this Chromium session\'s MV3 background is a service worker whose CSP blocks eval. Pass context: "background" explicitly to target the worker (works on Firefox/MV2 builds).';
|
|
3812
|
+
if (false === parsed.ok && /cannot access|chrome-extension:\/\/|chrome:\/\//i.test(JSON.stringify(parsed.error ?? ""))) parsed.hint = "The active tab is a browser or extension page that eval cannot reach. Navigate the dev browser to a regular web page, or pass url (match pattern) or tab to pick one; extension_dom_inspect with listTabs: true lists open tabs.";
|
|
3813
|
+
return JSON.stringify(parsed);
|
|
3814
|
+
}
|
|
3815
|
+
} catch {}
|
|
3609
3816
|
return raw;
|
|
3610
3817
|
}
|
|
3611
3818
|
const storage_schema = {
|
|
@@ -3834,7 +4041,7 @@ async function navigateToUrl(projectPath, browser, url) {
|
|
|
3834
4041
|
} catch {}
|
|
3835
4042
|
}
|
|
3836
4043
|
}
|
|
3837
|
-
function
|
|
4044
|
+
function open_unpackedExtensionId(distPath) {
|
|
3838
4045
|
const digest = node_crypto.createHash("sha256").update(distPath).digest();
|
|
3839
4046
|
let id = "";
|
|
3840
4047
|
for(let i = 0; i < 16; i++){
|
|
@@ -3845,7 +4052,7 @@ function unpackedExtensionId(distPath) {
|
|
|
3845
4052
|
}
|
|
3846
4053
|
async function resolveExtensionId(projectPath, browser) {
|
|
3847
4054
|
const distPath = readDistPath(projectPath, browser);
|
|
3848
|
-
const computed = distPath ?
|
|
4055
|
+
const computed = distPath ? open_unpackedExtensionId(distPath) : null;
|
|
3849
4056
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
3850
4057
|
if (!resolved) return computed;
|
|
3851
4058
|
const ids = new Set();
|
|
@@ -3909,6 +4116,57 @@ function surfaceDocument(projectPath, browser, surface) {
|
|
|
3909
4116
|
}
|
|
3910
4117
|
return null;
|
|
3911
4118
|
}
|
|
4119
|
+
const SURFACE_MANIFEST_KEYS = {
|
|
4120
|
+
popup: "action.default_popup",
|
|
4121
|
+
options: "options_ui.page (or options_page)",
|
|
4122
|
+
sidebar: "side_panel.default_path (or sidebar_action.default_panel)",
|
|
4123
|
+
newtab: "chrome_url_overrides.newtab",
|
|
4124
|
+
history: "chrome_url_overrides.history",
|
|
4125
|
+
bookmarks: "chrome_url_overrides.bookmarks"
|
|
4126
|
+
};
|
|
4127
|
+
function declaredSurfaces(projectPath, browser) {
|
|
4128
|
+
const candidates = [
|
|
4129
|
+
node_path.join(projectPath, "dist", browser, "manifest.json"),
|
|
4130
|
+
node_path.join(projectPath, "dist", "manifest.json"),
|
|
4131
|
+
node_path.join(projectPath, "src", "manifest.json"),
|
|
4132
|
+
node_path.join(projectPath, "manifest.json")
|
|
4133
|
+
];
|
|
4134
|
+
const readable = candidates.some((file)=>{
|
|
4135
|
+
try {
|
|
4136
|
+
JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
4137
|
+
return true;
|
|
4138
|
+
} catch {
|
|
4139
|
+
return false;
|
|
4140
|
+
}
|
|
4141
|
+
});
|
|
4142
|
+
if (!readable) return null;
|
|
4143
|
+
return Object.keys(SURFACE_MANIFEST_KEYS).filter((s)=>null !== surfaceDocument(projectPath, browser, s));
|
|
4144
|
+
}
|
|
4145
|
+
function missingSurfaceError(projectPath, browser, surface, consequence) {
|
|
4146
|
+
const declared = declaredSurfaces(projectPath, browser);
|
|
4147
|
+
if (null === declared) return JSON.stringify({
|
|
4148
|
+
ok: false,
|
|
4149
|
+
error: {
|
|
4150
|
+
name: "NoSurfaceDocument",
|
|
4151
|
+
message: `No readable manifest was found for this project (checked dist/${browser}, dist, src, and the project root), so the ${surface} document cannot be resolved.`
|
|
4152
|
+
},
|
|
4153
|
+
hint: "Check projectPath, or build the project first (extension_build)."
|
|
4154
|
+
});
|
|
4155
|
+
const key = SURFACE_MANIFEST_KEYS[surface] ?? surface;
|
|
4156
|
+
const others = declared.filter((s)=>s !== surface);
|
|
4157
|
+
const nextVerb = "popup" === surface ? 'To exercise the toolbar button of a popup-less extension, call extension_open with surface: "action", which replays chrome.action.onClicked. To give the extension a popup, set action.default_popup in the manifest and rebuild.' : `To add one, set ${key} in the manifest and rebuild.`;
|
|
4158
|
+
return JSON.stringify({
|
|
4159
|
+
ok: false,
|
|
4160
|
+
error: {
|
|
4161
|
+
name: "NoSurfaceDocument",
|
|
4162
|
+
message: `This extension declares no ${surface}: nothing in its manifest sets ${key}, ${consequence}. That is how the extension is built, not a failure of the session or the tooling.`
|
|
4163
|
+
},
|
|
4164
|
+
...others.length ? {
|
|
4165
|
+
declaredSurfaces: others
|
|
4166
|
+
} : {},
|
|
4167
|
+
hint: (others.length ? `Surfaces this extension does declare: ${others.join(", ")}; extension_open can target those. ` : "The manifest declares no other UI surface documents either. ") + nextVerb
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
3912
4170
|
const POPUP_MIN = 25;
|
|
3913
4171
|
const POPUP_MAX_WIDTH = 800;
|
|
3914
4172
|
const POPUP_MAX_HEIGHT = 600;
|
|
@@ -3967,14 +4225,7 @@ async function applyPopupBounds(projectPath, browser, targetId) {
|
|
|
3967
4225
|
}
|
|
3968
4226
|
async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
3969
4227
|
const doc = surfaceDocument(projectPath, browser, surface);
|
|
3970
|
-
if (!doc) return
|
|
3971
|
-
ok: false,
|
|
3972
|
-
error: {
|
|
3973
|
-
name: "NoSurfaceDocument",
|
|
3974
|
-
message: `The manifest declares no document for surface "${surface}", so there is no page to render as a tab.`
|
|
3975
|
-
},
|
|
3976
|
-
hint: "Check the manifest: popup needs action.default_popup, options needs options_ui.page or options_page, sidebar needs side_panel.default_path."
|
|
3977
|
-
});
|
|
4228
|
+
if (!doc) return missingSurfaceError(projectPath, browser, surface, "so there is no page to render as a tab");
|
|
3978
4229
|
const id = await resolveExtensionId(projectPath, browser);
|
|
3979
4230
|
if (!id) return JSON.stringify({
|
|
3980
4231
|
ok: false,
|
|
@@ -4087,6 +4338,10 @@ async function open_handler(args) {
|
|
|
4087
4338
|
hint: declared.length ? `Declared commands are: ${declared.join(", ")}. Check for a typo, or add "${args.name}" to the manifest.` : "This manifest declares no commands at all. Add a `commands` block, rebuild, then retry."
|
|
4088
4339
|
});
|
|
4089
4340
|
}
|
|
4341
|
+
if ("popup" === args.surface) {
|
|
4342
|
+
const declared = declaredSurfaces(args.projectPath, browser);
|
|
4343
|
+
if (declared && !declared.includes("popup")) return missingSurfaceError(args.projectPath, browser, "popup", "so there is no popup to open");
|
|
4344
|
+
}
|
|
4090
4345
|
const cli = [
|
|
4091
4346
|
"open",
|
|
4092
4347
|
args.surface,
|
|
@@ -4110,20 +4365,38 @@ async function open_handler(args) {
|
|
|
4110
4365
|
try {
|
|
4111
4366
|
const parsedFallback = JSON.parse(fallback);
|
|
4112
4367
|
if (parsedFallback?.ok) {
|
|
4113
|
-
parsedFallback.note = "The dev browser is headless (EXTENSION_HEADLESS),
|
|
4368
|
+
parsedFallback.note = "The dev browser is headless (EXTENSION_HEADLESS=1), and a real popup/sidebar window can only open in a headed session, so the surface was rendered as a tab instead. For the real window, start a headed session: extension_dev with replace: true, with EXTENSION_HEADLESS=0 (or unset) in the environment, then open the surface again without asTab.";
|
|
4114
4369
|
return JSON.stringify(parsedFallback);
|
|
4115
4370
|
}
|
|
4116
4371
|
} catch {}
|
|
4117
4372
|
}
|
|
4118
|
-
if (!parsed.hint) parsed.hint = /user gesture/i.test(msg) ? "This surface can only open from a real user gesture, which headless automation cannot produce. Retry with asTab: true to render the surface document in a tab instead." : "The dev browser is running headless (EXTENSION_HEADLESS),
|
|
4373
|
+
if (!parsed.hint) parsed.hint = /user gesture/i.test(msg) ? "This surface can only open from a real user gesture, which headless automation cannot produce. Retry with asTab: true to render the surface document in a tab instead." : "The dev browser is running headless (EXTENSION_HEADLESS=1), and a popup/sidebar window needs a headed session. Retry with asTab: true to render the surface document in a tab, or start a headed session for the real window: extension_dev with replace: true, with EXTENSION_HEADLESS=0 (or unset) in the environment.";
|
|
4119
4374
|
return JSON.stringify(parsed);
|
|
4120
4375
|
}
|
|
4121
4376
|
} catch {}
|
|
4122
4377
|
return raw;
|
|
4123
4378
|
}
|
|
4379
|
+
const TARGET_ID_NOTE = "targetId is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_inspect with listTabs: true.";
|
|
4380
|
+
function filterPageTargets(raw) {
|
|
4381
|
+
return raw.filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
|
|
4382
|
+
targetId: String(t.id),
|
|
4383
|
+
type: String(t.type),
|
|
4384
|
+
url: String(t.url ?? ""),
|
|
4385
|
+
title: String(t.title ?? "")
|
|
4386
|
+
}));
|
|
4387
|
+
}
|
|
4388
|
+
async function listPageTargets(port) {
|
|
4389
|
+
return filterPageTargets(await CDPClient.discoverTargets(port));
|
|
4390
|
+
}
|
|
4391
|
+
function matchTargetsByUrl(targets, needle) {
|
|
4392
|
+
const wanted = needle.toLowerCase();
|
|
4393
|
+
const byUrl = targets.filter((t)=>t.url.toLowerCase().includes(wanted));
|
|
4394
|
+
if (byUrl.length > 0) return byUrl;
|
|
4395
|
+
return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
|
|
4396
|
+
}
|
|
4124
4397
|
const dom_inspect_schema = {
|
|
4125
4398
|
name: "extension_dom_inspect",
|
|
4126
|
-
description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Requires the dev session to be started with allowControl: true (extension_dev). For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
|
|
4399
|
+
description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Target a tab by `tabUrl` (case-insensitive URL substring resolved against the browser's live page targets; zero or several matches return the candidates instead of guessing), by `url`, or by numeric `tab`. Discover what is open with listTargets: true (CDP targetIds) or listTabs: true (numeric chrome.tabs ids). Requires the dev session to be started with allowControl: true (extension_dev). For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
|
|
4127
4400
|
inputSchema: {
|
|
4128
4401
|
type: "object",
|
|
4129
4402
|
properties: {
|
|
@@ -4139,6 +4412,15 @@ const dom_inspect_schema = {
|
|
|
4139
4412
|
type: "string",
|
|
4140
4413
|
description: "For content/page: selects the target tab by url (match pattern, then substring fallback). Preferred over `tab`."
|
|
4141
4414
|
},
|
|
4415
|
+
tabUrl: {
|
|
4416
|
+
type: "string",
|
|
4417
|
+
description: "Target the tab whose URL contains this substring (case-insensitive; titles are checked only when no url matches). Resolved against the live browser's CDP page targets BEFORE inspecting: exactly one match proceeds; zero or several matches return the candidate targets (targetId/url/title) so you can narrow, never a guess. Chromium sessions only; on Firefox use `url`/`tab`. Alternative to `url`."
|
|
4418
|
+
},
|
|
4419
|
+
listTargets: {
|
|
4420
|
+
type: "boolean",
|
|
4421
|
+
default: false,
|
|
4422
|
+
description: "Enumerate the browser's CDP page targets as {targetId,url,title,type} and return, ignoring the other args. The discovery path for `tabUrl`. targetId is a CDP target id, NOT a numeric chrome.tabs id (for those use listTabs)."
|
|
4423
|
+
},
|
|
4142
4424
|
listTabs: {
|
|
4143
4425
|
type: "boolean",
|
|
4144
4426
|
default: false,
|
|
@@ -4199,8 +4481,55 @@ const dom_inspect_schema = {
|
|
|
4199
4481
|
]
|
|
4200
4482
|
}
|
|
4201
4483
|
};
|
|
4484
|
+
async function cdpPortOrError(projectPath, browser, feature) {
|
|
4485
|
+
if (!isChromiumFamily(browser)) return {
|
|
4486
|
+
error: JSON.stringify({
|
|
4487
|
+
ok: false,
|
|
4488
|
+
error: {
|
|
4489
|
+
name: "Unsupported",
|
|
4490
|
+
message: `${feature} reads the browser's CDP page targets, which ${browser} (Gecko) does not expose. Target the tab with \`url\` or \`tab\` instead, and discover tabs with listTabs: true (agent bridge, works on every browser).`
|
|
4491
|
+
}
|
|
4492
|
+
})
|
|
4493
|
+
};
|
|
4494
|
+
const resolved = await resolveCdpPort(projectPath, browser);
|
|
4495
|
+
if (!resolved) return {
|
|
4496
|
+
error: JSON.stringify({
|
|
4497
|
+
ok: false,
|
|
4498
|
+
error: {
|
|
4499
|
+
name: "NoSession",
|
|
4500
|
+
message: `No active dev session / CDP port for ${browser}, so ${feature} has no browser to ask. Start extension_dev and extension_wait for ready. ${CDP_PORT_MISSING_HINT}`
|
|
4501
|
+
}
|
|
4502
|
+
})
|
|
4503
|
+
};
|
|
4504
|
+
return {
|
|
4505
|
+
port: resolved.port
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4202
4508
|
async function dom_inspect_handler(args) {
|
|
4203
4509
|
const withConsole = true === args.withConsole ? 50 : false === args.withConsole ? void 0 : args.withConsole;
|
|
4510
|
+
if (args.listTargets) {
|
|
4511
|
+
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
4512
|
+
const cdp = await cdpPortOrError(args.projectPath, browser, "listTargets");
|
|
4513
|
+
if ("error" in cdp) return cdp.error;
|
|
4514
|
+
try {
|
|
4515
|
+
const targets = await listPageTargets(cdp.port);
|
|
4516
|
+
return JSON.stringify({
|
|
4517
|
+
ok: true,
|
|
4518
|
+
browser,
|
|
4519
|
+
targets,
|
|
4520
|
+
note: TARGET_ID_NOTE
|
|
4521
|
+
});
|
|
4522
|
+
} catch (e) {
|
|
4523
|
+
return JSON.stringify({
|
|
4524
|
+
ok: false,
|
|
4525
|
+
error: {
|
|
4526
|
+
name: "CdpError",
|
|
4527
|
+
message: `Could not list page targets: ${e instanceof Error ? e.message : String(e)}`
|
|
4528
|
+
},
|
|
4529
|
+
hint: "Confirm the session is ready (extension_wait), then retry. listTabs: true is the CDP-free alternative."
|
|
4530
|
+
});
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4204
4533
|
if (args.listTabs) return runActVerb([
|
|
4205
4534
|
"inspect",
|
|
4206
4535
|
args.projectPath,
|
|
@@ -4212,19 +4541,78 @@ async function dom_inspect_handler(args) {
|
|
|
4212
4541
|
String(args.timeout)
|
|
4213
4542
|
] : []
|
|
4214
4543
|
], args.projectPath, args.timeout);
|
|
4544
|
+
let targetUrl = args.url;
|
|
4545
|
+
let resolvedTarget = null;
|
|
4546
|
+
if (args.tabUrl) {
|
|
4547
|
+
if (null != args.tab || args.url) return JSON.stringify({
|
|
4548
|
+
ok: false,
|
|
4549
|
+
error: {
|
|
4550
|
+
name: "BadRequest",
|
|
4551
|
+
message: "Pass ONE tab selector: `tabUrl` (URL substring, resolved against live targets), `url` (engine-side match), or `tab` (numeric chrome.tabs id), not several."
|
|
4552
|
+
}
|
|
4553
|
+
});
|
|
4554
|
+
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
4555
|
+
const cdp = await cdpPortOrError(args.projectPath, browser, "tabUrl");
|
|
4556
|
+
if ("error" in cdp) return cdp.error;
|
|
4557
|
+
let targets;
|
|
4558
|
+
try {
|
|
4559
|
+
targets = await listPageTargets(cdp.port);
|
|
4560
|
+
} catch (e) {
|
|
4561
|
+
return JSON.stringify({
|
|
4562
|
+
ok: false,
|
|
4563
|
+
error: {
|
|
4564
|
+
name: "CdpError",
|
|
4565
|
+
message: `Could not list page targets to resolve tabUrl: ${e instanceof Error ? e.message : String(e)}`
|
|
4566
|
+
},
|
|
4567
|
+
hint: "Confirm the session is ready (extension_wait), then retry, or target with `url`/`tab` instead."
|
|
4568
|
+
});
|
|
4569
|
+
}
|
|
4570
|
+
const matches = matchTargetsByUrl(targets, args.tabUrl);
|
|
4571
|
+
if (0 === matches.length) return JSON.stringify({
|
|
4572
|
+
ok: false,
|
|
4573
|
+
error: {
|
|
4574
|
+
name: "NoMatchingTarget",
|
|
4575
|
+
message: `No open page target's url (or title) contains "${args.tabUrl}" (case-insensitive).`
|
|
4576
|
+
},
|
|
4577
|
+
availableTargets: targets,
|
|
4578
|
+
hint: `Pick one from availableTargets and retry with a \`tabUrl\` substring of its url, or open the page first (extension_open with \`url\`). ${TARGET_ID_NOTE}`
|
|
4579
|
+
});
|
|
4580
|
+
if (matches.length > 1) return JSON.stringify({
|
|
4581
|
+
ok: false,
|
|
4582
|
+
error: {
|
|
4583
|
+
name: "AmbiguousTabUrl",
|
|
4584
|
+
message: `${matches.length} page targets match "${args.tabUrl}"; refusing to guess which tab you mean.`
|
|
4585
|
+
},
|
|
4586
|
+
matchingTargets: matches,
|
|
4587
|
+
hint: `Narrow \`tabUrl\` to a longer substring that matches exactly one url in matchingTargets. ${TARGET_ID_NOTE}`
|
|
4588
|
+
});
|
|
4589
|
+
resolvedTarget = matches[0];
|
|
4590
|
+
targetUrl = resolvedTarget.url;
|
|
4591
|
+
}
|
|
4215
4592
|
const cli = [
|
|
4216
4593
|
"inspect",
|
|
4217
4594
|
args.projectPath
|
|
4218
4595
|
];
|
|
4219
4596
|
if (null != args.tab) cli.push("--tab", String(args.tab));
|
|
4220
|
-
if (
|
|
4597
|
+
if (targetUrl) cli.push("--url", targetUrl);
|
|
4221
4598
|
if (args.context) cli.push("--context", args.context);
|
|
4222
4599
|
if (args.include?.length) cli.push("--include", args.include.join(","));
|
|
4223
4600
|
if (null != args.maxBytes) cli.push("--max-bytes", String(args.maxBytes));
|
|
4224
4601
|
if (null != withConsole) cli.push("--with-console", String(withConsole));
|
|
4225
4602
|
cli.push("--browser", resolveSessionBrowser(args.projectPath, args.browser).browser);
|
|
4226
4603
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
4227
|
-
|
|
4604
|
+
const raw = await runActVerb(cli, args.projectPath, args.timeout);
|
|
4605
|
+
if (!resolvedTarget) return raw;
|
|
4606
|
+
try {
|
|
4607
|
+
const parsed = JSON.parse(raw);
|
|
4608
|
+
parsed.resolvedTarget = {
|
|
4609
|
+
...resolvedTarget,
|
|
4610
|
+
matchedBy: "tabUrl"
|
|
4611
|
+
};
|
|
4612
|
+
return JSON.stringify(parsed);
|
|
4613
|
+
} catch {
|
|
4614
|
+
return raw;
|
|
4615
|
+
}
|
|
4228
4616
|
}
|
|
4229
4617
|
function credentialsPath() {
|
|
4230
4618
|
if ("win32" === process.platform) {
|
|
@@ -5227,6 +5615,8 @@ async function doctor_handler(args) {
|
|
|
5227
5615
|
}
|
|
5228
5616
|
}
|
|
5229
5617
|
const SAFE_CEILING_MS = 50000;
|
|
5618
|
+
const DEFAULT_TIMEOUT_MS = 45000;
|
|
5619
|
+
const MIN_TIMEOUT_MS = 1000;
|
|
5230
5620
|
function wait_isAlive(pid) {
|
|
5231
5621
|
try {
|
|
5232
5622
|
process.kill(pid, 0);
|
|
@@ -5237,7 +5627,7 @@ function wait_isAlive(pid) {
|
|
|
5237
5627
|
}
|
|
5238
5628
|
const wait_schema = {
|
|
5239
5629
|
name: "extension_wait",
|
|
5240
|
-
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status
|
|
5630
|
+
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status with two separate facts: compiled (the compiler finished) and browserAttached (the extension's runtime connected from a live browser). Every result reports budgetMs (this call's wait budget) and elapsedMs; on status:'timeout' call again to keep waiting (polling resumes on the same contract). In a noBrowser (build-only) session it returns as soon as the compile lands instead of waiting for a browser that will never attach. Ports in the result come from the ready contract, so they always match what the dev server actually bound.",
|
|
5241
5631
|
inputSchema: {
|
|
5242
5632
|
type: "object",
|
|
5243
5633
|
properties: {
|
|
@@ -5249,10 +5639,14 @@ const wait_schema = {
|
|
|
5249
5639
|
type: "string",
|
|
5250
5640
|
description: "Browser to check readiness for. Defaults to the active dev session's browser for this project."
|
|
5251
5641
|
},
|
|
5642
|
+
timeoutMs: {
|
|
5643
|
+
type: "number",
|
|
5644
|
+
default: DEFAULT_TIMEOUT_MS,
|
|
5645
|
+
description: `Wait budget in milliseconds for this call. Default ${DEFAULT_TIMEOUT_MS}; clamped to ${MIN_TIMEOUT_MS}-${SAFE_CEILING_MS} (the ceiling keeps one call under the MCP client's 60s request timeout). On timeout the result reports elapsedMs plus what was observed (compiled, browserAttached); call again to keep waiting, polling resumes on the same contract.`
|
|
5646
|
+
},
|
|
5252
5647
|
timeout: {
|
|
5253
5648
|
type: "number",
|
|
5254
|
-
|
|
5255
|
-
description: "Requested wait in milliseconds. Clamped to ~50s per call so it never exceeds the MCP client request timeout; call again to keep waiting for slower builds."
|
|
5649
|
+
description: "Deprecated alias of timeoutMs, kept for callers that already pass it. timeoutMs wins when both are given."
|
|
5256
5650
|
}
|
|
5257
5651
|
},
|
|
5258
5652
|
required: [
|
|
@@ -5262,26 +5656,47 @@ const wait_schema = {
|
|
|
5262
5656
|
};
|
|
5263
5657
|
async function wait_handler(args) {
|
|
5264
5658
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
5265
|
-
const requested = args.timeout ??
|
|
5266
|
-
const
|
|
5659
|
+
const requested = args.timeoutMs ?? args.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
5660
|
+
const budgetMs = Math.min(Math.max(requested, MIN_TIMEOUT_MS), SAFE_CEILING_MS);
|
|
5267
5661
|
const clamped = requested > SAFE_CEILING_MS;
|
|
5268
5662
|
const readyPath = node_path.resolve(args.projectPath, "dist", "extension-js", browser, "ready.json");
|
|
5663
|
+
const buildOnly = findSessionInfo(args.projectPath, browser)?.noBrowser === true;
|
|
5269
5664
|
const start = Date.now();
|
|
5270
5665
|
const pollInterval = 1000;
|
|
5271
5666
|
let sawCompiledButUnattached = false;
|
|
5272
|
-
|
|
5667
|
+
let lastContractStatus = null;
|
|
5668
|
+
while(Date.now() - start < budgetMs){
|
|
5273
5669
|
try {
|
|
5274
5670
|
const raw = node_fs.readFileSync(readyPath, "utf8");
|
|
5275
5671
|
const contract = JSON.parse(raw);
|
|
5672
|
+
lastContractStatus = contract.status;
|
|
5276
5673
|
if ("ready" === contract.status) {
|
|
5277
5674
|
if ("number" == typeof contract.pid && !wait_isAlive(contract.pid)) return JSON.stringify({
|
|
5278
5675
|
status: "stale",
|
|
5279
5676
|
message: `ready.json reports ready but its dev-server pid ${contract.pid} is dead, the session exited. Restart with extension_dev; extension_doctor will confirm.`,
|
|
5280
5677
|
browser: contract.browser,
|
|
5281
5678
|
pid: contract.pid,
|
|
5282
|
-
|
|
5679
|
+
budgetMs,
|
|
5680
|
+
elapsedMs: Date.now() - start
|
|
5283
5681
|
});
|
|
5284
5682
|
const attached = "attached" === contract.runtime || "string" == typeof contract.executorAttachedAt;
|
|
5683
|
+
if (!attached && buildOnly) return JSON.stringify({
|
|
5684
|
+
status: "ready",
|
|
5685
|
+
buildOnly: true,
|
|
5686
|
+
compiled: true,
|
|
5687
|
+
browserAttached: false,
|
|
5688
|
+
message: "Build-only session (noBrowser): the extension compiled and the dev server is live, but no browser was launched, so browserAttached will never become true. Do not call extension_wait again to wait for a browser. The control verbs (storage/reload/open/dom_inspect/eval) need a live browser and will not work against this session.",
|
|
5689
|
+
command: contract.command,
|
|
5690
|
+
browser: contract.browser,
|
|
5691
|
+
port: contract.port,
|
|
5692
|
+
pid: contract.pid,
|
|
5693
|
+
distPath: contract.distPath,
|
|
5694
|
+
manifestPath: contract.manifestPath,
|
|
5695
|
+
compiledAt: contract.compiledAt,
|
|
5696
|
+
startedAt: contract.startedAt,
|
|
5697
|
+
budgetMs,
|
|
5698
|
+
elapsedMs: Date.now() - start
|
|
5699
|
+
});
|
|
5285
5700
|
if (!attached) {
|
|
5286
5701
|
await new Promise((r)=>setTimeout(r, pollInterval));
|
|
5287
5702
|
sawCompiledButUnattached = true;
|
|
@@ -5290,6 +5705,8 @@ async function wait_handler(args) {
|
|
|
5290
5705
|
const runtimeErrors = recentErrorLogs(args.projectPath, browser, 3);
|
|
5291
5706
|
return JSON.stringify({
|
|
5292
5707
|
status: "ready",
|
|
5708
|
+
compiled: true,
|
|
5709
|
+
browserAttached: true,
|
|
5293
5710
|
command: contract.command,
|
|
5294
5711
|
browser: contract.browser,
|
|
5295
5712
|
port: contract.port,
|
|
@@ -5298,7 +5715,8 @@ async function wait_handler(args) {
|
|
|
5298
5715
|
manifestPath: contract.manifestPath,
|
|
5299
5716
|
compiledAt: contract.compiledAt,
|
|
5300
5717
|
startedAt: contract.startedAt,
|
|
5301
|
-
|
|
5718
|
+
budgetMs,
|
|
5719
|
+
elapsedMs: Date.now() - start,
|
|
5302
5720
|
...runtimeErrors.length ? {
|
|
5303
5721
|
runtimeErrors,
|
|
5304
5722
|
warning: `Compiled and attached, but the extension is throwing at runtime (${runtimeErrors.length} recent error event${1 === runtimeErrors.length ? "" : "s"} above). Check extension_logs (level: error) or extension_doctor before trusting this session.`
|
|
@@ -5311,23 +5729,30 @@ async function wait_handler(args) {
|
|
|
5311
5729
|
errors: contract.errors,
|
|
5312
5730
|
code: contract.code,
|
|
5313
5731
|
browser: contract.browser,
|
|
5314
|
-
|
|
5732
|
+
budgetMs,
|
|
5733
|
+
elapsedMs: Date.now() - start
|
|
5315
5734
|
});
|
|
5316
5735
|
} catch {}
|
|
5317
5736
|
await new Promise((resolve)=>setTimeout(resolve, pollInterval));
|
|
5318
5737
|
}
|
|
5319
5738
|
if (sawCompiledButUnattached) return JSON.stringify({
|
|
5320
5739
|
status: "compiled-not-attached",
|
|
5321
|
-
|
|
5740
|
+
compiled: true,
|
|
5741
|
+
browserAttached: false,
|
|
5742
|
+
message: `The extension compiled, but the runtime executor never attached within this call's ${budgetMs}ms budget. The build is fine; the browser side is not connected, so extension_eval/storage/reload/open will fail with "no executor connected".`,
|
|
5322
5743
|
readyPath,
|
|
5323
|
-
|
|
5744
|
+
budgetMs,
|
|
5745
|
+
elapsedMs: Date.now() - start,
|
|
5324
5746
|
hint: "This is usually transient: call extension_wait again. If it persists, stop and restart the session with extension_dev (a restart reliably reattaches); extension_doctor reports the executor leg."
|
|
5325
5747
|
});
|
|
5326
5748
|
return JSON.stringify({
|
|
5327
5749
|
status: "timeout",
|
|
5328
|
-
|
|
5750
|
+
compiled: false,
|
|
5751
|
+
browserAttached: false,
|
|
5752
|
+
message: "starting" === lastContractStatus ? `Not ready after ${budgetMs}ms this call: the dev server stamped its contract (status: starting) but the first compile has not landed yet.` : `Not ready after ${budgetMs}ms this call: no ready contract was observed at ${readyPath}, so neither the compile nor a browser attach has been seen.`,
|
|
5329
5753
|
readyPath,
|
|
5330
|
-
|
|
5754
|
+
budgetMs,
|
|
5755
|
+
elapsedMs: Date.now() - start,
|
|
5331
5756
|
clamped: clamped ? `requested ${requested}ms was clamped to ${SAFE_CEILING_MS}ms to stay under the MCP client request timeout` : void 0,
|
|
5332
5757
|
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."
|
|
5333
5758
|
});
|
|
@@ -5936,7 +6361,7 @@ async function login_handler(args) {
|
|
|
5936
6361
|
}
|
|
5937
6362
|
const whoami_schema = {
|
|
5938
6363
|
name: "extension_whoami",
|
|
5939
|
-
description: "Report the locally stored extension.dev
|
|
6364
|
+
description: "Report the identity carried by the locally stored extension.dev token that extension_login minted (workspace/project scoped), plus its expiry, without revealing the token. The identity comes from that stored token alone; it does not change with the current working directory or whichever project folder you are in. Returns logged-out status when no credentials are stored.",
|
|
5940
6365
|
inputSchema: {
|
|
5941
6366
|
type: "object",
|
|
5942
6367
|
properties: {}
|
|
@@ -5961,7 +6386,7 @@ async function whoami_handler() {
|
|
|
5961
6386
|
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
5962
6387
|
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
5963
6388
|
expired,
|
|
5964
|
-
message: expired ? "
|
|
6389
|
+
message: expired ? "The stored token has expired. Run extension_login to refresh it." : `Logged in as ${creds.workspaceSlug}/${creds.projectSlug}, per the token extension_login stored on this machine. That token is what scopes the identity: it does not follow the current working directory or project folder.`
|
|
5965
6390
|
});
|
|
5966
6391
|
}
|
|
5967
6392
|
const logout_schema = {
|