@extension.dev/mcp 5.7.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.js CHANGED
@@ -138,7 +138,8 @@ __webpack_require__.r(open_namespaceObject);
138
138
  __webpack_require__.d(open_namespaceObject, {
139
139
  clampPopupBounds: ()=>clampPopupBounds,
140
140
  handler: ()=>open_handler,
141
- schema: ()=>open_schema
141
+ schema: ()=>open_schema,
142
+ sessionIsHeadless: ()=>sessionIsHeadless
142
143
  });
143
144
  var preview_namespaceObject = {};
144
145
  __webpack_require__.r(preview_namespaceObject);
@@ -221,7 +222,260 @@ __webpack_require__.d(whoami_namespaceObject, {
221
222
  handler: ()=>whoami_handler,
222
223
  schema: ()=>whoami_schema
223
224
  });
224
- var package_namespaceObject = JSON.parse('{"rE":"5.7.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
225
+ var package_namespaceObject = JSON.parse('{"rE":"6.0.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
226
+ function credentialsPath() {
227
+ if ("win32" === process.platform) {
228
+ const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
229
+ return node_path.join(base, "extension-dev", "auth.json");
230
+ }
231
+ const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
232
+ const base = xdg || node_path.join(node_os.homedir(), ".config");
233
+ return node_path.join(base, "extension-dev", "auth.json");
234
+ }
235
+ function readCredentials() {
236
+ try {
237
+ const raw = node_fs.readFileSync(credentialsPath(), "utf8");
238
+ const data = JSON.parse(raw);
239
+ if (!data || "object" != typeof data) return null;
240
+ if (1 !== data.version) return null;
241
+ const token = String(data.token || "").trim();
242
+ if (!token) return null;
243
+ const provider = "extensiondev" === data.provider || "github" === data.provider ? data.provider : void 0;
244
+ return {
245
+ version: 1,
246
+ token,
247
+ workspaceSlug: String(data.workspaceSlug || ""),
248
+ projectSlug: String(data.projectSlug || ""),
249
+ expiresAt: Number(data.expiresAt || 0),
250
+ api: String(data.api || ""),
251
+ ...provider ? {
252
+ provider
253
+ } : {}
254
+ };
255
+ } catch {
256
+ return null;
257
+ }
258
+ }
259
+ function writeCredentials(creds) {
260
+ const file = credentialsPath();
261
+ const dir = node_path.dirname(file);
262
+ node_fs.mkdirSync(dir, {
263
+ recursive: true,
264
+ mode: 448
265
+ });
266
+ try {
267
+ node_fs.chmodSync(dir, 448);
268
+ } catch {}
269
+ node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
270
+ mode: 384
271
+ });
272
+ try {
273
+ node_fs.chmodSync(file, 384);
274
+ } catch {}
275
+ return file;
276
+ }
277
+ function clearCredentials() {
278
+ const file = credentialsPath();
279
+ try {
280
+ node_fs.unlinkSync(file);
281
+ return {
282
+ cleared: true,
283
+ path: file
284
+ };
285
+ } catch {
286
+ return {
287
+ cleared: false,
288
+ path: file
289
+ };
290
+ }
291
+ }
292
+ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
293
+ const creds = readCredentials();
294
+ if (!creds) return null;
295
+ if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
296
+ return creds;
297
+ }
298
+ const PROD_ORIGINS = {
299
+ www: "https://www.extension.dev",
300
+ console: "https://console.extension.dev",
301
+ inspect: "https://inspect.extension.dev",
302
+ templates: "https://templates.extension.dev",
303
+ intelligence: "https://intelligence.extension.dev",
304
+ registry: "https://registry.extension.land",
305
+ media: "https://media.extension.land"
306
+ };
307
+ const DEV_LOCALHOST_ORIGINS = {
308
+ www: "http://localhost:3100",
309
+ console: "http://console.extension.localhost",
310
+ inspect: "http://inspect.extension.localhost",
311
+ templates: "http://templates.extension.localhost",
312
+ intelligence: "http://intelligence.extension.localhost",
313
+ registry: "https://registry.extension.land",
314
+ media: "https://media.extension.land"
315
+ };
316
+ function strip(value) {
317
+ return String(value ?? "").trim().replace(/\/+$/, "");
318
+ }
319
+ function isLocalOrigin(url) {
320
+ const raw = strip(url);
321
+ if (!raw) return false;
322
+ let host;
323
+ try {
324
+ host = new URL(raw).hostname;
325
+ } catch {
326
+ return false;
327
+ }
328
+ return "localhost" === host || "127.0.0.1" === host || "::1" === host || "[::1]" === host || "extension.localhost" === host || host.endsWith(".extension.localhost");
329
+ }
330
+ function resolveOrigins(overrides = {}, opts = {}) {
331
+ const devLike = isLocalOrigin(overrides.www) || isLocalOrigin(overrides.console) || isLocalOrigin(opts.hint);
332
+ const base = devLike ? DEV_LOCALHOST_ORIGINS : PROD_ORIGINS;
333
+ const pick = (key)=>strip(overrides[key]) || base[key];
334
+ return {
335
+ www: pick("www"),
336
+ console: pick("console"),
337
+ inspect: pick("inspect"),
338
+ templates: pick("templates"),
339
+ intelligence: pick("intelligence"),
340
+ registry: pick("registry"),
341
+ media: pick("media")
342
+ };
343
+ }
344
+ const seg = (value)=>encodeURIComponent(String(value));
345
+ function urls_paths_join(base, sub) {
346
+ if (!sub) return base;
347
+ return `${base}/${sub.replace(/^\/+/, "")}`;
348
+ }
349
+ function consoleProjectPath(ref, page = "") {
350
+ return urls_paths_join(`/${seg(ref.workspace)}/${seg(ref.project)}`, page);
351
+ }
352
+ function withQuery(path, query) {
353
+ if (!query) return path;
354
+ const params = new URLSearchParams();
355
+ for (const [key, value] of Object.entries(query))if (null != value && "" !== value) params.set(key, String(value));
356
+ const qs = params.toString();
357
+ return qs ? `${path}?${qs}` : path;
358
+ }
359
+ function wwwNewPath(query) {
360
+ return withQuery("/new", query);
361
+ }
362
+ PROD_ORIGINS.registry;
363
+ function mcpOrigins(apiHint) {
364
+ const www = String(apiHint || process.env.EXTENSION_DEV_API_URL || "").trim() || void 0;
365
+ return resolveOrigins({
366
+ www,
367
+ console: process.env.EXTENSION_DEV_CONSOLE_URL,
368
+ inspect: process.env.EXTENSION_DEV_INSPECT_URL,
369
+ registry: process.env.EXTENSION_DEV_REGISTRY_URL,
370
+ media: process.env.EXTENSION_MEDIA_ORIGIN
371
+ }, {
372
+ hint: www
373
+ });
374
+ }
375
+ function consoleBase(apiHint) {
376
+ return mcpOrigins(apiHint).console;
377
+ }
378
+ function registryBase() {
379
+ return mcpOrigins().registry;
380
+ }
381
+ function resolveProjectRef(overrides) {
382
+ const workspace = String(overrides?.workspace || "").trim();
383
+ const project = String(overrides?.project || "").trim();
384
+ if (workspace && project) return {
385
+ workspace,
386
+ project
387
+ };
388
+ const creds = readCredentials();
389
+ const ws = workspace || String(creds?.workspaceSlug || "").trim();
390
+ const proj = project || String(creds?.projectSlug || "").trim();
391
+ if (!ws || !proj) return null;
392
+ return {
393
+ workspace: ws,
394
+ project: proj
395
+ };
396
+ }
397
+ function registryFileUrl(ref, file) {
398
+ return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
399
+ }
400
+ function consoleProjectUrl(ref, page, apiHint) {
401
+ const base = consoleBase(apiHint);
402
+ if (!ref) return base;
403
+ return `${base}${consoleProjectPath(ref, page)}`;
404
+ }
405
+ async function fetchRegistryJson(url, fetchImpl = fetch) {
406
+ let res;
407
+ try {
408
+ res = await fetchImpl(url);
409
+ } catch (err) {
410
+ return {
411
+ ok: false,
412
+ message: `Could not reach ${url}: ${err?.message || err}`
413
+ };
414
+ }
415
+ if (!res.ok) return {
416
+ ok: false,
417
+ status: res.status,
418
+ message: `${url} returned ${res.status}`
419
+ };
420
+ try {
421
+ const text = await res.text();
422
+ return {
423
+ ok: true,
424
+ json: JSON.parse(text)
425
+ };
426
+ } catch {
427
+ return {
428
+ ok: false,
429
+ message: `${url} did not return valid JSON`
430
+ };
431
+ }
432
+ }
433
+ function parseChannels(json) {
434
+ if (!json || "object" != typeof json || Array.isArray(json)) return [];
435
+ const out = [];
436
+ for (const [channel, raw] of Object.entries(json)){
437
+ if (!raw || "object" != typeof raw) continue;
438
+ const row = raw;
439
+ const description = "string" == typeof row.description ? row.description : void 0;
440
+ const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
441
+ const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
442
+ const entry = {
443
+ channel,
444
+ sha: String(row.sha ?? "")
445
+ };
446
+ if (row.buildId) entry.buildId = String(row.buildId);
447
+ if (row.version) entry.version = String(row.version);
448
+ const promotedAt = promotedAtField || fromDescription;
449
+ if (promotedAt) entry.promotedAt = promotedAt;
450
+ if (description) entry.description = description;
451
+ out.push(entry);
452
+ }
453
+ return out;
454
+ }
455
+ function parseBuildIndex(json) {
456
+ const items = json?.items;
457
+ if (!Array.isArray(items)) return [];
458
+ const out = [];
459
+ for (const raw of items){
460
+ if (!raw || "object" != typeof raw) continue;
461
+ const row = raw;
462
+ const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
463
+ if (!sha) continue;
464
+ const entry = {
465
+ sha
466
+ };
467
+ if (row.commit) entry.commit = String(row.commit);
468
+ if (row.channel) entry.channel = String(row.channel);
469
+ if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
470
+ if (row.status) entry.status = String(row.status);
471
+ if (row.version) entry.version = String(row.version);
472
+ if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
473
+ if (row.timestamp) entry.timestamp = String(row.timestamp);
474
+ if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
475
+ out.push(entry);
476
+ }
477
+ return out;
478
+ }
225
479
  function scaffoldEnginePin(projectPath) {
226
480
  try {
227
481
  const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
@@ -358,6 +612,10 @@ async function create_handler(args) {
358
612
  const engineWarning = pin && "latest" !== pin && !pinMatches ? `The scaffold pins "extension": "${scaffoldPin ?? "unknown"}"; the project-local engine wins over EXTENSION_MCP_CLI_VERSION=${pin}. Run \`(cd ${result.projectPath} && ${addDev(`extension@${pin}`)})\` to match the pinned engine.` : void 0;
359
613
  const resolvedParent = args.parentDir ? node_path.resolve(args.parentDir) : process.cwd();
360
614
  const gitInit = node_fs.existsSync(node_path.join(result.projectPath, ".git"));
615
+ const wwwOrigin = mcpOrigins().www;
616
+ const deployUrl = `${wwwOrigin}${wwwNewPath({
617
+ template: result.template
618
+ })}`;
361
619
  return JSON.stringify({
362
620
  resolvedPath: result.projectPath,
363
621
  projectPath: result.projectPath,
@@ -365,20 +623,27 @@ async function create_handler(args) {
365
623
  template: result.template,
366
624
  depsInstalled: result.depsInstalled,
367
625
  packageManager: result.depsInstalled ? packageManager : null,
626
+ deployUrl,
368
627
  defaultsApplied: {
369
628
  parentDir: args.parentDir ? `${resolvedParent} (explicit)` : `${resolvedParent} (default: the MCP server process cwd, not yours; pass parentDir to choose)`,
629
+ ...void 0 === args.template ? {
630
+ template: "typescript (default; run extension_list_templates to pick another, e.g. javascript for plain JS)"
631
+ } : {},
370
632
  packageManager: `${packageManager} (auto-detected by the scaffolder, not asked)`,
371
633
  browser: "chrome (default: extension_dev and extension_build target chrome unless you pass browser)",
372
634
  gitInit
373
635
  },
374
636
  duration: Date.now() - start,
375
- nextSteps: result.depsInstalled ? [
376
- `cd ${result.projectPath}`,
377
- runDev
378
- ] : [
379
- `cd ${result.projectPath}`,
380
- `${packageManager} install`,
381
- runDev
637
+ nextSteps: [
638
+ ...result.depsInstalled ? [
639
+ `cd ${result.projectPath}`,
640
+ runDev
641
+ ] : [
642
+ `cd ${result.projectPath}`,
643
+ `${packageManager} install`,
644
+ runDev
645
+ ],
646
+ `To ship: extension_create scaffolds and runs locally, it does not host. Open ${deployUrl} to deploy this template to the web.`
382
647
  ],
383
648
  ...engineWarning ? {
384
649
  engineWarning
@@ -388,10 +653,96 @@ async function create_handler(args) {
388
653
  }
389
654
  });
390
655
  }
656
+ const DEFAULT_MEDIA_ORIGIN = "https://media.extension.land";
657
+ const DEFAULT_CHANNEL = "latest";
658
+ const PINNED_COMMIT = "2d2ed9668cca002148d9eecd953a08b54d0bad9d";
659
+ const CHANNEL_CACHE_TTL_MS = 300000;
660
+ function mediaOrigin() {
661
+ return (process.env.EXTENSION_MEDIA_ORIGIN || "").trim() || DEFAULT_MEDIA_ORIGIN;
662
+ }
663
+ function channelName() {
664
+ return (process.env.EXTENSION_TEMPLATES_CHANNEL || "").trim() || DEFAULT_CHANNEL;
665
+ }
666
+ function pinnedCommitOverride() {
667
+ return (process.env.EXTENSION_TEMPLATES_COMMIT || "").trim();
668
+ }
669
+ function rawBaseForCommit(commit) {
670
+ return `https://raw.githubusercontent.com/extension-js/examples/${commit}`;
671
+ }
672
+ let releaseCache = null;
673
+ let releaseCacheExpiresAt = 0;
674
+ let releaseRequest = null;
675
+ function releaseForCommit(origin, commit) {
676
+ return {
677
+ commit,
678
+ metaUrl: `${origin}/templates/${commit}/templates-meta.json`,
679
+ filesBaseUrl: `${origin}/templates/${commit}/files`
680
+ };
681
+ }
682
+ async function resolveRelease() {
683
+ const now = Date.now();
684
+ if (releaseCache && now < releaseCacheExpiresAt) return releaseCache;
685
+ if (releaseRequest) return releaseRequest;
686
+ const origin = mediaOrigin();
687
+ const override = pinnedCommitOverride();
688
+ if (override) {
689
+ releaseCache = releaseForCommit(origin, override);
690
+ releaseCacheExpiresAt = now + CHANNEL_CACHE_TTL_MS;
691
+ return releaseCache;
692
+ }
693
+ releaseRequest = (async ()=>{
694
+ try {
695
+ const pointerUrl = `${origin}/templates/${channelName()}.json`;
696
+ const response = await fetch(pointerUrl, {
697
+ headers: {
698
+ Accept: "application/json"
699
+ }
700
+ });
701
+ if (!response.ok) return null;
702
+ const pointer = await response.json();
703
+ const commit = String(pointer?.commit || "").trim();
704
+ if (!commit) return null;
705
+ releaseCache = releaseForCommit(origin, commit);
706
+ releaseCacheExpiresAt = Date.now() + CHANNEL_CACHE_TTL_MS;
707
+ return releaseCache;
708
+ } catch {
709
+ return null;
710
+ }
711
+ })();
712
+ try {
713
+ return await releaseRequest;
714
+ } finally{
715
+ releaseRequest = null;
716
+ }
717
+ }
718
+ async function templateMetaUrls() {
719
+ const urls = [];
720
+ const release = await resolveRelease();
721
+ if (release) urls.push(release.metaUrl);
722
+ urls.push(`${rawBaseForCommit(PINNED_COMMIT)}/templates-meta.json`);
723
+ return urls;
724
+ }
725
+ function stripTemplatePathPrefix(slug, relativePath) {
726
+ for (const dir of [
727
+ "public",
728
+ "examples"
729
+ ]){
730
+ const prefix = `${dir}/${slug}/`;
731
+ if (relativePath.startsWith(prefix)) return relativePath.slice(prefix.length);
732
+ }
733
+ return relativePath;
734
+ }
735
+ async function templateFileUrls(slug, relativePath) {
736
+ const relative = stripTemplatePathPrefix(slug, relativePath);
737
+ const urls = [];
738
+ const release = await resolveRelease();
739
+ if (release) urls.push(`${release.filesBaseUrl}/${slug}/${relative}`);
740
+ urls.push(`${rawBaseForCommit(PINNED_COMMIT)}/examples/${slug}/${relative}`);
741
+ return urls;
742
+ }
391
743
  const CACHE_DIR = node_path.join(node_os.homedir(), ".cache", "extension-js");
392
744
  const CACHE_FILE = node_path.join(CACHE_DIR, "templates-meta.json");
393
745
  const CACHE_TTL_MS = 3600000;
394
- const TEMPLATES_META_URL = "https://github.com/extension-js/examples/releases/download/nightly/templates-meta.json";
395
746
  function isCacheValid() {
396
747
  try {
397
748
  const stat = node_fs.statSync(CACHE_FILE);
@@ -405,17 +756,22 @@ async function fetchTemplatesMeta() {
405
756
  const cached = JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
406
757
  return cached;
407
758
  }
408
- const response = await fetch(TEMPLATES_META_URL);
409
- if (!response.ok) {
410
- if (node_fs.existsSync(CACHE_FILE)) return JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
411
- throw new Error(`Failed to fetch templates-meta.json: ${response.status}`);
412
- }
413
- const data = await response.json();
414
- node_fs.mkdirSync(CACHE_DIR, {
415
- recursive: true
416
- });
417
- node_fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
418
- return data;
759
+ let lastStatus = 0;
760
+ for (const url of (await templateMetaUrls()))try {
761
+ const response = await fetch(url);
762
+ if (!response.ok) {
763
+ lastStatus = response.status;
764
+ continue;
765
+ }
766
+ const data = await response.json();
767
+ node_fs.mkdirSync(CACHE_DIR, {
768
+ recursive: true
769
+ });
770
+ node_fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
771
+ return data;
772
+ } catch {}
773
+ if (node_fs.existsSync(CACHE_FILE)) return JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
774
+ throw new Error(`Failed to fetch templates-meta.json (${lastStatus || "network error"}).`);
419
775
  }
420
776
  async function listTemplates(filters) {
421
777
  const meta = await fetchTemplatesMeta();
@@ -525,7 +881,8 @@ async function list_templates_handler(args) {
525
881
  const results = templates.map((t)=>({
526
882
  slug: t.slug,
527
883
  description: t.description,
528
- uiFramework: t.uiFramework || "vanilla",
884
+ uiFramework: t.uiFramework,
885
+ frameworkLabel: t.uiFramework || "vanilla",
529
886
  surfaces: t.surfaces,
530
887
  tags: t.tags,
531
888
  difficulty: t.difficulty,
@@ -883,6 +1240,7 @@ function isGeckoFamily(browser) {
883
1240
  return GECKO_FAMILY.has(browser);
884
1241
  }
885
1242
  const CARRIER_DIR_NAME = "extension-dev-live-preview";
1243
+ const CARRIER_EXTENSION_ID = "ibppeifnekhjjjmpjfiobccjlicbmgcb";
886
1244
  const MARKER_FILE = "managed-by-extension-dev-mcp.json";
887
1245
  function deriveCarrierId(source) {
888
1246
  try {
@@ -1005,6 +1363,7 @@ function materializeCarrier(projectPath, browser) {
1005
1363
  "Bridged calls run under the CARRIER's identity, not your extension's. The preview assumes a single active guest and does not namespace per-extension state, so storage, action/badge state, messaging delivery, offscreen documents and relative script paths belong to the carrier. Rows affected are badged carrier-scoped in the Trace tab.",
1006
1364
  "Chromium-family only: Firefox has no externally_connectable channel for web pages."
1007
1365
  ],
1366
+ graduation: "The carrier lane is the SHARED real lane: bridged calls run as the carrier, by design (see limitations). Your guest is already loaded as ITSELF in this same session, so for its own storage, identity, badge and messaging (the isolated real thing), drive the guest directly instead of the carrier bridge: extension_storage, extension_eval and extension_dom_inspect against this projectPath all operate on the guest as itself. Start (or replace) this session with allowControl: true (or allowEval: true) to unlock them. Use the carrier bridge for the shared real-lane TRACE; use the control verbs for the guest's OWN state.",
1008
1367
  ...carrierId ? {
1009
1368
  bridgeProtocol: {
1010
1369
  carrierExtensionId: carrierId,
@@ -1751,6 +2110,7 @@ async function dev_handler(args) {
1751
2110
  child.on("exit", ()=>removeSession(args.projectPath, browser));
1752
2111
  await new Promise((resolve)=>setTimeout(resolve, 3000));
1753
2112
  const earlyOutput = spawned.readOutput();
2113
+ const cleanOutput = denoiseEarlyOutput(earlyOutput);
1754
2114
  if (null !== child.exitCode || null !== child.signalCode) {
1755
2115
  const code = child.exitCode;
1756
2116
  const signal = child.signalCode;
@@ -1763,12 +2123,12 @@ async function dev_handler(args) {
1763
2123
  exitCode: code,
1764
2124
  signal,
1765
2125
  error: `The dev server exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running, so extension_logs/wait/eval and the control verbs have nothing to attach to.`,
1766
- output: denoiseEarlyOutput(earlyOutput).slice(0, 2000),
2126
+ output: cleanOutput.slice(0, 2000),
1767
2127
  logPath,
1768
2128
  hint: "Read `output` above for the cause: a port already in use, a manifest the build rejects, or a missing browser binary are the common ones. Fix it and call extension_dev again; extension_doctor with this projectPath will also report what the last session recorded."
1769
2129
  });
1770
2130
  }
1771
- const compileFailed = /compiled with errors|✖✖✖|ERROR in |Module not found|NOT FOUND/i.test(earlyOutput);
2131
+ const compileFailed = /compiled with errors|✖✖✖|ERROR in |Module not found|NOT FOUND/i.test(cleanOutput);
1772
2132
  if (compileFailed) return JSON.stringify({
1773
2133
  ok: false,
1774
2134
  status: "compile-failed",
@@ -1776,12 +2136,12 @@ async function dev_handler(args) {
1776
2136
  browser,
1777
2137
  pid,
1778
2138
  error: "The dev server started but the FIRST COMPILE FAILED, so the browser has nothing usable to load. The session is running; the extension is not.",
1779
- output: denoiseEarlyOutput(earlyOutput).slice(0, 2000),
2139
+ output: cleanOutput.slice(0, 2000),
1780
2140
  logPath,
1781
2141
  hint: "Fix the compile error in `output` above and save: the dev server is still running and will recompile. Do not call extension_wait yet, it will report ready for a build that failed."
1782
2142
  });
1783
2143
  const exitStamp = args.noBrowser ? null : browserExitStamp(args.projectPath, browser, spawnedAt);
1784
- const profileLockHit = !args.noBrowser && /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(earlyOutput);
2144
+ const profileLockHit = !args.noBrowser && /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(cleanOutput);
1785
2145
  if (exitStamp || profileLockHit) {
1786
2146
  const profileDir = node_path.join(args.projectPath, "dist", `extension-profile-${browser}`);
1787
2147
  return JSON.stringify({
@@ -1792,7 +2152,7 @@ async function dev_handler(args) {
1792
2152
  pid,
1793
2153
  ...exitStamp ?? {},
1794
2154
  error: `The dev server is running but the ${browser} browser it launched died during startup` + (profileLockHit ? " because its profile is locked by another browser instance." : "."),
1795
- output: denoiseEarlyOutput(earlyOutput).slice(0, 2000),
2155
+ output: cleanOutput.slice(0, 2000),
1796
2156
  logPath,
1797
2157
  hint: `A locked profile means another session's browser still holds it: call extension_stop with this projectPath to kill that session, then start extension_dev again. If the lock survives a crash, remove ${profileDir} manually before retrying.`
1798
2158
  });
@@ -1840,7 +2200,7 @@ async function dev_handler(args) {
1840
2200
  } : {},
1841
2201
  capabilities,
1842
2202
  hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/dom_inspect/eval) need a live browser and will not work against this session. When you are done, call extension_stop to shut down the dev server." : "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_inspect need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser.",
1843
- earlyOutput: denoiseEarlyOutput(earlyOutput).slice(0, 500),
2203
+ earlyOutput: cleanOutput.slice(0, 500),
1844
2204
  logPath
1845
2205
  });
1846
2206
  }
@@ -1849,6 +2209,8 @@ function denoiseEarlyOutput(raw) {
1849
2209
  /^npm warn Unknown project config/i,
1850
2210
  /This will stop working in the next major version of npm/i,
1851
2211
  /^npm warn config/i,
2212
+ /^npm warn exec/i,
2213
+ /The following package(s)? (was|were) not found and will be installed/i,
1852
2214
  /V8: .*Invalid asm\.js/i,
1853
2215
  /^\(node:\d+\) V8:/i,
1854
2216
  /Use `node --trace-warnings/i,
@@ -2092,7 +2454,6 @@ async function preview_handler(args) {
2092
2454
  logPath
2093
2455
  });
2094
2456
  }
2095
- const RAW_BASE = "https://raw.githubusercontent.com/extension-js/examples/main/examples";
2096
2457
  const get_template_source_schema = {
2097
2458
  name: "extension_get_template_source",
2098
2459
  description: "Read source files from a template in the extension.dev template catalog. Use this to learn implementation patterns before building something similar.",
@@ -2134,20 +2495,23 @@ async function get_template_source_handler(args) {
2134
2495
  };
2135
2496
  if (!args.files?.length) return JSON.stringify({
2136
2497
  ...meta,
2137
- files: template.files,
2498
+ files: template.files.map((f)=>stripTemplatePathPrefix(template.slug, f)),
2138
2499
  hint: "Pass specific file paths in the files parameter to read their contents."
2139
2500
  });
2140
2501
  const fileContents = {};
2141
2502
  const errors = [];
2142
2503
  await Promise.all(args.files.map(async (filePath)=>{
2143
- const url = `${RAW_BASE}/${args.slug}/${filePath}`;
2144
- try {
2504
+ const urls = await templateFileUrls(args.slug, filePath);
2505
+ let lastStatus = 0;
2506
+ for (const url of urls)try {
2145
2507
  const response = await fetch(url);
2146
- if (response.ok) fileContents[filePath] = await response.text();
2147
- else errors.push(`${filePath}: ${response.status}`);
2148
- } catch (err) {
2149
- errors.push(`${filePath}: ${err instanceof Error ? err.message : "fetch failed"}`);
2150
- }
2508
+ if (response.ok) {
2509
+ fileContents[filePath] = await response.text();
2510
+ return;
2511
+ }
2512
+ lastStatus = response.status;
2513
+ } catch {}
2514
+ errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
2151
2515
  }));
2152
2516
  return JSON.stringify({
2153
2517
  ...meta,
@@ -5303,6 +5667,11 @@ const open_schema = {
5303
5667
  ]
5304
5668
  }
5305
5669
  };
5670
+ function sessionIsHeadless() {
5671
+ if (/^(1|true)$/i.test(process.env.EXTENSION_HEADLESS ?? "")) return true;
5672
+ return /(^|\s|=)-{1,2}headless\b/i.test(process.env.EXTENSION_BROWSER_FLAGS ?? "");
5673
+ }
5674
+ const HEADED_RELAUNCH = "start a headed session: extension_dev with replace: true, and in the environment set EXTENSION_HEADLESS=0 AND clear EXTENSION_BROWSER_FLAGS (it may carry --headless=new, which keeps the window hidden even with EXTENSION_HEADLESS=0)";
5306
5675
  async function open_handler(args) {
5307
5676
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
5308
5677
  if (args.url) return navigateToUrl(args.projectPath, browser, args.url, args.timeout);
@@ -5347,7 +5716,7 @@ async function open_handler(args) {
5347
5716
  cli.push("--browser", browser);
5348
5717
  if (null != args.timeout) cli.push("--timeout", String(args.timeout));
5349
5718
  const raw = await runActVerb(cli, args.projectPath, args.timeout);
5350
- const headless = /^(1|true)$/i.test(process.env.EXTENSION_HEADLESS ?? "");
5719
+ const headless = sessionIsHeadless();
5351
5720
  if (headless && [
5352
5721
  "popup",
5353
5722
  "action",
@@ -5361,12 +5730,12 @@ async function open_handler(args) {
5361
5730
  try {
5362
5731
  const parsedFallback = JSON.parse(fallback);
5363
5732
  if (parsedFallback?.ok) {
5364
- parsedFallback.note = "The dev browser is headless (EXTENSION_HEADLESS=1), and a real popup/sidebar window can only open in a headed session, so the surface was rendered as a tab instead. For the real window, start a headed session: extension_dev with replace: true, with EXTENSION_HEADLESS=0 (or unset) in the environment, then open the surface again without asTab.";
5733
+ parsedFallback.note = `The dev browser is headless, and a real popup/sidebar window can only open in a headed session, so the surface was rendered as a tab instead. For the real window, ${HEADED_RELAUNCH}, then open the surface again without asTab.`;
5365
5734
  return JSON.stringify(parsedFallback);
5366
5735
  }
5367
5736
  } catch {}
5368
5737
  }
5369
- if (!parsed.hint) parsed.hint = /user gesture/i.test(msg) ? "This surface can only open from a real user gesture, which headless automation cannot produce. Retry with asTab: true to render the surface document in a tab instead." : "The dev browser is running headless (EXTENSION_HEADLESS=1), and a popup/sidebar window needs a headed session. Retry with asTab: true to render the surface document in a tab, or start a headed session for the real window: extension_dev with replace: true, with EXTENSION_HEADLESS=0 (or unset) in the environment.";
5738
+ if (!parsed.hint) parsed.hint = /user gesture/i.test(msg) ? "This surface can only open from a real user gesture, which headless automation cannot produce. Retry with asTab: true to render the surface document in a tab instead." : `The dev browser is running headless, and a popup/sidebar window needs a headed session. Retry with asTab: true to render the surface document in a tab, or for the real window, ${HEADED_RELAUNCH}.`;
5370
5739
  return JSON.stringify(parsed);
5371
5740
  }
5372
5741
  } catch {}
@@ -5680,187 +6049,12 @@ async function dom_inspect_handler(args) {
5680
6049
  return raw;
5681
6050
  }
5682
6051
  }
5683
- function credentialsPath() {
5684
- if ("win32" === process.platform) {
5685
- const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
5686
- return node_path.join(base, "extension-dev", "auth.json");
5687
- }
5688
- const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
5689
- const base = xdg || node_path.join(node_os.homedir(), ".config");
5690
- return node_path.join(base, "extension-dev", "auth.json");
5691
- }
5692
- function readCredentials() {
5693
- try {
5694
- const raw = node_fs.readFileSync(credentialsPath(), "utf8");
5695
- const data = JSON.parse(raw);
5696
- if (!data || "object" != typeof data) return null;
5697
- if (1 !== data.version) return null;
5698
- const token = String(data.token || "").trim();
5699
- if (!token) return null;
5700
- const provider = "extensiondev" === data.provider || "github" === data.provider ? data.provider : void 0;
5701
- return {
5702
- version: 1,
5703
- token,
5704
- workspaceSlug: String(data.workspaceSlug || ""),
5705
- projectSlug: String(data.projectSlug || ""),
5706
- expiresAt: Number(data.expiresAt || 0),
5707
- api: String(data.api || ""),
5708
- ...provider ? {
5709
- provider
5710
- } : {}
5711
- };
5712
- } catch {
5713
- return null;
5714
- }
5715
- }
5716
- function writeCredentials(creds) {
5717
- const file = credentialsPath();
5718
- const dir = node_path.dirname(file);
5719
- node_fs.mkdirSync(dir, {
5720
- recursive: true,
5721
- mode: 448
5722
- });
5723
- try {
5724
- node_fs.chmodSync(dir, 448);
5725
- } catch {}
5726
- node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
5727
- mode: 384
5728
- });
5729
- try {
5730
- node_fs.chmodSync(file, 384);
5731
- } catch {}
5732
- return file;
5733
- }
5734
- function clearCredentials() {
5735
- const file = credentialsPath();
5736
- try {
5737
- node_fs.unlinkSync(file);
5738
- return {
5739
- cleared: true,
5740
- path: file
5741
- };
5742
- } catch {
5743
- return {
5744
- cleared: false,
5745
- path: file
5746
- };
5747
- }
5748
- }
5749
- function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
5750
- const creds = readCredentials();
5751
- if (!creds) return null;
5752
- if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
5753
- return creds;
5754
- }
5755
- const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
5756
- const CONSOLE_BASE = "https://console.extension.dev";
5757
- function registryBase() {
5758
- const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
5759
- return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
5760
- }
5761
- function resolveProjectRef(overrides) {
5762
- const workspace = String(overrides?.workspace || "").trim();
5763
- const project = String(overrides?.project || "").trim();
5764
- if (workspace && project) return {
5765
- workspace,
5766
- project
5767
- };
5768
- const creds = readCredentials();
5769
- const ws = workspace || String(creds?.workspaceSlug || "").trim();
5770
- const proj = project || String(creds?.projectSlug || "").trim();
5771
- if (!ws || !proj) return null;
5772
- return {
5773
- workspace: ws,
5774
- project: proj
5775
- };
5776
- }
5777
- function registryFileUrl(ref, file) {
5778
- return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
5779
- }
5780
- function consoleProjectUrl(ref, page) {
5781
- if (!ref) return `${CONSOLE_BASE}`;
5782
- return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
5783
- }
5784
- async function fetchRegistryJson(url, fetchImpl = fetch) {
5785
- let res;
5786
- try {
5787
- res = await fetchImpl(url);
5788
- } catch (err) {
5789
- return {
5790
- ok: false,
5791
- message: `Could not reach ${url}: ${err?.message || err}`
5792
- };
5793
- }
5794
- if (!res.ok) return {
5795
- ok: false,
5796
- status: res.status,
5797
- message: `${url} returned ${res.status}`
5798
- };
5799
- try {
5800
- const text = await res.text();
5801
- return {
5802
- ok: true,
5803
- json: JSON.parse(text)
5804
- };
5805
- } catch {
5806
- return {
5807
- ok: false,
5808
- message: `${url} did not return valid JSON`
5809
- };
5810
- }
5811
- }
5812
- function parseChannels(json) {
5813
- if (!json || "object" != typeof json || Array.isArray(json)) return [];
5814
- const out = [];
5815
- for (const [channel, raw] of Object.entries(json)){
5816
- if (!raw || "object" != typeof raw) continue;
5817
- const row = raw;
5818
- const description = "string" == typeof row.description ? row.description : void 0;
5819
- const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
5820
- const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
5821
- const entry = {
5822
- channel,
5823
- sha: String(row.sha ?? "")
5824
- };
5825
- if (row.buildId) entry.buildId = String(row.buildId);
5826
- if (row.version) entry.version = String(row.version);
5827
- const promotedAt = promotedAtField || fromDescription;
5828
- if (promotedAt) entry.promotedAt = promotedAt;
5829
- if (description) entry.description = description;
5830
- out.push(entry);
5831
- }
5832
- return out;
5833
- }
5834
- function parseBuildIndex(json) {
5835
- const items = json?.items;
5836
- if (!Array.isArray(items)) return [];
5837
- const out = [];
5838
- for (const raw of items){
5839
- if (!raw || "object" != typeof raw) continue;
5840
- const row = raw;
5841
- const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
5842
- if (!sha) continue;
5843
- const entry = {
5844
- sha
5845
- };
5846
- if (row.commit) entry.commit = String(row.commit);
5847
- if (row.channel) entry.channel = String(row.channel);
5848
- if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
5849
- if (row.status) entry.status = String(row.status);
5850
- if (row.version) entry.version = String(row.version);
5851
- if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
5852
- if (row.timestamp) entry.timestamp = String(row.timestamp);
5853
- if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
5854
- out.push(entry);
5855
- }
5856
- return out;
5857
- }
5858
- const DEFAULT_API = "https://www.extension.dev";
6052
+ const DEFAULT_API = PROD_ORIGINS.www;
5859
6053
  function tokenTtlNote(workspaceSlug, projectSlug) {
5860
6054
  const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
5861
6055
  workspace: workspaceSlug,
5862
6056
  project: projectSlug
5863
- }, "settings/access-tokens") : CONSOLE_BASE;
6057
+ }, "settings/access-tokens") : consoleBase();
5864
6058
  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}`;
5865
6059
  }
5866
6060
  function resolveApiBase(api) {
@@ -6101,7 +6295,6 @@ async function publish_handler(args) {
6101
6295
  }
6102
6296
  return JSON.stringify(data);
6103
6297
  }
6104
- const release_promote_DEFAULT_API = "https://www.extension.dev";
6105
6298
  const release_promote_schema = {
6106
6299
  name: "extension_release_promote",
6107
6300
  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).",
@@ -6161,7 +6354,7 @@ async function release_promote_handler(args) {
6161
6354
  const buildId = String(args.buildId || "").trim();
6162
6355
  const channel = String(args.channel || "").trim();
6163
6356
  if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
6164
- const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
6357
+ const apiCheck = safeApiBase(resolveApiBase(args.api));
6165
6358
  if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
6166
6359
  const url = `${apiCheck.base}/api/cli/release/promote`;
6167
6360
  const body = {
@@ -6199,7 +6392,7 @@ async function release_promote_handler(args) {
6199
6392
  const enrich = {};
6200
6393
  const ref = resolveProjectRef();
6201
6394
  if (404 === res.status || "UNKNOWN_BUILD" === code) {
6202
- enrich.buildsPageUrl = consoleProjectUrl(ref, "builds");
6395
+ enrich.buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
6203
6396
  enrich.hint = "Run extension_release_list to see this project's channels, their promoted shas, and recent builds.";
6204
6397
  if (ref) {
6205
6398
  const channelsUrl = registryFileUrl(ref, "channels.json");
@@ -6299,7 +6492,6 @@ async function release_list_handler(args) {
6299
6492
  if (!buildsRes.ok) result.buildsUnavailable = `builds/index.json unreadable: ${buildsRes.message}`;
6300
6493
  return JSON.stringify(result);
6301
6494
  }
6302
- const deploy_DEFAULT_API = "https://www.extension.dev";
6303
6495
  function storeMdWarnings(browsers, cwd) {
6304
6496
  const wantsFirefox = browsers.includes("firefox");
6305
6497
  const wantsEdge = browsers.includes("edge");
@@ -6389,7 +6581,7 @@ async function deploy_handler(args) {
6389
6581
  if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
6390
6582
  const buildSha = String(args.buildSha || "").trim();
6391
6583
  if (!buildSha) return deploy_fail("DeployInputError", "buildSha is required (the built commit to submit).");
6392
- const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || deploy_DEFAULT_API));
6584
+ const apiCheck = safeApiBase(resolveApiBase(args.api));
6393
6585
  if (!apiCheck.ok) return deploy_fail("DeployConfigError", apiCheck.message);
6394
6586
  const url = `${apiCheck.base}/api/cli/stores/submit`;
6395
6587
  const dryRun = false !== args.dryRun;
@@ -6434,7 +6626,7 @@ async function deploy_handler(args) {
6434
6626
  };
6435
6627
  if (dryRun) {
6436
6628
  const ref = resolveProjectRef();
6437
- const consoleStoresUrl = consoleProjectUrl(ref, "stores");
6629
+ const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
6438
6630
  const storeModeNote = `Store publish mode (draft / skip-publish / live) is not readable with the CLI token, so it cannot be verified from here; check per-store settings at ${consoleStoresUrl}.`;
6439
6631
  let health = null;
6440
6632
  let healthUnreadable = null;
@@ -6710,6 +6902,63 @@ async function store_status_handler(args) {
6710
6902
  if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
6711
6903
  return JSON.stringify(result);
6712
6904
  }
6905
+ const ENGINE_COMPANION_IDS = new Set([
6906
+ "kgdaecdpfkikjncaalnmmnjjfpofkcbl",
6907
+ CARRIER_EXTENSION_ID
6908
+ ]);
6909
+ const EXTENSION_URL = /^chrome-extension:\/\/([a-p]{32})\//i;
6910
+ async function verifyGuestLoaded(projectPath, browser, options) {
6911
+ let cdpPort;
6912
+ try {
6913
+ const resolved = await resolveCdpPort(projectPath, browser, {
6914
+ waitMs: options?.waitMs ?? 0
6915
+ });
6916
+ if (!resolved) return {
6917
+ checked: false,
6918
+ loaded: false,
6919
+ guestTargets: [],
6920
+ guestIds: [],
6921
+ reason: "No CDP port in the session's ready contract, so the browser's target list could not be queried (a headless Chromium session still exposes one; a gecko/Firefox session does not)."
6922
+ };
6923
+ cdpPort = resolved.port;
6924
+ const timeoutMs = options?.timeoutMs ?? 3000;
6925
+ const targets = await Promise.race([
6926
+ CDPClient.discoverTargets(cdpPort),
6927
+ new Promise((_, reject)=>setTimeout(()=>reject(new Error(`CDP /json timed out after ${timeoutMs}ms`)), timeoutMs))
6928
+ ]);
6929
+ const guestTargets = [];
6930
+ for (const t of targets){
6931
+ const match = EXTENSION_URL.exec(String(t.url ?? ""));
6932
+ if (!match) continue;
6933
+ const id = match[1].toLowerCase();
6934
+ if (!ENGINE_COMPANION_IDS.has(id)) guestTargets.push({
6935
+ id,
6936
+ type: String(t.type),
6937
+ url: String(t.url)
6938
+ });
6939
+ }
6940
+ const guestIds = [
6941
+ ...new Set(guestTargets.map((t)=>t.id))
6942
+ ];
6943
+ return {
6944
+ checked: true,
6945
+ loaded: guestTargets.length > 0,
6946
+ guestTargets,
6947
+ guestIds,
6948
+ cdpPort,
6949
+ reason: guestTargets.length > 0 ? `The browser lists ${guestIds.length} extension target${1 === guestIds.length ? "" : "s"} that are not the engine companion (${guestIds.join(", ")}), so the guest is loaded.` : "The browser's target list has no chrome-extension:// target other than the engine's devtools companion. On Chrome this is the signature of a silently rejected --load-extension (extension.js BUGS_TO_FIX §83): the CLI and ready.json cannot see it."
6950
+ };
6951
+ } catch (err) {
6952
+ return {
6953
+ checked: false,
6954
+ loaded: false,
6955
+ guestTargets: [],
6956
+ guestIds: [],
6957
+ cdpPort,
6958
+ reason: `Could not query the browser's target list${cdpPort ? ` on CDP port ${cdpPort}` : ""}: ${err?.message ?? String(err)}.`
6959
+ };
6960
+ }
6961
+ }
6713
6962
  function doctor_readReadyContract(projectPath, browser) {
6714
6963
  try {
6715
6964
  const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
@@ -6905,7 +7154,7 @@ function wait_isAlive(pid) {
6905
7154
  }
6906
7155
  const wait_schema = {
6907
7156
  name: "extension_wait",
6908
- description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status with two separate facts: compiled (the compiler finished) and browserAttached (the extension's runtime connected from a live browser). Every result reports budgetMs (this call's wait budget) and elapsedMs; on status:'timeout' call again to keep waiting (polling resumes on the same contract). In a noBrowser (build-only) session it returns as soon as the compile lands instead of waiting for a browser that will never attach. Ports in the result come from the ready contract, so they always match what the dev server actually bound.",
7157
+ description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status with these facts: compiled (the compiler finished), browserAttached (the engine's runtime executor connected), and for a browser session guestLoaded (the browser's OWN target list actually shows your extension). guestLoaded is the trustworthy load signal: it catches a silently rejected --load-extension that leaves ready.json stamped attached with empty logs (extension.js BUGS_TO_FIX §83); it is null when it could not be checked (no CDP port, e.g. a gecko session). Every result reports budgetMs (this call's wait budget) and elapsedMs; on status:'timeout' call again to keep waiting (polling resumes on the same contract). In a noBrowser (build-only) session it returns as soon as the compile lands instead of waiting for a browser that will never attach. Ports in the result come from the ready contract, so they always match what the dev server actually bound.",
6909
7158
  inputSchema: {
6910
7159
  type: "object",
6911
7160
  properties: {
@@ -6981,10 +7230,20 @@ async function wait_handler(args) {
6981
7230
  continue;
6982
7231
  }
6983
7232
  const runtimeErrors = recentErrorLogs(args.projectPath, browser, 3);
7233
+ const guestCheck = await verifyGuestLoaded(args.projectPath, browser);
7234
+ const warnings = [];
7235
+ if (runtimeErrors.length) warnings.push(`Compiled and attached, but the extension is throwing at runtime (${runtimeErrors.length} recent error event${1 === runtimeErrors.length ? "" : "s"} above). Check extension_logs (level: error) or extension_doctor before trusting this session.`);
7236
+ if (guestCheck.checked && !guestCheck.loaded) warnings.push("The engine reports the runtime attached, but the browser's own target list shows no chrome-extension:// target for your extension, only the engine companion. This is the signature of a silently rejected --load-extension (extension.js BUGS_TO_FIX §83): the CLI and ready.json cannot see it, and the control verbs will fail against a guest that is not there. Check the manifest and extension_logs.");
6984
7237
  return JSON.stringify({
6985
7238
  status: "ready",
6986
7239
  compiled: true,
6987
7240
  browserAttached: true,
7241
+ guestLoaded: guestCheck.checked ? guestCheck.loaded : null,
7242
+ ...guestCheck.checked ? {
7243
+ guestIds: guestCheck.guestIds
7244
+ } : {
7245
+ guestLoadNote: guestCheck.reason
7246
+ },
6988
7247
  command: contract.command,
6989
7248
  browser: contract.browser,
6990
7249
  port: contract.port,
@@ -6996,8 +7255,10 @@ async function wait_handler(args) {
6996
7255
  budgetMs,
6997
7256
  elapsedMs: Date.now() - start,
6998
7257
  ...runtimeErrors.length ? {
6999
- runtimeErrors,
7000
- warning: `Compiled and attached, but the extension is throwing at runtime (${runtimeErrors.length} recent error event${1 === runtimeErrors.length ? "" : "s"} above). Check extension_logs (level: error) or extension_doctor before trusting this session.`
7258
+ runtimeErrors
7259
+ } : {},
7260
+ ...warnings.length ? {
7261
+ warning: warnings.join(" ")
7001
7262
  } : {}
7002
7263
  });
7003
7264
  }
@@ -7035,6 +7296,7 @@ async function wait_handler(args) {
7035
7296
  hint: "Still building, call extension_wait again to keep waiting (it resumes polling the same contract). If it never readies, check the dev process with extension_doctor."
7036
7297
  });
7037
7298
  }
7299
+ const EXAMPLES_TREE_BASE = `https://github.com/extension-js/examples/tree/${PINNED_COMMIT}/examples`;
7038
7300
  const add_feature_schema = {
7039
7301
  name: "extension_add_feature",
7040
7302
  description: "Plan a new feature surface for an existing extension. Returns step-by-step instructions, the manifest additions to make, and reference templates from the extension.dev catalog. Does not modify files; apply the returned plan yourself.",
@@ -7278,7 +7540,7 @@ async function add_feature_handler(args) {
7278
7540
  framework,
7279
7541
  referenceTemplate: {
7280
7542
  slug: templateSlug,
7281
- repositoryUrl: `https://github.com/extension-js/examples/tree/main/examples/${templateSlug}`,
7543
+ repositoryUrl: `${EXAMPLES_TREE_BASE}/${templateSlug}`,
7282
7544
  referenceFiles: referenceFiles.filter((f)=>f.includes(featureDir) || f.includes("manifest"))
7283
7545
  },
7284
7546
  manifestUpdates,
@@ -7292,7 +7554,7 @@ async function add_feature_handler(args) {
7292
7554
  "2. Create the following files in your project:",
7293
7555
  ...filesToCreate.map((f)=>` - ${f.path} (${f.hint})`),
7294
7556
  "sidebar" === args.feature ? "3. Add background.ts to handle sidebar open: chromium uses chrome.sidePanel.setPanelBehavior, firefox uses browser.sidebarAction.open()" : "",
7295
- `4. Reference template source: https://github.com/extension-js/examples/tree/main/examples/${templateSlug}/src`,
7557
+ `4. Reference template source: ${EXAMPLES_TREE_BASE}/${templateSlug}/src`,
7296
7558
  "5. Run npm run dev to test"
7297
7559
  ].filter(Boolean),
7298
7560
  hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
@@ -7696,7 +7958,10 @@ const logout_schema = {
7696
7958
  };
7697
7959
  async function logout_handler() {
7698
7960
  const creds = readCredentials();
7699
- const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ? `https://console.extension.dev/${creds.workspaceSlug}/${creds.projectSlug}/settings/access-tokens` : null;
7961
+ const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ? consoleProjectUrl({
7962
+ workspace: creds.workspaceSlug,
7963
+ project: creds.projectSlug
7964
+ }, "settings/access-tokens") : null;
7700
7965
  const result = clearCredentials();
7701
7966
  return JSON.stringify({
7702
7967
  ok: true,