@extension.dev/mcp 5.5.0 → 5.5.2

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
@@ -193,6 +193,14 @@ __webpack_require__.d(storage_namespaceObject, {
193
193
  handler: ()=>storage_handler,
194
194
  schema: ()=>storage_schema
195
195
  });
196
+ var store_status_namespaceObject = {};
197
+ __webpack_require__.r(store_status_namespaceObject);
198
+ __webpack_require__.d(store_status_namespaceObject, {
199
+ handler: ()=>store_status_handler,
200
+ latestSubmissionsByStore: ()=>latestSubmissionsByStore,
201
+ normalizeStoresStatus: ()=>normalizeStoresStatus,
202
+ schema: ()=>store_status_schema
203
+ });
196
204
  var uninstall_browser_namespaceObject = {};
197
205
  __webpack_require__.r(uninstall_browser_namespaceObject);
198
206
  __webpack_require__.d(uninstall_browser_namespaceObject, {
@@ -211,7 +219,7 @@ __webpack_require__.d(whoami_namespaceObject, {
211
219
  handler: ()=>whoami_handler,
212
220
  schema: ()=>whoami_schema
213
221
  });
214
- var package_namespaceObject = JSON.parse('{"rE":"5.5.0","El":{"OP":"^4.0.13"}}');
222
+ var package_namespaceObject = JSON.parse('{"rE":"5.5.2","El":{"OP":"^4.0.13"}}');
215
223
  function scaffoldEnginePin(projectPath) {
216
224
  try {
217
225
  const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
@@ -3004,9 +3012,178 @@ function isCdpEndpoint(port) {
3004
3012
  });
3005
3013
  });
3006
3014
  }
