@extension.dev/mcp 10.10.10 → 10.10.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.js CHANGED
@@ -247,7 +247,7 @@ __webpack_require__.d(workspace_create_namespaceObject, {
247
247
  handler: ()=>workspace_create_handler,
248
248
  schema: ()=>workspace_create_schema
249
249
  });
250
- var package_namespaceObject = JSON.parse('{"rE":"10.10.10","El":{"OP":"4.1.31"}}');
250
+ var package_namespaceObject = JSON.parse('{"rE":"10.10.11","El":{"OP":"4.1.31"}}');
251
251
  const ENVELOPE_SCHEMA = 1;
252
252
  const collectWarnings = (warnings)=>{
253
253
  if (!warnings) return [];
@@ -301,8 +301,24 @@ function descendantPids(pid) {
301
301
  return [];
302
302
  }
303
303
  }
304
+ function killWindowsTree(pid) {
305
+ try {
306
+ execFileSync("taskkill", [
307
+ "/PID",
308
+ String(pid),
309
+ "/T",
310
+ "/F"
311
+ ], {
312
+ stdio: "ignore"
313
+ });
314
+ return true;
315
+ } catch {
316
+ return false;
317
+ }
318
+ }
304
319
  function killTree(pid, signal) {
305
320
  if (!pid) return;
321
+ if ("win32" === process.platform) return void killWindowsTree(pid);
306
322
  for (const target of [
307
323
  ...descendantPids(pid).reverse(),
308
324
  pid
@@ -1148,6 +1164,21 @@ function safeApiBase(raw, callerSupplied) {
1148
1164
  base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
1149
1165
  };
1150
1166
  }
1167
+ function readJsonObject(text) {
1168
+ try {
1169
+ const parsed = JSON.parse(text);
1170
+ if (!parsed || "object" != typeof parsed || Array.isArray(parsed)) return {
1171
+ problem: "not a JSON object"
1172
+ };
1173
+ return {
1174
+ value: parsed
1175
+ };
1176
+ } catch (err) {
1177
+ return {
1178
+ problem: err instanceof Error ? err.message : String(err)
1179
+ };
1180
+ }
1181
+ }
1151
1182
  async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
1152
1183
  const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
1153
1184
  headers: {
@@ -1155,7 +1186,9 @@ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
1155
1186
  }
1156
1187
  });
1157
1188
  if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
1158
- const data = await res.json().catch(()=>({}));
1189
+ const read = readJsonObject(await res.text());
1190
+ if ("problem" in read) throw new Error(`The login config at ${apiBase}/api/cli/login/config answered ${res.status} but could not be read (${read.problem}), so what this platform supports is unknown.`);
1191
+ const data = read.value;
1159
1192
  return {
1160
1193
  deviceCodeUrl: String(data.deviceCodeUrl || "/api/cli/device/code"),
1161
1194
  deviceTokenUrl: String(data.deviceTokenUrl || "/api/cli/device/token"),
@@ -1625,6 +1658,23 @@ function userlandProjectUrl(ref, page = "", apiHint) {
1625
1658
  return "";
1626
1659
  }
1627
1660
  }
1661
+ function channelsShapeProblem(json) {
1662
+ if (!json || "object" != typeof json || Array.isArray(json)) return "the body is not a channels map";
1663
+ const bad = Object.entries(json).find(([, row])=>!row || "object" != typeof row);
1664
+ return bad ? `channel ${bad[0]} is not an object` : null;
1665
+ }
1666
+ function buildIndexShapeProblem(json) {
1667
+ const items = json?.items;
1668
+ return Array.isArray(items) ? null : "the body has no items list";
1669
+ }
1670
+ function requireShape(read, problem) {
1671
+ if (!read.ok) return read;
1672
+ const why = problem(read.json);
1673
+ return why ? {
1674
+ ok: false,
1675
+ message: `answered 200 but ${why}`
1676
+ } : read;
1677
+ }
1628
1678
  async function readJson(url, res) {
1629
1679
  try {
1630
1680
  const text = await res.text();
@@ -2317,7 +2367,7 @@ const SESSION_BROWSER = {
2317
2367
  };
2318
2368
  const CALL_TIMEOUT = {
2319
2369
  type: "number",
2320
- description: "Command timeout in ms (default 5000)"
2370
+ description: "Command timeout in ms. When omitted, the default depends on the route: 30000 through the dev session's control channel, 10000 over the Firefox debugger protocol, and 15000 per command over the Chromium debug port."
2321
2371
  };
2322
2372
  const API_BASE = {
2323
2373
  type: "string",
@@ -4709,7 +4759,10 @@ function writeMarkerBestEffort(info, registeredAtMs) {
4709
4759
  serverPid: process.pid,
4710
4760
  registeredAt: new Date(registeredAtMs).toISOString()
4711
4761
  }));
4712
- } catch {}
4762
+ return null;
4763
+ } catch (error) {
4764
+ return `The session marker could not be written to ${markerDir()} (${error instanceof Error ? error.message : String(error)}), so if this server restarts, extension_stop (all: true) and the fork guard will not see this session; stop it by projectPath and browser, or by its pid ${info.pid}.`;
4765
+ }
4713
4766
  }
4714
4767
  function registerSession(info) {
4715
4768
  const key = sessionKey(info.projectPath, info.browser);
@@ -4720,7 +4773,7 @@ function registerSession(info) {
4720
4773
  at
4721
4774
  });
4722
4775
  process_manager_sessions.set(key, info);
4723
- writeMarkerBestEffort(info, at);
4776
+ return writeMarkerBestEffort(info, at);
4724
4777
  }
