@extension.dev/mcp 3.17.0-canary.1779910200.ddcd9eb → 4.0.8
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/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +52 -10
- package/bin/extension-mcp.js +15 -2
- package/claude/CLAUDE.md +8 -0
- package/claude/commands/extension-debug.md +12 -2
- package/claude/rules/mcp-tools.md +7 -0
- package/dist/module.d.ts +1 -1
- package/dist/module.js +1036 -285
- package/dist/src/index.d.ts +8 -0
- package/dist/src/lib/cdp-connection.d.ts +20 -0
- package/dist/src/lib/cdp-page-scripts.d.ts +5 -0
- package/dist/src/lib/cdp.d.ts +2 -19
- package/dist/src/lib/credentials.d.ts +24 -0
- package/dist/src/lib/exec.d.ts +1 -1
- package/dist/src/lib/github-device.d.ts +40 -0
- package/dist/src/lib/login-flow.d.ts +25 -0
- package/dist/src/tools/list-extensions.d.ts +22 -0
- package/dist/src/tools/login.d.ts +27 -0
- package/dist/src/tools/logout.d.ts +9 -0
- package/dist/src/tools/logs-constants.d.ts +6 -0
- package/dist/src/tools/logs-filter.d.ts +14 -0
- package/dist/src/tools/logs-schema.d.ts +65 -0
- package/dist/src/tools/logs.d.ts +2 -79
- package/dist/src/tools/open.d.ts +5 -0
- package/dist/src/tools/publish.d.ts +17 -0
- package/dist/src/tools/release-promote.d.ts +50 -0
- package/dist/src/tools/whoami.d.ts +9 -0
- package/package.json +22 -15
- package/dist/rslib.config.d.ts +0 -2
- package/dist/vitest.config.d.ts +0 -3
package/dist/module.js
CHANGED
|
@@ -7,10 +7,12 @@ import node_fs, { existsSync, readdirSync, statSync } from "node:fs";
|
|
|
7
7
|
import node_path, { join } from "node:path";
|
|
8
8
|
import node_os from "node:os";
|
|
9
9
|
import { extensionBuild } from "extension-develop";
|
|
10
|
-
import
|
|
10
|
+
import cross_spawn from "cross-spawn";
|
|
11
|
+
import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
|
|
11
12
|
import node_net from "node:net";
|
|
12
13
|
import ws from "ws";
|
|
13
14
|
import { extensionInstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
14
16
|
import { promisify } from "node:util";
|
|
15
17
|
var add_feature_namespaceObject = {};
|
|
16
18
|
__webpack_require__.r(add_feature_namespaceObject);
|
|
@@ -78,17 +80,35 @@ __webpack_require__.d(list_browsers_namespaceObject, {
|
|
|
78
80
|
handler: ()=>list_browsers_handler,
|
|
79
81
|
schema: ()=>list_browsers_schema
|
|
80
82
|
});
|
|
83
|
+
var list_extensions_namespaceObject = {};
|
|
84
|
+
__webpack_require__.r(list_extensions_namespaceObject);
|
|
85
|
+
__webpack_require__.d(list_extensions_namespaceObject, {
|
|
86
|
+
handler: ()=>list_extensions_handler,
|
|
87
|
+
schema: ()=>list_extensions_schema
|
|
88
|
+
});
|
|
81
89
|
var list_templates_namespaceObject = {};
|
|
82
90
|
__webpack_require__.r(list_templates_namespaceObject);
|
|
83
91
|
__webpack_require__.d(list_templates_namespaceObject, {
|
|
84
92
|
handler: ()=>list_templates_handler,
|
|
85
93
|
schema: ()=>list_templates_schema
|
|
86
94
|
});
|
|
95
|
+
var login_namespaceObject = {};
|
|
96
|
+
__webpack_require__.r(login_namespaceObject);
|
|
97
|
+
__webpack_require__.d(login_namespaceObject, {
|
|
98
|
+
handler: ()=>login_handler,
|
|
99
|
+
schema: ()=>login_schema
|
|
100
|
+
});
|
|
101
|
+
var logout_namespaceObject = {};
|
|
102
|
+
__webpack_require__.r(logout_namespaceObject);
|
|
103
|
+
__webpack_require__.d(logout_namespaceObject, {
|
|
104
|
+
handler: ()=>logout_handler,
|
|
105
|
+
schema: ()=>logout_schema
|
|
106
|
+
});
|
|
87
107
|
var logs_namespaceObject = {};
|
|
88
108
|
__webpack_require__.r(logs_namespaceObject);
|
|
89
109
|
__webpack_require__.d(logs_namespaceObject, {
|
|
90
110
|
handler: ()=>logs_handler,
|
|
91
|
-
schema: ()=>
|
|
111
|
+
schema: ()=>logs_schema_schema
|
|
92
112
|
});
|
|
93
113
|
var manifest_validate_namespaceObject = {};
|
|
94
114
|
__webpack_require__.r(manifest_validate_namespaceObject);
|
|
@@ -112,8 +132,16 @@ var publish_namespaceObject = {};
|
|
|
112
132
|
__webpack_require__.r(publish_namespaceObject);
|
|
113
133
|
__webpack_require__.d(publish_namespaceObject, {
|
|
114
134
|
handler: ()=>publish_handler,
|
|
135
|
+
resolveToken: ()=>resolveToken,
|
|
136
|
+
safeApiBase: ()=>safeApiBase,
|
|
115
137
|
schema: ()=>publish_schema
|
|
116
138
|
});
|
|
139
|
+
var release_promote_namespaceObject = {};
|
|
140
|
+
__webpack_require__.r(release_promote_namespaceObject);
|
|
141
|
+
__webpack_require__.d(release_promote_namespaceObject, {
|
|
142
|
+
handler: ()=>release_promote_handler,
|
|
143
|
+
schema: ()=>release_promote_schema
|
|
144
|
+
});
|
|
117
145
|
var reload_namespaceObject = {};
|
|
118
146
|
__webpack_require__.r(reload_namespaceObject);
|
|
119
147
|
__webpack_require__.d(reload_namespaceObject, {
|
|
@@ -144,6 +172,15 @@ __webpack_require__.d(wait_namespaceObject, {
|
|
|
144
172
|
handler: ()=>wait_handler,
|
|
145
173
|
schema: ()=>wait_schema
|
|
146
174
|
});
|
|
175
|
+
var whoami_namespaceObject = {};
|
|
176
|
+
__webpack_require__.r(whoami_namespaceObject);
|
|
177
|
+
__webpack_require__.d(whoami_namespaceObject, {
|
|
178
|
+
handler: ()=>whoami_handler,
|
|
179
|
+
schema: ()=>whoami_schema
|
|
180
|
+
});
|
|
181
|
+
var package_namespaceObject = {
|
|
182
|
+
rE: "4.0.8"
|
|
183
|
+
};
|
|
147
184
|
const schema = {
|
|
148
185
|
name: "extension_create",
|
|
149
186
|
description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options.",
|
|
@@ -373,7 +410,7 @@ async function build_handler(args) {
|
|
|
373
410
|
}
|
|
374
411
|
function runExtensionCli(args, options) {
|
|
375
412
|
return new Promise((resolve)=>{
|
|
376
|
-
const child =
|
|
413
|
+
const child = cross_spawn("npx", [
|
|
377
414
|
"extension",
|
|
378
415
|
...args
|
|
379
416
|
], {
|
|
@@ -388,7 +425,6 @@ function runExtensionCli(args, options) {
|
|
|
388
425
|
FORCE_COLOR: "0",
|
|
389
426
|
NO_COLOR: "1"
|
|
390
427
|
},
|
|
391
|
-
shell: true,
|
|
392
428
|
timeout: options?.timeoutMs ?? 30000
|
|
393
429
|
});
|
|
394
430
|
let stdout = "";
|
|
@@ -408,7 +444,7 @@ function runExtensionCli(args, options) {
|
|
|
408
444
|
});
|
|
409
445
|
}
|
|
410
446
|
function spawnExtensionCli(args, options) {
|
|
411
|
-
const child =
|
|
447
|
+
const child = cross_spawn("npx", [
|
|
412
448
|
"extension",
|
|
413
449
|
...args
|
|
414
450
|
], {
|
|
@@ -423,8 +459,7 @@ function spawnExtensionCli(args, options) {
|
|
|
423
459
|
...process.env,
|
|
424
460
|
FORCE_COLOR: "0",
|
|
425
461
|
NO_COLOR: "1"
|
|
426
|
-
}
|
|
427
|
-
shell: true
|
|
462
|
+
}
|
|
428
463
|
});
|
|
429
464
|
child.unref();
|
|
430
465
|
return child;
|
|
@@ -753,9 +788,8 @@ async function manifest_validate_handler(args) {
|
|
|
753
788
|
}
|
|
754
789
|
if (!manifest.name) result.errors.push("Missing required field: name");
|
|
755
790
|
if (!manifest.version) result.warnings.push("Missing field: version (required for store submission)");
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
if (!chromiumMv && !manifest.manifest_version) result.errors.push('Missing manifest_version. Use "chromium:manifest_version": 3 and "firefox:manifest_version": 2 for cross-browser support.');
|
|
791
|
+
const chromiumManifest = filterKeysForThisBrowser(manifest, "chrome");
|
|
792
|
+
if (!chromiumManifest.manifest_version) result.errors.push('Missing manifest_version. Use "chromium:manifest_version": 3 and "firefox:manifest_version": 2 for cross-browser support.');
|
|
759
793
|
for (const browser of browsers){
|
|
760
794
|
const isChromium = [
|
|
761
795
|
"chrome",
|
|
@@ -766,25 +800,26 @@ async function manifest_validate_handler(args) {
|
|
|
766
800
|
"firefox",
|
|
767
801
|
"gecko-based"
|
|
768
802
|
].includes(browser);
|
|
803
|
+
const effective = filterKeysForThisBrowser(manifest, browser);
|
|
769
804
|
const issues = [];
|
|
770
805
|
if (isChromium) {
|
|
771
|
-
const mv =
|
|
806
|
+
const mv = effective.manifest_version;
|
|
772
807
|
if (mv && mv < 3) issues.push("Manifest V2 is deprecated on Chromium. Use chromium:manifest_version: 3.");
|
|
773
|
-
if (
|
|
774
|
-
const perms =
|
|
808
|
+
if (effective.side_panel) {
|
|
809
|
+
const perms = effective.permissions ?? [];
|
|
775
810
|
if (!perms.includes("sidePanel")) issues.push('Side panel declared but "sidePanel" permission is missing.');
|
|
776
811
|
}
|
|
777
|
-
if (manifest["firefox:browser_action"] && !
|
|
812
|
+
if (manifest["firefox:browser_action"] && !effective.action) issues.push('Firefox browser_action found but no chromium:action. Chromium MV3 uses "action" instead of "browser_action".');
|
|
778
813
|
}
|
|
779
814
|
if (isFirefox) {
|
|
780
|
-
const contentScripts =
|
|
815
|
+
const contentScripts = effective.content_scripts;
|
|
781
816
|
if (contentScripts) {
|
|
782
817
|
for (const cs of contentScripts)if ("MAIN" === cs.world || "MAIN" === cs["world"]) issues.push('content_scripts.world: "MAIN" is Chromium-only. Use "chromium:world": "MAIN" and provide a Firefox fallback.');
|
|
783
818
|
}
|
|
784
|
-
if (
|
|
785
|
-
const bg =
|
|
819
|
+
if (chromiumManifest.side_panel && !effective.sidebar_action) issues.push("Chromium side_panel declared but no firefox:sidebar_action. Firefox uses sidebar_action for sidebars.");
|
|
820
|
+
const bg = effective.background;
|
|
786
821
|
if (bg) {
|
|
787
|
-
if (bg.service_worker && !bg
|
|
822
|
+
if (bg.service_worker && !bg.scripts) issues.push('Background service_worker declared but no firefox:scripts. Firefox uses "scripts" (array) instead of "service_worker".');
|
|
788
823
|
}
|
|
789
824
|
}
|
|
790
825
|
result.browserSupport[browser] = {
|
|
@@ -794,11 +829,11 @@ async function manifest_validate_handler(args) {
|
|
|
794
829
|
if (issues.length) result.valid = false;
|
|
795
830
|
}
|
|
796
831
|
const surfaces = [];
|
|
797
|
-
if (
|
|
798
|
-
if (
|
|
799
|
-
if (
|
|
800
|
-
if (
|
|
801
|
-
if (
|
|
832
|
+
if (chromiumManifest.content_scripts) surfaces.push("content");
|
|
833
|
+
if (chromiumManifest.side_panel) surfaces.push("sidebar");
|
|
834
|
+
if (chromiumManifest.action || manifest["firefox:browser_action"]) surfaces.push("action");
|
|
835
|
+
if (chromiumManifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
|
|
836
|
+
if (chromiumManifest.background) surfaces.push("background");
|
|
802
837
|
if (surfaces.length) try {
|
|
803
838
|
const templates = await listTemplates();
|
|
804
839
|
result.similarTemplates = templates.filter((t)=>t.surfaces.some((s)=>surfaces.includes(s))).slice(0, 5).map((t)=>({
|
|
@@ -975,7 +1010,7 @@ function _define_property(obj, key, value) {
|
|
|
975
1010
|
return obj;
|
|
976
1011
|
}
|
|
977
1012
|
const COMMAND_TIMEOUT_MS = 15000;
|
|
978
|
-
class
|
|
1013
|
+
class CDPConnection {
|
|
979
1014
|
async connect(wsUrl) {
|
|
980
1015
|
return new Promise((resolve, reject)=>{
|
|
981
1016
|
this.ws = new ws(wsUrl);
|
|
@@ -1067,6 +1102,186 @@ class CDPClient {
|
|
|
1067
1102
|
this.ws.send(JSON.stringify(message));
|
|
1068
1103
|
});
|
|
1069
1104
|
}
|
|
1105
|
+
getConsoleMessages() {
|
|
1106
|
+
return [
|
|
1107
|
+
...this.consoleMessages
|
|
1108
|
+
];
|
|
1109
|
+
}
|
|
1110
|
+
getConsoleSummary() {
|
|
1111
|
+
const counts = {};
|
|
1112
|
+
const uniqueByLevel = {};
|
|
1113
|
+
for (const msg of this.consoleMessages){
|
|
1114
|
+
counts[msg.level] = (counts[msg.level] ?? 0) + 1;
|
|
1115
|
+
if (!uniqueByLevel[msg.level]) uniqueByLevel[msg.level] = new Map();
|
|
1116
|
+
const key = msg.text.slice(0, 200);
|
|
1117
|
+
uniqueByLevel[msg.level].set(key, (uniqueByLevel[msg.level].get(key) ?? 0) + 1);
|
|
1118
|
+
}
|
|
1119
|
+
const topMessages = [];
|
|
1120
|
+
for (const [level, msgs] of Object.entries(uniqueByLevel)){
|
|
1121
|
+
const sorted = [
|
|
1122
|
+
...msgs.entries()
|
|
1123
|
+
].sort((a, b)=>b[1] - a[1]);
|
|
1124
|
+
for (const [text, count] of sorted.slice(0, 5))topMessages.push({
|
|
1125
|
+
level,
|
|
1126
|
+
text,
|
|
1127
|
+
count
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
total: this.consoleMessages.length,
|
|
1132
|
+
counts,
|
|
1133
|
+
topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
onEvent(handler) {
|
|
1137
|
+
this.eventListeners.add(handler);
|
|
1138
|
+
return ()=>{
|
|
1139
|
+
this.eventListeners.delete(handler);
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
constructor(){
|
|
1143
|
+
_define_property(this, "ws", null);
|
|
1144
|
+
_define_property(this, "messageId", 0);
|
|
1145
|
+
_define_property(this, "eventListeners", new Set());
|
|
1146
|
+
_define_property(this, "pendingRequests", new Map());
|
|
1147
|
+
_define_property(this, "consoleMessages", []);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const PAGE_HTML_SCRIPT = `(() => {
|
|
1151
|
+
try {
|
|
1152
|
+
const doctype = document.doctype;
|
|
1153
|
+
const dt = doctype
|
|
1154
|
+
? '<!DOCTYPE ' + doctype.name
|
|
1155
|
+
+ (doctype.publicId ? ' PUBLIC "' + doctype.publicId + '"' : '')
|
|
1156
|
+
+ (doctype.systemId ? ' "' + doctype.systemId + '"' : '')
|
|
1157
|
+
+ '>'
|
|
1158
|
+
: '';
|
|
1159
|
+
// Include shadow DOM content from extension roots
|
|
1160
|
+
const roots = Array.from(document.querySelectorAll(
|
|
1161
|
+
'#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])'
|
|
1162
|
+
));
|
|
1163
|
+
if (roots.length) {
|
|
1164
|
+
const clone = document.documentElement.cloneNode(true);
|
|
1165
|
+
const clonedRoots = Array.from(clone.querySelectorAll(
|
|
1166
|
+
'#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])'
|
|
1167
|
+
));
|
|
1168
|
+
const s = new XMLSerializer();
|
|
1169
|
+
for (let i = 0; i < Math.min(roots.length, clonedRoots.length); i++) {
|
|
1170
|
+
const sr = roots[i].shadowRoot;
|
|
1171
|
+
if (!sr) continue;
|
|
1172
|
+
try {
|
|
1173
|
+
const shadow = Array.from(sr.childNodes).map(n => {
|
|
1174
|
+
try { return s.serializeToString(n); } catch { return ''; }
|
|
1175
|
+
}).join('');
|
|
1176
|
+
if (shadow) clonedRoots[i].innerHTML = shadow;
|
|
1177
|
+
} catch {}
|
|
1178
|
+
}
|
|
1179
|
+
return dt + '\\n' + clone.outerHTML;
|
|
1180
|
+
}
|
|
1181
|
+
return dt + '\\n' + document.documentElement.outerHTML;
|
|
1182
|
+
} catch (e) { return ''; }
|
|
1183
|
+
})()`;
|
|
1184
|
+
const PAGE_META_SCRIPT = `(() => {
|
|
1185
|
+
try {
|
|
1186
|
+
return {
|
|
1187
|
+
title: document.title,
|
|
1188
|
+
url: location.href,
|
|
1189
|
+
readyState: document.readyState,
|
|
1190
|
+
viewport: {
|
|
1191
|
+
width: window.innerWidth,
|
|
1192
|
+
height: window.innerHeight,
|
|
1193
|
+
devicePixelRatio: window.devicePixelRatio
|
|
1194
|
+
},
|
|
1195
|
+
frameCount: window.frames.length,
|
|
1196
|
+
scriptCount: document.querySelectorAll('script').length,
|
|
1197
|
+
styleCount: document.querySelectorAll('style,link[rel="stylesheet"]').length
|
|
1198
|
+
};
|
|
1199
|
+
} catch { return {}; }
|
|
1200
|
+
})()`;
|
|
1201
|
+
const EXTENSION_ROOT_META_SCRIPT = `(() => {
|
|
1202
|
+
try {
|
|
1203
|
+
const readGeneration = (node) => {
|
|
1204
|
+
const raw = node.getAttribute && node.getAttribute('data-extjs-reinject-generation');
|
|
1205
|
+
const parsed = Number(raw);
|
|
1206
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1207
|
+
};
|
|
1208
|
+
const normalize = (node) => ({
|
|
1209
|
+
tag: node.tagName ? String(node.tagName).toLowerCase() : 'unknown',
|
|
1210
|
+
id: node.id || undefined,
|
|
1211
|
+
key: node.getAttribute ? node.getAttribute('data-extjs-reinject-key') || undefined : undefined,
|
|
1212
|
+
generation: readGeneration(node),
|
|
1213
|
+
status: node.getAttribute ? node.getAttribute('data-extjs-reinject-status') || undefined : undefined
|
|
1214
|
+
});
|
|
1215
|
+
const roots = Array.from(
|
|
1216
|
+
document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])')
|
|
1217
|
+
).slice(0, 10).map(normalize);
|
|
1218
|
+
const markers = Array.from(
|
|
1219
|
+
document.querySelectorAll('[data-extjs-reinject-marker="true"]')
|
|
1220
|
+
).slice(0, 10).map(normalize);
|
|
1221
|
+
if (!roots.length && !markers.length) return null;
|
|
1222
|
+
const generations = [...roots, ...markers]
|
|
1223
|
+
.map(e => e.generation)
|
|
1224
|
+
.filter(g => typeof g === 'number');
|
|
1225
|
+
return {
|
|
1226
|
+
rootCount: roots.length,
|
|
1227
|
+
markerCount: markers.length,
|
|
1228
|
+
latestGeneration: generations.length ? Math.max(...generations) : 0,
|
|
1229
|
+
roots,
|
|
1230
|
+
markers
|
|
1231
|
+
};
|
|
1232
|
+
} catch { return null; }
|
|
1233
|
+
})()`;
|
|
1234
|
+
function probeSelectorsScript(selectors) {
|
|
1235
|
+
return `(() => {
|
|
1236
|
+
const selectors = ${JSON.stringify(selectors)};
|
|
1237
|
+
return selectors.map(selector => {
|
|
1238
|
+
try {
|
|
1239
|
+
const els = Array.from(document.querySelectorAll(selector));
|
|
1240
|
+
return {
|
|
1241
|
+
selector,
|
|
1242
|
+
count: els.length,
|
|
1243
|
+
samples: els.slice(0, 3).map(el => ({
|
|
1244
|
+
tag: el.tagName.toLowerCase(),
|
|
1245
|
+
id: el.id || undefined,
|
|
1246
|
+
classes: Array.from(el.classList).join(' ') || undefined,
|
|
1247
|
+
role: el.getAttribute('role') || undefined,
|
|
1248
|
+
ariaLabel: el.getAttribute('aria-label') || undefined,
|
|
1249
|
+
textLength: (el.textContent || '').length,
|
|
1250
|
+
textSnippet: (el.textContent || '').trim().slice(0, 80)
|
|
1251
|
+
}))
|
|
1252
|
+
};
|
|
1253
|
+
} catch (e) {
|
|
1254
|
+
return { selector, count: 0, samples: [], error: String(e) };
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
})()`;
|
|
1258
|
+
}
|
|
1259
|
+
function domSnapshotScript(maxNodes) {
|
|
1260
|
+
return `(() => {
|
|
1261
|
+
const maxNodes = ${maxNodes};
|
|
1262
|
+
const nodes = [];
|
|
1263
|
+
const walk = (node, depth) => {
|
|
1264
|
+
if (nodes.length >= maxNodes || depth > 20) return;
|
|
1265
|
+
if (node.nodeType !== 1) return;
|
|
1266
|
+
const el = node;
|
|
1267
|
+
nodes.push({
|
|
1268
|
+
tag: el.tagName.toLowerCase(),
|
|
1269
|
+
depth,
|
|
1270
|
+
id: el.id || undefined,
|
|
1271
|
+
classes: Array.from(el.classList).slice(0, 5).join(' ') || undefined,
|
|
1272
|
+
role: el.getAttribute('role') || undefined,
|
|
1273
|
+
ariaLabel: el.getAttribute('aria-label') || undefined,
|
|
1274
|
+
childCount: el.children.length
|
|
1275
|
+
});
|
|
1276
|
+
for (const child of el.children) {
|
|
1277
|
+
walk(child, depth + 1);
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
walk(document.documentElement, 0);
|
|
1281
|
+
return nodes;
|
|
1282
|
+
})()`;
|
|
1283
|
+
}
|
|
1284
|
+
class CDPClient extends CDPConnection {
|
|
1070
1285
|
static async discoverBrowserWsUrl(port, host = "127.0.0.1") {
|
|
1071
1286
|
const res = await fetch(`http://${host}:${port}/json/version`);
|
|
1072
1287
|
if (!res.ok) throw new Error(`CDP /json/version failed: ${res.status}`);
|
|
@@ -1121,40 +1336,7 @@ class CDPClient {
|
|
|
1121
1336
|
return response.result?.value;
|
|
1122
1337
|
}
|
|
1123
1338
|
async getPageHTML(sessionId) {
|
|
1124
|
-
const result = await this.evaluate(sessionId,
|
|
1125
|
-
try {
|
|
1126
|
-
const doctype = document.doctype;
|
|
1127
|
-
const dt = doctype
|
|
1128
|
-
? '<!DOCTYPE ' + doctype.name
|
|
1129
|
-
+ (doctype.publicId ? ' PUBLIC "' + doctype.publicId + '"' : '')
|
|
1130
|
-
+ (doctype.systemId ? ' "' + doctype.systemId + '"' : '')
|
|
1131
|
-
+ '>'
|
|
1132
|
-
: '';
|
|
1133
|
-
// Include shadow DOM content from extension roots
|
|
1134
|
-
const roots = Array.from(document.querySelectorAll(
|
|
1135
|
-
'#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])'
|
|
1136
|
-
));
|
|
1137
|
-
if (roots.length) {
|
|
1138
|
-
const clone = document.documentElement.cloneNode(true);
|
|
1139
|
-
const clonedRoots = Array.from(clone.querySelectorAll(
|
|
1140
|
-
'#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])'
|
|
1141
|
-
));
|
|
1142
|
-
const s = new XMLSerializer();
|
|
1143
|
-
for (let i = 0; i < Math.min(roots.length, clonedRoots.length); i++) {
|
|
1144
|
-
const sr = roots[i].shadowRoot;
|
|
1145
|
-
if (!sr) continue;
|
|
1146
|
-
try {
|
|
1147
|
-
const shadow = Array.from(sr.childNodes).map(n => {
|
|
1148
|
-
try { return s.serializeToString(n); } catch { return ''; }
|
|
1149
|
-
}).join('');
|
|
1150
|
-
if (shadow) clonedRoots[i].innerHTML = shadow;
|
|
1151
|
-
} catch {}
|
|
1152
|
-
}
|
|
1153
|
-
return dt + '\\n' + clone.outerHTML;
|
|
1154
|
-
}
|
|
1155
|
-
return dt + '\\n' + document.documentElement.outerHTML;
|
|
1156
|
-
} catch (e) { return ''; }
|
|
1157
|
-
})()`);
|
|
1339
|
+
const result = await this.evaluate(sessionId, PAGE_HTML_SCRIPT);
|
|
1158
1340
|
return "string" == typeof result ? result : "";
|
|
1159
1341
|
}
|
|
1160
1342
|
async getClosedShadowRoots(sessionId, maxBytes = 65536) {
|
|
@@ -1208,157 +1390,21 @@ class CDPClient {
|
|
|
1208
1390
|
return out;
|
|
1209
1391
|
}
|
|
1210
1392
|
async getPageMeta(sessionId) {
|
|
1211
|
-
const result = await this.evaluate(sessionId,
|
|
1212
|
-
try {
|
|
1213
|
-
return {
|
|
1214
|
-
title: document.title,
|
|
1215
|
-
url: location.href,
|
|
1216
|
-
readyState: document.readyState,
|
|
1217
|
-
viewport: {
|
|
1218
|
-
width: window.innerWidth,
|
|
1219
|
-
height: window.innerHeight,
|
|
1220
|
-
devicePixelRatio: window.devicePixelRatio
|
|
1221
|
-
},
|
|
1222
|
-
frameCount: window.frames.length,
|
|
1223
|
-
scriptCount: document.querySelectorAll('script').length,
|
|
1224
|
-
styleCount: document.querySelectorAll('style,link[rel="stylesheet"]').length
|
|
1225
|
-
};
|
|
1226
|
-
} catch { return {}; }
|
|
1227
|
-
})()`);
|
|
1393
|
+
const result = await this.evaluate(sessionId, PAGE_META_SCRIPT);
|
|
1228
1394
|
return result ?? {};
|
|
1229
1395
|
}
|
|
1230
1396
|
async probeSelectors(sessionId, selectors) {
|
|
1231
|
-
const result = await this.evaluate(sessionId,
|
|
1232
|
-
const selectors = ${JSON.stringify(selectors)};
|
|
1233
|
-
return selectors.map(selector => {
|
|
1234
|
-
try {
|
|
1235
|
-
const els = Array.from(document.querySelectorAll(selector));
|
|
1236
|
-
return {
|
|
1237
|
-
selector,
|
|
1238
|
-
count: els.length,
|
|
1239
|
-
samples: els.slice(0, 3).map(el => ({
|
|
1240
|
-
tag: el.tagName.toLowerCase(),
|
|
1241
|
-
id: el.id || undefined,
|
|
1242
|
-
classes: Array.from(el.classList).join(' ') || undefined,
|
|
1243
|
-
role: el.getAttribute('role') || undefined,
|
|
1244
|
-
ariaLabel: el.getAttribute('aria-label') || undefined,
|
|
1245
|
-
textLength: (el.textContent || '').length,
|
|
1246
|
-
textSnippet: (el.textContent || '').trim().slice(0, 80)
|
|
1247
|
-
}))
|
|
1248
|
-
};
|
|
1249
|
-
} catch (e) {
|
|
1250
|
-
return { selector, count: 0, samples: [], error: String(e) };
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
})()`);
|
|
1397
|
+
const result = await this.evaluate(sessionId, probeSelectorsScript(selectors));
|
|
1254
1398
|
return result ?? [];
|
|
1255
1399
|
}
|
|
1256
1400
|
async getDomSnapshot(sessionId, maxNodes = 500) {
|
|
1257
|
-
const result = await this.evaluate(sessionId,
|
|
1258
|
-
const maxNodes = ${maxNodes};
|
|
1259
|
-
const nodes = [];
|
|
1260
|
-
const walk = (node, depth) => {
|
|
1261
|
-
if (nodes.length >= maxNodes || depth > 20) return;
|
|
1262
|
-
if (node.nodeType !== 1) return;
|
|
1263
|
-
const el = node;
|
|
1264
|
-
nodes.push({
|
|
1265
|
-
tag: el.tagName.toLowerCase(),
|
|
1266
|
-
depth,
|
|
1267
|
-
id: el.id || undefined,
|
|
1268
|
-
classes: Array.from(el.classList).slice(0, 5).join(' ') || undefined,
|
|
1269
|
-
role: el.getAttribute('role') || undefined,
|
|
1270
|
-
ariaLabel: el.getAttribute('aria-label') || undefined,
|
|
1271
|
-
childCount: el.children.length
|
|
1272
|
-
});
|
|
1273
|
-
for (const child of el.children) {
|
|
1274
|
-
walk(child, depth + 1);
|
|
1275
|
-
}
|
|
1276
|
-
};
|
|
1277
|
-
walk(document.documentElement, 0);
|
|
1278
|
-
return nodes;
|
|
1279
|
-
})()`);
|
|
1401
|
+
const result = await this.evaluate(sessionId, domSnapshotScript(maxNodes));
|
|
1280
1402
|
return result ?? [];
|
|
1281
1403
|
}
|
|
1282
1404
|
async getExtensionRootMeta(sessionId) {
|
|
1283
|
-
const result = await this.evaluate(sessionId,
|
|
1284
|
-
try {
|
|
1285
|
-
const readGeneration = (node) => {
|
|
1286
|
-
const raw = node.getAttribute && node.getAttribute('data-extjs-reinject-generation');
|
|
1287
|
-
const parsed = Number(raw);
|
|
1288
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1289
|
-
};
|
|
1290
|
-
const normalize = (node) => ({
|
|
1291
|
-
tag: node.tagName ? String(node.tagName).toLowerCase() : 'unknown',
|
|
1292
|
-
id: node.id || undefined,
|
|
1293
|
-
key: node.getAttribute ? node.getAttribute('data-extjs-reinject-key') || undefined : undefined,
|
|
1294
|
-
generation: readGeneration(node),
|
|
1295
|
-
status: node.getAttribute ? node.getAttribute('data-extjs-reinject-status') || undefined : undefined
|
|
1296
|
-
});
|
|
1297
|
-
const roots = Array.from(
|
|
1298
|
-
document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])')
|
|
1299
|
-
).slice(0, 10).map(normalize);
|
|
1300
|
-
const markers = Array.from(
|
|
1301
|
-
document.querySelectorAll('[data-extjs-reinject-marker="true"]')
|
|
1302
|
-
).slice(0, 10).map(normalize);
|
|
1303
|
-
if (!roots.length && !markers.length) return null;
|
|
1304
|
-
const generations = [...roots, ...markers]
|
|
1305
|
-
.map(e => e.generation)
|
|
1306
|
-
.filter(g => typeof g === 'number');
|
|
1307
|
-
return {
|
|
1308
|
-
rootCount: roots.length,
|
|
1309
|
-
markerCount: markers.length,
|
|
1310
|
-
latestGeneration: generations.length ? Math.max(...generations) : 0,
|
|
1311
|
-
roots,
|
|
1312
|
-
markers
|
|
1313
|
-
};
|
|
1314
|
-
} catch { return null; }
|
|
1315
|
-
})()`);
|
|
1405
|
+
const result = await this.evaluate(sessionId, EXTENSION_ROOT_META_SCRIPT);
|
|
1316
1406
|
return result ?? null;
|
|
1317
1407
|
}
|
|
1318
|
-
getConsoleMessages() {
|
|
1319
|
-
return [
|
|
1320
|
-
...this.consoleMessages
|
|
1321
|
-
];
|
|
1322
|
-
}
|
|
1323
|
-
getConsoleSummary() {
|
|
1324
|
-
const counts = {};
|
|
1325
|
-
const uniqueByLevel = {};
|
|
1326
|
-
for (const msg of this.consoleMessages){
|
|
1327
|
-
counts[msg.level] = (counts[msg.level] ?? 0) + 1;
|
|
1328
|
-
if (!uniqueByLevel[msg.level]) uniqueByLevel[msg.level] = new Map();
|
|
1329
|
-
const key = msg.text.slice(0, 200);
|
|
1330
|
-
uniqueByLevel[msg.level].set(key, (uniqueByLevel[msg.level].get(key) ?? 0) + 1);
|
|
1331
|
-
}
|
|
1332
|
-
const topMessages = [];
|
|
1333
|
-
for (const [level, msgs] of Object.entries(uniqueByLevel)){
|
|
1334
|
-
const sorted = [
|
|
1335
|
-
...msgs.entries()
|
|
1336
|
-
].sort((a, b)=>b[1] - a[1]);
|
|
1337
|
-
for (const [text, count] of sorted.slice(0, 5))topMessages.push({
|
|
1338
|
-
level,
|
|
1339
|
-
text,
|
|
1340
|
-
count
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
return {
|
|
1344
|
-
total: this.consoleMessages.length,
|
|
1345
|
-
counts,
|
|
1346
|
-
topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
onEvent(handler) {
|
|
1350
|
-
this.eventListeners.add(handler);
|
|
1351
|
-
return ()=>{
|
|
1352
|
-
this.eventListeners.delete(handler);
|
|
1353
|
-
};
|
|
1354
|
-
}
|
|
1355
|
-
constructor(){
|
|
1356
|
-
_define_property(this, "ws", null);
|
|
1357
|
-
_define_property(this, "messageId", 0);
|
|
1358
|
-
_define_property(this, "eventListeners", new Set());
|
|
1359
|
-
_define_property(this, "pendingRequests", new Map());
|
|
1360
|
-
_define_property(this, "consoleMessages", []);
|
|
1361
|
-
}
|
|
1362
1408
|
}
|
|
1363
1409
|
const source_inspect_schema = {
|
|
1364
1410
|
name: "extension_source_inspect",
|
|
@@ -1546,13 +1592,171 @@ async function source_inspect_handler(args) {
|
|
|
1546
1592
|
cdp.disconnect();
|
|
1547
1593
|
}
|
|
1548
1594
|
}
|
|
1549
|
-
const
|
|
1595
|
+
const list_extensions_schema = {
|
|
1596
|
+
name: "extension_list_extensions",
|
|
1597
|
+
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). Identity is read read-only via the Extensions domain — other extensions' contexts are never attached to or evaluated in. 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.",
|
|
1598
|
+
inputSchema: {
|
|
1599
|
+
type: "object",
|
|
1600
|
+
properties: {
|
|
1601
|
+
projectPath: {
|
|
1602
|
+
type: "string",
|
|
1603
|
+
description: "Path to the extension project root (must have an active dev session)"
|
|
1604
|
+
},
|
|
1605
|
+
browser: {
|
|
1606
|
+
type: "string",
|
|
1607
|
+
default: "chrome"
|
|
1608
|
+
}
|
|
1609
|
+
},
|
|
1610
|
+
required: [
|
|
1611
|
+
"projectPath"
|
|
1612
|
+
]
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
async function list_extensions_handler(args) {
|
|
1616
|
+
const browser = args.browser ?? "chrome";
|
|
1617
|
+
const isChromium = [
|
|
1618
|
+
"chrome",
|
|
1619
|
+
"edge",
|
|
1620
|
+
"chromium-based"
|
|
1621
|
+
].includes(browser);
|
|
1622
|
+
if (!isChromium) return JSON.stringify({
|
|
1623
|
+
error: `Listing extensions for ${browser} uses RDP (Remote Debug Protocol). Currently only Chromium CDP is supported.`,
|
|
1624
|
+
hint: "Use --browser=chrome."
|
|
1625
|
+
});
|
|
1626
|
+
const cdpPort = await list_extensions_findCdpPort(args.projectPath, browser);
|
|
1627
|
+
if (!cdpPort) return JSON.stringify({
|
|
1628
|
+
error: "No active dev session found. Cannot connect to Chrome DevTools Protocol.",
|
|
1629
|
+
hint: "Start a dev session first with extension_dev, then use extension_wait to confirm it is ready."
|
|
1630
|
+
});
|
|
1631
|
+
const cdp = new CDPClient();
|
|
1632
|
+
try {
|
|
1633
|
+
const browserWsUrl = await CDPClient.discoverBrowserWsUrl(cdpPort);
|
|
1634
|
+
await cdp.connect(browserWsUrl);
|
|
1635
|
+
const targets = await cdp.getTargets();
|
|
1636
|
+
const byId = new Map();
|
|
1637
|
+
for (const t of targets){
|
|
1638
|
+
const url = String(t.url ?? "");
|
|
1639
|
+
if (!url.startsWith("chrome-extension://")) continue;
|
|
1640
|
+
const id = url.slice(19).split("/")[0];
|
|
1641
|
+
if (!id) continue;
|
|
1642
|
+
const list = byId.get(id) ?? [];
|
|
1643
|
+
list.push({
|
|
1644
|
+
type: String(t.type ?? ""),
|
|
1645
|
+
url
|
|
1646
|
+
});
|
|
1647
|
+
byId.set(id, list);
|
|
1648
|
+
}
|
|
1649
|
+
const extensions = [];
|
|
1650
|
+
for (const [id, ctxTargets] of byId){
|
|
1651
|
+
const entry = {
|
|
1652
|
+
id,
|
|
1653
|
+
contexts: ctxTargets.map((c)=>({
|
|
1654
|
+
type: c.type,
|
|
1655
|
+
url: c.url
|
|
1656
|
+
})),
|
|
1657
|
+
source: "target-only"
|
|
1658
|
+
};
|
|
1659
|
+
try {
|
|
1660
|
+
const info = await cdp.sendCommand("Extensions.getExtensionInfo", {
|
|
1661
|
+
extensionId: id
|
|
1662
|
+
});
|
|
1663
|
+
if (info?.extensionInfo) {
|
|
1664
|
+
entry.name = info.extensionInfo.name;
|
|
1665
|
+
entry.version = info.extensionInfo.version;
|
|
1666
|
+
entry.source = "extensions-domain";
|
|
1667
|
+
}
|
|
1668
|
+
} catch {}
|
|
1669
|
+
extensions.push(entry);
|
|
1670
|
+
}
|
|
1671
|
+
extensions.sort((a, b)=>(a.name ?? a.id).localeCompare(b.name ?? b.id));
|
|
1672
|
+
return JSON.stringify({
|
|
1673
|
+
cdpPort,
|
|
1674
|
+
browser,
|
|
1675
|
+
count: extensions.length,
|
|
1676
|
+
extensions,
|
|
1677
|
+
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. Identity is read read-only via the Extensions domain; other extensions' contexts are never attached to or evaluated in."
|
|
1678
|
+
});
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
return JSON.stringify({
|
|
1681
|
+
error: `Failed to list extensions: ${error.message}`
|
|
1682
|
+
});
|
|
1683
|
+
} finally{
|
|
1684
|
+
cdp.disconnect();
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
async function list_extensions_findCdpPort(projectPath, browser) {
|
|
1688
|
+
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1689
|
+
try {
|
|
1690
|
+
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1691
|
+
if ("number" == typeof contract.cdpPort) return contract.cdpPort;
|
|
1692
|
+
} catch {}
|
|
1693
|
+
const defaultPort = 9222;
|
|
1694
|
+
return new Promise((resolve)=>{
|
|
1695
|
+
const socket = new node_net.Socket();
|
|
1696
|
+
socket.setTimeout(1000);
|
|
1697
|
+
socket.on("connect", ()=>{
|
|
1698
|
+
socket.destroy();
|
|
1699
|
+
resolve(defaultPort);
|
|
1700
|
+
});
|
|
1701
|
+
socket.on("error", ()=>resolve(null));
|
|
1702
|
+
socket.on("timeout", ()=>{
|
|
1703
|
+
socket.destroy();
|
|
1704
|
+
resolve(null);
|
|
1705
|
+
});
|
|
1706
|
+
socket.connect(defaultPort, "127.0.0.1");
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1550
1709
|
const CONTROL_WS_PATH = "/extjs-control";
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1710
|
+
const LEVEL_ORDER = [
|
|
1711
|
+
"error",
|
|
1712
|
+
"warn",
|
|
1713
|
+
"info",
|
|
1714
|
+
"debug",
|
|
1715
|
+
"trace"
|
|
1716
|
+
];
|
|
1717
|
+
function levelRank(level) {
|
|
1718
|
+
const l = "log" === level ? "info" : level;
|
|
1719
|
+
const i = LEVEL_ORDER.indexOf(l);
|
|
1720
|
+
return -1 === i ? LEVEL_ORDER.length : i;
|
|
1721
|
+
}
|
|
1722
|
+
function makeUrlMatcher(pattern) {
|
|
1723
|
+
const hasGlob = pattern.includes("*");
|
|
1724
|
+
let re = null;
|
|
1725
|
+
if (hasGlob) {
|
|
1726
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1727
|
+
re = new RegExp(escaped);
|
|
1728
|
+
}
|
|
1729
|
+
return (event)=>{
|
|
1730
|
+
const candidates = [
|
|
1731
|
+
event.url,
|
|
1732
|
+
event.hostname
|
|
1733
|
+
].filter((v)=>"string" == typeof v);
|
|
1734
|
+
if (0 === candidates.length) return false;
|
|
1735
|
+
return candidates.some((c)=>re ? re.test(c) : c.includes(pattern));
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function makeFilter(args) {
|
|
1739
|
+
const minLevel = String(args.level || "all").toLowerCase();
|
|
1740
|
+
const rawContexts = Array.isArray(args.context) ? args.context : "string" == typeof args.context ? args.context.split(",") : null;
|
|
1741
|
+
const contexts = rawContexts && rawContexts.length ? new Set(rawContexts.map((c)=>c.trim()).filter(Boolean)) : null;
|
|
1742
|
+
const sinceSeq = null != args.since ? Number(args.since) : null;
|
|
1743
|
+
const urlMatches = args.url ? makeUrlMatcher(args.url) : null;
|
|
1744
|
+
const tabId = null != args.tab ? Number(args.tab) : null;
|
|
1745
|
+
return (event)=>{
|
|
1746
|
+
if (!event || "object" != typeof event) return false;
|
|
1747
|
+
if ("header" === event.type) return false;
|
|
1748
|
+
if (args.signalsOnly && "dx.signal" !== event.eventType) return false;
|
|
1749
|
+
if (contexts && !contexts.has(event.context)) return false;
|
|
1750
|
+
if ("all" !== minLevel && "off" !== minLevel) {
|
|
1751
|
+
if (levelRank(event.level) > levelRank(minLevel)) return false;
|
|
1752
|
+
}
|
|
1753
|
+
if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
|
|
1754
|
+
if (urlMatches && !urlMatches(event)) return false;
|
|
1755
|
+
if (null != tabId && Number.isFinite(tabId) && event.tabId !== tabId) return false;
|
|
1756
|
+
return true;
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
const logs_schema_schema = {
|
|
1556
1760
|
name: "extension_logs",
|
|
1557
1761
|
description: "Read or stream logs from every context of a running dev session (service worker, content scripts, popup, options, sidebar, devtools, pages) in one ordered timeline. Reads the same agent-bridge plane as the `extension logs` CLI: a one-shot returns the most recent matching lines from logs.ndjson; `follow:true` collects from the live control channel for a bounded window. Requires an active `extension dev` session.",
|
|
1558
1762
|
inputSchema: {
|
|
@@ -1621,12 +1825,12 @@ const logs_schema = {
|
|
|
1621
1825
|
},
|
|
1622
1826
|
followMs: {
|
|
1623
1827
|
type: "number",
|
|
1624
|
-
default:
|
|
1625
|
-
description:
|
|
1828
|
+
default: 4000,
|
|
1829
|
+
description: "How long to collect live frames when follow=true (clamped 500–15000ms)."
|
|
1626
1830
|
},
|
|
1627
1831
|
limit: {
|
|
1628
1832
|
type: "number",
|
|
1629
|
-
default:
|
|
1833
|
+
default: 200,
|
|
1630
1834
|
description: "Maximum number of (most recent) events to return."
|
|
1631
1835
|
}
|
|
1632
1836
|
},
|
|
@@ -1634,56 +1838,7 @@ const logs_schema = {
|
|
|
1634
1838
|
"projectPath"
|
|
1635
1839
|
]
|
|
1636
1840
|
}
|
|
1637
|
-
};
|
|
1638
|
-
const LEVEL_ORDER = [
|
|
1639
|
-
"error",
|
|
1640
|
-
"warn",
|
|
1641
|
-
"info",
|
|
1642
|
-
"debug",
|
|
1643
|
-
"trace"
|
|
1644
|
-
];
|
|
1645
|
-
function levelRank(level) {
|
|
1646
|
-
const l = "log" === level ? "info" : level;
|
|
1647
|
-
const i = LEVEL_ORDER.indexOf(l);
|
|
1648
|
-
return -1 === i ? LEVEL_ORDER.length : i;
|
|
1649
|
-
}
|
|
1650
|
-
function makeUrlMatcher(pattern) {
|
|
1651
|
-
const hasGlob = pattern.includes("*");
|
|
1652
|
-
let re = null;
|
|
1653
|
-
if (hasGlob) {
|
|
1654
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1655
|
-
re = new RegExp(escaped);
|
|
1656
|
-
}
|
|
1657
|
-
return (event)=>{
|
|
1658
|
-
const candidates = [
|
|
1659
|
-
event.url,
|
|
1660
|
-
event.hostname
|
|
1661
|
-
].filter((v)=>"string" == typeof v);
|
|
1662
|
-
if (0 === candidates.length) return false;
|
|
1663
|
-
return candidates.some((c)=>re ? re.test(c) : c.includes(pattern));
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
function makeFilter(args) {
|
|
1667
|
-
const minLevel = String(args.level || "all").toLowerCase();
|
|
1668
|
-
const rawContexts = Array.isArray(args.context) ? args.context : "string" == typeof args.context ? args.context.split(",") : null;
|
|
1669
|
-
const contexts = rawContexts && rawContexts.length ? new Set(rawContexts.map((c)=>c.trim()).filter(Boolean)) : null;
|
|
1670
|
-
const sinceSeq = null != args.since ? Number(args.since) : null;
|
|
1671
|
-
const urlMatches = args.url ? makeUrlMatcher(args.url) : null;
|
|
1672
|
-
const tabId = null != args.tab ? Number(args.tab) : null;
|
|
1673
|
-
return (event)=>{
|
|
1674
|
-
if (!event || "object" != typeof event) return false;
|
|
1675
|
-
if ("header" === event.type) return false;
|
|
1676
|
-
if (args.signalsOnly && "dx.signal" !== event.eventType) return false;
|
|
1677
|
-
if (contexts && !contexts.has(event.context)) return false;
|
|
1678
|
-
if ("all" !== minLevel && "off" !== minLevel) {
|
|
1679
|
-
if (levelRank(event.level) > levelRank(minLevel)) return false;
|
|
1680
|
-
}
|
|
1681
|
-
if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
|
|
1682
|
-
if (urlMatches && !urlMatches(event)) return false;
|
|
1683
|
-
if (null != tabId && Number.isFinite(tabId) && event.tabId !== tabId) return false;
|
|
1684
|
-
return true;
|
|
1685
|
-
};
|
|
1686
|
-
}
|
|
1841
|
+
};
|
|
1687
1842
|
function logsFilePath(projectPath, browser) {
|
|
1688
1843
|
return node_path.resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
|
|
1689
1844
|
}
|
|
@@ -1759,7 +1914,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
1759
1914
|
error: `No active control channel found for ${browser}.`,
|
|
1760
1915
|
hint: `Run extension_dev (browser: ${browser}) and wait for it to be ready, then retry. For past logs without a live channel, call without follow.`
|
|
1761
1916
|
});
|
|
1762
|
-
const followMs = Math.min(Math.max(args.followMs ??
|
|
1917
|
+
const followMs = Math.min(Math.max(args.followMs ?? 4000, 500), 15000);
|
|
1763
1918
|
const matches = makeFilter(args);
|
|
1764
1919
|
const events = [];
|
|
1765
1920
|
let dropped = 0;
|
|
@@ -1790,7 +1945,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
1790
1945
|
try {
|
|
1791
1946
|
socket.send(JSON.stringify({
|
|
1792
1947
|
type: "hello",
|
|
1793
|
-
v:
|
|
1948
|
+
v: 1,
|
|
1794
1949
|
role: "consumer",
|
|
1795
1950
|
instanceId: ready.instanceId
|
|
1796
1951
|
}));
|
|
@@ -1822,7 +1977,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
1822
1977
|
}
|
|
1823
1978
|
async function logs_handler(args) {
|
|
1824
1979
|
const browser = args.browser ?? "chromium";
|
|
1825
|
-
const limit = args.limit && args.limit > 0 ? args.limit :
|
|
1980
|
+
const limit = args.limit && args.limit > 0 ? args.limit : 200;
|
|
1826
1981
|
if (args.follow) return readFromStream(args, browser, limit);
|
|
1827
1982
|
return readFromFile(args, browser, limit);
|
|
1828
1983
|
}
|
|
@@ -2046,7 +2201,7 @@ async function reload_handler(args) {
|
|
|
2046
2201
|
}
|
|
2047
2202
|
const open_schema = {
|
|
2048
2203
|
name: "extension_open",
|
|
2049
|
-
description: "Open an extension surface
|
|
2204
|
+
description: "Open an extension surface or replay an event in a running session. 'popup'/'options'/'sidebar' open UI surfaces. 'action' triggers the toolbar action: opens the action's popup, or (no popup) replays chrome.action.onClicked. 'command' replays a chrome.commands.onCommand keyboard shortcut (pass `name`). NOTE: action/command replay invokes your listener WITHOUT a user gesture, so the gesture-derived activeTab grant does not apply (the result includes gesture:false and a warning when activeTab is declared). Requires --allow-control. Wraps `extension open`.",
|
|
2050
2205
|
inputSchema: {
|
|
2051
2206
|
type: "object",
|
|
2052
2207
|
properties: {
|
|
@@ -2059,9 +2214,15 @@ const open_schema = {
|
|
|
2059
2214
|
enum: [
|
|
2060
2215
|
"popup",
|
|
2061
2216
|
"options",
|
|
2062
|
-
"sidebar"
|
|
2217
|
+
"sidebar",
|
|
2218
|
+
"action",
|
|
2219
|
+
"command"
|
|
2063
2220
|
],
|
|
2064
|
-
description: "Which surface to open"
|
|
2221
|
+
description: "Which surface to open or event to replay. 'action' triggers the toolbar action; 'command' replays a keyboard-shortcut command (requires `name`)."
|
|
2222
|
+
},
|
|
2223
|
+
name: {
|
|
2224
|
+
type: "string",
|
|
2225
|
+
description: "For surface 'command': the chrome.commands name to trigger."
|
|
2065
2226
|
},
|
|
2066
2227
|
browser: {
|
|
2067
2228
|
type: "string",
|
|
@@ -2084,6 +2245,7 @@ async function open_handler(args) {
|
|
|
2084
2245
|
args.surface,
|
|
2085
2246
|
args.projectPath
|
|
2086
2247
|
];
|
|
2248
|
+
if ("command" === args.surface && args.name) cli.push("--name", args.name);
|
|
2087
2249
|
if (args.browser) cli.push("--browser", args.browser);
|
|
2088
2250
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
2089
2251
|
return runActVerb(cli, args.projectPath, args.timeout);
|
|
@@ -2179,9 +2341,73 @@ async function dom_inspect_handler(args) {
|
|
|
2179
2341
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
2180
2342
|
return runActVerb(cli, args.projectPath, args.timeout);
|
|
2181
2343
|
}
|
|
2344
|
+
function credentialsPath() {
|
|
2345
|
+
if ("win32" === process.platform) {
|
|
2346
|
+
const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
|
|
2347
|
+
return node_path.join(base, "extension-dev", "auth.json");
|
|
2348
|
+
}
|
|
2349
|
+
const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
|
|
2350
|
+
const base = xdg || node_path.join(node_os.homedir(), ".config");
|
|
2351
|
+
return node_path.join(base, "extension-dev", "auth.json");
|
|
2352
|
+
}
|
|
2353
|
+
function readCredentials() {
|
|
2354
|
+
try {
|
|
2355
|
+
const raw = node_fs.readFileSync(credentialsPath(), "utf8");
|
|
2356
|
+
const data = JSON.parse(raw);
|
|
2357
|
+
if (!data || "object" != typeof data) return null;
|
|
2358
|
+
if (1 !== data.version) return null;
|
|
2359
|
+
const token = String(data.token || "").trim();
|
|
2360
|
+
if (!token) return null;
|
|
2361
|
+
return {
|
|
2362
|
+
version: 1,
|
|
2363
|
+
token,
|
|
2364
|
+
workspaceSlug: String(data.workspaceSlug || ""),
|
|
2365
|
+
projectSlug: String(data.projectSlug || ""),
|
|
2366
|
+
expiresAt: Number(data.expiresAt || 0),
|
|
2367
|
+
api: String(data.api || "")
|
|
2368
|
+
};
|
|
2369
|
+
} catch {
|
|
2370
|
+
return null;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
function writeCredentials(creds) {
|
|
2374
|
+
const file = credentialsPath();
|
|
2375
|
+
node_fs.mkdirSync(node_path.dirname(file), {
|
|
2376
|
+
recursive: true
|
|
2377
|
+
});
|
|
2378
|
+
node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
|
|
2379
|
+
mode: 384
|
|
2380
|
+
});
|
|
2381
|
+
try {
|
|
2382
|
+
node_fs.chmodSync(file, 384);
|
|
2383
|
+
} catch {}
|
|
2384
|
+
return file;
|
|
2385
|
+
}
|
|
2386
|
+
function clearCredentials() {
|
|
2387
|
+
const file = credentialsPath();
|
|
2388
|
+
try {
|
|
2389
|
+
node_fs.unlinkSync(file);
|
|
2390
|
+
return {
|
|
2391
|
+
cleared: true,
|
|
2392
|
+
path: file
|
|
2393
|
+
};
|
|
2394
|
+
} catch {
|
|
2395
|
+
return {
|
|
2396
|
+
cleared: false,
|
|
2397
|
+
path: file
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
2402
|
+
const creds = readCredentials();
|
|
2403
|
+
if (!creds) return null;
|
|
2404
|
+
if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
|
|
2405
|
+
return creds;
|
|
2406
|
+
}
|
|
2407
|
+
const DEFAULT_API = "https://www.extension.dev";
|
|
2182
2408
|
const publish_schema = {
|
|
2183
2409
|
name: "extension_publish",
|
|
2184
|
-
description: "Publish a project to extension.dev and return a shareable URL. Auth-gated: requires EXTENSION_DEV_TOKEN (a workspace/project access token) in the environment.
|
|
2410
|
+
description: "Publish a project to extension.dev and return a shareable URL. Auth-gated: requires EXTENSION_DEV_TOKEN (a workspace/project access token) in the environment. Posts to the platform's CLI publish endpoint directly. This is the only tool that talks to the hosted platform rather than the local browser.",
|
|
2185
2411
|
inputSchema: {
|
|
2186
2412
|
type: "object",
|
|
2187
2413
|
properties: {
|
|
@@ -2207,32 +2433,171 @@ const publish_schema = {
|
|
|
2207
2433
|
]
|
|
2208
2434
|
}
|
|
2209
2435
|
};
|
|
2210
|
-
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2436
|
+
function resolveToken() {
|
|
2437
|
+
const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
|
|
2438
|
+
if (fromEnv) return fromEnv;
|
|
2439
|
+
const creds = readValidCredentials();
|
|
2440
|
+
return creds?.token ? String(creds.token).trim() : "";
|
|
2441
|
+
}
|
|
2442
|
+
function safeApiBase(raw) {
|
|
2443
|
+
let parsed;
|
|
2444
|
+
try {
|
|
2445
|
+
parsed = new URL(raw);
|
|
2446
|
+
} catch {
|
|
2447
|
+
return {
|
|
2448
|
+
ok: false,
|
|
2449
|
+
message: `Invalid platform URL: ${raw}`
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
const isLocalhost = "localhost" === parsed.hostname || "127.0.0.1" === parsed.hostname || "[::1]" === parsed.hostname || "::1" === parsed.hostname;
|
|
2453
|
+
if ("https:" !== parsed.protocol && !("http:" === parsed.protocol && isLocalhost)) return {
|
|
2454
|
+
ok: false,
|
|
2455
|
+
message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
|
|
2456
|
+
};
|
|
2457
|
+
return {
|
|
2458
|
+
ok: true,
|
|
2459
|
+
base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
function fail(name, message) {
|
|
2463
|
+
return JSON.stringify({
|
|
2464
|
+
ok: false,
|
|
2465
|
+
error: {
|
|
2466
|
+
name,
|
|
2467
|
+
message
|
|
2468
|
+
}
|
|
2222
2469
|
});
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2470
|
+
}
|
|
2471
|
+
async function publish_handler(args) {
|
|
2472
|
+
const token = resolveToken();
|
|
2473
|
+
if (!token) return fail("PublishAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
|
|
2474
|
+
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API));
|
|
2475
|
+
if (!apiCheck.ok) return fail("PublishConfigError", apiCheck.message);
|
|
2476
|
+
const url = `${apiCheck.base}/api/cli/publish`;
|
|
2477
|
+
const body = {};
|
|
2478
|
+
if (null != args.ttlHours) body.ttlHours = Number(args.ttlHours);
|
|
2479
|
+
if (args.buildSha) body.buildSha = args.buildSha;
|
|
2480
|
+
let res;
|
|
2481
|
+
try {
|
|
2482
|
+
res = await fetch(url, {
|
|
2483
|
+
method: "POST",
|
|
2484
|
+
headers: {
|
|
2485
|
+
authorization: `Bearer ${token}`,
|
|
2486
|
+
"content-type": "application/json"
|
|
2487
|
+
},
|
|
2488
|
+
body: JSON.stringify(body)
|
|
2489
|
+
});
|
|
2490
|
+
} catch (err) {
|
|
2491
|
+
return fail("PublishNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
2492
|
+
}
|
|
2493
|
+
const text = await res.text();
|
|
2494
|
+
let data;
|
|
2495
|
+
try {
|
|
2496
|
+
data = JSON.parse(text);
|
|
2497
|
+
} catch {
|
|
2498
|
+
data = {
|
|
2499
|
+
message: text
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
if (!res.ok) return fail("PublishError", `publish failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
2503
|
+
return JSON.stringify(data);
|
|
2504
|
+
}
|
|
2505
|
+
const release_promote_DEFAULT_API = "https://www.extension.dev";
|
|
2506
|
+
const release_promote_schema = {
|
|
2507
|
+
name: "extension_release_promote",
|
|
2508
|
+
description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: requires a release token in EXTENSION_DEV_TOKEN (mint and revoke it in the dashboard under project settings -> Access tokens). Posts to the platform's CLI release endpoint; the project is identified by the token. Cutting a version-bump PR is not available headlessly (it writes to your source repo and needs an interactive login).",
|
|
2509
|
+
inputSchema: {
|
|
2510
|
+
type: "object",
|
|
2511
|
+
properties: {
|
|
2512
|
+
buildId: {
|
|
2513
|
+
type: "string",
|
|
2514
|
+
description: "Build commit SHA to promote (a 7-char short SHA is fine)"
|
|
2515
|
+
},
|
|
2516
|
+
channel: {
|
|
2517
|
+
type: "string",
|
|
2518
|
+
description: "Target release channel, e.g. stable, preview, beta"
|
|
2519
|
+
},
|
|
2520
|
+
sourceChannel: {
|
|
2521
|
+
type: "string",
|
|
2522
|
+
description: "Channel to promote from (optional; inferred otherwise)"
|
|
2523
|
+
},
|
|
2524
|
+
browsers: {
|
|
2525
|
+
type: "array",
|
|
2526
|
+
items: {
|
|
2527
|
+
type: "string"
|
|
2528
|
+
},
|
|
2529
|
+
description: "Browsers to release (optional; auto-detected from the build)"
|
|
2530
|
+
},
|
|
2531
|
+
version: {
|
|
2532
|
+
type: "string",
|
|
2533
|
+
description: "Version label for the release (optional)"
|
|
2534
|
+
},
|
|
2535
|
+
releaseNotes: {
|
|
2536
|
+
type: "string",
|
|
2537
|
+
description: "Release notes markdown (optional)"
|
|
2538
|
+
},
|
|
2539
|
+
api: {
|
|
2540
|
+
type: "string",
|
|
2541
|
+
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
2542
|
+
}
|
|
2543
|
+
},
|
|
2544
|
+
required: [
|
|
2545
|
+
"buildId",
|
|
2546
|
+
"channel"
|
|
2547
|
+
]
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
function release_promote_fail(name, message) {
|
|
2228
2551
|
return JSON.stringify({
|
|
2229
2552
|
ok: false,
|
|
2230
2553
|
error: {
|
|
2231
|
-
name
|
|
2232
|
-
message
|
|
2554
|
+
name,
|
|
2555
|
+
message
|
|
2233
2556
|
}
|
|
2234
2557
|
});
|
|
2235
2558
|
}
|
|
2559
|
+
async function release_promote_handler(args) {
|
|
2560
|
+
const token = resolveToken();
|
|
2561
|
+
if (!token) return release_promote_fail("ReleaseAuthError", "No token. Set EXTENSION_DEV_TOKEN to a release token (create one in the extension.dev dashboard under project settings -> Access tokens), or run extension_login.");
|
|
2562
|
+
const buildId = String(args.buildId || "").trim();
|
|
2563
|
+
const channel = String(args.channel || "").trim();
|
|
2564
|
+
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
2565
|
+
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
|
|
2566
|
+
if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
|
|
2567
|
+
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
2568
|
+
const body = {
|
|
2569
|
+
buildId,
|
|
2570
|
+
channel
|
|
2571
|
+
};
|
|
2572
|
+
if (args.sourceChannel) body.sourceChannel = String(args.sourceChannel).trim();
|
|
2573
|
+
if (Array.isArray(args.browsers) && args.browsers.length) body.browsers = args.browsers.map((b)=>String(b).trim()).filter(Boolean);
|
|
2574
|
+
if (args.version) body.version = String(args.version).trim();
|
|
2575
|
+
if (args.releaseNotes) body.releaseNotes = String(args.releaseNotes);
|
|
2576
|
+
let res;
|
|
2577
|
+
try {
|
|
2578
|
+
res = await fetch(url, {
|
|
2579
|
+
method: "POST",
|
|
2580
|
+
headers: {
|
|
2581
|
+
authorization: `Bearer ${token}`,
|
|
2582
|
+
"content-type": "application/json"
|
|
2583
|
+
},
|
|
2584
|
+
body: JSON.stringify(body)
|
|
2585
|
+
});
|
|
2586
|
+
} catch (err) {
|
|
2587
|
+
return release_promote_fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
2588
|
+
}
|
|
2589
|
+
const text = await res.text();
|
|
2590
|
+
let data;
|
|
2591
|
+
try {
|
|
2592
|
+
data = JSON.parse(text);
|
|
2593
|
+
} catch {
|
|
2594
|
+
data = {
|
|
2595
|
+
message: text
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
if (!res.ok) return release_promote_fail("ReleaseError", `promote failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
2599
|
+
return JSON.stringify(data);
|
|
2600
|
+
}
|
|
2236
2601
|
const wait_schema = {
|
|
2237
2602
|
name: "extension_wait",
|
|
2238
2603
|
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status.",
|
|
@@ -2528,6 +2893,303 @@ async function add_feature_handler(args) {
|
|
|
2528
2893
|
hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
|
|
2529
2894
|
});
|
|
2530
2895
|
}
|
|
2896
|
+
const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
2897
|
+
const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
2898
|
+
const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
2899
|
+
const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
2900
|
+
async function startDeviceCode(args) {
|
|
2901
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
2902
|
+
const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
|
|
2903
|
+
method: "POST",
|
|
2904
|
+
headers: {
|
|
2905
|
+
accept: "application/json",
|
|
2906
|
+
"content-type": "application/json"
|
|
2907
|
+
},
|
|
2908
|
+
body: JSON.stringify({
|
|
2909
|
+
client_id: args.clientId,
|
|
2910
|
+
scope: args.scope || "read:user"
|
|
2911
|
+
})
|
|
2912
|
+
});
|
|
2913
|
+
const data = await res.json().catch(()=>({}));
|
|
2914
|
+
if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
|
|
2915
|
+
return {
|
|
2916
|
+
deviceCode: String(data.device_code || ""),
|
|
2917
|
+
userCode: String(data.user_code || ""),
|
|
2918
|
+
verificationUri: String(data.verification_uri || "https://github.com/login/device"),
|
|
2919
|
+
interval: Number(data.interval || 5),
|
|
2920
|
+
expiresIn: Number(data.expires_in || 900)
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
async function pollForToken(args) {
|
|
2924
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
2925
|
+
const sleep = args.sleepImpl ?? defaultSleep;
|
|
2926
|
+
let intervalMs = 1000 * Math.max(1, args.interval);
|
|
2927
|
+
const deadline = Date.now() + Math.max(0, args.budgetMs);
|
|
2928
|
+
while(Date.now() < deadline){
|
|
2929
|
+
await sleep(intervalMs);
|
|
2930
|
+
const res = await doFetch(GITHUB_TOKEN_URL, {
|
|
2931
|
+
method: "POST",
|
|
2932
|
+
headers: {
|
|
2933
|
+
accept: "application/json",
|
|
2934
|
+
"content-type": "application/json"
|
|
2935
|
+
},
|
|
2936
|
+
body: JSON.stringify({
|
|
2937
|
+
client_id: args.clientId,
|
|
2938
|
+
device_code: args.deviceCode,
|
|
2939
|
+
grant_type: DEVICE_GRANT_TYPE
|
|
2940
|
+
})
|
|
2941
|
+
});
|
|
2942
|
+
const data = await res.json().catch(()=>({}));
|
|
2943
|
+
const token = String(data.access_token || "").trim();
|
|
2944
|
+
if (token) return {
|
|
2945
|
+
ok: true,
|
|
2946
|
+
githubToken: token
|
|
2947
|
+
};
|
|
2948
|
+
const error = String(data.error || "");
|
|
2949
|
+
if ("authorization_pending" === error) continue;
|
|
2950
|
+
if ("slow_down" === error) {
|
|
2951
|
+
intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
|
|
2952
|
+
continue;
|
|
2953
|
+
}
|
|
2954
|
+
if ("expired_token" === error) return {
|
|
2955
|
+
ok: false,
|
|
2956
|
+
reason: "expired"
|
|
2957
|
+
};
|
|
2958
|
+
if ("access_denied" === error) return {
|
|
2959
|
+
ok: false,
|
|
2960
|
+
reason: "denied"
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
return {
|
|
2964
|
+
ok: false,
|
|
2965
|
+
reason: "pending"
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
const login_flow_DEFAULT_API = "https://www.extension.dev";
|
|
2969
|
+
function resolveApiBase(api) {
|
|
2970
|
+
return String(api || process.env.EXTENSION_DEV_API_URL || login_flow_DEFAULT_API).replace(/\/+$/, "");
|
|
2971
|
+
}
|
|
2972
|
+
async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
2973
|
+
const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
|
|
2974
|
+
if (override) return {
|
|
2975
|
+
clientId: override,
|
|
2976
|
+
scope: "read:user"
|
|
2977
|
+
};
|
|
2978
|
+
const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
|
|
2979
|
+
headers: {
|
|
2980
|
+
accept: "application/json"
|
|
2981
|
+
}
|
|
2982
|
+
});
|
|
2983
|
+
if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
|
|
2984
|
+
const data = await res.json().catch(()=>({}));
|
|
2985
|
+
const clientId = String(data.githubClientId || "").trim();
|
|
2986
|
+
if (!clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
|
|
2987
|
+
return {
|
|
2988
|
+
clientId,
|
|
2989
|
+
scope: String(data.scope || "read:user")
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
async function exchangeAndPersist(args) {
|
|
2993
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
2994
|
+
const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
|
|
2995
|
+
method: "POST",
|
|
2996
|
+
headers: {
|
|
2997
|
+
"content-type": "application/json",
|
|
2998
|
+
accept: "application/json"
|
|
2999
|
+
},
|
|
3000
|
+
body: JSON.stringify({
|
|
3001
|
+
githubToken: args.githubToken,
|
|
3002
|
+
project: args.project
|
|
3003
|
+
})
|
|
3004
|
+
});
|
|
3005
|
+
const text = await res.text();
|
|
3006
|
+
let data;
|
|
3007
|
+
try {
|
|
3008
|
+
data = JSON.parse(text);
|
|
3009
|
+
} catch {
|
|
3010
|
+
data = {
|
|
3011
|
+
message: text
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
|
|
3015
|
+
const token = String(data.token || "").trim();
|
|
3016
|
+
if (!token) throw new Error("Login exchange returned no token.");
|
|
3017
|
+
const creds = {
|
|
3018
|
+
version: 1,
|
|
3019
|
+
token,
|
|
3020
|
+
workspaceSlug: String(data.workspaceSlug || ""),
|
|
3021
|
+
projectSlug: String(data.projectSlug || ""),
|
|
3022
|
+
expiresAt: Number(data.expiresAt || 0),
|
|
3023
|
+
api: args.apiBase
|
|
3024
|
+
};
|
|
3025
|
+
writeCredentials(creds);
|
|
3026
|
+
return creds;
|
|
3027
|
+
}
|
|
3028
|
+
const login_schema = {
|
|
3029
|
+
name: "extension_login",
|
|
3030
|
+
description: "Authenticate to extension.dev via GitHub device-code and store a project-scoped access token locally so extension_publish can use it. Two-phase: call with `project` to get a code + URL for the user to authorize, then call again with the returned `deviceCode` to finish. Never returns the token. This is the only tool besides extension_publish that talks to the hosted platform.",
|
|
3031
|
+
inputSchema: {
|
|
3032
|
+
type: "object",
|
|
3033
|
+
properties: {
|
|
3034
|
+
project: {
|
|
3035
|
+
type: "string",
|
|
3036
|
+
description: "Target project as '<workspace>/<project>' (the token is scoped to it)"
|
|
3037
|
+
},
|
|
3038
|
+
deviceCode: {
|
|
3039
|
+
type: "string",
|
|
3040
|
+
description: "Resume token from a prior call's `deviceCode`; omit on the first call"
|
|
3041
|
+
},
|
|
3042
|
+
api: {
|
|
3043
|
+
type: "string",
|
|
3044
|
+
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
3045
|
+
}
|
|
3046
|
+
},
|
|
3047
|
+
required: [
|
|
3048
|
+
"project"
|
|
3049
|
+
]
|
|
3050
|
+
}
|
|
3051
|
+
};
|
|
3052
|
+
const FIRST_CALL_BUDGET_MS = 8000;
|
|
3053
|
+
const RESUME_BUDGET_MS = 22000;
|
|
3054
|
+
function login_fail(name, message) {
|
|
3055
|
+
return JSON.stringify({
|
|
3056
|
+
ok: false,
|
|
3057
|
+
error: {
|
|
3058
|
+
name,
|
|
3059
|
+
message
|
|
3060
|
+
}
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
function success(creds) {
|
|
3064
|
+
return JSON.stringify({
|
|
3065
|
+
ok: true,
|
|
3066
|
+
status: "logged-in",
|
|
3067
|
+
workspaceSlug: creds.workspaceSlug,
|
|
3068
|
+
projectSlug: creds.projectSlug,
|
|
3069
|
+
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
3070
|
+
message: `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}. extension_publish can now use the stored token.`
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
3073
|
+
function login_pending(start) {
|
|
3074
|
+
return JSON.stringify({
|
|
3075
|
+
ok: false,
|
|
3076
|
+
status: "authorization_pending",
|
|
3077
|
+
userCode: start.userCode,
|
|
3078
|
+
verificationUri: start.verificationUri,
|
|
3079
|
+
deviceCode: start.deviceCode,
|
|
3080
|
+
message: `Open ${start.verificationUri} and enter code ${start.userCode}, then call extension_login again with this deviceCode (and the same project).`
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
async function login_handler(args) {
|
|
3084
|
+
const project = String(args.project || "").trim();
|
|
3085
|
+
if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.");
|
|
3086
|
+
const apiBase = resolveApiBase(args.api);
|
|
3087
|
+
let config;
|
|
3088
|
+
try {
|
|
3089
|
+
config = await fetchLoginConfig(apiBase);
|
|
3090
|
+
} catch (err) {
|
|
3091
|
+
return login_fail("LoginConfigError", err?.message || "Could not load login config.");
|
|
3092
|
+
}
|
|
3093
|
+
if (args.deviceCode) {
|
|
3094
|
+
const poll = await pollForToken({
|
|
3095
|
+
clientId: config.clientId,
|
|
3096
|
+
deviceCode: String(args.deviceCode),
|
|
3097
|
+
interval: 5,
|
|
3098
|
+
budgetMs: RESUME_BUDGET_MS
|
|
3099
|
+
});
|
|
3100
|
+
if (!poll.ok) {
|
|
3101
|
+
if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
|
|
3102
|
+
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
|
|
3103
|
+
return login_pending({
|
|
3104
|
+
deviceCode: String(args.deviceCode),
|
|
3105
|
+
userCode: "(see the previous response)",
|
|
3106
|
+
verificationUri: "https://github.com/login/device"
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
try {
|
|
3110
|
+
const creds = await exchangeAndPersist({
|
|
3111
|
+
apiBase,
|
|
3112
|
+
githubToken: poll.githubToken,
|
|
3113
|
+
project
|
|
3114
|
+
});
|
|
3115
|
+
return success(creds);
|
|
3116
|
+
} catch (err) {
|
|
3117
|
+
return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
let start;
|
|
3121
|
+
try {
|
|
3122
|
+
start = await startDeviceCode({
|
|
3123
|
+
clientId: config.clientId,
|
|
3124
|
+
scope: config.scope
|
|
3125
|
+
});
|
|
3126
|
+
} catch (err) {
|
|
3127
|
+
return login_fail("LoginStartError", err?.message || "Could not start device flow.");
|
|
3128
|
+
}
|
|
3129
|
+
const poll = await pollForToken({
|
|
3130
|
+
clientId: config.clientId,
|
|
3131
|
+
deviceCode: start.deviceCode,
|
|
3132
|
+
interval: start.interval,
|
|
3133
|
+
budgetMs: FIRST_CALL_BUDGET_MS
|
|
3134
|
+
});
|
|
3135
|
+
if (poll.ok) try {
|
|
3136
|
+
const creds = await exchangeAndPersist({
|
|
3137
|
+
apiBase,
|
|
3138
|
+
githubToken: poll.githubToken,
|
|
3139
|
+
project
|
|
3140
|
+
});
|
|
3141
|
+
return success(creds);
|
|
3142
|
+
} catch (err) {
|
|
3143
|
+
return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
|
|
3144
|
+
}
|
|
3145
|
+
if (!poll.ok && "denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
|
|
3146
|
+
return login_pending(start);
|
|
3147
|
+
}
|
|
3148
|
+
const whoami_schema = {
|
|
3149
|
+
name: "extension_whoami",
|
|
3150
|
+
description: "Report the locally stored extension.dev login (workspace/project and token expiry) without revealing the token. Returns logged-out status when no credentials are stored.",
|
|
3151
|
+
inputSchema: {
|
|
3152
|
+
type: "object",
|
|
3153
|
+
properties: {}
|
|
3154
|
+
}
|
|
3155
|
+
};
|
|
3156
|
+
async function whoami_handler() {
|
|
3157
|
+
const creds = readCredentials();
|
|
3158
|
+
if (!creds) return JSON.stringify({
|
|
3159
|
+
ok: true,
|
|
3160
|
+
status: "logged-out",
|
|
3161
|
+
message: "No stored credentials. Run extension_login to authenticate."
|
|
3162
|
+
});
|
|
3163
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3164
|
+
const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
|
|
3165
|
+
return JSON.stringify({
|
|
3166
|
+
ok: true,
|
|
3167
|
+
status: expired ? "expired" : "logged-in",
|
|
3168
|
+
workspaceSlug: creds.workspaceSlug,
|
|
3169
|
+
projectSlug: creds.projectSlug,
|
|
3170
|
+
api: creds.api,
|
|
3171
|
+
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
3172
|
+
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
3173
|
+
expired,
|
|
3174
|
+
message: expired ? "Stored token has expired. Run extension_login to refresh it." : `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}.`
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
const logout_schema = {
|
|
3178
|
+
name: "extension_logout",
|
|
3179
|
+
description: "Delete the locally stored extension.dev credentials. Does not revoke the token server-side (revoke from the dashboard if needed); only removes it from this machine.",
|
|
3180
|
+
inputSchema: {
|
|
3181
|
+
type: "object",
|
|
3182
|
+
properties: {}
|
|
3183
|
+
}
|
|
3184
|
+
};
|
|
3185
|
+
async function logout_handler() {
|
|
3186
|
+
const result = clearCredentials();
|
|
3187
|
+
return JSON.stringify({
|
|
3188
|
+
ok: true,
|
|
3189
|
+
cleared: result.cleared,
|
|
3190
|
+
message: result.cleared ? "Local credentials removed." : "No stored credentials to remove."
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
2531
3193
|
const install_browser_schema = {
|
|
2532
3194
|
name: "extension_install_browser",
|
|
2533
3195
|
description: "Install a managed browser binary for extension testing. Useful in CI, Docker, or fresh environments where browsers are not pre-installed.",
|
|
@@ -2867,6 +3529,7 @@ const tools = [
|
|
|
2867
3529
|
manifest_validate_namespaceObject,
|
|
2868
3530
|
inspect_namespaceObject,
|
|
2869
3531
|
source_inspect_namespaceObject,
|
|
3532
|
+
list_extensions_namespaceObject,
|
|
2870
3533
|
logs_namespaceObject,
|
|
2871
3534
|
eval_namespaceObject,
|
|
2872
3535
|
storage_namespaceObject,
|
|
@@ -2874,8 +3537,12 @@ const tools = [
|
|
|
2874
3537
|
open_namespaceObject,
|
|
2875
3538
|
dom_inspect_namespaceObject,
|
|
2876
3539
|
publish_namespaceObject,
|
|
3540
|
+
release_promote_namespaceObject,
|
|
2877
3541
|
wait_namespaceObject,
|
|
2878
3542
|
add_feature_namespaceObject,
|
|
3543
|
+
login_namespaceObject,
|
|
3544
|
+
whoami_namespaceObject,
|
|
3545
|
+
logout_namespaceObject,
|
|
2879
3546
|
install_browser_namespaceObject,
|
|
2880
3547
|
list_browsers_namespaceObject,
|
|
2881
3548
|
detect_browsers_namespaceObject
|
|
@@ -2885,7 +3552,7 @@ for (const tool of tools)toolMap.set(tool.schema.name, tool);
|
|
|
2885
3552
|
async function startServer() {
|
|
2886
3553
|
const server = new Server({
|
|
2887
3554
|
name: "extension-dev",
|
|
2888
|
-
version:
|
|
3555
|
+
version: package_namespaceObject.rE
|
|
2889
3556
|
}, {
|
|
2890
3557
|
capabilities: {
|
|
2891
3558
|
tools: {}
|
|
@@ -2941,4 +3608,88 @@ async function startServer() {
|
|
|
2941
3608
|
const transport = new StdioServerTransport();
|
|
2942
3609
|
await server.connect(transport);
|
|
2943
3610
|
}
|
|
2944
|
-
|
|
3611
|
+
async function runCli(cmd, args) {
|
|
3612
|
+
const log = (msg)=>process.stderr.write(`${msg}\n`);
|
|
3613
|
+
const flag = (name)=>{
|
|
3614
|
+
const i = args.indexOf(`--${name}`);
|
|
3615
|
+
return i >= 0 ? args[i + 1] : void 0;
|
|
3616
|
+
};
|
|
3617
|
+
if ("whoami" === cmd) {
|
|
3618
|
+
log(await whoami_handler());
|
|
3619
|
+
return 0;
|
|
3620
|
+
}
|
|
3621
|
+
if ("release" === cmd) {
|
|
3622
|
+
const sub = String(args[0] || "").trim();
|
|
3623
|
+
if ("promote" === sub) {
|
|
3624
|
+
const buildId = String(flag("build") || flag("build-id") || "").trim();
|
|
3625
|
+
const channel = String(flag("channel") || "").trim();
|
|
3626
|
+
if (!buildId || !channel) {
|
|
3627
|
+
log("Usage: extension-mcp release promote --build <sha> --channel <channel> [--source-channel <c>] [--version <v>] [--api <url>]");
|
|
3628
|
+
return 1;
|
|
3629
|
+
}
|
|
3630
|
+
const out = await release_promote_handler({
|
|
3631
|
+
buildId,
|
|
3632
|
+
channel,
|
|
3633
|
+
sourceChannel: flag("source-channel"),
|
|
3634
|
+
version: flag("version"),
|
|
3635
|
+
api: flag("api")
|
|
3636
|
+
});
|
|
3637
|
+
log(out);
|
|
3638
|
+
let parsed = null;
|
|
3639
|
+
try {
|
|
3640
|
+
parsed = JSON.parse(out);
|
|
3641
|
+
} catch {
|
|
3642
|
+
parsed = null;
|
|
3643
|
+
}
|
|
3644
|
+
return parsed?.ok === false ? 1 : 0;
|
|
3645
|
+
}
|
|
3646
|
+
log("Usage: extension-mcp release promote --build <sha> --channel <channel>");
|
|
3647
|
+
return 1;
|
|
3648
|
+
}
|
|
3649
|
+
if ("logout" === cmd) {
|
|
3650
|
+
log(await logout_handler());
|
|
3651
|
+
return 0;
|
|
3652
|
+
}
|
|
3653
|
+
if ("login" === cmd) {
|
|
3654
|
+
const project = String(flag("project") || "").trim();
|
|
3655
|
+
if (!/^[^/]+\/[^/]+$/.test(project)) {
|
|
3656
|
+
log("Usage: extension-mcp login --project <workspace>/<project> [--api <url>]");
|
|
3657
|
+
return 1;
|
|
3658
|
+
}
|
|
3659
|
+
const apiBase = resolveApiBase(flag("api"));
|
|
3660
|
+
try {
|
|
3661
|
+
const config = await fetchLoginConfig(apiBase);
|
|
3662
|
+
const start = await startDeviceCode({
|
|
3663
|
+
clientId: config.clientId,
|
|
3664
|
+
scope: config.scope
|
|
3665
|
+
});
|
|
3666
|
+
log("");
|
|
3667
|
+
log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
|
|
3668
|
+
log("");
|
|
3669
|
+
log(" Waiting for authorization...");
|
|
3670
|
+
const poll = await pollForToken({
|
|
3671
|
+
clientId: config.clientId,
|
|
3672
|
+
deviceCode: start.deviceCode,
|
|
3673
|
+
interval: start.interval,
|
|
3674
|
+
budgetMs: 1000 * start.expiresIn
|
|
3675
|
+
});
|
|
3676
|
+
if (!poll.ok) {
|
|
3677
|
+
log("denied" === poll.reason ? "Authorization was denied on GitHub." : "Timed out waiting for authorization. Run login again.");
|
|
3678
|
+
return 1;
|
|
3679
|
+
}
|
|
3680
|
+
const creds = await exchangeAndPersist({
|
|
3681
|
+
apiBase,
|
|
3682
|
+
githubToken: poll.githubToken,
|
|
3683
|
+
project
|
|
3684
|
+
});
|
|
3685
|
+
log(`Logged in to ${creds.workspaceSlug}/${creds.projectSlug}.`);
|
|
3686
|
+
return 0;
|
|
3687
|
+
} catch (err) {
|
|
3688
|
+
log(err instanceof Error ? err.message : String(err));
|
|
3689
|
+
return 1;
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami, release.`);
|
|
3693
|
+
return 1;
|
|
3694
|
+
}
|
|
3695
|
+
export { runCli, startServer };
|