3015
+ function toMcpSpeak(text) {
3016
+ return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
3017
+ }
3018
+ function withSessionContext(message, projectPath) {
3019
+ const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
3020
+ if (!isControlError) return message;
3021
+ const dead = deadReadySession(projectPath);
3022
+ if (dead) return `${message}\nLikely cause: the dev server has exited, ${dead.browser} ready.json still says ready but its pid ${dead.pid} is dead. Restart with extension_dev (this is not an allowControl problem); extension_doctor confirms.`;
3023
+ const running = knownSessionBrowsers(projectPath);
3024
+ if (0 === running.length) return message;
3025
+ return `${message} Active session browser(s) for this project: ${running.join(", ")}, pass that as \`browser\`, or restart it via extension_dev with allowControl: true if the control channel is off.`;
3026
+ }
3027
+ function translateFrame(frame, projectPath) {
3028
+ if (!frame || false !== frame.ok) return frame;
3029
+ if (frame.error && "string" == typeof frame.error.message) frame.error.message = withSessionContext(toMcpSpeak(frame.error.message), projectPath);
3030
+ if ("string" == typeof frame.error?.hint) frame.error.hint = toMcpSpeak(frame.error.hint);
3031
+ if ("string" == typeof frame.hint) frame.hint = toMcpSpeak(frame.hint);
3032
+ return frame;
3033
+ }
3034
+ async function runActVerb(args, projectPath, timeoutMs) {
3035
+ const { code, stdout, stderr } = await runExtensionCli([
3036
+ ...args,
3037
+ "--output",
3038
+ "json"
3039
+ ], {
3040
+ cwd: projectPath,
3041
+ timeoutMs
3042
+ });
3043
+ const out = stdout.trim();
3044
+ if (out) try {
3045
+ const frame = JSON.parse(out);
3046
+ if (frame && false === frame.ok) return JSON.stringify(translateFrame(frame, projectPath));
3047
+ return out;
3048
+ } catch {}
3049
+ const message = stderr.trim() || `extension exited with code ${code}`;
3050
+ return JSON.stringify({
3051
+ ok: false,
3052
+ error: {
3053
+ name: "CliError",
3054
+ message: withSessionContext(toMcpSpeak(message), projectPath)
3055
+ }
3056
+ });
3057
+ }
3058
+ function commonFlags(args) {
3059
+ const flags = [];
3060
+ if (args.context) flags.push("--context", args.context);
3061
+ if (args.url) flags.push("--url", args.url);
3062
+ if (null != args.tab) flags.push("--tab", String(args.tab));
3063
+ if (args.browser) flags.push("--browser", args.browser);
3064
+ if (null != args.timeout) flags.push("--timeout", String(args.timeout));
3065
+ return flags;
3066
+ }
3067
+ async function listBridgeTabs(projectPath, browser, timeout) {
3068
+ const raw = await runActVerb([
3069
+ "inspect",
3070
+ projectPath,
3071
+ "--list-tabs",
3072
+ "--browser",
3073
+ browser,
3074
+ ...null != timeout ? [
3075
+ "--timeout",
3076
+ String(timeout)
3077
+ ] : []
3078
+ ], projectPath, timeout);
3079
+ let parsed;
3080
+ try {
3081
+ parsed = JSON.parse(raw);
3082
+ } catch {
3083
+ return {
3084
+ error: raw
3085
+ };
3086
+ }
3087
+ if (parsed?.ok === false) return {
3088
+ error: raw
3089
+ };
3090
+ const list = Array.isArray(parsed?.tabs) ? parsed.tabs : Array.isArray(parsed?.value) ? parsed.value : Array.isArray(parsed?.value?.tabs) ? parsed.value.tabs : null;
3091
+ if (!list) return {
3092
+ error: raw
3093
+ };
3094
+ return {
3095
+ tabs: list.map((t)=>({
3096
+ tabId: "number" == typeof t?.tabId ? t.tabId : "number" == typeof t?.id ? t.id : null,
3097
+ url: String(t?.url ?? ""),
3098
+ title: String(t?.title ?? "")
3099
+ }))
3100
+ };
3101
+ }
3102
+ function matchTabsByUrl(tabs, needle) {
3103
+ const wanted = needle.toLowerCase();
3104
+ const byUrl = tabs.filter((t)=>t.url.toLowerCase().includes(wanted));
3105
+ if (byUrl.length > 0) return byUrl;
3106
+ return tabs.filter((t)=>t.title.toLowerCase().includes(wanted));
3107
+ }
3108
+ async function pollForBridgeTab(projectPath, browser, url, budgetMs) {
3109
+ const deadline = Date.now() + budgetMs;
3110
+ const wanted = url.replace(/#.*$/, "");
3111
+ for(;;){
3112
+ const listed = await listBridgeTabs(projectPath, browser);
3113
+ if ("tabs" in listed) {
3114
+ for (const t of listed.tabs)if (t.url === wanted || t.url.startsWith(wanted)) return t;
3115
+ }
3116
+ if (Date.now() >= deadline) return null;
3117
+ await new Promise((r)=>setTimeout(r, 250));
3118
+ }
3119
+ }
3120
+ async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
3121
+ const expression = `(async () => { const api = typeof browser !== "undefined" ? browser : chrome; const tabs = await api.tabs.query({ active: true, currentWindow: true }); const active = tabs && tabs[0]; const tab = active && active.id != null ? await api.tabs.update(active.id, { url: ${JSON.stringify(url)} }) : await api.tabs.create({ url: ${JSON.stringify(url)} }); return { tabId: tab && tab.id != null ? tab.id : null }; })()`;
3122
+ const raw = await runActVerb([
3123
+ "eval",
3124
+ expression,
3125
+ projectPath,
3126
+ "--context",
3127
+ "background",
3128
+ "--browser",
3129
+ browser,
3130
+ ...null != timeout ? [
3131
+ "--timeout",
3132
+ String(timeout)
3133
+ ] : []
3134
+ ], projectPath, timeout);
3135
+ try {
3136
+ const parsed = JSON.parse(raw);
3137
+ if (parsed?.ok === false) {
3138
+ if (!parsed.hint) parsed.hint = "On this browser family URL navigation rides the agent bridge (a background eval of tabs.update), so the dev session must be started with allowEval: true (extension_dev).";
3139
+ return JSON.stringify(parsed);
3140
+ }
3141
+ } catch {
3142
+ return raw;
3143
+ }
3144
+ const settled = await pollForBridgeTab(projectPath, browser, url, null != timeout ? Math.min(timeout, 6000) : 6000);
3145
+ if (!settled) return JSON.stringify({
3146
+ ok: false,
3147
+ error: {
3148
+ name: "NavigateFailed",
3149
+ message: `Navigation to ${url} did not produce a tab reporting that URL. The URL may not exist, or the browser refused the navigation (Firefox rejects privileged about:/chrome: URLs and other extensions' moz-extension: pages).`
3150
+ },
3151
+ hint: "Confirm the URL, or discover open tabs with extension_dom_inspect listTabs: true. For an extension page, the path must match the BUILT manifest."
3152
+ });
3153
+ return JSON.stringify({
3154
+ ok: true,
3155
+ navigated: url,
3156
+ tab: {
3157
+ tabId: settled.tabId,
3158
+ url: settled.url,
3159
+ title: settled.title
3160
+ },
3161
+ hint: "Inspect it with extension_dom_inspect or extension_eval using url or this numeric tab id (context: 'page'/'content')."
3162
+ });
3163
+ }
3164
+ async function resolveBridgeBaseUrl(projectPath, browser, timeout) {
3165
+ const raw = await runActVerb([
3166
+ "eval",
3167
+ '(typeof browser !== "undefined" ? browser : chrome).runtime.getURL("")',
3168
+ projectPath,
3169
+ "--context",
3170
+ "background",
3171
+ "--browser",
3172
+ browser,
3173
+ ...null != timeout ? [
3174
+ "--timeout",
3175
+ String(timeout)
3176
+ ] : []
3177
+ ], projectPath, timeout);
3178
+ try {
3179
+ const parsed = JSON.parse(raw);
3180
+ if (parsed?.ok && "string" == typeof parsed.value && parsed.value) return parsed.value.endsWith("/") ? parsed.value : `${parsed.value}/`;
3181
+ } catch {}
3182
+ return null;
3183
+ }
3007
3184
  const source_inspect_schema = {
3008
3185
  name: "extension_source_inspect",
3009
- description: "Inspect a running extension's live state via Chrome DevTools Protocol: full HTML (with shadow DOM), DOM structure, content script injection, console messages, and CSS selector queries. Requires an active dev or start session.",
3186
+ description: "Inspect a running extension's live state: full HTML (with shadow DOM), DOM structure, content script injection, console messages, and CSS selector queries. Chromium sessions ride Chrome DevTools Protocol; Firefox sessions ride the agent bridge (needs allowEval: true) and cover summary/meta/html/probes, while dom_snapshot, extension_roots, console, and deepDom stay CDP-only. Requires an active dev or start session.",
3010
3187
  inputSchema: {
3011
3188
  type: "object",
3012
3189
  properties: {
@@ -3073,10 +3250,7 @@ async function source_inspect_handler(args) {
3073
3250
  "console"
3074
3251
  ]);
3075
3252
  const maxBytes = args.maxBytes ?? 262144;
3076
- if (!isChromiumFamily(browser)) return JSON.stringify({
3077
- error: `Source inspection reads the live DOM over Chrome DevTools Protocol, which ${browser} (Gecko) does not expose. This is a capability limit, not a missing session.`,
3078
- hint: `For the ${browser} session, read runtime state with extension_logs, or extension_eval / extension_dom_inspect (content/page, an open surface like popup/options/sidebar, or an override page like newtab) which work against Firefox over the control channel. To get CDP DOM inspection, run a Chromium-family dev session (extension_dev with browser: "chrome") in parallel.`
3079
- });
3253
+ if (!isChromiumFamily(browser)) return inspectViaBridge(args, browser, include, maxBytes);
3080
3254
  const resolved = await resolveCdpPort(args.projectPath, browser);
3081
3255
  if (!resolved) return JSON.stringify({
3082
3256
  error: "No active dev session found. Cannot connect to Chrome DevTools Protocol.",
@@ -3178,6 +3352,117 @@ async function source_inspect_handler(args) {
3178
3352
  cdp.disconnect();
3179
3353
  }
3180
3354
  }
3355
+ function buildBridgeInspectExpression(opts) {
3356
+ const parts = [
3357
+ "const out = {};"
3358
+ ];
3359
+ if (opts.meta) parts.push("try { out.meta = { url: location.href, title: document.title, readyState: document.readyState }; } catch (e) {}");
3360
+ if (opts.summary) parts.push(`try {
3361
+ const roots = document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])');
3362
+ out.summary = {
3363
+ htmlLength: document.documentElement.outerHTML.length,
3364
+ scriptCount: document.querySelectorAll('script').length,
3365
+ styleCount: document.querySelectorAll('style').length,
3366
+ linkCount: document.querySelectorAll('link').length,
3367
+ extensionRootCount: roots.length,
3368
+ bodyChildCount: document.body ? document.body.children.length : 0
3369
+ };
3370
+ } catch (e) { out.summary = {}; }`);
3371
+ if (opts.html) parts.push(`try {
3372
+ const html = document.documentElement.outerHTML;
3373
+ const cap = ${JSON.stringify(opts.maxBytes)};
3374
+ out.htmlTruncated = cap > 0 && html.length > cap;
3375
+ out.html = out.htmlTruncated ? html.slice(0, cap) : html;
3376
+ } catch (e) {}`);
3377
+ if (opts.probes.length) parts.push(`out.probes = {};
3378
+ for (const sel of ${JSON.stringify(opts.probes)}) {
3379
+ try {
3380
+ const nodes = document.querySelectorAll(sel);
3381
+ const first = nodes[0];
3382
+ out.probes[sel] = { count: nodes.length, sample: first ? String(first.outerHTML || "").slice(0, 200) : null };
3383
+ } catch (e) { out.probes[sel] = { error: String((e && e.message) || e) }; }
3384
+ }`);
3385
+ parts.push("return out;");
3386
+ return `(() => { ${parts.join("\n")} })()`;
3387
+ }
3388
+ async function inspectViaBridge(args, browser, include, maxBytes) {
3389
+ const notes = [];
3390
+ const cdpOnly = [
3391
+ "dom_snapshot",
3392
+ "extension_roots"
3393
+ ].filter((k)=>include.has(k));
3394
+ if (cdpOnly.length) notes.push(`${cdpOnly.join(" and ")} require CDP and are Chromium-only; on ${browser}, extension_dom_inspect's summary reports extension roots and open shadow roots.`);
3395
+ if (args.deepDom) notes.push("deepDom (closed shadow roots) requires CDP and is Chromium-only.");
3396
+ if (include.has("console")) notes.push(`Console capture rides CDP; on ${browser} read extension_logs, where the engine streams console output.`);
3397
+ if (args.url) {
3398
+ const listed = await listBridgeTabs(args.projectPath, browser, args.timeout);
3399
+ if ("error" in listed) return listed.error;
3400
+ const already = listed.tabs.some((t)=>t.url.includes(args.url));
3401
+ if (!already) {
3402
+ const nav = await navigateToUrlViaBridge(args.projectPath, browser, args.url, args.timeout);
3403
+ try {
3404
+ if (JSON.parse(nav)?.ok !== true) return nav;
3405
+ } catch {
3406
+ return nav;
3407
+ }
3408
+ }
3409
+ }
3410
+ const expression = buildBridgeInspectExpression({
3411
+ summary: include.has("summary"),
3412
+ meta: true,
3413
+ html: include.has("html"),
3414
+ probes: args.probe ?? [],
3415
+ maxBytes
3416
+ });
3417
+ const raw = await runActVerb([
3418
+ "eval",
3419
+ expression,
3420
+ args.projectPath,
3421
+ "--context",
3422
+ "page",
3423
+ ...args.url ? [
3424
+ "--url",
3425
+ args.url
3426
+ ] : [],
3427
+ "--browser",
3428
+ browser,
3429
+ ...null != args.timeout ? [
3430
+ "--timeout",
3431
+ String(args.timeout)
3432
+ ] : []
3433
+ ], args.projectPath, args.timeout);
3434
+ let parsed;
3435
+ try {
3436
+ parsed = JSON.parse(raw);
3437
+ } catch {
3438
+ return raw;
3439
+ }
3440
+ if (parsed?.ok !== true) return raw;
3441
+ const value = parsed.value ?? {};
3442
+ const result = {
3443
+ browser,
3444
+ transport: "bridge"
3445
+ };
3446
+ if (value.meta) {
3447
+ result.target = {
3448
+ url: value.meta.url,
3449
+ title: value.meta.title
3450
+ };
3451
+ if (include.has("meta")) result.meta = value.meta;
3452
+ }
3453
+ if (include.has("summary") && value.summary) result.summary = value.summary;
3454
+ if (include.has("html") && "string" == typeof value.html) {
3455
+ result.html = value.html;
3456
+ if (value.htmlTruncated) result.htmlTruncated = true;
3457
+ }
3458
+ if (value.probes) {
3459
+ result.probes = value.probes;
3460
+ const jsLooking = (args.probe ?? []).filter((p)=>/^typeof\s|^(chrome|browser|window|document)\.|\(\)|=>|===/.test(p));
3461
+ if (jsLooking.length) result.probeWarning = `Probes are CSS selectors run through querySelectorAll against the live page, NOT JavaScript expressions. ${jsLooking.map((s)=>`"${s}"`).join(", ")} parsed as selectors and will match nothing. To evaluate JS, use extension_eval.`;
3462
+ }
3463
+ if (notes.length) result.notes = notes;
3464
+ return JSON.stringify(result);
3465
+ }
3181
3466
  const list_extensions_schema = {
3182
3467
  name: "extension_list_extensions",
3183
3468
  description: "List the extensions with a live context in the running dev browser via Chrome DevTools Protocol. Returns each extension's id, name, version, and live contexts (service worker, page). The entry for THIS dev session's extension (the project being served) is flagged ownExtension: true, with name and version resolved from the session's ready contract even when the browser exposes no identity. Other extensions resolve via the read-only Extensions domain when available; their contexts are never attached to or evaluated in, so an id that the domain does not describe stays id-only with a note saying why. A dormant MV3 service worker with no open page may be absent until it wakes. Chromium only (Firefox uses RDP, not yet supported). Requires an active dev or start session.",
@@ -3657,58 +3942,6 @@ async function logs_handler(args) {
3657
3942
  if (args.follow) return readFromStream(args, browser, limit);
3658
3943
  return readFromFile(args, browser, limit);
3659
3944
  }
3660
- function toMcpSpeak(text) {
3661
- return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
3662
- }
3663
- function withSessionContext(message, projectPath) {
3664
- const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
3665
- if (!isControlError) return message;
3666
- const dead = deadReadySession(projectPath);
3667
- if (dead) return `${message}\nLikely cause: the dev server has exited, ${dead.browser} ready.json still says ready but its pid ${dead.pid} is dead. Restart with extension_dev (this is not an allowControl problem); extension_doctor confirms.`;
3668
- const running = knownSessionBrowsers(projectPath);
3669
- if (0 === running.length) return message;
3670
- return `${message} Active session browser(s) for this project: ${running.join(", ")}, pass that as \`browser\`, or restart it via extension_dev with allowControl: true if the control channel is off.`;
3671
- }
3672
- function translateFrame(frame, projectPath) {
3673
- if (!frame || false !== frame.ok) return frame;
3674
- if (frame.error && "string" == typeof frame.error.message) frame.error.message = withSessionContext(toMcpSpeak(frame.error.message), projectPath);
3675
- if ("string" == typeof frame.error?.hint) frame.error.hint = toMcpSpeak(frame.error.hint);
3676
- if ("string" == typeof frame.hint) frame.hint = toMcpSpeak(frame.hint);
3677
- return frame;
3678
- }
3679
- async function runActVerb(args, projectPath, timeoutMs) {
3680
- const { code, stdout, stderr } = await runExtensionCli([
3681
- ...args,
3682
- "--output",
3683
- "json"
3684
- ], {
3685
- cwd: projectPath,
3686
- timeoutMs
3687
- });
3688
- const out = stdout.trim();
3689
- if (out) try {
3690
- const frame = JSON.parse(out);
3691
- if (frame && false === frame.ok) return JSON.stringify(translateFrame(frame, projectPath));
3692
- return out;
3693
- } catch {}
3694
- const message = stderr.trim() || `extension exited with code ${code}`;
3695
- return JSON.stringify({
3696
- ok: false,
3697
- error: {
3698
- name: "CliError",
3699
- message: withSessionContext(toMcpSpeak(message), projectPath)
3700
- }
3701
- });
3702
- }
3703
- function commonFlags(args) {
3704
- const flags = [];
3705
- if (args.context) flags.push("--context", args.context);
3706
- if (args.url) flags.push("--url", args.url);
3707
- if (null != args.tab) flags.push("--tab", String(args.tab));
3708
- if (args.browser) flags.push("--browser", args.browser);
3709
- if (null != args.timeout) flags.push("--timeout", String(args.timeout));
3710
- return flags;
3711
- }
3712
3945
  const eval_schema = {
3713
3946
  name: "extension_eval",
3714
3947
  description: "Evaluate an expression in a running extension context. Requires the dev session to be started with allowEval: true (extension_dev; writes a 0600 session token the CLI reads). Context default: on a Chromium session whose manifest is MV3 (the default template) the default is `page` (the active tab), because the MV3 background is a service worker whose CSP blocks eval, so a background default would fail on the most common path; on Firefox/MV2 sessions the default stays `background`. Pass `context: \"background\"` explicitly to target the worker anyway (on Chromium MV3 that returns the CSP explanation). Targeting for context content/page: pass `url` to pick the matching tab, or omit both `url` and `tab` to use the ACTIVE tab; a numeric `tab` id is only needed to disambiguate. Extension surfaces (popup/options/sidebar/devtools) and override pages (newtab/history/bookmarks) evaluate over the in-bundle relay and need NO tab id; the surface must be OPEN (extension_open first; a closed surface returns an explicit error). Use extension_dom_inspect with listTabs: true to enumerate {tabId,url,title}. Wraps `extension eval`.",
@@ -3976,14 +4209,8 @@ async function pollForTarget(port, url, budgetMs) {
3976
4209
  await new Promise((r)=>setTimeout(r, 250));
3977
4210
  }
3978
4211
  }
3979
- async function navigateToUrl(projectPath, browser, url) {
3980
- if (!isChromiumFamily(browser)) return JSON.stringify({
3981
- ok: false,
3982
- error: {
3983
- name: "Unsupported",
3984
- message: `URL navigation drives a tab over Chrome DevTools Protocol, which ${browser} (Gecko) does not expose. On Firefox, drive the page via extension_eval (context: "page"/"content") or read extension_logs.`
3985
- }
3986
- });
4212
+ async function navigateToUrl(projectPath, browser, url, timeout) {
4213
+ if (!isChromiumFamily(browser)) return navigateToUrlViaBridge(projectPath, browser, url, timeout);
3987
4214
  const resolved = await resolveCdpPort(projectPath, browser);
3988
4215
  if (!resolved) return JSON.stringify({
3989
4216
  ok: false,
@@ -4226,16 +4453,32 @@ async function applyPopupBounds(projectPath, browser, targetId) {
4226
4453
  async function openSurfaceAsTab(projectPath, browser, surface) {
4227
4454
  const doc = surfaceDocument(projectPath, browser, surface);
4228
4455
  if (!doc) return missingSurfaceError(projectPath, browser, surface, "so there is no page to render as a tab");
4229
- const id = await resolveExtensionId(projectPath, browser);
4230
- if (!id) return JSON.stringify({
4231
- ok: false,
4232
- error: {
4233
- name: "NoExtensionId",
4234
- message: "Could not resolve the extension id from the live session's CDP targets."
4235
- },
4236
- hint: `Confirm the session is ready (extension_wait). ${CDP_PORT_MISSING_HINT}`
4237
- });
4238
- const url = `chrome-extension://${id}/${doc}`;
4456
+ let url;
4457
+ let extensionId = null;
4458
+ if (isChromiumFamily(browser)) {
4459
+ extensionId = await resolveExtensionId(projectPath, browser);
4460
+ if (!extensionId) return JSON.stringify({
4461
+ ok: false,
4462
+ error: {
4463
+ name: "NoExtensionId",
4464
+ message: "Could not resolve the extension id from the live session's CDP targets."
4465
+ },
4466
+ hint: `Confirm the session is ready (extension_wait). ${CDP_PORT_MISSING_HINT}`
4467
+ });
4468
+ url = `chrome-extension://${extensionId}/${doc}`;
4469
+ } else {
4470
+ const base = await resolveBridgeBaseUrl(projectPath, browser);
4471
+ if (!base) return JSON.stringify({
4472
+ ok: false,
4473
+ error: {
4474
+ name: "NoExtensionId",
4475
+ message: "Could not resolve the extension's moz-extension:// base URL from the live session (a background eval of runtime.getURL)."
4476
+ },
4477
+ hint: "Confirm the session is ready (extension_wait) and was started with allowEval: true (extension_dev)."
4478
+ });
4479
+ url = `${base}${doc}`;
4480
+ extensionId = base.replace(/^.*:\/\//, "").replace(/\/$/, "");
4481
+ }
4239
4482
  const raw = await navigateToUrl(projectPath, browser, url);
4240
4483
  try {
4241
4484
  const parsed = JSON.parse(raw);
@@ -4243,14 +4486,14 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
4243
4486
  parsed.renderedAsTab = {
4244
4487
  surface,
4245
4488
  document: doc,
4246
- extensionId: id
4489
+ extensionId
4247
4490
  };
4248
4491
  let popupBounds = null;
4249
4492
  if (("popup" === surface || "action" === surface) && "string" == typeof parsed.target?.targetId) {
4250
4493
  popupBounds = await applyPopupBounds(projectPath, browser, parsed.target.targetId);
4251
4494
  if (popupBounds) parsed.renderedAsTab.popupBounds = popupBounds;
4252
4495
  }
4253
- parsed.hint = `Rendered the ${surface} document in a real tab, which is how you inspect a surface headlessly. ` + (popupBounds ? `The window was resized to the popup's content size (${popupBounds.width}x${popupBounds.height}${popupBounds.clamped ? ", clamped to Chrome's 25x25-800x600 popup bounds" : ""}), approximating real popup rendering. This resizes the WHOLE browser window for the session. It is the same page with the same extension APIs, but window.close() closes the tab. ` : "It is the same page with the same extension APIs, but it is NOT hosted in a popup window: no popup sizing, and window.close() closes the tab. ") + `Inspect it with extension_dom_inspect context: '${surface}' (include: ['html']), or extension_source_inspect with this url. ` + "Do NOT pass this chrome-extension:// url to extension_dom_inspect or extension_eval as a tab target: script injection cannot reach extension pages, only the surface context or CDP can.";
4496
+ parsed.hint = `Rendered the ${surface} document in a real tab, which is how you inspect a surface headlessly. ` + (popupBounds ? `The window was resized to the popup's content size (${popupBounds.width}x${popupBounds.height}${popupBounds.clamped ? ", clamped to Chrome's 25x25-800x600 popup bounds" : ""}), approximating real popup rendering. This resizes the WHOLE browser window for the session. It is the same page with the same extension APIs, but window.close() closes the tab. ` : "It is the same page with the same extension APIs, but it is NOT hosted in a popup window: no popup sizing, and window.close() closes the tab. ") + `Inspect it with extension_dom_inspect context: '${surface}' (include: ['html']), or extension_source_inspect with this url. ` + "Do NOT pass this extension-page url to extension_dom_inspect or extension_eval as a tab target: script injection cannot reach extension pages, only the surface context or CDP can.";
4254
4497
  return JSON.stringify(parsed);
4255
4498
  }
4256
4499
  } catch {}
@@ -4286,7 +4529,7 @@ const open_schema = {
4286
4529
  },
4287
4530
  url: {
4288
4531
  type: "string",
4289
- description: "Navigate a real tab to this URL (Chromium only, via CDP) instead of opening a surface. Use for content-script/webNavigation test pages, or the popup as a page: chrome-extension://<id>/popup.html. Alternative to `surface`."
4532
+ description: "Navigate a real tab to this URL instead of opening a surface (Chromium via CDP; Firefox via the agent bridge, which needs allowEval: true). Use for content-script/webNavigation test pages, or the popup as a page: chrome-extension://<id>/popup.html. Alternative to `surface`."
4290
4533
  },
4291
4534
  asTab: {
4292
4535
  type: "boolean",
@@ -4309,7 +4552,7 @@ const open_schema = {
4309
4552
  };
4310
4553
  async function open_handler(args) {
4311
4554
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
4312
- if (args.url) return navigateToUrl(args.projectPath, browser, args.url);
4555
+ if (args.url) return navigateToUrl(args.projectPath, browser, args.url, args.timeout);
4313
4556
  const AS_TAB_SURFACES = [
4314
4557
  "popup",
4315
4558
  "options",
@@ -4360,7 +4603,7 @@ async function open_handler(args) {
4360
4603
  const parsed = JSON.parse(raw);
4361
4604
  const msg = String(parsed?.error?.message ?? "");
4362
4605
  if (parsed?.ok === false && /active browser window|no active|headless|user gesture/i.test(msg)) {
4363
- if (AS_TAB_SURFACES.includes(args.surface) && isChromiumFamily(browser)) {
4606
+ if (AS_TAB_SURFACES.includes(args.surface)) {
4364
4607
  const fallback = await openSurfaceAsTab(args.projectPath, browser, args.surface);
4365
4608
  try {
4366
4609
  const parsedFallback = JSON.parse(fallback);
@@ -4414,7 +4657,7 @@ const dom_inspect_schema = {
4414
4657
  },
4415
4658
  tabUrl: {
4416
4659
  type: "string",
4417
- description: "Target the tab whose URL contains this substring (case-insensitive; titles are checked only when no url matches). Resolved against the live browser's CDP page targets BEFORE inspecting: exactly one match proceeds; zero or several matches return the candidate targets (targetId/url/title) so you can narrow, never a guess. Chromium sessions only; on Firefox use `url`/`tab`. Alternative to `url`."
4660
+ description: "Target the tab whose URL contains this substring (case-insensitive; titles are checked only when no url matches). Resolved against the live browser BEFORE inspecting (Chromium: CDP page targets; Firefox: the agent bridge tab list): exactly one match proceeds; zero or several matches return the candidates so you can narrow, never a guess. Alternative to `url`."
4418
4661
  },
4419
4662
  listTargets: {
4420
4663
  type: "boolean",
@@ -4542,6 +4785,7 @@ async function dom_inspect_handler(args) {
4542
4785
  ] : []
4543
4786
  ], args.projectPath, args.timeout);
4544
4787
  let targetUrl = args.url;
4788
+ let targetTab = args.tab;
4545
4789
  let resolvedTarget = null;
4546
4790
  if (args.tabUrl) {
4547
4791
  if (null != args.tab || args.url) return JSON.stringify({
@@ -4552,48 +4796,79 @@ async function dom_inspect_handler(args) {
4552
4796
  }
4553
4797
  });
4554
4798
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
4555
- const cdp = await cdpPortOrError(args.projectPath, browser, "tabUrl");
4556
- if ("error" in cdp) return cdp.error;
4557
- let targets;
4558
- try {
4559
- targets = await listPageTargets(cdp.port);
4560
- } catch (e) {
4561
- return JSON.stringify({
4799
+ if (isChromiumFamily(browser)) {
4800
+ const cdp = await cdpPortOrError(args.projectPath, browser, "tabUrl");
4801
+ if ("error" in cdp) return cdp.error;
4802
+ let targets;
4803
+ try {
4804
+ targets = await listPageTargets(cdp.port);
4805
+ } catch (e) {
4806
+ return JSON.stringify({
4807
+ ok: false,
4808
+ error: {
4809
+ name: "CdpError",
4810
+ message: `Could not list page targets to resolve tabUrl: ${e instanceof Error ? e.message : String(e)}`
4811
+ },
4812
+ hint: "Confirm the session is ready (extension_wait), then retry, or target with `url`/`tab` instead."
4813
+ });
4814
+ }
4815
+ const matches = matchTargetsByUrl(targets, args.tabUrl);
4816
+ if (0 === matches.length) return JSON.stringify({
4562
4817
  ok: false,
4563
4818
  error: {
4564
- name: "CdpError",
4565
- message: `Could not list page targets to resolve tabUrl: ${e instanceof Error ? e.message : String(e)}`
4819
+ name: "NoMatchingTarget",
4820
+ message: `No open page target's url (or title) contains "${args.tabUrl}" (case-insensitive).`
4821
+ },
4822
+ availableTargets: targets,
4823
+ hint: `Pick one from availableTargets and retry with a \`tabUrl\` substring of its url, or open the page first (extension_open with \`url\`). ${TARGET_ID_NOTE}`
4824
+ });
4825
+ if (matches.length > 1) return JSON.stringify({
4826
+ ok: false,
4827
+ error: {
4828
+ name: "AmbiguousTabUrl",
4829
+ message: `${matches.length} page targets match "${args.tabUrl}"; refusing to guess which tab you mean.`
4830
+ },
4831
+ matchingTargets: matches,
4832
+ hint: `Narrow \`tabUrl\` to a longer substring that matches exactly one url in matchingTargets. ${TARGET_ID_NOTE}`
4833
+ });
4834
+ resolvedTarget = {
4835
+ ...matches[0]
4836
+ };
4837
+ targetUrl = matches[0].url;
4838
+ } else {
4839
+ const listed = await listBridgeTabs(args.projectPath, browser, args.timeout);
4840
+ if ("error" in listed) return listed.error;
4841
+ const matches = matchTabsByUrl(listed.tabs, args.tabUrl);
4842
+ if (0 === matches.length) return JSON.stringify({
4843
+ ok: false,
4844
+ error: {
4845
+ name: "NoMatchingTarget",
4846
+ message: `No open tab's url (or title) contains "${args.tabUrl}" (case-insensitive).`
4847
+ },
4848
+ availableTabs: listed.tabs,
4849
+ hint: "Pick one from availableTabs and retry with a `tabUrl` substring of its url, or open the page first (extension_open with `url`)."
4850
+ });
4851
+ if (matches.length > 1) return JSON.stringify({
4852
+ ok: false,
4853
+ error: {
4854
+ name: "AmbiguousTabUrl",
4855
+ message: `${matches.length} tabs match "${args.tabUrl}"; refusing to guess which tab you mean.`
4566
4856
  },
4567
- hint: "Confirm the session is ready (extension_wait), then retry, or target with `url`/`tab` instead."
4857
+ matchingTabs: matches,
4858
+ hint: "Narrow `tabUrl` to a longer substring that matches exactly one url in matchingTabs, or pass its numeric tabId as `tab`."
4568
4859
  });
4860
+ resolvedTarget = {
4861
+ ...matches[0]
4862
+ };
4863
+ if (null != matches[0].tabId) targetTab = matches[0].tabId;
4864
+ else targetUrl = matches[0].url;
4569
4865
  }
4570
- const matches = matchTargetsByUrl(targets, args.tabUrl);
4571
- if (0 === matches.length) return JSON.stringify({
4572
- ok: false,
4573
- error: {
4574
- name: "NoMatchingTarget",
4575
- message: `No open page target's url (or title) contains "${args.tabUrl}" (case-insensitive).`
4576
- },
4577
- availableTargets: targets,
4578
- hint: `Pick one from availableTargets and retry with a \`tabUrl\` substring of its url, or open the page first (extension_open with \`url\`). ${TARGET_ID_NOTE}`
4579
- });
4580
- if (matches.length > 1) return JSON.stringify({
4581
- ok: false,
4582
- error: {
4583
- name: "AmbiguousTabUrl",
4584
- message: `${matches.length} page targets match "${args.tabUrl}"; refusing to guess which tab you mean.`
4585
- },
4586
- matchingTargets: matches,
4587
- hint: `Narrow \`tabUrl\` to a longer substring that matches exactly one url in matchingTargets. ${TARGET_ID_NOTE}`
4588
- });
4589
- resolvedTarget = matches[0];
4590
- targetUrl = resolvedTarget.url;
4591
4866
  }
4592
4867
  const cli = [
4593
4868
  "inspect",
4594
4869
  args.projectPath
4595
4870
  ];
4596
- if (null != args.tab) cli.push("--tab", String(args.tab));
4871
+ if (null != targetTab) cli.push("--tab", String(targetTab));
4597
4872
  if (targetUrl) cli.push("--url", targetUrl);
4598
4873
  if (args.context) cli.push("--context", args.context);
4599
4874
  if (args.include?.length) cli.push("--include", args.include.join(","));
@@ -4686,7 +4961,117 @@ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
4686
4961
  if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
4687
4962
  return creds;
4688
4963
  }
4964
+ const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
4965
+ const CONSOLE_BASE = "https://console.extension.dev";
4966
+ function registryBase() {
4967
+ const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
4968
+ return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
4969
+ }
4970
+ function resolveProjectRef(overrides) {
4971
+ const workspace = String(overrides?.workspace || "").trim();
4972
+ const project = String(overrides?.project || "").trim();
4973
+ if (workspace && project) return {
4974
+ workspace,
4975
+ project
4976
+ };
4977
+ const creds = readCredentials();
4978
+ const ws = workspace || String(creds?.workspaceSlug || "").trim();
4979
+ const proj = project || String(creds?.projectSlug || "").trim();
4980
+ if (!ws || !proj) return null;
4981
+ return {
4982
+ workspace: ws,
4983
+ project: proj
4984
+ };
4985
+ }
4986
+ function registryFileUrl(ref, file) {
4987
+ return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
4988
+ }
4989
+ function consoleProjectUrl(ref, page) {
4990
+ if (!ref) return `${CONSOLE_BASE}`;
4991
+ return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
4992
+ }
4993
+ async function fetchRegistryJson(url, fetchImpl = fetch) {
4994
+ let res;
4995
+ try {
4996
+ res = await fetchImpl(url);
4997
+ } catch (err) {
4998
+ return {
4999
+ ok: false,
5000
+ message: `Could not reach ${url}: ${err?.message || err}`
5001
+ };
5002
+ }
5003
+ if (!res.ok) return {
5004
+ ok: false,
5005
+ status: res.status,
5006
+ message: `${url} returned ${res.status}`
5007
+ };
5008
+ try {
5009
+ const text = await res.text();
5010
+ return {
5011
+ ok: true,
5012
+ json: JSON.parse(text)
5013
+ };
5014
+ } catch {
5015
+ return {
5016
+ ok: false,
5017
+ message: `${url} did not return valid JSON`
5018
+ };
5019
+ }
5020
+ }
5021
+ function parseChannels(json) {
5022
+ if (!json || "object" != typeof json || Array.isArray(json)) return [];
5023
+ const out = [];
5024
+ for (const [channel, raw] of Object.entries(json)){
5025
+ if (!raw || "object" != typeof raw) continue;
5026
+ const row = raw;
5027
+ const description = "string" == typeof row.description ? row.description : void 0;
5028
+ const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
5029
+ const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
5030
+ const entry = {
5031
+ channel,
5032
+ sha: String(row.sha ?? "")
5033
+ };
5034
+ if (row.buildId) entry.buildId = String(row.buildId);
5035
+ if (row.version) entry.version = String(row.version);
5036
+ const promotedAt = promotedAtField || fromDescription;
5037
+ if (promotedAt) entry.promotedAt = promotedAt;
5038
+ if (description) entry.description = description;
5039
+ out.push(entry);
5040
+ }
5041
+ return out;
5042
+ }
5043
+ function parseBuildIndex(json) {
5044
+ const items = json?.items;
5045
+ if (!Array.isArray(items)) return [];
5046
+ const out = [];
5047
+ for (const raw of items){
5048
+ if (!raw || "object" != typeof raw) continue;
5049
+ const row = raw;
5050
+ const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
5051
+ if (!sha) continue;
5052
+ const entry = {
5053
+ sha
5054
+ };
5055
+ if (row.commit) entry.commit = String(row.commit);
5056
+ if (row.channel) entry.channel = String(row.channel);
5057
+ if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
5058
+ if (row.status) entry.status = String(row.status);
5059
+ if (row.version) entry.version = String(row.version);
5060
+ if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
5061
+ if (row.timestamp) entry.timestamp = String(row.timestamp);
5062
+ if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
5063
+ out.push(entry);
5064
+ }
5065
+ return out;
5066
+ }
4689
5067
  const DEFAULT_API = "https://www.extension.dev";
5068
+ function tokenTtlNote(workspaceSlug, projectSlug) {
5069
+ const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
5070
+ workspace: workspaceSlug,
5071
+ project: projectSlug
5072
+ }, "settings/access-tokens") : CONSOLE_BASE;
5073
+ return `extension.dev CLI tokens live at most 7 days (server-enforced). CI pipelines must re-mint before expiry on the console's Access tokens page: ${tokensUrl}`;
5074
+ }
4690
5075
  function resolveApiBase(api) {
4691
5076
  return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
4692
5077
  }
@@ -4851,109 +5236,6 @@ async function publish(options = {}) {
4851
5236
  data
4852
5237
  };
4853
5238
  }
4854
- const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
4855
- const CONSOLE_BASE = "https://console.extension.dev";
4856
- function registryBase() {
4857
- const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
4858
- return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
4859
- }
4860
- function resolveProjectRef(overrides) {
4861
- const workspace = String(overrides?.workspace || "").trim();
4862
- const project = String(overrides?.project || "").trim();
4863
- if (workspace && project) return {
4864
- workspace,
4865
- project
4866
- };
4867
- const creds = readCredentials();
4868
- const ws = workspace || String(creds?.workspaceSlug || "").trim();
4869
- const proj = project || String(creds?.projectSlug || "").trim();
4870
- if (!ws || !proj) return null;
4871
- return {
4872
- workspace: ws,
4873
- project: proj
4874
- };
4875
- }
4876
- function registryFileUrl(ref, file) {
4877
- return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
4878
- }
4879
- function consoleProjectUrl(ref, page) {
4880
- if (!ref) return `${CONSOLE_BASE}`;
4881
- return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
4882
- }
4883
- async function fetchRegistryJson(url, fetchImpl = fetch) {
4884
- let res;
4885
- try {
4886
- res = await fetchImpl(url);
4887
- } catch (err) {
4888
- return {
4889
- ok: false,
4890
- message: `Could not reach ${url}: ${err?.message || err}`
4891
- };
4892
- }
4893
- if (!res.ok) return {
4894
- ok: false,
4895
- status: res.status,
4896
- message: `${url} returned ${res.status}`
4897
- };
4898
- try {
4899
- const text = await res.text();
4900
- return {
4901
- ok: true,
4902
- json: JSON.parse(text)
4903
- };
4904
- } catch {
4905
- return {
4906
- ok: false,
4907
- message: `${url} did not return valid JSON`
4908
- };
4909
- }
4910
- }
4911
- function parseChannels(json) {
4912
- if (!json || "object" != typeof json || Array.isArray(json)) return [];
4913
- const out = [];
4914
- for (const [channel, raw] of Object.entries(json)){
4915
- if (!raw || "object" != typeof raw) continue;
4916
- const row = raw;
4917
- const description = "string" == typeof row.description ? row.description : void 0;
4918
- const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
4919
- const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
4920
- const entry = {
4921
- channel,
4922
- sha: String(row.sha ?? "")
4923
- };
4924
- if (row.buildId) entry.buildId = String(row.buildId);
4925
- if (row.version) entry.version = String(row.version);
4926
- const promotedAt = promotedAtField || fromDescription;
4927
- if (promotedAt) entry.promotedAt = promotedAt;
4928
- if (description) entry.description = description;
4929
- out.push(entry);
4930
- }
4931
- return out;
4932
- }
4933
- function parseBuildIndex(json) {
4934
- const items = json?.items;
4935
- if (!Array.isArray(items)) return [];
4936
- const out = [];
4937
- for (const raw of items){
4938
- if (!raw || "object" != typeof raw) continue;
4939
- const row = raw;
4940
- const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
4941
- if (!sha) continue;
4942
- const entry = {
4943
- sha
4944
- };
4945
- if (row.commit) entry.commit = String(row.commit);
4946
- if (row.channel) entry.channel = String(row.channel);
4947
- if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
4948
- if (row.status) entry.status = String(row.status);
4949
- if (row.version) entry.version = String(row.version);
4950
- if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
4951
- if (row.timestamp) entry.timestamp = String(row.timestamp);
4952
- if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
4953
- out.push(entry);
4954
- }
4955
- return out;
4956
- }
4957
5239
  const publish_schema = {
4958
5240
  name: "extension_publish",
4959
5241
  description: "Publish the project your stored token is scoped to (from extension_login, or EXTENSION_DEV_TOKEN) to extension.dev and return its shareable URL. The publish target is the token's project -- there is no projectPath, the local files are not uploaded. For a PUBLIC project the URL is the canonical public page and ttlHours does not apply; for a PRIVATE project it is a fresh time-limited share link (?share=) whose lifetime is ttlHours. Posts to the platform's CLI publish endpoint. Besides extension_login this is the only tool that talks to the hosted platform.",
@@ -5031,7 +5313,7 @@ async function publish_handler(args) {
5031
5313
  const release_promote_DEFAULT_API = "https://www.extension.dev";
5032
5314
  const release_promote_schema = {
5033
5315
  name: "extension_release_promote",
5034
- description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: uses your stored login (extension_login) or 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).",
5316
+ description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: uses your stored login (extension_login) or a release token in EXTENSION_DEV_TOKEN (mint and revoke it in the dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry). 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).",
5035
5317
  inputSchema: {
5036
5318
  type: "object",
5037
5319
  properties: {
@@ -5084,7 +5366,7 @@ function release_promote_fail(name, message) {
5084
5366
  }
5085
5367
  async function release_promote_handler(args) {
5086
5368
  const token = resolveToken();
5087
- 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.");
5369
+ 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; tokens live at most 7 days, so CI must re-mint before expiry), or run extension_login.");
5088
5370
  const buildId = String(args.buildId || "").trim();
5089
5371
  const channel = String(args.channel || "").trim();
5090
5372
  if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
@@ -5236,7 +5518,7 @@ function storeMdWarnings(browsers, cwd) {
5236
5518
  content = node_fs.readFileSync(node_path.join(cwd, "STORE.md"), "utf8");
5237
5519
  } catch {
5238
5520
  return [
5239
- "No STORE.md found in the project root. Reviewer notes (Firefox) and certification notes (Edge) will not accompany the submission. See the extension-dev skill's store-md reference."
5521
+ "No STORE.md found in the current working directory. Platform submissions read STORE.md from the project's source repository, so this may not apply here; make sure STORE.md exists there for Firefox reviewer notes and Edge certification notes. See the extension-dev skill's store-md reference."
5240
5522
  ];
5241
5523
  }
5242
5524
  const hasField = (section, field)=>{
@@ -5255,7 +5537,7 @@ function storeMdWarnings(browsers, cwd) {
5255
5537
  }
5256
5538
  const deploy_schema = {
5257
5539
  name: "extension_deploy",
5258
- description: "Submit a built extension to the Chrome Web Store, Firefox AMO, and/or Edge Add-ons THROUGH extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI. DEFAULTS TO A DRY RUN (preflight: verifies auth, the project, that the build exists, and the store workflow - dispatches nothing); pass dryRun:false to actually submit, which is irreversible and enters store review. The target project is identified by your token (extension_login or a release token in EXTENSION_DEV_TOKEN); store credentials are never tool arguments and local files are not uploaded. Pass browsers + buildSha. Posts to the platform's CLI store-submission endpoint.",
5540
+ description: "Submit a built extension to the Chrome Web Store, Firefox AMO, and/or Edge Add-ons THROUGH extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI. DEFAULTS TO A DRY RUN (preflight - dispatches nothing): the platform side verifies auth, the project, that the build exists, and the store workflow, and this tool then adds the per-store verdict from each store's public credential-health record; trust the per-store rows in the result over the platform's bare preflight line, which does not check store health. Pass dryRun:false to actually submit, which is irreversible and enters store review. The target project is identified by your token (extension_login or a release token in EXTENSION_DEV_TOKEN; tokens live at most 7 days, so CI must re-mint from the console's Access tokens page). Store credentials are never tool arguments and local files are not uploaded. Pass browsers + buildSha (extension_release_list lists valid shas); after a real submission, extension_store_status reads the recorded outcome and review state. Posts to the platform's CLI store-submission endpoint.",
5259
5541
  inputSchema: {
5260
5542
  type: "object",
5261
5543
  properties: {
@@ -5311,7 +5593,7 @@ function deploy_fail(name, message) {
5311
5593
  }
5312
5594
  async function deploy_handler(args) {
5313
5595
  const token = resolveToken();
5314
- if (!token) return deploy_fail("DeployAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard under project settings -> Access tokens).");
5596
+ if (!token) return deploy_fail("DeployAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry).");
5315
5597
  const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
5316
5598
  if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge"]).');
5317
5599
  const buildSha = String(args.buildSha || "").trim();
@@ -5429,9 +5711,214 @@ async function deploy_handler(args) {
5429
5711
  if ("string" == typeof data?.message) result.platformMessage = data.message;
5430
5712
  result.message = summaryParts.join(" ");
5431
5713
  }
5714
+ if (!dryRun) result.statusNote = "Track this submission with extension_store_status: it reads the recorded outcome, per-store credential health, and review state from the public registry.";
5432
5715
  if (warnings.length > 0) result.warnings = warnings;
5433
5716
  return JSON.stringify(result);
5434
5717
  }
5718
+ const KNOWN_STORES = [
5719
+ "chrome",
5720
+ "firefox",
5721
+ "edge",
5722
+ "safari"
5723
+ ];
5724
+ const store_status_schema = {
5725
+ name: "extension_store_status",
5726
+ description: "Report the project's browser-store state after an extension_deploy submission: per store (chrome, firefox, edge, safari) whether it is configured, its latest credential health check, the last recorded submission (version, status, store URL, submitted-at), and the latest review status. Reads the project's public registry (registry.extension.land: stores/health.json, stores/status.json, stores/submissions.json) - read-only, dispatches nothing, no auth needed for public projects. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another public project. Registry state can lag the store dashboards by up to a polling interval.",
5727
+ inputSchema: {
5728
+ type: "object",
5729
+ properties: {
5730
+ workspace: {
5731
+ type: "string",
5732
+ description: "Workspace slug override (defaults to the stored login's workspace)."
5733
+ },
5734
+ project: {
5735
+ type: "string",
5736
+ description: "Project slug override (defaults to the stored login's project)."
5737
+ }
5738
+ },
5739
+ required: []
5740
+ }
5741
+ };
5742
+ function isPlainObject(value) {
5743
+ return Boolean(value) && "object" == typeof value && !Array.isArray(value);
5744
+ }
5745
+ function str(value) {
5746
+ const text = String(value ?? "").trim();
5747
+ return text || void 0;
5748
+ }
5749
+ function submissionView(row) {
5750
+ const view = {};
5751
+ const version = str(row.version);
5752
+ const status = str(row.status);
5753
+ const storeUrl = str(row.storeUrl);
5754
+ const submittedAt = str(row.submittedAt) || str(row.timestamp);
5755
+ const channel = str(row.channel);
5756
+ const buildSha = str(row.buildSha) || str(row.buildId);
5757
+ const storeSubmissionId = str(row.storeSubmissionId);
5758
+ const failureReason = str(row.failureReason);
5759
+ if (version) view.version = version;
5760
+ if (status) view.status = status;
5761
+ if (storeUrl) view.storeUrl = storeUrl;
5762
+ if (submittedAt) view.submittedAt = submittedAt;
5763
+ if (channel) view.channel = channel;
5764
+ if (buildSha) view.buildSha = buildSha;
5765
+ if (storeSubmissionId) view.storeSubmissionId = storeSubmissionId;
5766
+ if (failureReason) view.failureReason = failureReason;
5767
+ return view;
5768
+ }
5769
+ function latestSubmissionsByStore(json) {
5770
+ const list = json?.submissions;
5771
+ const out = {};
5772
+ for (const raw of Array.isArray(list) ? list : []){
5773
+ if (!isPlainObject(raw)) continue;
5774
+ const store = str(raw.store);
5775
+ if (!store) continue;
5776
+ const at = Date.parse(String(raw.submittedAt || raw.updatedAt || ""));
5777
+ const stamp = Number.isFinite(at) ? at : 0;
5778
+ if (!out[store] || stamp >= out[store].at) out[store] = {
5779
+ at: stamp,
5780
+ view: submissionView(raw)
5781
+ };
5782
+ }
5783
+ const flat = {};
5784
+ for (const [store, entry] of Object.entries(out))flat[store] = entry.view;
5785
+ return flat;
5786
+ }
5787
+ function normalizeStoresStatus(json) {
5788
+ const base = isPlainObject(json) ? json : {};
5789
+ const out = {
5790
+ reviews: {}
5791
+ };
5792
+ if (isPlainObject(base.lastSubmission)) out.lastSubmission = base.lastSubmission;
5793
+ if (isPlainObject(base.lastOverride)) out.lastOverride = base.lastOverride;
5794
+ if (isPlainObject(base.reviews)) {
5795
+ for (const [store, raw] of Object.entries(base.reviews))if (isPlainObject(raw)) out.reviews[store] = {
5796
+ status: str(raw.status),
5797
+ version: str(raw.version),
5798
+ buildId: str(raw.buildId),
5799
+ checkedAt: str(raw.checkedAt)
5800
+ };
5801
+ }
5802
+ const lastPoll = isPlainObject(base.lastPoll) ? base.lastPoll : null;
5803
+ const looksLikeLegacyPoll = !lastPoll && ("store_status" === base.kind || Array.isArray(base.updates));
5804
+ if (lastPoll) out.lastPollAt = str(lastPoll.timestamp);
5805
+ else if (looksLikeLegacyPoll) {
5806
+ out.lastPollAt = str(base.updatedAt);
5807
+ for (const raw of Array.isArray(base.updates) ? base.updates : []){
5808
+ if (!isPlainObject(raw)) continue;
5809
+ const store = str(raw.store);
5810
+ if (store && !out.reviews[store]) out.reviews[store] = {
5811
+ status: str(raw.status),
5812
+ version: str(raw.version),
5813
+ buildId: str(raw.buildId),
5814
+ checkedAt: str(base.updatedAt)
5815
+ };
5816
+ }
5817
+ }
5818
+ return out;
5819
+ }
5820
+ function store_status_fail(name, message, extra) {
5821
+ return JSON.stringify({
5822
+ ok: false,
5823
+ error: {
5824
+ name,
5825
+ message
5826
+ },
5827
+ ...extra ?? {}
5828
+ });
5829
+ }
5830
+ async function store_status_handler(args) {
5831
+ const ref = resolveProjectRef(args);
5832
+ if (!ref) return store_status_fail("StoreStatusInputError", "No project to inspect. Run extension_login (the stored login names the project), or pass workspace + project explicitly.");
5833
+ const healthUrl = registryFileUrl(ref, "stores/health.json");
5834
+ const statusUrl = registryFileUrl(ref, "stores/status.json");
5835
+ const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
5836
+ const consoleStoresUrl = consoleProjectUrl(ref, "stores");
5837
+ const [healthRes, statusRes, submissionsRes] = await Promise.all([
5838
+ fetchRegistryJson(healthUrl),
5839
+ fetchRegistryJson(statusUrl),
5840
+ fetchRegistryJson(submissionsUrl)
5841
+ ]);
5842
+ if (!healthRes.ok && !statusRes.ok && !submissionsRes.ok) return store_status_fail("StoreStatusNotFound", `No store data on the registry for ${ref.workspace}/${ref.project} (${healthUrl} returned ${healthRes.status ?? "no response"}). The project may have no stores configured yet, be private (private registry data needs a share token), or the workspace/project slugs may be wrong. Configure stores at ${consoleStoresUrl}/new; the console Stores page is the authoritative view: ${consoleStoresUrl}`, {
5843
+ workspace: ref.workspace,
5844
+ project: ref.project,
5845
+ registryUrls: {
5846
+ health: healthUrl,
5847
+ status: statusUrl,
5848
+ submissions: submissionsUrl
5849
+ },
5850
+ consoleStoresUrl
5851
+ });
5852
+ const healthStores = healthRes.ok ? isPlainObject(healthRes.json?.stores) ? healthRes.json.stores ?? null : null : null;
5853
+ const status = normalizeStoresStatus(statusRes.ok ? statusRes.json : null);
5854
+ const submissionsByStore = submissionsRes.ok ? latestSubmissionsByStore(submissionsRes.json) : {};
5855
+ const statusLastStore = str(status.lastSubmission?.store);
5856
+ if (statusLastStore && !submissionsByStore[statusLastStore] && status.lastSubmission) submissionsByStore[statusLastStore] = submissionView(status.lastSubmission);
5857
+ const stores = [
5858
+ ...KNOWN_STORES,
5859
+ ...Object.keys({
5860
+ ...healthStores,
5861
+ ...submissionsByStore,
5862
+ ...status.reviews
5863
+ }).filter((s)=>!KNOWN_STORES.includes(s))
5864
+ ];
5865
+ const rows = stores.map((store)=>{
5866
+ const healthRow = healthStores?.[store];
5867
+ const configured = healthStores ? Boolean(healthRow) : "unknown";
5868
+ const row = {
5869
+ store,
5870
+ configured
5871
+ };
5872
+ if (healthRow) row.health = {
5873
+ ok: true === healthRow.ok,
5874
+ checkedAt: str(healthRow.checkedAt),
5875
+ message: str(healthRow.message)
5876
+ };
5877
+ const submission = submissionsByStore[store];
5878
+ if (submission && Object.keys(submission).length > 0) row.lastSubmission = submission;
5879
+ const review = status.reviews[store];
5880
+ if (review && Object.values(review).some(Boolean)) row.review = review;
5881
+ return row;
5882
+ });
5883
+ const summaryParts = rows.map((row)=>{
5884
+ const store = String(row.store);
5885
+ const health = row.health;
5886
+ const submission = row.lastSubmission;
5887
+ const review = row.review;
5888
+ let head;
5889
+ head = "unknown" === row.configured ? `${store}: configuration unknown (stores/health.json is unreadable)` : false === row.configured ? `${store}: not configured (add it at ${consoleStoresUrl}/new)` : health && !health.ok ? `${store}: configured but its credentials FAILED the last health check (${health.message || "no reason recorded"}) - fix them at ${consoleStoresUrl}/${store}` : `${store}: configured, credentials healthy`;
5890
+ const tail = [];
5891
+ if (submission) {
5892
+ tail.push(`last submission${submission.version ? ` v${submission.version}` : ""} ${submission.status || "recorded"}${submission.submittedAt ? ` at ${submission.submittedAt}` : ""}${submission.failureReason ? ` (${submission.failureReason})` : ""}`);
5893
+ if (submission.storeUrl) tail.push(`listing ${submission.storeUrl}`);
5894
+ } else if (true === row.configured) tail.push("no submissions recorded");
5895
+ if (review?.status) tail.push(`review ${review.status}${review.checkedAt ? ` (checked ${review.checkedAt})` : ""}`);
5896
+ return tail.length > 0 ? `${head}; ${tail.join("; ")}` : head;
5897
+ });
5898
+ const result = {
5899
+ ok: true,
5900
+ workspace: ref.workspace,
5901
+ project: ref.project,
5902
+ stores: rows,
5903
+ ...status.lastSubmission ? {
5904
+ lastSubmission: status.lastSubmission
5905
+ } : {},
5906
+ ...status.lastPollAt ? {
5907
+ lastPollAt: status.lastPollAt
5908
+ } : {},
5909
+ registryUrls: {
5910
+ health: healthUrl,
5911
+ status: statusUrl,
5912
+ submissions: submissionsUrl
5913
+ },
5914
+ consoleStoresUrl,
5915
+ message: `${summaryParts.join(". ")}. This is the registry's recorded state (submissions and the review poller write it); the store dashboards are authoritative and may be ahead of it.`
5916
+ };
5917
+ if (!healthRes.ok) result.healthUnavailable = `stores/health.json unreadable: ${healthRes.message}`;
5918
+ if (!statusRes.ok) result.statusUnavailable = `stores/status.json unreadable: ${statusRes.message}`;
5919
+ if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
5920
+ return JSON.stringify(result);
5921
+ }
5435
5922
  function doctor_readReadyContract(projectPath, browser) {
5436
5923
  try {
5437
5924
  const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
@@ -6187,7 +6674,7 @@ async function pollDeviceToken(args) {
6187
6674
  }
6188
6675
  const login_schema = {
6189
6676
  name: "extension_login",
6190
- description: "Authenticate to extension.dev 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. The server picks the flow: the extension.dev-gated device flow (you authorize at extension.dev/device; GitHub federation happens server-side, no GitHub token on this machine) or, as a fallback, the legacy GitHub device flow. Never returns the token. This is the only tool besides extension_publish that talks to the hosted platform.",
6677
+ description: "Authenticate to extension.dev 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. The server picks the flow: the extension.dev-gated device flow (you authorize at extension.dev/device; GitHub federation happens server-side, no GitHub token on this machine) or, as a fallback, the legacy GitHub device flow. Never returns the token. Minted tokens live at most 7 days (server-enforced), so CI pipelines must re-mint before expiry on the console's Access tokens page (project settings -> Access tokens). This is the only tool besides extension_publish that talks to the hosted platform.",
6191
6678
  inputSchema: {
6192
6679
  type: "object",
6193
6680
  properties: {
@@ -6221,13 +6708,15 @@ function login_fail(name, message) {
6221
6708
  });
6222
6709
  }
6223
6710
  function success(creds) {
6711
+ const expiresAt = creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null;
6224
6712
  return JSON.stringify({
6225
6713
  ok: true,
6226
6714
  status: "logged-in",
6227
6715
  workspaceSlug: creds.workspaceSlug,
6228
6716
  projectSlug: creds.projectSlug,
6229
- expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
6230
- message: `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}. extension_publish can now use the stored token.`
6717
+ expiresAt,
6718
+ tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
6719
+ message: `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}. extension_publish can now use the stored token. The token expires ${expiresAt ?? "within 7 days"}: extension.dev CLI tokens live at most 7 days, so CI must re-mint before then (console: project settings -> Access tokens).`
6231
6720
  });
6232
6721
  }
6233
6722
  function login_pending(start) {
@@ -6243,6 +6732,7 @@ function login_pending(start) {
6243
6732
  verificationUriComplete: complete
6244
6733
  } : {},
6245
6734
  deviceCode: start.deviceCode,
6735
+ tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
6246
6736
  message
6247
6737
  });
6248
6738
  }
@@ -6252,6 +6742,7 @@ function resumePending(deviceCode, verificationUri) {
6252
6742
  status: "authorization_pending",
6253
6743
  verificationUri,
6254
6744
  deviceCode,
6745
+ tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
6255
6746
  message: `Still waiting for authorization. The one-click link and code from the previous response are still valid: open that link (or enter the code at ${verificationUri}), then call extension_login again with this same deviceCode (and the same project).`
6256
6747
  });
6257
6748
  }
@@ -6361,7 +6852,7 @@ async function login_handler(args) {
6361
6852
  }
6362
6853
  const whoami_schema = {
6363
6854
  name: "extension_whoami",
6364
- description: "Report the identity carried by the locally stored extension.dev token that extension_login minted (workspace/project scoped), plus its expiry, without revealing the token. The identity comes from that stored token alone; it does not change with the current working directory or whichever project folder you are in. Returns logged-out status when no credentials are stored.",
6855
+ description: "Report the identity carried by the locally stored extension.dev token that extension_login minted (workspace/project scoped), plus its expiry, without revealing the token. The identity comes from that stored token alone; it does not change with the current working directory or whichever project folder you are in. The result records where the login was minted (apiRecordedAtLogin) without asserting a platform base URL: authenticated tools target their own api argument, EXTENSION_DEV_API_URL, or the production default. Returns logged-out status when no credentials are stored.",
6365
6856
  inputSchema: {
6366
6857
  type: "object",
6367
6858
  properties: {}
@@ -6376,17 +6867,32 @@ async function whoami_handler() {
6376
6867
  });
6377
6868
  const now = Math.floor(Date.now() / 1000);
6378
6869
  const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
6870
+ const recordedApi = String(creds.api || "").trim();
6871
+ const effectiveDefaultApi = resolveApiBase();
6872
+ const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
6873
+ const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
6874
+ const messageParts = [];
6875
+ messageParts.push(expired ? "The stored token has expired. Run extension_login to refresh it." : `Logged in as ${creds.workspaceSlug}/${creds.projectSlug}, per the token extension_login stored on this machine. That token is what scopes the identity: it does not follow the current working directory or project folder.`);
6876
+ if (apiDiverges) messageParts.push(`This login was minted via ${recordedApi}, but authenticated tools do not read that recorded value: they target ${effectiveDefaultApi} unless given an api argument.`);
6877
+ if (envTokenSet) messageParts.push("EXTENSION_DEV_TOKEN is set and takes precedence over this stored login for authenticated tools; this report describes only the stored login.");
6379
6878
  return JSON.stringify({
6380
6879
  ok: true,
6381
6880
  status: expired ? "expired" : "logged-in",
6382
6881
  workspaceSlug: creds.workspaceSlug,
6383
6882
  projectSlug: creds.projectSlug,
6384
- api: creds.api,
6883
+ ...recordedApi ? {
6884
+ apiRecordedAtLogin: recordedApi
6885
+ } : {},
6886
+ apiDefault: effectiveDefaultApi,
6385
6887
  provider: creds.provider ?? "github",
6386
6888
  expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
6387
6889
  expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
6388
6890
  expired,
6389
- message: expired ? "The stored token has expired. Run extension_login to refresh it." : `Logged in as ${creds.workspaceSlug}/${creds.projectSlug}, per the token extension_login stored on this machine. That token is what scopes the identity: it does not follow the current working directory or project folder.`
6891
+ ...envTokenSet ? {
6892
+ envTokenOverride: true
6893
+ } : {},
6894
+ tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
6895
+ message: messageParts.join(" ")
6390
6896
  });
6391
6897
  }
6392
6898
  const logout_schema = {
@@ -7073,6 +7579,7 @@ const tools = [
7073
7579
  release_list_namespaceObject,
7074
7580
  release_promote_namespaceObject,
7075
7581
  deploy_namespaceObject,
7582
+ store_status_namespaceObject,
7076
7583
  wait_namespaceObject,
7077
7584
  add_feature_namespaceObject,
7078
7585
  login_namespaceObject,