4725
4778
  function sessionSinceMs(projectPath, browser) {
4726
4779
  const stamp = registrationStamps.get(sessionKey(projectPath, browser));
@@ -6200,8 +6253,8 @@ async function build_handler(args) {
6200
6253
  const engineRefusedJsonOutput = !engineKnownTooOld && 0 !== attempt.code && refusedTheOutputFlag(attempt.stderr ?? "");
6201
6254
  if (engineRefusedJsonOutput) attempt = await runExtensionCli(cliArgs, spawn);
6202
6255
  const { code, stdout, stderr } = attempt;
6203
- if (engineRefusedJsonOutput) warnings.push("The Extension.js installed in this project is older than the one this server expects: it rejected --output json on build, so the build was run a second time without that flag and the result was read from the build summary the engine writes into dist/extension-js/. The extension that came out is exactly the same one. Upgrade the project's Extension.js to get the richer report back, including the Safari app identity and the byte totals from the run that just happened, and to stop paying for the second build.");
6204
- else if (engineKnownTooOld) warnings.push(`The Extension.js installed in this project is older than the one this server expects: it reports ${verdict.version}, and --output json only reached extension build in ${verdict.floor}, so the build was run without that flag and the result was read from the build summary the engine writes into dist/extension-js/. The extension that came out is exactly the same one, and nothing was built twice. Upgrade the project's Extension.js to get the richer report back, including the Safari app identity and the byte totals from the run that just happened.`);
6256
+ if (engineRefusedJsonOutput) warnings.push("The Extension.js installed in this project is older than the one this server expects: it rejected --output json on build, so the build was run a second time without that flag and the result comes from the build summary the engine writes into dist/extension-js/ when this run left one there (a separate warning names the path when it did not). The extension that came out is exactly the same one. Upgrade the project's Extension.js to get the richer report back, including the Safari app identity and the byte totals from the run that just happened, and to stop paying for the second build.");
6257
+ else if (engineKnownTooOld) warnings.push(`The Extension.js installed in this project is older than the one this server expects: it reports ${verdict.version}, and --output json only reached extension build in ${verdict.floor}, so the build was run without that flag and the result comes from the build summary the engine writes into dist/extension-js/ when this run left one there (a separate warning names the path when it did not). The extension that came out is exactly the same one, and nothing was built twice. Upgrade the project's Extension.js to get the richer report back, including the Safari app identity and the byte totals from the run that just happened.`);
6205
6258
  const duration = Date.now() - start;
6206
6259
  const engine = readEngineOutput(stdout ?? "", stderr ?? "");
6207
6260
  const out = engine.narration;
@@ -6788,14 +6841,31 @@ function sessionProcessPids(projectPath, hints = {
6788
6841
  verified
6789
6842
  };
6790
6843
  }
6791
- function reapSessionProcesses(projectPath, hints = {
6844
+ async function reapSessionProcesses(projectPath, hints = {
6792
6845
  pids: []
6793
6846
  }) {
6794
6847
  const { pids } = sessionProcessPids(projectPath, hints);
6795
- for (const pid of pids)try {
6796
- process.kill(pid, "SIGKILL");
6797
- } catch {}
6798
- return pids;
6848
+ if (0 === pids.length) return {
6849
+ reaped: [],
6850
+ unconfirmed: []
6851
+ };
6852
+ for (const pid of pids){
6853
+ if ("win32" === process.platform) {
6854
+ killWindowsTree(pid);
6855
+ continue;
6856
+ }
6857
+ try {
6858
+ process.kill(pid, "SIGKILL");
6859
+ } catch {}
6860
+ }
6861
+ await new Promise((resolve)=>setTimeout(resolve, 250));
6862
+ const reaped = [];
6863
+ const unconfirmed = [];
6864
+ for (const pid of pids)("dead" === pidState(pid) ? reaped : unconfirmed).push(pid);
6865
+ return {
6866
+ reaped,
6867
+ unconfirmed
6868
+ };
6799
6869
  }
6800
6870
  function isAlive(pid) {
6801
6871
  try {
@@ -6806,6 +6876,7 @@ function isAlive(pid) {
6806
6876
  }
6807
6877
  }
6808
6878
  function stop_signal(pid, sig) {
6879
+ if ("win32" === process.platform) return killWindowsTree(pid);
6809
6880
  try {
6810
6881
  process.kill(-pid, sig);
6811
6882
  return true;
@@ -6832,16 +6903,21 @@ async function stopOne(projectPath, browser) {
6832
6903
  const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
6833
6904
  const hints = contractProcessHints(projectPath, browser);
6834
6905
  if (null == pid) {
6835
- const reaped = reapSessionProcesses(projectPath, hints);
6906
+ const { reaped, unconfirmed } = await reapSessionProcesses(projectPath, hints);
6836
6907
  removeSessionMarker(projectPath, browser);
6908
+ let detail;
6909
+ detail = unconfirmed.length ? `No dev pid on record. Killed orphaned browser process(es) from the profile dir, but ${unconfirmed.length} still report alive 250 ms later (pids ${unconfirmed.join(", ")})${reaped.length ? `; ${reaped.length} confirmed gone` : ""}.` : reaped.length ? `No dev pid on record, but reaped ${reaped.length} orphaned browser process(es) from the profile dir, each confirmed gone.` : "No known session for this project/browser (nothing registered in this server and no ready.json contract found).";
6837
6910
  return {
6838
6911
  projectPath,
6839
6912
  browser,
6840
6913
  pid: null,
6841
- stopped: 0 !== reaped.length,
6914
+ stopped: reaped.length > 0 && 0 === unconfirmed.length,
6842
6915
  reaped,
6916
+ ...unconfirmed.length ? {
6917
+ reapUnconfirmed: unconfirmed
6918
+ } : {},
6843
6919
  ...cleanCarrier(projectPath),
6844
- detail: 0 === reaped.length ? "No known session for this project/browser (nothing registered in this server and no ready.json contract found)." : `No dev pid on record, but reaped ${reaped.length} orphaned browser process(es) from the profile dir.`
6920
+ detail
6845
6921
  };
6846
6922
  }
6847
6923
  let detail;
@@ -6875,7 +6951,7 @@ async function stopOne(projectPath, browser) {
6875
6951
  }
6876
6952
  detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
6877
6953
  }
6878
- const reaped = reapSessionProcesses(projectPath, hints);
6954
+ const { reaped } = await reapSessionProcesses(projectPath, hints);
6879
6955
  const { pids: survivors, verified } = sessionProcessPids(projectPath, hints);
6880
6956
  const dead = "dead" === pidState(pid);
6881
6957
  const stopped = dead && verified && 0 === survivors.length;
@@ -7157,7 +7233,7 @@ async function dev_handler(args) {
7157
7233
  return spawnFailedEnvelope(dev_schema.name, spawned);
7158
7234
  }
7159
7235
  const pid = child.pid;
7160
- registerSession({
7236
+ let markerWarning = registerSession({
7161
7237
  pid,
7162
7238
  browser,
7163
7239
  port: args.port,
@@ -7351,7 +7427,7 @@ async function dev_handler(args) {
7351
7427
  unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, inspect, wait, doctor)"
7352
7428
  };
7353
7429
  const boundPort = contractBoundPort(args.projectPath, browser, spawnedAt);
7354
- if (null !== boundPort && boundPort !== args.port) registerSession({
7430
+ if (null !== boundPort && boundPort !== args.port) markerWarning = registerSession({
7355
7431
  pid,
7356
7432
  browser,
7357
7433
  port: boundPort,
@@ -7395,7 +7471,8 @@ async function dev_handler(args) {
7395
7471
  warnings: [
7396
7472
  portNote,
7397
7473
  profileReused && !args.noBrowser ? REUSED_PROFILE_NOTE : null,
7398
- ...boot.warnings
7474
+ ...boot.warnings,
7475
+ markerWarning
7399
7476
  ],
7400
7477
  hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/dom_snapshot/eval) need a live browser and will not work against this session. When you are done, call extension_stop to shut down the dev server." : "Use extension_wait to check when the extension is fully loaded, then extension_inspect to inspect the live state. " + (allowControl ? controlBound ? `Control channel is ON (port ${control.port} per ready.json): extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : control.unavailableReason ? `Control was requested, but the engine reports no control port (${control.unavailableReason}), so extension_${controlVerbs.split(", ").join("/extension_")} will be refused until the session is relaunched.` : `Control was requested; ready.json has not stamped the control port yet, so extension_wait is what confirms extension_${controlVerbs.split(", ").join("/extension_")} will work.` : "Control channel is OFF: extension_storage/reload/open/dom_snapshot need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser."
7401
7478
  });
@@ -7420,12 +7497,12 @@ const start_schema = {
7420
7497
  },
7421
7498
  port: {
7422
7499
  type: "number",
7423
- description: "Server port (0 for auto-assign)"
7500
+ description: "Passed to the engine as --port (0 for auto-assign). A production start serves nothing over it today; it matters only to a toolchain that reads it."
7424
7501
  },
7425
7502
  noBrowser: {
7426
7503
  type: "boolean",
7427
7504
  default: false,
7428
- description: "Serve without launching a browser"
7505
+ description: "Build (or, with build:false, check the dist) without launching a browser. A production start serves nothing, so with no browser the engine process ends once the build does; read the result with extension_build rather than a session."
7429
7506
  },
7430
7507
  outputPath: {
7431
7508
  type: "string",
@@ -7457,6 +7534,26 @@ async function start_handler(args) {
7457
7534
  }
7458
7535
  const building = false !== args.build && !outputPath;
7459
7536
  const command = building ? "start" : "preview";
7537
+ if ("safari" === browser || "webkit-based" === browser) return envelope_envelope({
7538
+ ok: false,
7539
+ command: start_schema.name,
7540
+ status: "unsupported-browser",
7541
+ error: {
7542
+ code: "E_UNSUPPORTED_BROWSER",
7543
+ message: `The engine's ${command} verb does not run Safari; extension_dev builds, packages and opens the Safari app.`
7544
+ },
7545
+ hint: "Use extension_dev with browser: \"safari\", or extension_build with browser: \"safari\" for the packaged app."
7546
+ });
7547
+ if (!building && (args.host || args.publicHost)) return envelope_envelope({
7548
+ ok: false,
7549
+ command: start_schema.name,
7550
+ status: "bad-request",
7551
+ error: {
7552
+ code: "E_BAD_REQUEST",
7553
+ message: "host and publicHost are not options of the engine's preview verb, which build: false and outputPath run; they apply only when start builds."
7554
+ },
7555
+ hint: "Drop host and publicHost, or let extension_start build (build: true, no outputPath)."
7556
+ });
7460
7557
  const cliArgs = [
7461
7558
  command,
7462
7559
  args.projectPath,
@@ -7477,7 +7574,7 @@ async function start_handler(args) {
7477
7574
  const { child, logPath } = spawned;
7478
7575
  if (void 0 === child.pid) return spawnFailedEnvelope(start_schema.name, spawned);
7479
7576
  const pid = child.pid;
7480
- registerSession({
7577
+ const markerWarning = registerSession({
7481
7578
  pid,
7482
7579
  browser,
7483
7580
  projectPath: args.projectPath,
@@ -7607,16 +7704,19 @@ async function start_handler(args) {
7607
7704
  return envelope_envelope({
7608
7705
  ok: true,
7609
7706
  command: start_schema.name,
7610
- status: building ? "started" : "launched",
7707
+ status: "started",
7611
7708
  value: {
7612
7709
  pid,
7613
7710
  browser,
7614
7711
  projectPath: args.projectPath,
7615
- logPath
7712
+ logPath,
7713
+ verb: command,
7714
+ observed: "the engine process was alive 5 s after it was spawned"
7616
7715
  },
7617
- hint: building ? "Use extension_wait to check when the build and browser launch are complete. When you are done, call extension_stop to shut down the session." : "Call extension_stop when you are done to close the preview browser.",
7716
+ hint: building ? "The engine process was alive 5 s after spawn, which is all this answer knows; extension_wait reads whether the build landed. When you are done, call extension_stop to shut the session down." : "The engine's preview process was alive 5 s after spawn, which is all this answer knows: whether a browser shows the extension is not read here. Call extension_stop when you are done.",
7618
7717
  warnings: [
7619
7718
  ...boot.warnings,
7719
+ markerWarning,
7620
7720
  stale.removed && "Removed a Live Preview carrier left behind by an earlier dev session, so it was not loaded beside your extension here.",
7621
7721
  !stale.removed && stale.note && /Could not remove/i.test(stale.note) && `${stale.note} A Live Preview carrier is still in ./extensions and the engine loads that folder, so this run has the debug companion beside your extension.`
7622
7722
  ]
@@ -7719,11 +7819,21 @@ async function runActVerb(args, projectPath, timeoutMs, tool) {
7719
7819
  "json",
7720
7820
  ...args.slice(separatorAt)
7721
7821
  ];
7722
- const { code, stdout, stderr } = await runExtensionCli(cliArgs, {
7822
+ const { code, stdout, stderr, timedOut, signal } = await runExtensionCli(cliArgs, {
7723
7823
  cwd: projectPath,
7724
7824
  timeoutMs
7725
7825
  });
7726
7826
  const out = stdout.trim();
7827
+ if (timedOut) return envelope_envelope({
7828
+ ok: false,
7829
+ command,
7830
+ status: "cli-timeout",
7831
+ error: {
7832
+ code: "E_CLI",
7833
+ name: "CliTimeout",
7834
+ message: `This server stopped the extension CLI after ${timeoutMs ?? 30000} ms (${signal ?? "a signal"}) before it answered. The ${args[0]} verb may already have acted; read the state it changes before repeating it.`
7835
+ }
7836
+ });
7727
7837
  if (out) try {
7728
7838
  const frame = translateFrame(JSON.parse(out), projectPath, browserFlag(args));
7729
7839
  if (isEnvelope(frame)) {
@@ -7744,6 +7854,16 @@ async function runActVerb(args, projectPath, timeoutMs, tool) {
7744
7854
  },
7745
7855
  hint: "extension_doctor reports the project's engine version next to the one this server pins."
7746
7856
  });
7857
+ if (0 === code && out) return envelope_envelope({
7858
+ ok: false,
7859
+ command,
7860
+ status: "cli-unreadable",
7861
+ error: {
7862
+ code: "E_CLI",
7863
+ name: "CliUnreadable",
7864
+ message: `The extension CLI exited 0 but its answer could not be read as JSON (${out.slice(0, 200)}). The ${args[0]} verb may have acted; read the state it changes before repeating it.`
7865
+ }
7866
+ });
7747
7867
  const message = stderr.trim() || `extension exited with code ${code}`;
7748
7868
  return envelope_envelope({
7749
7869
  ok: false,
@@ -9003,6 +9123,10 @@ const WORKER_TARGET_TYPES = new Set([
9003
9123
  "background_page",
9004
9124
  "worker"
9005
9125
  ]);
9126
+ const BACKGROUND_TARGET_TYPES = new Set([
9127
+ "service_worker",
9128
+ "background_page"
9129
+ ]);
9006
9130
  async function readExtensionWorkerTargets(port, extensionId) {
9007
9131
  try {
9008
9132
  const origin = `chrome-extension://${extensionId}/`;
@@ -9079,6 +9203,11 @@ async function findExtensionPageTargets(port, wantedUrl) {
9079
9203
  const read = await readExtensionPageTargets(port, wantedUrl);
9080
9204
  return "targets" in read ? read.targets : [];
9081
9205
  }
9206
+ function remoteValueNote(result) {
9207
+ if (!result) return;
9208
+ if ("undefined" === result.type) return "the expression returned undefined, which is answered as null";
9209
+ if ("string" == typeof result.unserializableValue) return `the expression returned ${result.unserializableValue}${"bigint" === result.type ? " (a BigInt)" : ""}, which JSON cannot carry, so value holds it as the string "${result.unserializableValue}"`;
9210
+ }
9082
9211
  function readRemoteValue(result) {
9083
9212
  if (!result) return null;
9084
9213
  if ("value" in result) return result.value;
@@ -9122,9 +9251,13 @@ async function evaluateOnExtensionPage(port, targetId, expression, timeoutMs) {
9122
9251
  thrown: true,
9123
9252
  message: describeException(response.exceptionDetails)
9124
9253
  };
9254
+ const note = remoteValueNote(response?.result);
9125
9255
  return {
9126
9256
  ok: true,
9127
- value: readRemoteValue(response?.result)
9257
+ value: readRemoteValue(response?.result),
9258
+ ...note ? {
9259
+ note
9260
+ } : {}
9128
9261
  };
9129
9262
  } catch (error) {
9130
9263
  const message = error instanceof Error ? error.message : String(error);
@@ -9361,11 +9494,18 @@ async function openDevToolsPanel(port, options) {
9361
9494
  reloadedInspected = false;
9362
9495
  }
9363
9496
  }
9497
+ let lastEvaluateError = null;
9364
9498
  const evaluate = async (expression)=>await cdp.sendCommand("Runtime.evaluate", {
9365
9499
  expression,
9366
9500
  returnByValue: true,
9367
9501
  awaitPromise: true
9368
- }, sessionId).catch(()=>null);
9502
+ }, sessionId).catch((err)=>{
9503
+ lastEvaluateError = err instanceof Error ? err.message : String(err);
9504
+ return null;
9505
+ });
9506
+ let registryAnswered = false;
9507
+ let registryError = null;
9508
+ let devtoolsPageLoaded = false;
9369
9509
  const wantedId = "string" == typeof options.panelTitle && options.panelTitle ? `${panelIdPrefix(options.extensionId)}${options.panelTitle}` : null;
9370
9510
  const prefix = panelIdPrefix(options.extensionId);
9371
9511
  let panels = [];
@@ -9375,7 +9515,11 @@ async function openDevToolsPanel(port, options) {
9375
9515
  while(!panelId && Date.now() < deadline){
9376
9516
  const response = await evaluate(PANEL_IDS_EXPRESSION);
9377
9517
  const ids = response?.result?.value;
9518
+ if (response?.exceptionDetails) registryError = response.exceptionDetails.exception?.description ?? response.exceptionDetails.text ?? "the registry read threw";
9519
+ else if (!response) registryError = lastEvaluateError ?? "the registry read did not answer";
9520
+ if (!devtoolsPageLoaded) devtoolsPageLoaded = (await listTargets().catch(()=>[])).some((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(options.devtoolsPageUrl));
9378
9521
  if (Array.isArray(ids)) {
9522
+ registryAnswered = true;
9379
9523
  panels = ids.map(String);
9380
9524
  panelId = panels.find((id)=>wantedId ? id === wantedId : id.startsWith(prefix)) ?? null;
9381
9525
  }
@@ -9398,7 +9542,11 @@ async function openDevToolsPanel(port, options) {
9398
9542
  stage: "panel",
9399
9543
  devtoolsTargetId,
9400
9544
  panels,
9401
- reason: wantedId ? `no panel with id ${wantedId} registered within ${budgetMs}ms` : `no panel from ${prefix} registered within ${budgetMs}ms`
9545
+ devtoolsPageLoaded,
9546
+ ...!registryAnswered && registryError ? {
9547
+ registryUnreadable: registryError
9548
+ } : {},
9549
+ reason: registryAnswered ? devtoolsPageLoaded ? wantedId ? `no panel with id ${wantedId} registered within ${budgetMs}ms` : `no panel from ${prefix} registered within ${budgetMs}ms` : `the devtools page ${options.devtoolsPageUrl} never appeared as a frame within ${budgetMs}ms, so nothing could register a panel` : `the DevTools panel registry could not be read (${registryError ?? "no answer"}), so whether a panel registered is unknown`
9402
9550
  };
9403
9551
  reached = "panel";
9404
9552
  const framesBefore = new Set((await listTargets().catch(()=>[])).filter((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(`${prefix}/`)).map((t)=>t.id));
@@ -9412,15 +9560,31 @@ async function openDevToolsPanel(port, options) {
9412
9560
  };
9413
9561
  reached = "show";
9414
9562
  let panelTarget = null;
9563
+ let panelTargetInferred = false;
9564
+ let panelFrameCandidates = 0;
9415
9565
  const showDeadline = Date.now() + 3000;
9416
- while(!panelTarget && Date.now() < showDeadline){
9566
+ for(;;){
9417
9567
  const frames = (await listTargets().catch(()=>[])).filter((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(`${prefix}/`) && !String(t.url ?? "").startsWith(options.devtoolsPageUrl));
9418
- const fresh = frames.find((t)=>!framesBefore.has(t.id)) ?? frames[0];
9419
- if (fresh) panelTarget = {
9420
- targetId: fresh.id,
9421
- url: String(fresh.url)
9422
- };
9423
- else await sleep(200);
9568
+ panelFrameCandidates = frames.length;
9569
+ const fresh = frames.find((t)=>!framesBefore.has(t.id));
9570
+ if (fresh) {
9571
+ panelTarget = {
9572
+ targetId: fresh.id,
9573
+ url: String(fresh.url)
9574
+ };
9575
+ break;
9576
+ }
9577
+ if (Date.now() >= showDeadline) {
9578
+ if (1 === frames.length) {
9579
+ panelTarget = {
9580
+ targetId: frames[0].id,
9581
+ url: String(frames[0].url)
9582
+ };
9583
+ panelTargetInferred = true;
9584
+ }
9585
+ break;
9586
+ }
9587
+ await sleep(200);
9424
9588
  }
9425
9589
  return {
9426
9590
  opened: true,
@@ -9428,6 +9592,10 @@ async function openDevToolsPanel(port, options) {
9428
9592
  panelId,
9429
9593
  panelTitle: panelTitleFromId(panelId, options.extensionId),
9430
9594
  panelTarget,
9595
+ ...panelTargetInferred ? {
9596
+ panelTargetInferred
9597
+ } : {},
9598
+ panelFrameCandidates,
9431
9599
  panels,
9432
9600
  reloadedInspected
9433
9601
  };
@@ -10354,7 +10522,7 @@ async function openDevToolsSurface(args, browser) {
10354
10522
  error: {
10355
10523
  code: "E_SURFACE_DID_NOT_OPEN",
10356
10524
  name: "PanelDidNotRegister",
10357
- message: "panel" === outcome.stage ? `DevTools opened on ${inspected.url} and ${doc} loaded in it, but ${outcome.reason}.${registered.length ? ` Panels this extension did register: ${registered.map((id)=>id.slice(`chrome-extension://${extensionId}`.length)).join(", ")}.` : ""}` : `DevTools opened on ${inspected.url} and the panel registered, but ${outcome.reason}.`
10525
+ message: "panel" === outcome.stage ? `DevTools opened on ${inspected.url}${false === outcome.devtoolsPageLoaded ? "" : ` and ${doc} loaded in it`}, but ${outcome.reason}.${registered.length ? ` Panels this extension did register: ${registered.map((id)=>id.slice(`chrome-extension://${extensionId}`.length)).join(", ")}.` : ""}` : `DevTools opened on ${inspected.url} and the panel registered, but ${outcome.reason}.`
10358
10526
  },
10359
10527
  value: {
10360
10528
  inspected: {
@@ -10364,7 +10532,7 @@ async function openDevToolsSurface(args, browser) {
10364
10532
  devtoolsTargetId: outcome.devtoolsTargetId ?? null,
10365
10533
  panels: outcome.panels ?? []
10366
10534
  },
10367
- hint: args.panel && registered.length ? `No panel is titled "${args.panel}"; pass one of the registered titles as \`panel\`, or omit it for the first.` : `The devtools page registers panels with chrome.devtools.panels.create; extension_logs (context: ['devtools']) shows what ${doc} wrote or threw. An extension that creates its panel only when the page reports to it (Preact Devtools does, through its content script) needs the page loaded with DevTools already open: retry with reload: true, and waitMs for a longer wait. DevTools stays open on the tab.`
10535
+ hint: "panel" === outcome.stage && outcome.registryUnreadable ? "This browser build's DevTools frontend did not answer the panel-registry read this server uses, so the panel may well exist; open DevTools by hand in a headed session to see it, and extension_logs (context: ['devtools']) reads the devtools page either way." : args.panel && registered.length ? `No panel is titled "${args.panel}"; pass one of the registered titles as \`panel\`, or omit it for the first.` : `The devtools page registers panels with chrome.devtools.panels.create; extension_logs (context: ['devtools']) shows what ${doc} wrote or threw. An extension that creates its panel only when the page reports to it (Preact Devtools does, through its content script) needs the page loaded with DevTools already open: retry with reload: true, and waitMs for a longer wait. DevTools stays open on the tab.`
10368
10536
  });
10369
10537
  }
10370
10538
  const panelUrl = outcome.panelTarget?.url ?? null;
@@ -10394,8 +10562,10 @@ async function openDevToolsSurface(args, browser) {
10394
10562
  ...true !== args.reload || outcome.reloadedInspected ? [] : [
10395
10563
  "reload: true was asked for, but the inspected tab could not be reloaded (the attach or Page.reload failed), so the panel was read without a reload."
10396
10564
  ],
10397
- ...outcome.panelTarget ? [] : [
10398
- "The panel is shown but its document target had not appeared within 3s; call extension_open surface: \"devtools\" again once it loads to get its url."
10565
+ ...outcome.panelTarget ? outcome.panelTargetInferred ? [
10566
+ "No new panel frame appeared after showPanel; the one frame of this extension already inside DevTools was taken as the panel, which is an inference, not a match."
10567
+ ] : [] : [
10568
+ (outcome.panelFrameCandidates ?? 0) > 1 ? `The panel is shown but ${outcome.panelFrameCandidates} frames of this extension sit inside DevTools and none appeared after showPanel, so which one is the panel is unknown; its url is not given.` : "The panel is shown but its document target had not appeared within 3s; call extension_open surface: \"devtools\" again once it loads to get its url."
10399
10569
  ]
10400
10570
  ],
10401
10571
  hint: panelUrl ? `DevTools is open on ${inspected.url} with the "${outcome.panelTitle}" panel shown. Read the panel with extension_eval context: "page", url: "${panelUrl}" (over CDP, with chrome.devtools available); context: "devtools" reads the devtools page itself (${doc}), the hidden document that registered the panel. The panel is an iframe inside DevTools, not a tab, so extension_dom_snapshot and extension_inspect do not reach it.` : `DevTools is open on ${inspected.url} with the "${outcome.panelTitle}" panel shown. context: "devtools" on extension_eval reads the devtools page itself (${doc}).`
@@ -10734,15 +10904,29 @@ async function listArtifacts(options = {}) {
10734
10904
  message: `Listing shares failed (${res.status}): ${data?.message || "unknown error"}`
10735
10905
  }
10736
10906
  };
10737
- const artifacts = Array.isArray(data.artifacts) ? data.artifacts.map((artifact)=>artifact && "string" == typeof artifact.revokeUrl ? {
10907
+ if (!Array.isArray(data.artifacts)) return {
10908
+ ok: false,
10909
+ error: {
10910
+ name: "SharesListError",
10911
+ status: res.status,
10912
+ message: `Listing shares answered ${res.status} without an artifacts list, so which shares exist is unknown.`
10913
+ }
10914
+ };
10915
+ const rows = data.artifacts;
10916
+ const valid = rows.filter((artifact)=>artifact && "string" == typeof artifact.artifactId);
10917
+ const malformedRows = rows.length - valid.length;
10918
+ const artifacts = valid.map((artifact)=>"string" == typeof artifact.revokeUrl ? {
10738
10919
  ...artifact,
10739
10920
  revokeUrl: wwwRevokeUrl(artifact.revokeUrl)
10740
- } : artifact) : [];
10921
+ } : artifact);
10741
10922
  const num = (value, fallback)=>"number" == typeof value && Number.isFinite(value) ? value : fallback;
10742
10923
  return {
10743
10924
  ok: true,
10744
10925
  data: {
10745
10926
  artifacts,
10927
+ ...malformedRows ? {
10928
+ malformedRows
10929
+ } : {},
10746
10930
  count: num(data.count, artifacts.length),
10747
10931
  matched: num(data.matched, artifacts.length),
10748
10932
  limit: num(data.limit, artifacts.length),
@@ -12312,7 +12496,8 @@ async function listShares(args) {
12312
12496
  "previewUrl and zipUrl are null for a share that is no longer live, because a revoked or expired link cannot resolve for anyone. revokeUrl stays on every row. Revocation is permanent: a revoked id is burned, and re-sharing that build mints a different link. Re-sharing a build that was NOT revoked returns its existing link instead.",
12313
12497
  "attribution.ownership says who the share belongs to and therefore who may revoke it: project means the owning workspace holds it and any member can pull it back, personal means one person holds it alone. attribution.credit names the publisher and is attribution only, granting and restricting nothing. A credit of \"CLI token ...\" means the platform could not resolve which human minted that token, and a credit of \"not recorded\" means it never knew; neither is a name, and neither should be reported as one.",
12314
12498
  handsOutAZip ? ZIP_URL_REDIRECT_NOTE : null,
12315
- truncatedNote
12499
+ truncatedNote,
12500
+ listing.data.malformedRows ? `${listing.data.malformedRows} row(s) of the platform's listing carried no artifact id and were left out, so the list may be short by that many.` : null
12316
12501
  ]
12317
12502
  });
12318
12503
  }
@@ -12382,7 +12567,7 @@ async function revokeShare(args) {
12382
12567
  });
12383
12568
  const local = args.projectPath ? readSharedPreviews(args.projectPath) : void 0;
12384
12569
  const entry = local ? localIndex(local.entries).get(ref) : void 0;
12385
- const recordNote = local ? entry ? `${local.path} still lists this share as its own append-only history and was not rewritten, so the entry stays with its original sharedAt. The platform is the truth for whether a link resolves.` : `${local.path} has no entry for this share, so it was made from another machine or another checkout.` : void 0;
12570
+ const recordNote = local ? local.unreadable ? `${local.path} exists but could not be parsed, so whether this project recorded the share is unknown.` : entry ? `${local.path} still lists this share as its own append-only history and was not rewritten, so the entry stays with its original sharedAt. The platform is the truth for whether a link resolves.` : `${local.path} has no entry for this share, so it was made from another machine or another checkout.` : void 0;
12386
12571
  if (!result.ok) {
12387
12572
  if (result.held) return platformHoldEnvelope({
12388
12573
  command: "extension_shares",
@@ -12727,7 +12912,13 @@ function verdictSentence(verdict) {
12727
12912
  return `${parts.join(", ")}.`;
12728
12913
  }
12729
12914
  function logs_filter_readLogEvents(projectPath, browser, query) {
12730
- return readLogEvents(engineProjectRoot(projectPath), browser, query);
12915
+ return readLogEvents(engineProjectRoot(projectPath), browser, query).filter((event)=>"string" != typeof event.type);
12916
+ }
12917
+ function readLogDropped(projectPath, browser) {
12918
+ return readLogEvents(engineProjectRoot(projectPath), browser, {}).reduce((sum, event)=>{
12919
+ const record = event;
12920
+ return "gap" === record.type && "number" == typeof record.dropped ? sum + record.dropped : sum;
12921
+ }, 0);
12731
12922
  }
12732
12923
  function makeFilter(args) {
12733
12924
  const level = String(args.level || "all").toLowerCase();
@@ -13046,6 +13237,11 @@ async function readFromStream(args, browser, limit) {
13046
13237
  let connectedAt = Date.now();
13047
13238
  let replayed = 0;
13048
13239
  let liveCount = 0;
13240
+ const startedAt = Date.now();
13241
+ let opened = false;
13242
+ let readySeen = false;
13243
+ let bufferedFrom;
13244
+ let evicted;
13049
13245
  return await new Promise((resolve)=>{
13050
13246
  let settled = false;
13051
13247
  const url = `ws://127.0.0.1:${ready.controlPort}${CONTROL_WS_PATH}`;
@@ -13072,14 +13268,27 @@ async function readFromStream(args, browser, limit) {
13072
13268
  try {
13073
13269
  socket.close();
13074
13270
  } catch {}
13075
- resolve(summarize(events, "stream", browser, runId, limit, dropped, args.projectPath, void 0, streamNote, {
13271
+ resolve(summarize(events, "stream", browser, runId, limit, dropped, args.projectPath, void 0, [
13272
+ streamNote,
13273
+ opened && !readySeen ? "The control channel opened but the broker never sent its ready frame, so whether the replay covered the run is unknown." : null,
13274
+ "number" == typeof evicted && evicted > 0 ? `The broker's ring had already evicted ${evicted} older event(s) before this read${"number" == typeof bufferedFrom ? ` (it holds from seq ${bufferedFrom})` : ""}; read the file (call without follow) for those.` : null
13275
+ ].filter((note)=>Boolean(note)).join(" ") || void 0, {
13076
13276
  replayed,
13077
13277
  live: liveCount,
13078
- followMs
13278
+ followMs,
13279
+ opened,
13280
+ readySeen,
13281
+ ..."number" == typeof bufferedFrom ? {
13282
+ bufferedFrom
13283
+ } : {},
13284
+ ..."number" == typeof evicted ? {
13285
+ evicted
13286
+ } : {}
13079
13287
  }));
13080
13288
  };
13081
13289
  const timer = setTimeout(finish, followMs);
13082
13290
  socket.on("open", ()=>{
13291
+ opened = true;
13083
13292
  connectedAt = Date.now();
13084
13293
  try {
13085
13294
  socket.send(JSON.stringify({
@@ -13097,8 +13306,12 @@ async function readFromStream(args, browser, limit) {
13097
13306
  } catch {
13098
13307
  return;
13099
13308
  }
13100
- if ("ready" === frame.type && frame.runId) runId = String(frame.runId);
13101
- else if ("log" === frame.type && frame.event) {
13309
+ if ("ready" === frame.type) {
13310
+ readySeen = true;
13311
+ if (frame.runId) runId = String(frame.runId);
13312
+ if ("number" == typeof frame.bufferedFrom) bufferedFrom = frame.bufferedFrom;
13313
+ if ("number" == typeof frame.evicted) evicted = frame.evicted;
13314
+ } else if ("log" === frame.type && frame.event) {
13102
13315
  if (matches(frame.event)) {
13103
13316
  events.push(frame.event);
13104
13317
  const at = Number(frame.event.timestamp);
@@ -13133,7 +13346,12 @@ async function readFromStream(args, browser, limit) {
13133
13346
  socket.on("close", (code, reason)=>{
13134
13347
  if (settled) return;
13135
13348
  const refusal = controlRefusal(code, reason?.toString() ?? "", url, CONTROL_ENVELOPE_VERSION);
13136
- if (!refusal) return void finish();
13349
+ if (!refusal) {
13350
+ const elapsed = Date.now() - startedAt;
13351
+ if (elapsed < followMs - 250) streamNote = `The control channel closed after ${Math.round(elapsed / 1000)} s of the ${Math.round(followMs / 1000)} s window (close code ${code}${reason?.length ? `, "${reason.toString()}"` : ""}), so this is a partial read; the dev session may have stopped or restarted. Re-check with extension_wait.`;
13352
+ finish();
13353
+ return;
13354
+ }
13137
13355
  if (events.length > 0 || dropped > 0) {
13138
13356
  streamNote = refusal.message;
13139
13357
  finish();
@@ -13310,6 +13528,19 @@ function parseClauses(raw) {
13310
13528
  const context = void 0 === clause.context ? void 0 : Array.isArray(clause.context) ? clause.context.map(String) : [
13311
13529
  String(clause.context)
13312
13530
  ];
13531
+ if (void 0 !== context) try {
13532
+ makeFilter({
13533
+ context
13534
+ })({
13535
+ context: "background",
13536
+ level: "info",
13537
+ seq: 0,
13538
+ ts: 0
13539
+ });
13540
+ } catch (error) {
13541
+ issues.push(`${at}.context: ${error instanceof Error ? error.message : String(error)}`);
13542
+ return;
13543
+ }
13313
13544
  const since = void 0 === clause.since ? void 0 : Number(clause.since);
13314
13545
  if (void 0 !== since && !Number.isFinite(since)) return void issues.push(`${at}.since must be a number, the seq cursor to read from`);
13315
13546
  const ignore = void 0 === clause.ignore ? void 0 : Array.isArray(clause.ignore) ? clause.ignore.map(String) : [
@@ -13916,6 +14147,12 @@ function assertConsoleErrorsEmpty(clause, stage) {
13916
14147
  errors: kept.slice(0, 20),
13917
14148
  runId
13918
14149
  });
14150
+ const droppedLines = readLogDropped(stage.projectPath, stage.browser);
14151
+ if (droppedLines > 0) return inconclusiveCheck(id, subject, `No error-level log event${scope} among the ${all.length} event(s) that reached the file in run ${runId || "(unnamed)"}, but the writer dropped ${droppedLines} line(s) when it fell behind, and any of them may have been an error.`, "Read extension_logs with follow: true while driving the extension, which streams lines the file writer dropped, then assert again.", {
14152
+ events: all.length,
14153
+ dropped: droppedLines,
14154
+ runId
14155
+ });
13919
14156
  return passCheck(id, subject, `No error-level log event${scope} among ${all.length} event(s) in run ${runId || "(unnamed)"}${ignored.length && errors.length ? `, and ${errors.length} matched an ignore entry and were not counted` : ""}${foreignErrors ? `; ${foreignErrors} browser-relayed error(s) from other origins were not counted` : ""}.`, {
13920
14157
  events: all.length,
13921
14158
  ignored: errors.length - kept.length,
@@ -16367,8 +16604,17 @@ function closedShadowWalkerCode(cap) {
16367
16604
  })();
16368
16605
  `;
16369
16606
  }
16607
+ function matchPatternRegexSource(pattern) {
16608
+ if ("<all_urls>" === pattern) return "^(https?|file|ftp):";
16609
+ const m = /^(\*|https?|file|ftp|wss?):\/\/([^/]*)(\/.*)?$/.exec(pattern);
16610
+ if (!m) return "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$";
16611
+ const scheme = "*" === m[1] ? "https?" : m[1];
16612
+ const host = "*" === m[2] ? "[^/]*" : m[2].startsWith("*.") ? "([^/]*\\.)?" + m[2].slice(2).replace(/[.]/g, "\\.") : m[2].replace(/[.]/g, "\\.");
16613
+ const pathPart = (m[3] ?? "/").replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
16614
+ return "^" + scheme + "://" + host + pathPart + "$";
16615
+ }
16370
16616
  function executeScriptExpression(urlFilter, code, tabId) {
16371
- const pick = "number" == typeof tabId ? `tabs.find(function (t) { return t.id === ${tabId}; })` : urlFilter ? `tabs.find(function (t) { return String(t.url || "").toLowerCase().indexOf(${JSON.stringify(urlFilter.toLowerCase())}) !== -1; })` : "(tabs.find(function (t) { return t.active; }) || tabs[0])";
16617
+ const pick = "number" == typeof tabId ? `tabs.find(function (t) { return t.id === ${tabId}; })` : urlFilter ? /\*|<all_urls>/.test(urlFilter) ? `tabs.find(function (t) { return new RegExp(${JSON.stringify(matchPatternRegexSource(urlFilter))}).test(String(t.url || "")); })` : `tabs.find(function (t) { return String(t.url || "").toLowerCase().indexOf(${JSON.stringify(urlFilter.toLowerCase())}) !== -1; })` : "(tabs.find(function (t) { return t.active; }) || tabs[0])";
16372
16618
  return `browser.tabs.query({}).then(function (tabs) {
16373
16619
  var tab = ${pick};
16374
16620
  if (!tab) return { error: "no matching tab" };
@@ -17043,6 +17289,7 @@ async function listGeckoExtensions(projectPath, browser) {
17043
17289
  const temporary = extensions.filter((e)=>e.temporarilyInstalled);
17044
17290
  if (1 === temporary.length) {
17045
17291
  temporary[0].ownExtension = true;
17292
+ temporary[0].ownExtensionInferred = true;
17046
17293
  if (void 0 === temporary[0].name && own?.name !== void 0) {
17047
17294
  temporary[0].name = own.name;
17048
17295
  if (void 0 !== own.version) temporary[0].version = own.version;
@@ -17064,9 +17311,13 @@ async function listGeckoExtensions(projectPath, browser) {
17064
17311
  browser,
17065
17312
  count: extensions.length,
17066
17313
  ownExtensionId: ownEntry?.id ?? null,
17314
+ ...ownEntry?.ownExtensionInferred ? {
17315
+ ownExtensionInferred: true
17316
+ } : {},
17067
17317
  extensions
17068
17318
  },
17069
17319
  warnings: [
17320
+ ownEntry?.ownExtensionInferred ? `No installed add-on carries this session's name${own?.name ? ` ("${own.name}")` : ""}, so ${ownEntry.id} is marked ownExtension only because it is the one temporary install. That is an inference: if this project's add-on failed to install, it may be another temporary add-on such as the engine's companion. extension_logs shows whether the install succeeded.` : null,
17070
17321
  "Lists INSTALLED add-ons via the RDP root actor (listAddons), regardless of whether a context is currently live, so entries carry no contexts. temporarilyInstalled marks temporary loads; ownExtension marks the extension this dev session serves, matched from the session's ready contract. Add-ons are never attached to or evaluated in."
17071
17322
  ]
17072
17323
  });
@@ -17147,7 +17398,7 @@ function readRelayFrame(value) {
17147
17398
  }
17148
17399
  const eval_schema = {
17149
17400
  name: "extension_eval",
17150
- description: "Evaluate an expression in a running extension context. Start the session with allowEval:true (extension_dev), which writes a 0600 session token; without that token every route of this tool, the debug port included, answers eval-disabled. Context defaults to 'background', except on a Chromium MV3 session (the default template) where it defaults to 'page', the active tab, because the MV3 service worker CSP blocks eval; pass context:'background' to target the worker anyway and get that explanation back. For content and page, pass `url` to pick the tab, or omit both `url` and `tab` for the active tab; a numeric `tab` only disambiguates. Extension surfaces (popup, options, sidebar, devtools) and override pages (newtab, history, bookmarks) need no tab id but must already be open: open one with extension_open first, because a closed one returns an explicit error. On a Chromium MV3 session those pages, and context:'page' with a chrome-extension:// url, evaluate over CDP, the inspector path the extension page CSP does not govern; elsewhere they evaluate over the in-bundle relay. On Firefox a document whose content security policy forbids eval (the extension's own pages, or a site's) is evaluated over the debugger protocol instead, which takes one expression; a page inside the extension that is no declared surface (pages/*) is reached the same way by context:'page' and its moz-extension:// url once a tab shows it. Call extension_dom_snapshot with listTabs:true to enumerate {tabId, url, title}.",
17401
+ description: "Evaluate an expression in a running extension context. Start the session with allowEval:true (extension_dev), which writes a 0600 session token; without that token every route of this tool, the debug port included, answers eval-disabled. Context defaults to 'background', except on a Chromium MV3 session (the default template) where it defaults to 'page', the active tab; pass context:'background' to evaluate in the service worker, which on Chromium goes over the debug port. Debug-port evaluates run with a user gesture, so gesture-gated APIs (permissions.request, sidePanel.open) can succeed here and still fail when the extension's own code calls them. For content and page, pass `url` to pick the tab, or omit both `url` and `tab` for the active tab; a numeric `tab` only disambiguates. Extension surfaces (popup, options, sidebar, devtools) and override pages (newtab, history, bookmarks) need no tab id but must already be open: open one with extension_open first, because a closed one returns an explicit error. On a Chromium MV3 session those pages, and context:'page' with a chrome-extension:// url, evaluate over CDP, the inspector path the extension page CSP does not govern; elsewhere they evaluate over the in-bundle relay. On Firefox a document whose content security policy forbids eval (the extension's own pages, or a site's) is evaluated over the debugger protocol instead, which takes one expression; a page inside the extension that is no declared surface (pages/*) is reached the same way by context:'page' and its moz-extension:// url once a tab shows it. Call extension_dom_snapshot with listTabs:true to enumerate {tabId, url, title}.",
17151
17402
  inputSchema: {
17152
17403
  type: "object",
17153
17404
  properties: {
@@ -17257,23 +17508,24 @@ async function evaluateOnChromiumExtensionPage(args, browser, context, resolved)
17257
17508
  });
17258
17509
  const workersRead = await readExtensionWorkerTargets(resolved.port, extensionId);
17259
17510
  if ("unreadable" in workersRead) return eval_targetsUnreadable(workersRead.unreadable);
17260
- let workers = workersRead.targets;
17511
+ let workers = workersRead.targets.filter((t)=>BACKGROUND_TARGET_TYPES.has(t.type));
17261
17512
  const wakeWarnings = [];
17262
17513
  if (0 === workers.length) {
17263
17514
  const wake = await wakeExtensionWorker(resolved.port, extensionId);
17264
- if (!wake.woken) return envelope_envelope({
17515
+ const woken = wake.woken ? wake.targets.filter((t)=>BACKGROUND_TARGET_TYPES.has(t.type)) : [];
17516
+ if (!wake.woken || !(woken.length > 0)) return envelope_envelope({
17265
17517
  ok: false,
17266
17518
  command: eval_schema.name,
17267
17519
  status: "no-target",
17268
17520
  error: {
17269
17521
  code: "E_NO_TARGET",
17270
17522
  name: "NoTarget",
17271
- message: `No running background for chrome-extension://${extensionId}/: Chrome stops an idle MV3 service worker and lists no target for it, and starting it through ServiceWorker.startWorker did not bring one back: ${wake.reason}.`
17523
+ message: `No running background for chrome-extension://${extensionId}/: Chrome stops an idle MV3 service worker and lists no target for it, and starting it through ServiceWorker.startWorker did not bring one back: ${wake.woken ? "only dedicated workers were listed, which are not the background" : wake.reason}.`
17272
17524
  },
17273
17525
  hint: "Check the extension declares a background (extension_manifest_validate), then wake it with an event it listens to (extension_reload, open a surface with extension_open, navigate a matching tab) and retry. extension_logs (context: ['background']) holds what it wrote before it idled."
17274
17526
  });
17275
- workers = wake.targets;
17276
- wakeWarnings.push("The background worker was idle (Chrome stops an MV3 service worker after about 30 s without events) and was started through ServiceWorker.startWorker before evaluating; state it held before idling is gone unless it was persisted.");
17527
+ workers = woken;
17528
+ wakeWarnings.push("No running background worker was listed, so it was started through ServiceWorker.startWorker before evaluating. Chrome stops an idle MV3 service worker after about 30 s without events, but a worker that never started looks the same here, so this does not say it idled; any in-memory state from an earlier run of it is gone unless it was persisted.");
17277
17529
  }
17278
17530
  const target = workers[0];
17279
17531
  const outcome = await evaluateOnExtensionPage(resolved.port, target.targetId, args.expression, args.timeout);
@@ -17294,6 +17546,7 @@ async function evaluateOnChromiumExtensionPage(args, browser, context, resolved)
17294
17546
  status: "evaluated",
17295
17547
  value: outcome.value,
17296
17548
  warnings: [
17549
+ outcome.note ?? null,
17297
17550
  ...wakeWarnings,
17298
17551
  ...workers.length > 1 ? [
17299
17552
  `${workers.length} background targets match the extension; evaluated in ${target.type} ${target.targetId}.`
@@ -17376,7 +17629,10 @@ async function evaluateOnChromiumExtensionPage(args, browser, context, resolved)
17376
17629
  command: eval_schema.name,
17377
17630
  status: "evaluated",
17378
17631
  value: outcome.value,
17379
- warnings,
17632
+ warnings: [
17633
+ ...warnings,
17634
+ outcome.note ?? null
17635
+ ],
17380
17636
  hint: `Evaluated over CDP in ${target.url} (target ${target.targetId}), the inspector path the extension page CSP does not govern. The result is serialized by value, so return plain data rather than DOM nodes; a promise is awaited before returning.`
17381
17637
  });
17382
17638
  }
@@ -17451,9 +17707,12 @@ async function evaluateOnWebTarget(args, matches, url, browser) {
17451
17707
  command: eval_schema.name,
17452
17708
  status: "evaluated",
17453
17709
  value: outcome.value,
17454
- warnings: live.length > 1 ? [
17455
- `${live.length} tabs match ${url}; evaluated in ${target.url} (target ${target.targetId}). Narrow the url to pick another.`
17456
- ] : [],
17710
+ warnings: [
17711
+ ...live.length > 1 ? [
17712
+ `${live.length} tabs match ${url}; evaluated in ${target.url} (target ${target.targetId}). Narrow the url to pick another.`
17713
+ ] : [],
17714
+ outcome.note ?? null
17715
+ ],
17457
17716
  hint: `Evaluated over CDP in ${target.url} (target ${target.targetId}), the page's main world through the inspector, which the site's CSP and Trusted Types do not govern. Pass context: "content" for the extension's isolated world instead.`
17458
17717
  });
17459
17718
  }
@@ -17589,13 +17848,14 @@ async function evaluatePastSurfaceCsp(args, context, run) {
17589
17848
  return direct;
17590
17849
  }
17591
17850
  const sameDocumentUrl = (a, b)=>String(a ?? "").replace(/[?#].*$/, "") === b.replace(/[?#].*$/, "");
17592
- function tabByUrl(tabs, url) {
17851
+ function tabByUrl(tabs, url, seen) {
17593
17852
  const exact = tabs.filter((tab)=>sameDocumentUrl(tab.url, url));
17594
17853
  const covered = exact.length ? exact : tabs.filter((tab)=>matchPatternCovers(url, String(tab.url ?? "")));
17595
17854
  const candidates = covered.length ? covered : tabs.filter((tab)=>String(tab.url ?? "").includes(url));
17855
+ if (seen) seen.matched = candidates.length;
17596
17856
  return candidates.find((tab)=>true === tab.selected) ?? candidates[0];
17597
17857
  }
17598
- async function evaluateInGeckoTab(args, browser, select, missing) {
17858
+ async function evaluateInGeckoTab(args, browser, select, missing, seen) {
17599
17859
  const resolved = await resolveRdpPort(args.projectPath, browser, {
17600
17860
  waitMs: 3000,
17601
17861
  graceMs: 1000
@@ -17625,7 +17885,11 @@ async function evaluateInGeckoTab(args, browser, select, missing) {
17625
17885
  ok: true,
17626
17886
  command: eval_schema.name,
17627
17887
  status: "ok",
17628
- value: void 0 === outcome.value ? null : outcome.value
17888
+ value: void 0 === outcome.value ? null : outcome.value,
17889
+ warnings: [
17890
+ `Evaluated in the tab showing ${outcome.tab.url || "an unknown url"}${outcome.tab.title ? ` ("${outcome.tab.title}")` : ""}.`,
17891
+ seen && seen.matched > 1 ? `${seen.matched} open tabs matched the url; the selected one (or the first) was used. Narrow the url to pick another.` : null
17892
+ ]
17629
17893
  });
17630
17894
  if ("TargetNotFound" === outcome.name) return envelope_envelope({
17631
17895
  ok: false,
@@ -17649,16 +17913,18 @@ async function evaluateInGeckoTab(args, browser, select, missing) {
17649
17913
  },
17650
17914
  hint: "The expression returned a promise that is still pending; pass a larger timeout to wait for it."
17651
17915
  });
17916
+ const lost = "EvalLost" === outcome.name;
17917
+ const unsupported = "Unsupported" === outcome.name;
17652
17918
  return envelope_envelope({
17653
17919
  ok: false,
17654
17920
  command: eval_schema.name,
17655
- status: "eval-failed",
17921
+ status: lost ? "eval-lost" : unsupported ? "eval-unsupported" : "eval-failed",
17656
17922
  error: {
17657
17923
  code: "E_EVAL",
17658
17924
  name: outcome.name,
17659
17925
  message: outcome.message
17660
17926
  },
17661
- hint: "The expression threw inside the document. It ran over the debugger protocol, which the document's content security policy does not govern, so this is the expression's own error."
17927
+ hint: lost ? "The document navigated or closed before the expression answered, so it may have run in part; read the page's state before repeating it." : unsupported ? "The debugger protocol could not evaluate in that tab, so the expression never ran. extension_dom_snapshot reads the page without eval." : "The expression threw inside the document. It ran over the debugger protocol, which the document's content security policy does not govern, so this is the expression's own error."
17662
17928
  });
17663
17929
  }
17664
17930
  function backgroundRouteRefusedStatements(args, browser, context, raw) {
@@ -17675,7 +17941,10 @@ async function evaluatePagePastSiteCsp(args, browser, context, raw) {
17675
17941
  if (!args.url && null != args.tab) return null;
17676
17942
  if (!isSingleExpression(args.expression)) return notOneExpression();
17677
17943
  const url = args.url;
17678
- return evaluateInGeckoTab(args, browser, url ? (tabs)=>tabByUrl(tabs, url) : (tabs)=>tabs.find((tab)=>true === tab.selected), url ? `No open tab matches url: ${url}` : "No tab is selected in the dev browser.");
17944
+ const seen = {
17945
+ matched: 0
17946
+ };
17947
+ return evaluateInGeckoTab(args, browser, url ? (tabs)=>tabByUrl(tabs, url, seen) : (tabs)=>tabs.find((tab)=>true === tab.selected), url ? `No open tab matches url: ${url}` : "No tab is selected in the dev browser.", seen);
17679
17948
  }
17680
17949
  async function evaluateThroughExecuteScript(args, browser, context, raw) {
17681
17950
  if ("page" !== context && "content" !== context) return null;
@@ -17835,13 +18104,10 @@ async function eval_handler(args) {
17835
18104
  if (defaulted) try {
17836
18105
  const parsed = JSON.parse(raw);
17837
18106
  if (parsed && "object" == typeof parsed) {
17838
- patchValue(parsed, {
17839
- defaultedContext: "page"
17840
- });
17841
- addWarning(parsed, '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).');
18107
+ addWarning(parsed, 'No context given: defaulted to "page" (the active tab). Pass context: "background" to evaluate in the service worker, which this tool reaches over the debug port.');
17842
18108
  const code = "string" == typeof parsed.error?.code ? parsed.error.code : "";
17843
- const unreachable = "E_TARGET_NOT_FOUND" === code || /cannot access|chrome-extension:\/\/|chrome:\/\//i.test(JSON.stringify(parsed.error ?? ""));
17844
- if (false === parsed.ok && unreachable) parsed.hint = "The active tab is a browser or extension page that eval cannot reach. Navigate the dev browser to a regular web page, or pass url (match pattern) or tab to pick one; extension_dom_snapshot with listTabs: true lists open tabs.";
18109
+ const unreachable = "E_TARGET_NOT_FOUND" === code || /cannot access|chrome-extension:\/\/|chrome:\/\/|no active tab|missing host permission/i.test(JSON.stringify(parsed.error ?? ""));
18110
+ if (false === parsed.ok && unreachable) parsed.hint = "The active tab could not be reached: it is a browser or extension page, a site outside the extension's host permissions, or there is no active tab. Navigate the dev browser to a page the extension may script, or pass url (match pattern) or tab to pick one; extension_dom_snapshot with listTabs: true lists open tabs.";
17845
18111
  return actFrameJson(parsed);
17846
18112
  }
17847
18113
  } catch {}
@@ -18549,7 +18815,8 @@ async function publish_handler(args) {
18549
18815
  ref,
18550
18816
  api: args.api
18551
18817
  });
18552
- if (buildsRes.ok) {
18818
+ if (buildsRes.ok && buildIndexShapeProblem(buildsRes.json)) buildNote = `The project's build index at ${buildsUrl} answered but ${buildIndexShapeProblem(buildsRes.json)}, so buildSha, version and builtAt were not filled in from it.`;
18819
+ else if (buildsRes.ok) {
18553
18820
  const items = parseBuildIndex(buildsRes.json);
18554
18821
  if (args.buildSha) {
18555
18822
  const pin = String(args.buildSha).toLowerCase();
@@ -18571,13 +18838,18 @@ async function publish_handler(args) {
18571
18838
  }
18572
18839
  } else {
18573
18840
  const newestSuccess = items.filter(isSuccessfulBuild).sort((a, b)=>String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")))[0];
18574
- if (newestSuccess) {
18575
- if (null == data.buildSha) data.buildSha = newestSuccess.sha;
18576
- if (null == data.builtAt && newestSuccess.timestamp) data.builtAt = newestSuccess.timestamp;
18577
- if (null == data.version && newestSuccess.version) data.version = newestSuccess.version;
18578
- if (null == data.channel && newestSuccess.channel) data.channel = newestSuccess.channel;
18841
+ const platformSha = "string" == typeof data.buildSha && data.buildSha ? String(data.buildSha).toLowerCase() : null;
18842
+ const served = platformSha ? items.find((item)=>item.sha.toLowerCase().startsWith(platformSha) || platformSha.startsWith(item.sha.toLowerCase())) : newestSuccess;
18843
+ if (served) {
18844
+ if (null == data.buildSha) data.buildSha = served.sha;
18845
+ if (null == data.builtAt && served.timestamp) data.builtAt = served.timestamp;
18846
+ if (null == data.version && served.version) data.version = served.version;
18847
+ if (null == data.channel && served.channel) data.channel = served.channel;
18848
+ data.registryUrl = buildsUrl;
18849
+ buildNote = platformSha ? `builtAt/version are read from the index row of build ${served.sha}, the build the platform says the link serves.` : "The platform named no build for this link; buildSha/builtAt/version describe the newest successful build in the project's registry index, which is what an unpinned link is expected to serve. Pin buildSha to serve a specific build.";
18850
+ } else if (platformSha) {
18579
18851
  data.registryUrl = buildsUrl;
18580
- buildNote = "buildSha/builtAt/version describe the newest successful build in the project's registry index, which is what the share link serves. Pin buildSha to serve a specific build.";
18852
+ buildNote = `The platform says the link serves build ${data.buildSha}, which the project's registry index does not list, so builtAt and version were not filled in from another build.`;
18581
18853
  }
18582
18854
  }
18583
18855
  }
@@ -18845,7 +19117,7 @@ async function release_promote_handler(args) {
18845
19117
  api: args.api
18846
19118
  });
18847
19119
  if (channelsRes.ok) {
18848
- const rows = parseChannels(channelsRes.json).filter((c)=>c.sha);
19120
+ const rows = channelsShapeProblem(channelsRes.json) ? [] : parseChannels(channelsRes.json).filter((c)=>c.sha);
18849
19121
  enrich.validChannelShas = Object.fromEntries(rows.map((c)=>[
18850
19122
  c.channel,
18851
19123
  c.sha
@@ -19192,7 +19464,7 @@ async function readReleases(args) {
19192
19464
  const channelsUrl = registryFileUrl(ref, "channels.json");
19193
19465
  const metaUrl = registryFileUrl(ref, "meta.json");
19194
19466
  const buildsUrl = registryFileUrl(ref, "builds/index.json");
19195
- const [channelsRes, metaRes, buildsRes] = await Promise.all([
19467
+ const [channelsRaw, metaRes, buildsRaw] = await Promise.all([
19196
19468
  fetchRegistryJson(channelsUrl, fetch, {
19197
19469
  ref,
19198
19470
  api: args.api
@@ -19206,6 +19478,8 @@ async function readReleases(args) {
19206
19478
  api: args.api
19207
19479
  })
19208
19480
  ]);
19481
+ const channelsRes = requireShape(channelsRaw, channelsShapeProblem);
19482
+ const buildsRes = requireShape(buildsRaw, buildIndexShapeProblem);
19209
19483
  const buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
19210
19484
  const heldRead = [
19211
19485
  channelsRes,
@@ -19730,7 +20004,7 @@ async function submit_handler(args) {
19730
20004
  health = stores && "object" == typeof stores ? stores : null;
19731
20005
  if (!health) healthUnreadable = "stores/health.json had no stores map";
19732
20006
  } else healthUnreadable = healthRes.message;
19733
- if (channelsRes.ok) channelRows = parseChannels(channelsRes.json);
20007
+ if (channelsRes.ok && !channelsShapeProblem(channelsRes.json)) channelRows = parseChannels(channelsRes.json);
19734
20008
  } else healthUnreadable = "no stored workspace/project to look up (run extension_auth)";
19735
20009
  const preflight = browsers.map((browser)=>{
19736
20010
  if (!health) return {
@@ -20360,6 +20634,36 @@ async function wait_handler(args) {
20360
20634
  continue;
20361
20635
  }
20362
20636
  lastContractStatus = contract.status;
20637
+ if ("build" === contract.command) return envelope_envelope({
20638
+ ok: false,
20639
+ command: wait_schema.name,
20640
+ status: "no-session",
20641
+ error: {
20642
+ code: "E_NO_SESSION",
20643
+ message: `${readyPath} is the contract a finished extension_build left (command: build), not a running session, so there is nothing to wait for.`
20644
+ },
20645
+ value: {
20646
+ readyPath,
20647
+ budgetMs,
20648
+ elapsedMs: Date.now() - start
20649
+ },
20650
+ hint: "Start a session with extension_dev (or extension_start), then call extension_wait."
20651
+ });
20652
+ if ("stopped" === contract.status) return envelope_envelope({
20653
+ ok: false,
20654
+ command: wait_schema.name,
20655
+ status: "stopped",
20656
+ error: {
20657
+ code: "E_SESSION_EXITED",
20658
+ message: `${readyPath} records status: stopped; that session was stopped and nothing will become ready.`
20659
+ },
20660
+ value: {
20661
+ readyPath,
20662
+ budgetMs,
20663
+ elapsedMs: Date.now() - start
20664
+ },
20665
+ hint: "Start a session with extension_dev, then call extension_wait."
20666
+ });
20363
20667
  if ("ready" === contract.status) {
20364
20668
  if ("number" == typeof contract.pid && !wait_isAlive(contract.pid)) return envelope_envelope({
20365
20669
  ok: false,
@@ -20431,7 +20735,7 @@ async function wait_handler(args) {
20431
20735
  if (!attached && ("start" === contract.command || "preview" === contract.command)) return envelope_envelope({
20432
20736
  ok: true,
20433
20737
  command: wait_schema.name,
20434
- status: "launched",
20738
+ status: "build-ready",
20435
20739
  value: {
20436
20740
  compiled: true,
20437
20741
  browserAttached: false,
@@ -20537,6 +20841,26 @@ async function wait_handler(args) {
20537
20841
  ],
20538
20842
  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."
20539
20843
  });
20844
+ if (null === lastContractStatus && !contractUnreadable && !staleContractNote && !findSessionInfo(args.projectPath, browser)) return envelope_envelope({
20845
+ ok: false,
20846
+ command: wait_schema.name,
20847
+ status: "no-session",
20848
+ error: {
20849
+ code: "E_NO_SESSION",
20850
+ message: `No ${browser} session is known for ${args.projectPath}: this server registered none and no contract appeared at ${readyPath} in ${budgetMs} ms.`
20851
+ },
20852
+ value: {
20853
+ compiled: false,
20854
+ browserAttached: false,
20855
+ readyPath,
20856
+ budgetMs,
20857
+ elapsedMs: Date.now() - start
20858
+ },
20859
+ warnings: [
20860
+ clampNote
20861
+ ],
20862
+ hint: "Check projectPath and browser, or start a session with extension_dev, then call extension_wait."
20863
+ });
20540
20864
  return envelope_envelope({
20541
20865
  ok: false,
20542
20866
  command: wait_schema.name,
@@ -23011,7 +23335,9 @@ async function runCli(cmd, args) {
23011
23335
  } catch {
23012
23336
  parsed = null;
23013
23337
  }
23014
- return parsed?.ok === false ? 1 : 0;
23338
+ if (parsed?.ok === true) return 0;
23339
+ if (null === parsed || "object" != typeof parsed) log("The promote answer above could not be read as an envelope, so whether the channel moved is unknown; check extension_release_list before retrying.");
23340
+ return 1;
23015
23341
  }
23016
23342
  log("Usage: extension-mcp release promote --build <sha> --channel <channel>");
23017
23343
  return 1;