@extension.dev/mcp 5.4.0 → 5.5.1

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
@@ -68,6 +68,7 @@ var eval_namespaceObject = {};
68
68
  __webpack_require__.r(eval_namespaceObject);
69
69
  __webpack_require__.d(eval_namespaceObject, {
70
70
  handler: ()=>eval_handler,
71
+ resolveDefaultEvalContext: ()=>resolveDefaultEvalContext,
71
72
  schema: ()=>eval_schema
72
73
  });
73
74
  var get_template_source_namespaceObject = {};
@@ -192,6 +193,14 @@ __webpack_require__.d(storage_namespaceObject, {
192
193
  handler: ()=>storage_handler,
193
194
  schema: ()=>storage_schema
194
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
+ });
195
204
  var uninstall_browser_namespaceObject = {};
196
205
  __webpack_require__.r(uninstall_browser_namespaceObject);
197
206
  __webpack_require__.d(uninstall_browser_namespaceObject, {
@@ -210,7 +219,7 @@ __webpack_require__.d(whoami_namespaceObject, {
210
219
  handler: ()=>whoami_handler,
211
220
  schema: ()=>whoami_schema
212
221
  });
213
- var package_namespaceObject = JSON.parse('{"rE":"5.4.0","El":{"OP":"^4.0.13"}}');
222
+ var package_namespaceObject = JSON.parse('{"rE":"5.5.1","El":{"OP":"^4.0.13"}}');
214
223
  function scaffoldEnginePin(projectPath) {
215
224
  try {
216
225
  const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
@@ -3179,7 +3188,7 @@ async function source_inspect_handler(args) {
3179
3188
  }
3180
3189
  const list_extensions_schema = {
3181
3190
  name: "extension_list_extensions",
3182
- description: "List the extensions with a live context in the running dev browser via Chrome DevTools Protocol. Returns each extension's id, name, version, and live contexts (service worker, page). Identity is read read-only via the Extensions domain, other extensions' contexts are never attached to or evaluated in. A dormant MV3 service worker with no open page may be absent until it wakes. Chromium only (Firefox uses RDP, not yet supported). Requires an active dev or start session.",
3191
+ 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.",
3183
3192
  inputSchema: {
3184
3193
  type: "object",
3185
3194
  properties: {
@@ -3197,6 +3206,47 @@ const list_extensions_schema = {
3197
3206
  ]
3198
3207
  }
3199
3208
  };
3209
+ function unpackedExtensionId(distPath) {
3210
+ const digest = node_crypto.createHash("sha256").update(distPath).digest();
3211
+ let id = "";
3212
+ for(let i = 0; i < 16; i++){
3213
+ id += String.fromCharCode(97 + (digest[i] >> 4));
3214
+ id += String.fromCharCode(97 + (0x0f & digest[i]));
3215
+ }
3216
+ return id;
3217
+ }
3218
+ function readOwnIdentity(projectPath, browser) {
3219
+ let contract;
3220
+ try {
3221
+ const file = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
3222
+ contract = JSON.parse(node_fs.readFileSync(file, "utf8"));
3223
+ } catch {
3224
+ return null;
3225
+ }
3226
+ const distPath = "string" == typeof contract?.distPath ? contract.distPath : null;
3227
+ const ids = [];
3228
+ if (distPath) {
3229
+ ids.push(unpackedExtensionId(distPath));
3230
+ try {
3231
+ const real = node_fs.realpathSync(distPath);
3232
+ if (real !== distPath) ids.push(unpackedExtensionId(real));
3233
+ } catch {}
3234
+ }
3235
+ let name = "string" == typeof contract?.extensionName ? contract.extensionName : void 0;
3236
+ let version = "string" == typeof contract?.extensionVersion ? contract.extensionVersion : void 0;
3237
+ if ((!name || !version) && distPath) try {
3238
+ const manifest = JSON.parse(node_fs.readFileSync(node_path.join(distPath, "manifest.json"), "utf8"));
3239
+ if (!name && "string" == typeof manifest?.name && !manifest.name.startsWith("__MSG_")) name = manifest.name;
3240
+ if (!version && "string" == typeof manifest?.version) version = manifest.version;
3241
+ } catch {}
3242
+ if (0 === ids.length && !name) return null;
3243
+ return {
3244
+ ids,
3245
+ name,
3246
+ version
3247
+ };
3248
+ }
3249
+ const UNRESOLVED_NOTE = "Identity unresolved: the browser's Extensions CDP domain returned nothing for this id, and other extensions' contexts are never attached to or evaluated in to read a manifest.";
3200
3250
  async function list_extensions_handler(args) {
3201
3251
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
3202
3252
  if (!isChromiumFamily(browser)) return JSON.stringify({
@@ -3237,6 +3287,7 @@ async function list_extensions_handler(args) {
3237
3287
  });
3238
3288
  byId.set(id, list);
3239
3289
  }
3290
+ const own = readOwnIdentity(args.projectPath, browser);
3240
3291
  const extensions = [];
3241
3292
  for (const [id, ctxTargets] of byId){
3242
3293
  const entry = {
@@ -3257,15 +3308,33 @@ async function list_extensions_handler(args) {
3257
3308
  entry.source = "extensions-domain";
3258
3309
  }
3259
3310
  } catch {}
3311
+ if (own?.ids.includes(id)) {
3312
+ entry.ownExtension = true;
3313
+ if (void 0 === entry.name && void 0 !== own.name) {
3314
+ entry.name = own.name;
3315
+ if (void 0 !== own.version) entry.version = own.version;
3316
+ entry.source = "session-contract";
3317
+ }
3318
+ }
3260
3319
  extensions.push(entry);
3261
3320
  }
3262
- extensions.sort((a, b)=>(a.name ?? a.id).localeCompare(b.name ?? b.id));
3321
+ if (own?.name && !extensions.some((e)=>e.ownExtension)) {
3322
+ const byName = extensions.filter((e)=>e.name === own.name);
3323
+ if (1 === byName.length) byName[0].ownExtension = true;
3324
+ }
3325
+ for (const entry of extensions)if (void 0 === entry.name) entry.note = UNRESOLVED_NOTE;
3326
+ extensions.sort((a, b)=>{
3327
+ if ((a.ownExtension ?? false) !== (b.ownExtension ?? false)) return a.ownExtension ? -1 : 1;
3328
+ return (a.name ?? a.id).localeCompare(b.name ?? b.id);
3329
+ });
3330
+ const ownEntry = extensions.find((e)=>e.ownExtension);
3263
3331
  return JSON.stringify({
3264
3332
  cdpPort,
3265
3333
  browser,
3266
3334
  count: extensions.length,
3335
+ ownExtensionId: ownEntry?.id ?? null,
3267
3336
  extensions,
3268
- note: "Lists extensions that currently have at least one live context (service worker or open page). An MV3 service worker that has gone dormant with no open page may be absent until it wakes. Identity is read read-only via the Extensions domain; other extensions' contexts are never attached to or evaluated in."
3337
+ note: "Lists extensions that currently have at least one live context (service worker or open page). An MV3 service worker that has gone dormant with no open page may be absent until it wakes. ownExtension marks the extension this dev session serves, identified from the session's ready contract. Other identity is read read-only via the Extensions domain; other extensions' contexts are never attached to or evaluated in."
3269
3338
  });
3270
3339
  } catch (error) {
3271
3340
  return JSON.stringify({
@@ -3597,7 +3666,7 @@ async function logs_handler(args) {
3597
3666
  return readFromFile(args, browser, limit);
3598
3667
  }
3599
3668
  function toMcpSpeak(text) {
3600
- return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/--context[= ]([\w-]+)/g, 'context: "$1"').replace(/--tab[= ](\d+)/g, "tab: $1").replace(/--url[= ](\S+)/g, 'url: "$1"').replace(/--browser[= ]([\w-]+)/g, 'browser: "$1"').replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev");
3669
+ 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`");
3601
3670
  }
3602
3671
  function withSessionContext(message, projectPath) {
3603
3672
  const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
@@ -3650,7 +3719,7 @@ function commonFlags(args) {
3650
3719
  }
3651
3720
  const eval_schema = {
3652
3721
  name: "extension_eval",
3653
- description: "Evaluate an expression in a running extension context. Requires the dev session to be started with allowEval: true (extension_dev; writes a 0600 session token the CLI reads). Targeting for context content/page: pass `url` to pick the matching tab, or omit both `url` and `tab` to use the ACTIVE tab; a numeric `tab` id is only needed to disambiguate. Extension surfaces (popup/options/sidebar/devtools) and override pages (newtab/history/bookmarks) evaluate over the in-bundle relay and need NO tab id; the surface must be OPEN (extension_open first; a closed surface returns an explicit error). Chromium caveat: eval in the MV3 background/service_worker is blocked by CSP (use an MV2/Firefox build for that context). Use extension_dom_inspect with listTabs: true to enumerate {tabId,url,title}. Wraps `extension eval`.",
3722
+ 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`.",
3654
3723
  inputSchema: {
3655
3724
  type: "object",
3656
3725
  properties: {
@@ -3676,8 +3745,7 @@ const eval_schema = {
3676
3745
  "content",
3677
3746
  "page"
3678
3747
  ],
3679
- default: "background",
3680
- description: "Which extension surface to evaluate in"
3748
+ description: "Which extension surface to evaluate in. Default: `background`, EXCEPT on Chromium sessions whose manifest is MV3, where the default is `page` (the active tab) because the MV3 service worker CSP blocks eval; pass `context: \"background\"` explicitly to target the worker anyway."
3681
3749
  },
3682
3750
  url: {
3683
3751
  type: "string",
@@ -3702,14 +3770,38 @@ const eval_schema = {
3702
3770
  ]
3703
3771
  }
3704
3772
  };
3773
+ function resolveDefaultEvalContext(projectPath, browser) {
3774
+ if (!isChromiumFamily(browser)) return "background";
3775
+ const candidates = [
3776
+ node_path.join(projectPath, "dist", browser, "manifest.json"),
3777
+ node_path.join(projectPath, "dist", "manifest.json"),
3778
+ node_path.join(projectPath, "src", "manifest.json"),
3779
+ node_path.join(projectPath, "manifest.json")
3780
+ ];
3781
+ for (const file of candidates){
3782
+ let manifest;
3783
+ try {
3784
+ manifest = JSON.parse(node_fs.readFileSync(file, "utf8"));
3785
+ } catch {
3786
+ continue;
3787
+ }
3788
+ const version = manifest["chromium:manifest_version"] ?? manifest.manifest_version;
3789
+ if (3 === version) return "page";
3790
+ if (2 === version) break;
3791
+ }
3792
+ return "background";
3793
+ }
3705
3794
  async function eval_handler(args) {
3706
3795
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
3796
+ const defaulted = !args.context && "page" === resolveDefaultEvalContext(args.projectPath, browser);
3797
+ const context = defaulted ? "page" : args.context;
3707
3798
  const raw = await runActVerb([
3708
3799
  "eval",
3709
3800
  args.expression,
3710
3801
  args.projectPath,
3711
3802
  ...commonFlags({
3712
3803
  ...args,
3804
+ context,
3713
3805
  browser
3714
3806
  })
3715
3807
  ], args.projectPath, args.timeout);
@@ -3720,6 +3812,15 @@ async function eval_handler(args) {
3720
3812
  return JSON.stringify(parsed);
3721
3813
  }
3722
3814
  } catch {}
3815
+ if (defaulted) try {
3816
+ const parsed = JSON.parse(raw);
3817
+ if (parsed && "object" == typeof parsed) {
3818
+ parsed.defaultedContext = "page";
3819
+ parsed.contextNote = 'No context given: defaulted to "page" (the active tab) because this Chromium session\'s MV3 background is a service worker whose CSP blocks eval. Pass context: "background" explicitly to target the worker (works on Firefox/MV2 builds).';
3820
+ if (false === parsed.ok && /cannot access|chrome-extension:\/\/|chrome:\/\//i.test(JSON.stringify(parsed.error ?? ""))) parsed.hint = "The active tab is a browser or extension page that eval cannot reach. Navigate the dev browser to a regular web page, or pass url (match pattern) or tab to pick one; extension_dom_inspect with listTabs: true lists open tabs.";
3821
+ return JSON.stringify(parsed);
3822
+ }
3823
+ } catch {}
3723
3824
  return raw;
3724
3825
  }
3725
3826
  const storage_schema = {
@@ -3948,7 +4049,7 @@ async function navigateToUrl(projectPath, browser, url) {
3948
4049
  } catch {}
3949
4050
  }
3950
4051
  }
3951
- function unpackedExtensionId(distPath) {
4052
+ function open_unpackedExtensionId(distPath) {
3952
4053
  const digest = node_crypto.createHash("sha256").update(distPath).digest();
3953
4054
  let id = "";
3954
4055
  for(let i = 0; i < 16; i++){
@@ -3959,7 +4060,7 @@ function unpackedExtensionId(distPath) {
3959
4060
  }
3960
4061
  async function resolveExtensionId(projectPath, browser) {
3961
4062
  const distPath = readDistPath(projectPath, browser);
3962
- const computed = distPath ? unpackedExtensionId(distPath) : null;
4063
+ const computed = distPath ? open_unpackedExtensionId(distPath) : null;
3963
4064
  const resolved = await resolveCdpPort(projectPath, browser);
3964
4065
  if (!resolved) return computed;
3965
4066
  const ids = new Set();
@@ -4283,9 +4384,27 @@ async function open_handler(args) {
4283
4384
  } catch {}
4284
4385
  return raw;
4285
4386
  }
4387
+ const TARGET_ID_NOTE = "targetId is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_inspect with listTabs: true.";
4388
+ function filterPageTargets(raw) {
4389
+ return raw.filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
4390
+ targetId: String(t.id),
4391
+ type: String(t.type),
4392
+ url: String(t.url ?? ""),
4393
+ title: String(t.title ?? "")
4394
+ }));
4395
+ }
4396
+ async function listPageTargets(port) {
4397
+ return filterPageTargets(await CDPClient.discoverTargets(port));
4398
+ }
4399
+ function matchTargetsByUrl(targets, needle) {
4400
+ const wanted = needle.toLowerCase();
4401
+ const byUrl = targets.filter((t)=>t.url.toLowerCase().includes(wanted));
4402
+ if (byUrl.length > 0) return byUrl;
4403
+ return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
4404
+ }
4286
4405
  const dom_inspect_schema = {
4287
4406
  name: "extension_dom_inspect",
4288
- description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Requires the dev session to be started with allowControl: true (extension_dev). For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
4407
+ description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Target a tab by `tabUrl` (case-insensitive URL substring resolved against the browser's live page targets; zero or several matches return the candidates instead of guessing), by `url`, or by numeric `tab`. Discover what is open with listTargets: true (CDP targetIds) or listTabs: true (numeric chrome.tabs ids). Requires the dev session to be started with allowControl: true (extension_dev). For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
4289
4408
  inputSchema: {
4290
4409
  type: "object",
4291
4410
  properties: {
@@ -4301,6 +4420,15 @@ const dom_inspect_schema = {
4301
4420
  type: "string",
4302
4421
  description: "For content/page: selects the target tab by url (match pattern, then substring fallback). Preferred over `tab`."
4303
4422
  },
4423
+ tabUrl: {
4424
+ type: "string",
4425
+ 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`."
4426
+ },
4427
+ listTargets: {
4428
+ type: "boolean",
4429
+ default: false,
4430
+ description: "Enumerate the browser's CDP page targets as {targetId,url,title,type} and return, ignoring the other args. The discovery path for `tabUrl`. targetId is a CDP target id, NOT a numeric chrome.tabs id (for those use listTabs)."
4431
+ },
4304
4432
  listTabs: {
4305
4433
  type: "boolean",
4306
4434
  default: false,
@@ -4361,8 +4489,55 @@ const dom_inspect_schema = {
4361
4489
  ]
4362
4490
  }
4363
4491
  };
4492
+ async function cdpPortOrError(projectPath, browser, feature) {
4493
+ if (!isChromiumFamily(browser)) return {
4494
+ error: JSON.stringify({
4495
+ ok: false,
4496
+ error: {
4497
+ name: "Unsupported",
4498
+ message: `${feature} reads the browser's CDP page targets, which ${browser} (Gecko) does not expose. Target the tab with \`url\` or \`tab\` instead, and discover tabs with listTabs: true (agent bridge, works on every browser).`
4499
+ }
4500
+ })
4501
+ };
4502
+ const resolved = await resolveCdpPort(projectPath, browser);
4503
+ if (!resolved) return {
4504
+ error: JSON.stringify({
4505
+ ok: false,
4506
+ error: {
4507
+ name: "NoSession",
4508
+ message: `No active dev session / CDP port for ${browser}, so ${feature} has no browser to ask. Start extension_dev and extension_wait for ready. ${CDP_PORT_MISSING_HINT}`
4509
+ }
4510
+ })
4511
+ };
4512
+ return {
4513
+ port: resolved.port
4514
+ };
4515
+ }
4364
4516
  async function dom_inspect_handler(args) {
4365
4517
  const withConsole = true === args.withConsole ? 50 : false === args.withConsole ? void 0 : args.withConsole;
4518
+ if (args.listTargets) {
4519
+ const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
4520
+ const cdp = await cdpPortOrError(args.projectPath, browser, "listTargets");
4521
+ if ("error" in cdp) return cdp.error;
4522
+ try {
4523
+ const targets = await listPageTargets(cdp.port);
4524
+ return JSON.stringify({
4525
+ ok: true,
4526
+ browser,
4527
+ targets,
4528
+ note: TARGET_ID_NOTE
4529
+ });
4530
+ } catch (e) {
4531
+ return JSON.stringify({
4532
+ ok: false,
4533
+ error: {
4534
+ name: "CdpError",
4535
+ message: `Could not list page targets: ${e instanceof Error ? e.message : String(e)}`
4536
+ },
4537
+ hint: "Confirm the session is ready (extension_wait), then retry. listTabs: true is the CDP-free alternative."
4538
+ });
4539
+ }
4540
+ }
4366
4541
  if (args.listTabs) return runActVerb([
4367
4542
  "inspect",
4368
4543
  args.projectPath,
@@ -4374,19 +4549,78 @@ async function dom_inspect_handler(args) {
4374
4549
  String(args.timeout)
4375
4550
  ] : []
4376
4551
  ], args.projectPath, args.timeout);
4552
+ let targetUrl = args.url;
4553
+ let resolvedTarget = null;
4554
+ if (args.tabUrl) {
4555
+ if (null != args.tab || args.url) return JSON.stringify({
4556
+ ok: false,
4557
+ error: {
4558
+ name: "BadRequest",
4559
+ message: "Pass ONE tab selector: `tabUrl` (URL substring, resolved against live targets), `url` (engine-side match), or `tab` (numeric chrome.tabs id), not several."
4560
+ }
4561
+ });
4562
+ const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
4563
+ const cdp = await cdpPortOrError(args.projectPath, browser, "tabUrl");
4564
+ if ("error" in cdp) return cdp.error;
4565
+ let targets;
4566
+ try {
4567
+ targets = await listPageTargets(cdp.port);
4568
+ } catch (e) {
4569
+ return JSON.stringify({
4570
+ ok: false,
4571
+ error: {
4572
+ name: "CdpError",
4573
+ message: `Could not list page targets to resolve tabUrl: ${e instanceof Error ? e.message : String(e)}`
4574
+ },
4575
+ hint: "Confirm the session is ready (extension_wait), then retry, or target with `url`/`tab` instead."
4576
+ });
4577
+ }
4578
+ const matches = matchTargetsByUrl(targets, args.tabUrl);
4579
+ if (0 === matches.length) return JSON.stringify({
4580
+ ok: false,
4581
+ error: {
4582
+ name: "NoMatchingTarget",
4583
+ message: `No open page target's url (or title) contains "${args.tabUrl}" (case-insensitive).`
4584
+ },
4585
+ availableTargets: targets,
4586
+ 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}`
4587
+ });
4588
+ if (matches.length > 1) return JSON.stringify({
4589
+ ok: false,
4590
+ error: {
4591
+ name: "AmbiguousTabUrl",
4592
+ message: `${matches.length} page targets match "${args.tabUrl}"; refusing to guess which tab you mean.`
4593
+ },
4594
+ matchingTargets: matches,
4595
+ hint: `Narrow \`tabUrl\` to a longer substring that matches exactly one url in matchingTargets. ${TARGET_ID_NOTE}`
4596
+ });
4597
+ resolvedTarget = matches[0];
4598
+ targetUrl = resolvedTarget.url;
4599
+ }
4377
4600
  const cli = [
4378
4601
  "inspect",
4379
4602
  args.projectPath
4380
4603
  ];
4381
4604
  if (null != args.tab) cli.push("--tab", String(args.tab));
4382
- if (args.url) cli.push("--url", args.url);
4605
+ if (targetUrl) cli.push("--url", targetUrl);
4383
4606
  if (args.context) cli.push("--context", args.context);
4384
4607
  if (args.include?.length) cli.push("--include", args.include.join(","));
4385
4608
  if (null != args.maxBytes) cli.push("--max-bytes", String(args.maxBytes));
4386
4609
  if (null != withConsole) cli.push("--with-console", String(withConsole));
4387
4610
  cli.push("--browser", resolveSessionBrowser(args.projectPath, args.browser).browser);
4388
4611
  if (null != args.timeout) cli.push("--timeout", String(args.timeout));
4389
- return runActVerb(cli, args.projectPath, args.timeout);
4612
+ const raw = await runActVerb(cli, args.projectPath, args.timeout);
4613
+ if (!resolvedTarget) return raw;
4614
+ try {
4615
+ const parsed = JSON.parse(raw);
4616
+ parsed.resolvedTarget = {
4617
+ ...resolvedTarget,
4618
+ matchedBy: "tabUrl"
4619
+ };
4620
+ return JSON.stringify(parsed);
4621
+ } catch {
4622
+ return raw;
4623
+ }
4390
4624
  }
4391
4625
  function credentialsPath() {
4392
4626
  if ("win32" === process.platform) {
@@ -4460,7 +4694,117 @@ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
4460
4694
  if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
4461
4695
  return creds;
4462
4696
  }
4697
+ const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
4698
+ const CONSOLE_BASE = "https://console.extension.dev";
4699
+ function registryBase() {
4700
+ const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
4701
+ return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
4702
+ }
4703
+ function resolveProjectRef(overrides) {
4704
+ const workspace = String(overrides?.workspace || "").trim();
4705
+ const project = String(overrides?.project || "").trim();
4706
+ if (workspace && project) return {
4707
+ workspace,
4708
+ project
4709
+ };
4710
+ const creds = readCredentials();
4711
+ const ws = workspace || String(creds?.workspaceSlug || "").trim();
4712
+ const proj = project || String(creds?.projectSlug || "").trim();
4713
+ if (!ws || !proj) return null;
4714
+ return {
4715
+ workspace: ws,
4716
+ project: proj
4717
+ };
4718
+ }
4719
+ function registryFileUrl(ref, file) {
4720
+ return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
4721
+ }
4722
+ function consoleProjectUrl(ref, page) {
4723
+ if (!ref) return `${CONSOLE_BASE}`;
4724
+ return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
4725
+ }
4726
+ async function fetchRegistryJson(url, fetchImpl = fetch) {
4727
+ let res;
4728
+ try {
4729
+ res = await fetchImpl(url);
4730
+ } catch (err) {
4731
+ return {
4732
+ ok: false,
4733
+ message: `Could not reach ${url}: ${err?.message || err}`
4734
+ };
4735
+ }
4736
+ if (!res.ok) return {
4737
+ ok: false,
4738
+ status: res.status,
4739
+ message: `${url} returned ${res.status}`
4740
+ };
4741
+ try {
4742
+ const text = await res.text();
4743
+ return {
4744
+ ok: true,
4745
+ json: JSON.parse(text)
4746
+ };
4747
+ } catch {
4748
+ return {
4749
+ ok: false,
4750
+ message: `${url} did not return valid JSON`
4751
+ };
4752
+ }
4753
+ }
4754
+ function parseChannels(json) {
4755
+ if (!json || "object" != typeof json || Array.isArray(json)) return [];
4756
+ const out = [];
4757
+ for (const [channel, raw] of Object.entries(json)){
4758
+ if (!raw || "object" != typeof raw) continue;
4759
+ const row = raw;
4760
+ const description = "string" == typeof row.description ? row.description : void 0;
4761
+ const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
4762
+ const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
4763
+ const entry = {
4764
+ channel,
4765
+ sha: String(row.sha ?? "")
4766
+ };
4767
+ if (row.buildId) entry.buildId = String(row.buildId);
4768
+ if (row.version) entry.version = String(row.version);
4769
+ const promotedAt = promotedAtField || fromDescription;
4770
+ if (promotedAt) entry.promotedAt = promotedAt;
4771
+ if (description) entry.description = description;
4772
+ out.push(entry);
4773
+ }
4774
+ return out;
4775
+ }
4776
+ function parseBuildIndex(json) {
4777
+ const items = json?.items;
4778
+ if (!Array.isArray(items)) return [];
4779
+ const out = [];
4780
+ for (const raw of items){
4781
+ if (!raw || "object" != typeof raw) continue;
4782
+ const row = raw;
4783
+ const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
4784
+ if (!sha) continue;
4785
+ const entry = {
4786
+ sha
4787
+ };
4788
+ if (row.commit) entry.commit = String(row.commit);
4789
+ if (row.channel) entry.channel = String(row.channel);
4790
+ if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
4791
+ if (row.status) entry.status = String(row.status);
4792
+ if (row.version) entry.version = String(row.version);
4793
+ if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
4794
+ if (row.timestamp) entry.timestamp = String(row.timestamp);
4795
+ if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
4796
+ out.push(entry);
4797
+ }
4798
+ return out;
4799
+ }
4463
4800
  const DEFAULT_API = "https://www.extension.dev";
4801
+ function tokenTtlNote(workspaceSlug, projectSlug) {
4802
+ const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
4803
+ workspace: workspaceSlug,
4804
+ project: projectSlug
4805
+ }, "settings/access-tokens") : CONSOLE_BASE;
4806
+ 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}`;
4807
+ }
4464
4808
  function resolveApiBase(api) {
4465
4809
  return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
4466
4810
  }
@@ -4625,109 +4969,6 @@ async function publish(options = {}) {
4625
4969
  data
4626
4970
  };
4627
4971
  }
4628
- const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
4629
- const CONSOLE_BASE = "https://console.extension.dev";
4630
- function registryBase() {
4631
- const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
4632
- return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
4633
- }
4634
- function resolveProjectRef(overrides) {
4635
- const workspace = String(overrides?.workspace || "").trim();
4636
- const project = String(overrides?.project || "").trim();
4637
- if (workspace && project) return {
4638
- workspace,
4639
- project
4640
- };
4641
- const creds = readCredentials();
4642
- const ws = workspace || String(creds?.workspaceSlug || "").trim();
4643
- const proj = project || String(creds?.projectSlug || "").trim();
4644
- if (!ws || !proj) return null;
4645
- return {
4646
- workspace: ws,
4647
- project: proj
4648
- };
4649
- }
4650
- function registryFileUrl(ref, file) {
4651
- return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
4652
- }
4653
- function consoleProjectUrl(ref, page) {
4654
- if (!ref) return `${CONSOLE_BASE}`;
4655
- return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
4656
- }
4657
- async function fetchRegistryJson(url, fetchImpl = fetch) {
4658
- let res;
4659
- try {
4660
- res = await fetchImpl(url);
4661
- } catch (err) {
4662
- return {
4663
- ok: false,
4664
- message: `Could not reach ${url}: ${err?.message || err}`
4665
- };
4666
- }
4667
- if (!res.ok) return {
4668
- ok: false,
4669
- status: res.status,
4670
- message: `${url} returned ${res.status}`
4671
- };
4672
- try {
4673
- const text = await res.text();
4674
- return {
4675
- ok: true,
4676
- json: JSON.parse(text)
4677
- };
4678
- } catch {
4679
- return {
4680
- ok: false,
4681
- message: `${url} did not return valid JSON`
4682
- };
4683
- }
4684
- }
4685
- function parseChannels(json) {
4686
- if (!json || "object" != typeof json || Array.isArray(json)) return [];
4687
- const out = [];
4688
- for (const [channel, raw] of Object.entries(json)){
4689
- if (!raw || "object" != typeof raw) continue;
4690
- const row = raw;
4691
- const description = "string" == typeof row.description ? row.description : void 0;
4692
- const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
4693
- const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
4694
- const entry = {
4695
- channel,
4696
- sha: String(row.sha ?? "")
4697
- };
4698
- if (row.buildId) entry.buildId = String(row.buildId);
4699
- if (row.version) entry.version = String(row.version);
4700
- const promotedAt = promotedAtField || fromDescription;
4701
- if (promotedAt) entry.promotedAt = promotedAt;
4702
- if (description) entry.description = description;
4703
- out.push(entry);
4704
- }
4705
- return out;
4706
- }
4707
- function parseBuildIndex(json) {
4708
- const items = json?.items;
4709
- if (!Array.isArray(items)) return [];
4710
- const out = [];
4711
- for (const raw of items){
4712
- if (!raw || "object" != typeof raw) continue;
4713
- const row = raw;
4714
- const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
4715
- if (!sha) continue;
4716
- const entry = {
4717
- sha
4718
- };
4719
- if (row.commit) entry.commit = String(row.commit);
4720
- if (row.channel) entry.channel = String(row.channel);
4721
- if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
4722
- if (row.status) entry.status = String(row.status);
4723
- if (row.version) entry.version = String(row.version);
4724
- if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
4725
- if (row.timestamp) entry.timestamp = String(row.timestamp);
4726
- if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
4727
- out.push(entry);
4728
- }
4729
- return out;
4730
- }
4731
4972
  const publish_schema = {
4732
4973
  name: "extension_publish",
4733
4974
  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.",
@@ -4805,7 +5046,7 @@ async function publish_handler(args) {
4805
5046
  const release_promote_DEFAULT_API = "https://www.extension.dev";
4806
5047
  const release_promote_schema = {
4807
5048
  name: "extension_release_promote",
4808
- 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).",
5049
+ 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).",
4809
5050
  inputSchema: {
4810
5051
  type: "object",
4811
5052
  properties: {
@@ -4858,7 +5099,7 @@ function release_promote_fail(name, message) {
4858
5099
  }
4859
5100
  async function release_promote_handler(args) {
4860
5101
  const token = resolveToken();
4861
- 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.");
5102
+ 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.");
4862
5103
  const buildId = String(args.buildId || "").trim();
4863
5104
  const channel = String(args.channel || "").trim();
4864
5105
  if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
@@ -5010,7 +5251,7 @@ function storeMdWarnings(browsers, cwd) {
5010
5251
  content = node_fs.readFileSync(node_path.join(cwd, "STORE.md"), "utf8");
5011
5252
  } catch {
5012
5253
  return [
5013
- "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."
5254
+ "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."
5014
5255
  ];
5015
5256
  }
5016
5257
  const hasField = (section, field)=>{
@@ -5029,7 +5270,7 @@ function storeMdWarnings(browsers, cwd) {
5029
5270
  }
5030
5271
  const deploy_schema = {
5031
5272
  name: "extension_deploy",
5032
- 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.",
5273
+ 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.",
5033
5274
  inputSchema: {
5034
5275
  type: "object",
5035
5276
  properties: {
@@ -5085,7 +5326,7 @@ function deploy_fail(name, message) {
5085
5326
  }
5086
5327
  async function deploy_handler(args) {
5087
5328
  const token = resolveToken();
5088
- 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).");
5329
+ 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).");
5089
5330
  const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
5090
5331
  if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge"]).');
5091
5332
  const buildSha = String(args.buildSha || "").trim();
@@ -5203,9 +5444,214 @@ async function deploy_handler(args) {
5203
5444
  if ("string" == typeof data?.message) result.platformMessage = data.message;
5204
5445
  result.message = summaryParts.join(" ");
5205
5446
  }
5447
+ 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.";
5206
5448
  if (warnings.length > 0) result.warnings = warnings;
5207
5449
  return JSON.stringify(result);
5208
5450
  }
5451
+ const KNOWN_STORES = [
5452
+ "chrome",
5453
+ "firefox",
5454
+ "edge",
5455
+ "safari"
5456
+ ];
5457
+ const store_status_schema = {
5458
+ name: "extension_store_status",
5459
+ 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.",
5460
+ inputSchema: {
5461
+ type: "object",
5462
+ properties: {
5463
+ workspace: {
5464
+ type: "string",
5465
+ description: "Workspace slug override (defaults to the stored login's workspace)."
5466
+ },
5467
+ project: {
5468
+ type: "string",
5469
+ description: "Project slug override (defaults to the stored login's project)."
5470
+ }
5471
+ },
5472
+ required: []
5473
+ }
5474
+ };
5475
+ function isPlainObject(value) {
5476
+ return Boolean(value) && "object" == typeof value && !Array.isArray(value);
5477
+ }
5478
+ function str(value) {
5479
+ const text = String(value ?? "").trim();
5480
+ return text || void 0;
5481
+ }
5482
+ function submissionView(row) {
5483
+ const view = {};
5484
+ const version = str(row.version);
5485
+ const status = str(row.status);
5486
+ const storeUrl = str(row.storeUrl);
5487
+ const submittedAt = str(row.submittedAt) || str(row.timestamp);
5488
+ const channel = str(row.channel);
5489
+ const buildSha = str(row.buildSha) || str(row.buildId);
5490
+ const storeSubmissionId = str(row.storeSubmissionId);
5491
+ const failureReason = str(row.failureReason);
5492
+ if (version) view.version = version;
5493
+ if (status) view.status = status;
5494
+ if (storeUrl) view.storeUrl = storeUrl;
5495
+ if (submittedAt) view.submittedAt = submittedAt;
5496
+ if (channel) view.channel = channel;
5497
+ if (buildSha) view.buildSha = buildSha;
5498
+ if (storeSubmissionId) view.storeSubmissionId = storeSubmissionId;
5499
+ if (failureReason) view.failureReason = failureReason;
5500
+ return view;
5501
+ }
5502
+ function latestSubmissionsByStore(json) {
5503
+ const list = json?.submissions;
5504
+ const out = {};
5505
+ for (const raw of Array.isArray(list) ? list : []){
5506
+ if (!isPlainObject(raw)) continue;
5507
+ const store = str(raw.store);
5508
+ if (!store) continue;
5509
+ const at = Date.parse(String(raw.submittedAt || raw.updatedAt || ""));
5510
+ const stamp = Number.isFinite(at) ? at : 0;
5511
+ if (!out[store] || stamp >= out[store].at) out[store] = {
5512
+ at: stamp,
5513
+ view: submissionView(raw)
5514
+ };
5515
+ }
5516
+ const flat = {};
5517
+ for (const [store, entry] of Object.entries(out))flat[store] = entry.view;
5518
+ return flat;
5519
+ }
5520
+ function normalizeStoresStatus(json) {
5521
+ const base = isPlainObject(json) ? json : {};
5522
+ const out = {
5523
+ reviews: {}
5524
+ };
5525
+ if (isPlainObject(base.lastSubmission)) out.lastSubmission = base.lastSubmission;
5526
+ if (isPlainObject(base.lastOverride)) out.lastOverride = base.lastOverride;
5527
+ if (isPlainObject(base.reviews)) {
5528
+ for (const [store, raw] of Object.entries(base.reviews))if (isPlainObject(raw)) out.reviews[store] = {
5529
+ status: str(raw.status),
5530
+ version: str(raw.version),
5531
+ buildId: str(raw.buildId),
5532
+ checkedAt: str(raw.checkedAt)
5533
+ };
5534
+ }
5535
+ const lastPoll = isPlainObject(base.lastPoll) ? base.lastPoll : null;
5536
+ const looksLikeLegacyPoll = !lastPoll && ("store_status" === base.kind || Array.isArray(base.updates));
5537
+ if (lastPoll) out.lastPollAt = str(lastPoll.timestamp);
5538
+ else if (looksLikeLegacyPoll) {
5539
+ out.lastPollAt = str(base.updatedAt);
5540
+ for (const raw of Array.isArray(base.updates) ? base.updates : []){
5541
+ if (!isPlainObject(raw)) continue;
5542
+ const store = str(raw.store);
5543
+ if (store && !out.reviews[store]) out.reviews[store] = {
5544
+ status: str(raw.status),
5545
+ version: str(raw.version),
5546
+ buildId: str(raw.buildId),
5547
+ checkedAt: str(base.updatedAt)
5548
+ };
5549
+ }
5550
+ }
5551
+ return out;
5552
+ }
5553
+ function store_status_fail(name, message, extra) {
5554
+ return JSON.stringify({
5555
+ ok: false,
5556
+ error: {
5557
+ name,
5558
+ message
5559
+ },
5560
+ ...extra ?? {}
5561
+ });
5562
+ }
5563
+ async function store_status_handler(args) {
5564
+ const ref = resolveProjectRef(args);
5565
+ 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.");
5566
+ const healthUrl = registryFileUrl(ref, "stores/health.json");
5567
+ const statusUrl = registryFileUrl(ref, "stores/status.json");
5568
+ const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
5569
+ const consoleStoresUrl = consoleProjectUrl(ref, "stores");
5570
+ const [healthRes, statusRes, submissionsRes] = await Promise.all([
5571
+ fetchRegistryJson(healthUrl),
5572
+ fetchRegistryJson(statusUrl),
5573
+ fetchRegistryJson(submissionsUrl)
5574
+ ]);
5575
+ 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}`, {
5576
+ workspace: ref.workspace,
5577
+ project: ref.project,
5578
+ registryUrls: {
5579
+ health: healthUrl,
5580
+ status: statusUrl,
5581
+ submissions: submissionsUrl
5582
+ },
5583
+ consoleStoresUrl
5584
+ });
5585
+ const healthStores = healthRes.ok ? isPlainObject(healthRes.json?.stores) ? healthRes.json.stores ?? null : null : null;
5586
+ const status = normalizeStoresStatus(statusRes.ok ? statusRes.json : null);
5587
+ const submissionsByStore = submissionsRes.ok ? latestSubmissionsByStore(submissionsRes.json) : {};
5588
+ const statusLastStore = str(status.lastSubmission?.store);
5589
+ if (statusLastStore && !submissionsByStore[statusLastStore] && status.lastSubmission) submissionsByStore[statusLastStore] = submissionView(status.lastSubmission);
5590
+ const stores = [
5591
+ ...KNOWN_STORES,
5592
+ ...Object.keys({
5593
+ ...healthStores,
5594
+ ...submissionsByStore,
5595
+ ...status.reviews
5596
+ }).filter((s)=>!KNOWN_STORES.includes(s))
5597
+ ];
5598
+ const rows = stores.map((store)=>{
5599
+ const healthRow = healthStores?.[store];
5600
+ const configured = healthStores ? Boolean(healthRow) : "unknown";
5601
+ const row = {
5602
+ store,
5603
+ configured
5604
+ };
5605
+ if (healthRow) row.health = {
5606
+ ok: true === healthRow.ok,
5607
+ checkedAt: str(healthRow.checkedAt),
5608
+ message: str(healthRow.message)
5609
+ };
5610
+ const submission = submissionsByStore[store];
5611
+ if (submission && Object.keys(submission).length > 0) row.lastSubmission = submission;
5612
+ const review = status.reviews[store];
5613
+ if (review && Object.values(review).some(Boolean)) row.review = review;
5614
+ return row;
5615
+ });
5616
+ const summaryParts = rows.map((row)=>{
5617
+ const store = String(row.store);
5618
+ const health = row.health;
5619
+ const submission = row.lastSubmission;
5620
+ const review = row.review;
5621
+ let head;
5622
+ 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`;
5623
+ const tail = [];
5624
+ if (submission) {
5625
+ tail.push(`last submission${submission.version ? ` v${submission.version}` : ""} ${submission.status || "recorded"}${submission.submittedAt ? ` at ${submission.submittedAt}` : ""}${submission.failureReason ? ` (${submission.failureReason})` : ""}`);
5626
+ if (submission.storeUrl) tail.push(`listing ${submission.storeUrl}`);
5627
+ } else if (true === row.configured) tail.push("no submissions recorded");
5628
+ if (review?.status) tail.push(`review ${review.status}${review.checkedAt ? ` (checked ${review.checkedAt})` : ""}`);
5629
+ return tail.length > 0 ? `${head}; ${tail.join("; ")}` : head;
5630
+ });
5631
+ const result = {
5632
+ ok: true,
5633
+ workspace: ref.workspace,
5634
+ project: ref.project,
5635
+ stores: rows,
5636
+ ...status.lastSubmission ? {
5637
+ lastSubmission: status.lastSubmission
5638
+ } : {},
5639
+ ...status.lastPollAt ? {
5640
+ lastPollAt: status.lastPollAt
5641
+ } : {},
5642
+ registryUrls: {
5643
+ health: healthUrl,
5644
+ status: statusUrl,
5645
+ submissions: submissionsUrl
5646
+ },
5647
+ consoleStoresUrl,
5648
+ 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.`
5649
+ };
5650
+ if (!healthRes.ok) result.healthUnavailable = `stores/health.json unreadable: ${healthRes.message}`;
5651
+ if (!statusRes.ok) result.statusUnavailable = `stores/status.json unreadable: ${statusRes.message}`;
5652
+ if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
5653
+ return JSON.stringify(result);
5654
+ }
5209
5655
  function doctor_readReadyContract(projectPath, browser) {
5210
5656
  try {
5211
5657
  const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
@@ -5961,7 +6407,7 @@ async function pollDeviceToken(args) {
5961
6407
  }
5962
6408
  const login_schema = {
5963
6409
  name: "extension_login",
5964
- 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.",
6410
+ 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.",
5965
6411
  inputSchema: {
5966
6412
  type: "object",
5967
6413
  properties: {
@@ -5995,13 +6441,15 @@ function login_fail(name, message) {
5995
6441
  });
5996
6442
  }
5997
6443
  function success(creds) {
6444
+ const expiresAt = creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null;
5998
6445
  return JSON.stringify({
5999
6446
  ok: true,
6000
6447
  status: "logged-in",
6001
6448
  workspaceSlug: creds.workspaceSlug,
6002
6449
  projectSlug: creds.projectSlug,
6003
- expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
6004
- message: `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}. extension_publish can now use the stored token.`
6450
+ expiresAt,
6451
+ tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
6452
+ 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).`
6005
6453
  });
6006
6454
  }
6007
6455
  function login_pending(start) {
@@ -6017,6 +6465,7 @@ function login_pending(start) {
6017
6465
  verificationUriComplete: complete
6018
6466
  } : {},
6019
6467
  deviceCode: start.deviceCode,
6468
+ tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
6020
6469
  message
6021
6470
  });
6022
6471
  }
@@ -6026,6 +6475,7 @@ function resumePending(deviceCode, verificationUri) {
6026
6475
  status: "authorization_pending",
6027
6476
  verificationUri,
6028
6477
  deviceCode,
6478
+ tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
6029
6479
  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).`
6030
6480
  });
6031
6481
  }
@@ -6135,7 +6585,7 @@ async function login_handler(args) {
6135
6585
  }
6136
6586
  const whoami_schema = {
6137
6587
  name: "extension_whoami",
6138
- 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.",
6588
+ 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.",
6139
6589
  inputSchema: {
6140
6590
  type: "object",
6141
6591
  properties: {}
@@ -6150,17 +6600,32 @@ async function whoami_handler() {
6150
6600
  });
6151
6601
  const now = Math.floor(Date.now() / 1000);
6152
6602
  const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
6603
+ const recordedApi = String(creds.api || "").trim();
6604
+ const effectiveDefaultApi = resolveApiBase();
6605
+ const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
6606
+ const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
6607
+ const messageParts = [];
6608
+ 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.`);
6609
+ 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.`);
6610
+ 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.");
6153
6611
  return JSON.stringify({
6154
6612
  ok: true,
6155
6613
  status: expired ? "expired" : "logged-in",
6156
6614
  workspaceSlug: creds.workspaceSlug,
6157
6615
  projectSlug: creds.projectSlug,
6158
- api: creds.api,
6616
+ ...recordedApi ? {
6617
+ apiRecordedAtLogin: recordedApi
6618
+ } : {},
6619
+ apiDefault: effectiveDefaultApi,
6159
6620
  provider: creds.provider ?? "github",
6160
6621
  expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
6161
6622
  expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
6162
6623
  expired,
6163
- 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.`
6624
+ ...envTokenSet ? {
6625
+ envTokenOverride: true
6626
+ } : {},
6627
+ tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
6628
+ message: messageParts.join(" ")
6164
6629
  });
6165
6630
  }
6166
6631
  const logout_schema = {
@@ -6847,6 +7312,7 @@ const tools = [
6847
7312
  release_list_namespaceObject,
6848
7313
  release_promote_namespaceObject,
6849
7314
  deploy_namespaceObject,
7315
+ store_status_namespaceObject,
6850
7316
  wait_namespaceObject,
6851
7317
  add_feature_namespaceObject,
6852
7318
  login_namespaceObject,