apiblaze 0.21.68 → 0.21.70

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.
Files changed (2) hide show
  1. package/dist/index.js +1100 -279
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__
34
34
  var version;
35
35
  var init_package = __esm({
36
36
  "package.json"() {
37
- version = "0.21.68";
37
+ version = "0.21.70";
38
38
  }
39
39
  });
40
40
 
@@ -445,8 +445,8 @@ function dualAuth(opts = {}) {
445
445
  function bothDoorsBody(opts = {}) {
446
446
  return { auth_type: "api_key", requests_auth: dualAuth(opts), login: GITHUB_LOGIN };
447
447
  }
448
- async function apiFetch(path19, options = {}, auth) {
449
- const url = `${DASHBOARD_BASE2}${path19}`;
448
+ async function apiFetch(path20, options = {}, auth) {
449
+ const url = `${DASHBOARD_BASE2}${path20}`;
450
450
  const res = await fetch(url, {
451
451
  ...options,
452
452
  headers: {
@@ -472,12 +472,12 @@ async function apiFetch(path19, options = {}, auth) {
472
472
  }
473
473
  return res.json();
474
474
  }
475
- async function agentCall(path19, method, body) {
475
+ async function agentCall(path20, method, body) {
476
476
  const token = getAccessToken();
477
477
  const res = await fetch(`${DASHBOARD_BASE2}/api/cli/agents`, {
478
478
  method: "POST",
479
479
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
480
- body: JSON.stringify({ path: path19, method, body })
480
+ body: JSON.stringify({ path: path20, method, body })
481
481
  });
482
482
  let data = null;
483
483
  try {
@@ -962,11 +962,11 @@ function decodeJwt(token) {
962
962
  return null;
963
963
  }
964
964
  }
965
- function maskPath(path19) {
966
- const q = path19.indexOf("?");
967
- if (q < 0) return path19;
968
- const base2 = path19.slice(0, q);
969
- const query = path19.slice(q + 1);
965
+ function maskPath(path20) {
966
+ const q = path20.indexOf("?");
967
+ if (q < 0) return path20;
968
+ const base2 = path20.slice(0, q);
969
+ const query = path20.slice(q + 1);
970
970
  const masked = query.split("&").map((pair) => {
971
971
  const eq = pair.indexOf("=");
972
972
  if (eq < 0) return pair;
@@ -1263,8 +1263,8 @@ function pageQuery(cursor, limit = PAGE_LIMIT) {
1263
1263
  if (cursor) qs.set("cursor", cursor);
1264
1264
  return qs.toString();
1265
1265
  }
1266
- function withPage(path19, cursor, limit = PAGE_LIMIT) {
1267
- return `${path19}${path19.includes("?") ? "&" : "?"}${pageQuery(cursor, limit)}`;
1266
+ function withPage(path20, cursor, limit = PAGE_LIMIT) {
1267
+ return `${path20}${path20.includes("?") ? "&" : "?"}${pageQuery(cursor, limit)}`;
1268
1268
  }
1269
1269
  var PAGE_LIMIT, MAX_HOPS;
1270
1270
  var init_paging = __esm({
@@ -2002,12 +2002,12 @@ var init_env_local = __esm({
2002
2002
  });
2003
2003
 
2004
2004
  // src/lib/preapproval.ts
2005
- async function iamCall(tenant2, path19, method = "GET", body) {
2005
+ async function iamCall(tenant2, path20, method = "GET", body) {
2006
2006
  const token = getAccessToken();
2007
2007
  const res = await fetch(`${DASHBOARD_BASE5}/api/cli/iam`, {
2008
2008
  method: "POST",
2009
2009
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
2010
- body: JSON.stringify({ tenant: tenant2, path: path19, method, body })
2010
+ body: JSON.stringify({ tenant: tenant2, path: path20, method, body })
2011
2011
  });
2012
2012
  const data = await res.json().catch(() => ({}));
2013
2013
  if (!res.ok) throw new ApiError(res.status, data?.details ?? data?.error ?? res.statusText, data);
@@ -3456,13 +3456,13 @@ async function ensureConsumerLogin(teamId, tenant2, version2) {
3456
3456
  console.log(` ${import_chalk15.default.green("\u2714")} Signed in as${creds.email ? ` ${import_chalk15.default.bold(creds.email)}` : " a consumer"} on ${tenant2}.`);
3457
3457
  return creds;
3458
3458
  }
3459
- async function cpPost(anon, path19, body, summary) {
3459
+ async function cpPost(anon, path20, body, summary) {
3460
3460
  if (anon) {
3461
3461
  const cred = loadAnonCred();
3462
3462
  if (!cred) throw new Error("Anonymous workspace credential missing.");
3463
- return cpFetch(cred.cp_key, path19, { method: "POST", body: JSON.stringify(body) });
3463
+ return cpFetch(cred.cp_key, path20, { method: "POST", body: JSON.stringify(body) });
3464
3464
  }
3465
- return admin({ method: "POST", path: path19, body, summary });
3465
+ return admin({ method: "POST", path: path20, body, summary });
3466
3466
  }
3467
3467
  async function provision(spec2, target2, opts) {
3468
3468
  const loggedIn = !!loadCredentials();
@@ -5165,12 +5165,148 @@ var init_key = __esm({
5165
5165
  }
5166
5166
  });
5167
5167
 
5168
+ // src/lib/zip.ts
5169
+ function crc32(buf) {
5170
+ let c = 4294967295;
5171
+ for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 255] ^ c >>> 8;
5172
+ return (c ^ 4294967295) >>> 0;
5173
+ }
5174
+ function zipStore(entries2) {
5175
+ const parts = [];
5176
+ const central = [];
5177
+ let offset = 0;
5178
+ const dosTime = 0, dosDate = 1 << 5 | 1;
5179
+ for (const e of entries2) {
5180
+ const name = new TextEncoder().encode(e.path.replace(/^\.\//, ""));
5181
+ const crc = crc32(e.data);
5182
+ const local = new Uint8Array(30 + name.length);
5183
+ const lv = new DataView(local.buffer);
5184
+ lv.setUint32(0, 67324752, true);
5185
+ lv.setUint16(4, 20, true);
5186
+ lv.setUint16(6, 2048, true);
5187
+ lv.setUint16(8, 0, true);
5188
+ lv.setUint16(10, dosTime, true);
5189
+ lv.setUint16(12, dosDate, true);
5190
+ lv.setUint32(14, crc, true);
5191
+ lv.setUint32(18, e.data.length, true);
5192
+ lv.setUint32(22, e.data.length, true);
5193
+ lv.setUint16(26, name.length, true);
5194
+ lv.setUint16(28, 0, true);
5195
+ local.set(name, 30);
5196
+ parts.push(local, e.data);
5197
+ const cd = new Uint8Array(46 + name.length);
5198
+ const cv = new DataView(cd.buffer);
5199
+ cv.setUint32(0, 33639248, true);
5200
+ cv.setUint16(4, 20, true);
5201
+ cv.setUint16(6, 20, true);
5202
+ cv.setUint16(8, 2048, true);
5203
+ cv.setUint16(10, 0, true);
5204
+ cv.setUint16(12, dosTime, true);
5205
+ cv.setUint16(14, dosDate, true);
5206
+ cv.setUint32(16, crc, true);
5207
+ cv.setUint32(20, e.data.length, true);
5208
+ cv.setUint32(24, e.data.length, true);
5209
+ cv.setUint16(28, name.length, true);
5210
+ cv.setUint16(30, 0, true);
5211
+ cv.setUint16(32, 0, true);
5212
+ cv.setUint16(34, 0, true);
5213
+ cv.setUint16(36, 0, true);
5214
+ cv.setUint32(38, 0, true);
5215
+ cv.setUint32(42, offset, true);
5216
+ cd.set(name, 46);
5217
+ central.push(cd);
5218
+ offset += local.length + e.data.length;
5219
+ }
5220
+ const cdSize = central.reduce((n, c) => n + c.length, 0);
5221
+ const end = new Uint8Array(22);
5222
+ const ev = new DataView(end.buffer);
5223
+ ev.setUint32(0, 101010256, true);
5224
+ ev.setUint16(4, 0, true);
5225
+ ev.setUint16(6, 0, true);
5226
+ ev.setUint16(8, entries2.length, true);
5227
+ ev.setUint16(10, entries2.length, true);
5228
+ ev.setUint32(12, cdSize, true);
5229
+ ev.setUint32(16, offset, true);
5230
+ ev.setUint16(20, 0, true);
5231
+ const total = offset + cdSize + end.length;
5232
+ const out = new Uint8Array(total);
5233
+ let p = 0;
5234
+ for (const part of [...parts, ...central, end]) {
5235
+ out.set(part, p);
5236
+ p += part.length;
5237
+ }
5238
+ return out;
5239
+ }
5240
+ function imageInfo(buf) {
5241
+ const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
5242
+ if (buf.length >= 24 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return { format: "png", width: dv.getUint32(16), height: dv.getUint32(20) };
5243
+ if (buf.length >= 4 && buf[0] === 255 && buf[1] === 216) {
5244
+ let i = 2;
5245
+ while (i + 9 < buf.length) {
5246
+ if (buf[i] !== 255) {
5247
+ i++;
5248
+ continue;
5249
+ }
5250
+ const marker2 = buf[i + 1];
5251
+ if (marker2 === 216 || marker2 === 1 || marker2 >= 208 && marker2 <= 215) {
5252
+ i += 2;
5253
+ continue;
5254
+ }
5255
+ const len = dv.getUint16(i + 2);
5256
+ if (marker2 >= 192 && marker2 <= 207 && marker2 !== 196 && marker2 !== 200 && marker2 !== 204) return { format: "jpeg", height: dv.getUint16(i + 5), width: dv.getUint16(i + 7) };
5257
+ i += 2 + len;
5258
+ }
5259
+ return { format: "jpeg", width: null, height: null };
5260
+ }
5261
+ if (buf.length >= 30 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
5262
+ const chunk = String.fromCharCode(buf[12], buf[13], buf[14], buf[15]);
5263
+ if (chunk === "VP8X") return { format: "webp", width: 1 + (buf[24] | buf[25] << 8 | buf[26] << 16), height: 1 + (buf[27] | buf[28] << 8 | buf[29] << 16) };
5264
+ if (chunk === "VP8 ") return { format: "webp", width: dv.getUint16(26, true) & 16383, height: dv.getUint16(28, true) & 16383 };
5265
+ if (chunk === "VP8L") {
5266
+ const b = dv.getUint32(21, true);
5267
+ return { format: "webp", width: (b & 16383) + 1, height: (b >> 14 & 16383) + 1 };
5268
+ }
5269
+ return { format: "webp", width: null, height: null };
5270
+ }
5271
+ const head = new TextDecoder().decode(buf.subarray(0, 512)).trim();
5272
+ if (/^(<\?xml[^>]*>\s*)?(<!--[\s\S]*?-->\s*)*<svg[\s>]/i.test(head)) return { format: "svg", width: null, height: null };
5273
+ return { format: "unknown", width: null, height: null };
5274
+ }
5275
+ function logoProblem(buf) {
5276
+ if (buf.length > 5 * 1024 * 1024) return `the icon is ${(buf.length / 1048576).toFixed(1)} MiB; OpenAI allows 5`;
5277
+ const info = imageInfo(buf);
5278
+ if (info.format === "unknown") return "the icon is not a PNG, JPEG, WebP or SVG";
5279
+ if (info.width != null && info.height != null) {
5280
+ if (info.width !== info.height) return `the icon is ${info.width}\xD7${info.height}; OpenAI wants a square`;
5281
+ if (info.width < 48) return `the icon is ${info.width} px; OpenAI wants at least 48`;
5282
+ if (info.width > 4096) return `the icon is ${info.width} px; OpenAI allows at most 4096`;
5283
+ }
5284
+ return null;
5285
+ }
5286
+ var CRC_TABLE, LOGO_EXT;
5287
+ var init_zip = __esm({
5288
+ "src/lib/zip.ts"() {
5289
+ "use strict";
5290
+ CRC_TABLE = (() => {
5291
+ const t = new Uint32Array(256);
5292
+ for (let n = 0; n < 256; n++) {
5293
+ let c = n;
5294
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
5295
+ t[n] = c >>> 0;
5296
+ }
5297
+ return t;
5298
+ })();
5299
+ LOGO_EXT = { png: "png", jpeg: "jpg", webp: "webp", svg: "svg", unknown: "bin" };
5300
+ }
5301
+ });
5302
+
5168
5303
  // src/lib/directory-probes.ts
5169
5304
  var directory_probes_exports = {};
5170
5305
  __export(directory_probes_exports, {
5171
5306
  PROBE_TIMEOUT_MS: () => PROBE_TIMEOUT_MS,
5172
5307
  asMetadataUrl: () => asMetadataUrl,
5173
- probeDirectoryTarget: () => probeDirectoryTarget
5308
+ probeDirectoryTarget: () => probeDirectoryTarget,
5309
+ probeUserinfo: () => probeUserinfo
5174
5310
  });
5175
5311
  async function probe(url, init, deps) {
5176
5312
  const f2 = deps.fetch ?? fetch;
@@ -5238,7 +5374,9 @@ async function probeDirectoryTarget(target2, deps = {}) {
5238
5374
  }
5239
5375
  })() ? redirected : null,
5240
5376
  sseServed: sse.status == null ? null : sse.status === 200,
5241
- ipv4Reachable: post.status == null && get.status == null ? ipv4 ? null : false : ipv4
5377
+ ipv4Reachable: post.status == null && get.status == null ? ipv4 ? null : false : ipv4,
5378
+ // WP1: a 200 `initialize` says whether the host advertises the MCP Apps UI extension (sign-in hosts answer 401: not probed).
5379
+ uiExtension: post.status === 200 && obj(post.json).result && typeof obj(post.json).result === "object" ? !!obj(obj(post.json).result).capabilities?.extensions?.["io.modelcontextprotocol/ui"] : null
5242
5380
  };
5243
5381
  const prmUrl = `${origin.origin}/.well-known/oauth-protected-resource${origin.pathname.replace(/\/$/, "")}`;
5244
5382
  const prm = await probe(prmUrl, { method: "GET" }, deps);
@@ -5286,6 +5424,45 @@ async function probeDirectoryTarget(target2, deps = {}) {
5286
5424
  const name = (target2.serverName ?? "").trim();
5287
5425
  out.metadata = { ...out.metadata ?? {}, docsStatus: p.status, docsContainsName: p.status === 200 && name ? body.toLowerCase().includes(name.toLowerCase()) : null };
5288
5426
  }
5427
+ for (const [url, key] of [[target2.websiteUrl, "websiteStatus"], [target2.supportUrl, "supportStatus"], [target2.termsUrl, "termsStatus"]]) {
5428
+ if (!url || !/^https?:\/\//i.test(url)) continue;
5429
+ let p = await probe(url, { method: "GET", headers: { accept: "text/html, */*;q=0.5" } }, deps);
5430
+ if (p.status != null && p.status >= 300 && p.status < 400 && p.location) p = await probe(absolute(p.location, url) ?? p.location, { method: "GET", headers: { accept: "text/html, */*;q=0.5" } }, deps);
5431
+ out.metadata = { ...out.metadata ?? {}, [key]: p.status };
5432
+ }
5433
+ if (target2.iconUrl && /^https:\/\//i.test(target2.iconUrl)) {
5434
+ const f2 = deps.fetch ?? fetch;
5435
+ const ctl = new AbortController();
5436
+ const timer = setTimeout(() => ctl.abort(), PROBE_TIMEOUT_MS);
5437
+ try {
5438
+ const res = await f2(target2.iconUrl, { redirect: "follow", signal: ctl.signal, headers: { "user-agent": "apiblaze-cli directory-check", accept: "image/*" } });
5439
+ const buf = new Uint8Array(await res.arrayBuffer());
5440
+ const info = res.status === 200 ? imageInfo(buf) : null;
5441
+ out.metadata = { ...out.metadata ?? {}, iconStatus: res.status, iconWidth: info?.width ?? null, iconHeight: info?.height ?? null, iconFormat: info?.format ?? null };
5442
+ } catch {
5443
+ out.metadata = { ...out.metadata ?? {}, iconStatus: null, iconWidth: null, iconHeight: null, iconFormat: null };
5444
+ } finally {
5445
+ clearTimeout(timer);
5446
+ }
5447
+ }
5448
+ if (target2.screenshots?.length) {
5449
+ const f2 = deps.fetch ?? fetch;
5450
+ const shots = await Promise.all(target2.screenshots.slice(0, 5).map(async (url) => {
5451
+ const ctl = new AbortController();
5452
+ const timer = setTimeout(() => ctl.abort(), PROBE_TIMEOUT_MS);
5453
+ try {
5454
+ const res = await f2(url, { redirect: "follow", signal: ctl.signal, headers: { "user-agent": "apiblaze-cli directory-check", accept: "image/*" } });
5455
+ const buf = new Uint8Array(await res.arrayBuffer());
5456
+ const info = res.status === 200 ? imageInfo(buf) : null;
5457
+ return { url, status: res.status, width: info?.width ?? null, height: info?.height ?? null, format: info?.format ?? null };
5458
+ } catch {
5459
+ return { url, status: null, width: null, height: null, format: null };
5460
+ } finally {
5461
+ clearTimeout(timer);
5462
+ }
5463
+ }));
5464
+ out.metadata = { ...out.metadata ?? {}, screenshots: shots };
5465
+ }
5289
5466
  const ch = await probe(`${origin.origin}/.well-known/openai-apps-challenge`, { method: "GET" }, deps);
5290
5467
  out.openaiChallenge = { servedStatus: ch.status, servedMatches: null };
5291
5468
  if (ch.status === 200 && typeof ch.json === "string") out.openaiChallenge.servedBody = ch.json.trim().slice(0, 128);
@@ -5294,17 +5471,25 @@ async function probeDirectoryTarget(target2, deps = {}) {
5294
5471
  function asMetadataUrl(issuer) {
5295
5472
  try {
5296
5473
  const u = new URL(issuer);
5297
- const path19 = u.pathname.replace(/\/$/, "");
5298
- return `${u.origin}/.well-known/oauth-authorization-server${path19}`;
5474
+ const path20 = u.pathname.replace(/\/$/, "");
5475
+ return `${u.origin}/.well-known/oauth-authorization-server${path20}`;
5299
5476
  } catch {
5300
5477
  return `${issuer.replace(/\/$/, "")}/.well-known/oauth-authorization-server`;
5301
5478
  }
5302
5479
  }
5480
+ async function probeUserinfo(as, bearer, deps = {}) {
5481
+ const ep = typeof as?.userinfo_endpoint === "string" ? as.userinfo_endpoint : null;
5482
+ if (!ep || !bearer) return { userinfoStatus: null, userinfoEmailVerified: null };
5483
+ const p = await probe(ep, { method: "GET", headers: { authorization: `Bearer ${bearer}` } }, deps);
5484
+ const body = obj(p.json);
5485
+ return { userinfoStatus: p.status, userinfoEmailVerified: p.status === 200 ? body.email_verified === true : null };
5486
+ }
5303
5487
  var import_promises, PROBE_TIMEOUT_MS, strArr, obj;
5304
5488
  var init_directory_probes = __esm({
5305
5489
  "src/lib/directory-probes.ts"() {
5306
5490
  "use strict";
5307
5491
  import_promises = __toESM(require("dns/promises"));
5492
+ init_zip();
5308
5493
  PROBE_TIMEOUT_MS = 5e3;
5309
5494
  strArr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : null;
5310
5495
  obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
@@ -5318,6 +5503,7 @@ __export(directory_exports, {
5318
5503
  AUTH_MODES: () => AUTH_MODES,
5319
5504
  DATA_HANDLING_MODES: () => DATA_HANDLING_MODES,
5320
5505
  DESCRIPTION_MIN: () => DESCRIPTION_MIN,
5506
+ OPENAI_CATEGORIES: () => OPENAI_CATEGORIES,
5321
5507
  PLATFORMS: () => PLATFORMS,
5322
5508
  attestationId: () => attestationId,
5323
5509
  blockerLines: () => blockerLines,
@@ -5325,12 +5511,14 @@ __export(directory_exports, {
5325
5511
  fetchDirectoryReport: () => fetchDirectoryReport,
5326
5512
  isApiError: () => isApiError,
5327
5513
  isReady: () => isReady,
5514
+ listingOf: () => listingOf,
5328
5515
  parseAuthMode: () => parseAuthMode,
5329
5516
  parseCompliant: () => parseCompliant,
5330
5517
  parseDataHandling: () => parseDataHandling,
5331
5518
  parsePlatforms: () => parsePlatforms,
5332
5519
  parseYesNo: () => parseYesNo,
5333
- printReport: () => printReport
5520
+ printReport: () => printReport,
5521
+ writeJsonLine: () => writeJsonLine
5334
5522
  });
5335
5523
  function attestationId(flag) {
5336
5524
  const f2 = flag.trim().toLowerCase();
@@ -5358,7 +5546,7 @@ function parseYesNo(flag, v) {
5358
5546
  }
5359
5547
  function directoryBodyFromFlags(f2) {
5360
5548
  const m = {};
5361
- for (const [flag, v] of [["--privacy-url", f2.privacyUrl], ["--docs-url", f2.docsUrl], ["--icon-url", f2.iconUrl]]) {
5549
+ for (const [flag, v] of [["--privacy-url", f2.privacyUrl], ["--docs-url", f2.docsUrl], ["--icon-url", f2.iconUrl], ["--website-url", f2.websiteUrl], ["--support-url", f2.supportUrl], ["--terms-url", f2.termsUrl], ["--demo-recording-url", f2.demoRecordingUrl]]) {
5362
5550
  if (v && !/^https:\/\//i.test(v.trim())) throw new Error(`${flag} must be an https URL (got ${v.trim()}).`);
5363
5551
  }
5364
5552
  if (f2.privacyUrl) m.privacy_url = f2.privacyUrl;
@@ -5372,8 +5560,39 @@ function directoryBodyFromFlags(f2) {
5372
5560
  if (f2.tagline) m.tagline = f2.tagline;
5373
5561
  if (f2.description) m.description = f2.description;
5374
5562
  if (f2.iconUrl) m.icon_url = f2.iconUrl;
5563
+ if (f2.slug) m.slug = f2.slug.trim().toLowerCase();
5564
+ if (f2.highRisk !== void 0) {
5565
+ const list = (Array.isArray(f2.highRisk) ? f2.highRisk : String(f2.highRisk).split(",")).map((x) => x.trim().toLowerCase()).filter((x) => x && x !== "none");
5566
+ const bad = list.filter((x) => !["legal", "healthcare", "insurance", "finance"].includes(x));
5567
+ if (bad.length) throw new Error(`--high-risk: unknown "${bad.join(", ")}"; use none, or any of legal, healthcare, insurance, finance (comma-separated).`);
5568
+ m.high_risk = list;
5569
+ }
5570
+ if (f2.mcpApp === true) m.mcp_app = true;
5571
+ if (f2.mcpApp === false) m.mcp_app = null;
5572
+ if (f2.screenshot?.length) {
5573
+ m.screenshots = f2.screenshot.map((x) => {
5574
+ const i = x.indexOf("=");
5575
+ const url = (i > 0 ? x.slice(0, i) : x).trim();
5576
+ const prompt = i > 0 ? x.slice(i + 1).trim() : "";
5577
+ if (!/^https:\/\//i.test(url) || !prompt) throw new Error(`--screenshot wants https://\u2026png="<the prompt that produced it>", got "${x}"`);
5578
+ return { url, prompt };
5579
+ });
5580
+ }
5375
5581
  if (f2.vulnContact) m.vuln_contact = f2.vulnContact;
5376
5582
  if (f2.allowedOrigin?.length) m.allowed_origins = f2.allowedOrigin.map((o) => o.trim()).filter(Boolean);
5583
+ if (f2.websiteUrl) m.website_url = f2.websiteUrl.trim();
5584
+ if (f2.supportUrl) m.support_url = f2.supportUrl.trim();
5585
+ if (f2.termsUrl) m.terms_url = f2.termsUrl.trim();
5586
+ if (f2.developerName) m.developer_name = f2.developerName.trim();
5587
+ if (f2.category) {
5588
+ const hit = OPENAI_CATEGORIES.find((c) => c.toLowerCase() === f2.category.trim().toLowerCase());
5589
+ if (!hit) throw new Error(`Unknown --category "${f2.category}". OpenAI's list: ${OPENAI_CATEGORIES.join(", ")}.`);
5590
+ m.category = hit;
5591
+ }
5592
+ if (f2.demoRecordingUrl) m.demo_recording_url = f2.demoRecordingUrl.trim();
5593
+ if (f2.releaseNotes) m.release_notes = f2.releaseNotes.trim();
5594
+ if (f2.shortDescription) m.short_description = f2.shortDescription.trim();
5595
+ if (f2.countries) m.countries = f2.countries.map((c) => c.trim().toUpperCase()).filter(Boolean);
5377
5596
  const mode = parseAuthMode(f2.authMode);
5378
5597
  if (mode) m.auth_mode = mode;
5379
5598
  const dh = parseDataHandling(f2.dataHandling, f2.dataHandlingStatement);
@@ -5396,6 +5615,11 @@ function directoryBodyFromFlags(f2) {
5396
5615
  }
5397
5616
  return out;
5398
5617
  }
5618
+ function listingOf(r) {
5619
+ const m = r?.target?.metadata ?? {};
5620
+ const s = (k) => typeof m[k] === "string" ? m[k] : null;
5621
+ return { privacyUrl: s("privacyUrl"), docsUrl: s("docsUrl"), serverName: s("serverName"), websiteUrl: s("websiteUrl"), supportUrl: s("supportUrl"), termsUrl: s("termsUrl") };
5622
+ }
5399
5623
  function parseCompliant(policy, platform) {
5400
5624
  if (platform && (policy === void 0 || policy === true)) return parsePlatforms(platform) ?? ["claude"];
5401
5625
  if (policy === void 0 || policy === false || policy === true) return ["claude"];
@@ -5431,11 +5655,12 @@ async function fetchDirectoryReport(caller, proj2, opts = {}) {
5431
5655
  first = await producer(caller, { method: "POST", path: reportPath(proj2), body: base2, summary: `Directory report for ${proj2.projectName}` });
5432
5656
  }
5433
5657
  const publishedUrl = opts.publishedUrl ?? first?.report?.project?.publishedUrl ?? null;
5434
- const meta = first?.target?.metadata;
5658
+ const meta = first?.target?.metadata ?? opts.listing ?? void 0;
5435
5659
  const privacyUrl = opts.privacyUrl ?? meta?.privacyUrl ?? null;
5436
- if (publishedUrl) probes = await probeDirectoryTarget({ publishedUrl, privacyUrl, docsUrl: meta?.docsUrl ?? null, serverName: meta?.serverName ?? null });
5660
+ if (publishedUrl) probes = await probeDirectoryTarget({ publishedUrl, privacyUrl, docsUrl: meta?.docsUrl ?? null, serverName: meta?.serverName ?? null, websiteUrl: meta?.websiteUrl ?? null, supportUrl: meta?.supportUrl ?? null, termsUrl: meta?.termsUrl ?? null, screenshots: meta?.mcpApp ? (meta.screenshots ?? []).map((x) => x.url) : null, iconUrl: meta?.iconUrl ?? null });
5437
5661
  else if (first && !opts.refreshSnapshot) return first;
5438
5662
  }
5663
+ if (opts.extraProbes) probes = { ...probes ?? {}, ...opts.extraProbes, metadata: { ...probes?.metadata ?? {}, ...opts.extraProbes.metadata ?? {} } };
5439
5664
  return producer(caller, {
5440
5665
  method: "POST",
5441
5666
  path: reportPath(proj2),
@@ -5447,9 +5672,12 @@ function isReady(r, platforms) {
5447
5672
  const want = platforms ?? r.report.platformsRequested ?? [...PLATFORMS];
5448
5673
  return want.every((p) => r.report.platforms[p]?.ready === true);
5449
5674
  }
5675
+ function writeJsonLine(value) {
5676
+ import_node_fs.default.writeSync(1, JSON.stringify(value) + "\n");
5677
+ }
5450
5678
  function printReport(r, json) {
5451
5679
  if (json) {
5452
- process.stdout.write(JSON.stringify(r) + "\n");
5680
+ writeJsonLine(r);
5453
5681
  return;
5454
5682
  }
5455
5683
  console.log(r.rendered.text.replace(/<project>/g, r.report.project.id));
@@ -5463,10 +5691,11 @@ function blockerLines(r, platforms) {
5463
5691
  function isApiError(e, status2) {
5464
5692
  return e instanceof ApiError && (status2 === void 0 || e.status === status2);
5465
5693
  }
5466
- var import_chalk17, PLATFORMS, ATTESTATIONS, AUTH_MODES, DATA_HANDLING_MODES, reportPath, DESCRIPTION_MIN;
5694
+ var import_node_fs, import_chalk17, PLATFORMS, ATTESTATIONS, OPENAI_CATEGORIES, AUTH_MODES, DATA_HANDLING_MODES, reportPath, DESCRIPTION_MIN;
5467
5695
  var init_directory = __esm({
5468
5696
  "src/lib/directory.ts"() {
5469
5697
  "use strict";
5698
+ import_node_fs = __toESM(require("fs"));
5470
5699
  import_chalk17 = __toESM(require("chalk"));
5471
5700
  init_caller();
5472
5701
  init_directory_probes();
@@ -5484,8 +5713,11 @@ var init_directory = __esm({
5484
5713
  // ChatGPT's guidelines add these three.
5485
5714
  { id: "no_digital_goods_sales", flags: ["no-digital-goods", "no-digital-goods-sales", "no_digital_goods_sales"], label: "No sale of digital goods, subscriptions or credits; no upgrade/checkout flows (ChatGPT)", platforms: ["chatgpt"] },
5486
5715
  { id: "content_13_no_ads", flags: ["content-13-no-ads", "content-13", "content_13_no_ads"], label: "Content suitable for 13+ and no advertising (ChatGPT)", platforms: ["chatgpt"] },
5716
+ // WP5 (Claude-lane review A11): both directories want the policy to say what is collected, used, shared and kept, and how to reach you.
5717
+ { id: "privacy_policy_covers", flags: ["privacy-policy-covers", "privacy_policy_covers", "privacy-covers"], label: "The privacy policy covers data collection, use, sharing, retention and a contact, and says tool traffic passes through APIblaze as a processor", platforms: PLATFORMS },
5487
5718
  { id: "no_restricted_data", flags: ["no-restricted-data", "no_restricted_data"], label: "No card data, health records, government ids or credentials are collected (ChatGPT)", platforms: ["chatgpt"] }
5488
5719
  ];
5720
+ OPENAI_CATEGORIES = ["Productivity", "Creativity", "Developer Tools", "Business & Operations", "Data & Analytics", "Communication", "Education & Research", "Security", "Finance", "Healthcare", "Travel", "Entertainment", "Other"];
5489
5721
  AUTH_MODES = ["signin", "open"];
5490
5722
  DATA_HANDLING_MODES = ["own", "proxied", "third-party"];
5491
5723
  reportPath = (proj2) => `/projects/${encodeURIComponent(proj2.projectId)}/${encodeURIComponent(proj2.apiVersion)}/directory-report`;
@@ -7490,6 +7722,58 @@ function mcpConfigDocument(url, bearer, runId2) {
7490
7722
  const headers = { ...bearer ? { Authorization: `Bearer ${bearer}` } : {}, ...runId2 ? { "apiblaze-directory-run": runId2 } : {} };
7491
7723
  return JSON.stringify({ mcpServers: { abz: { type: "http", url, ...Object.keys(headers).length ? { headers } : {} } } }, null, 2) + "\n";
7492
7724
  }
7725
+ function codexRecipe(o) {
7726
+ const toml = (s) => JSON.stringify(s);
7727
+ return [
7728
+ "exec",
7729
+ "--json",
7730
+ "--ephemeral",
7731
+ "--ignore-user-config",
7732
+ "--skip-git-repo-check",
7733
+ // Built-ins off, as for Claude Code: with web search or a browser on, Codex "looks at the site" itself
7734
+ // instead of calling the tool (seen live 2026-11-04: "the site wouldn't load through my browsing tools").
7735
+ "-c",
7736
+ 'web_search="disabled"',
7737
+ "--disable",
7738
+ "browser_use",
7739
+ "--disable",
7740
+ "browser_use_external",
7741
+ "--disable",
7742
+ "in_app_browser",
7743
+ ...o.cwd ? ["-C", o.cwd] : [],
7744
+ "-c",
7745
+ `mcp_servers.abz.url=${toml(o.serverUrl)}`,
7746
+ "-c",
7747
+ `mcp_servers.abz.http_headers={"apiblaze-directory-run"=${toml(o.runId)}}`,
7748
+ ...o.bearerEnvVar ? ["-c", `mcp_servers.abz.bearer_token_env_var=${toml(o.bearerEnvVar)}`] : [],
7749
+ "-c",
7750
+ "mcp_servers.abz.required=true",
7751
+ "-c",
7752
+ 'mcp_servers.abz.default_tools_approval_mode="approve"',
7753
+ "-c",
7754
+ `mcp_servers.abz.enabled_tools=[${o.enabledTools.map(toml).join(",")}]`,
7755
+ o.prompt
7756
+ ];
7757
+ }
7758
+ function toolCallsInCodexOutput(stdout) {
7759
+ const names = /* @__PURE__ */ new Set();
7760
+ for (const line of stdout.split("\n")) {
7761
+ const s = line.trim();
7762
+ if (!s.startsWith("{")) continue;
7763
+ let ev;
7764
+ try {
7765
+ ev = JSON.parse(s);
7766
+ } catch {
7767
+ continue;
7768
+ }
7769
+ const item = ev?.item ?? ev;
7770
+ if (item && item.type === "mcp_tool_call" && typeof item.tool === "string" && (item.server === void 0 || item.server === "abz")) names.add(item.tool);
7771
+ }
7772
+ return [...names];
7773
+ }
7774
+ function readOnlyToolNames(tools) {
7775
+ return tools.filter((t) => toolRole(t) === "read").map((t) => t.name);
7776
+ }
7493
7777
  function toolCallsInClaudeOutput(stdout) {
7494
7778
  const names = /* @__PURE__ */ new Set();
7495
7779
  for (const line of stdout.split("\n")) {
@@ -7524,11 +7808,14 @@ var init_exercise = __esm({
7524
7808
  var portal_drafts_exports = {};
7525
7809
  __export(portal_drafts_exports, {
7526
7810
  ANSWER_MIN: () => ANSWER_MIN,
7811
+ defaultNegativePrompts: () => defaultNegativePrompts,
7527
7812
  describeResult: () => describeResult,
7813
+ draftChatgptTestCases: () => draftChatgptTestCases,
7528
7814
  draftDescription: () => draftDescription,
7529
7815
  draftMissing: () => draftMissing,
7530
7816
  draftReviewerInstructions: () => draftReviewerInstructions,
7531
- draftUseCases: () => draftUseCases
7817
+ draftUseCases: () => draftUseCases,
7818
+ humanizeShape: () => humanizeShape
7532
7819
  });
7533
7820
  function describeResult(text) {
7534
7821
  const t = (text ?? "").trim();
@@ -7545,6 +7832,21 @@ function describeResult(text) {
7545
7832
  const head = t.replace(/\s+/g, " ").slice(0, 80);
7546
7833
  return `a text answer ("${head}${t.length > 80 ? "\u2026" : ""}")`;
7547
7834
  }
7835
+ function humanKey(k) {
7836
+ const words = k.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
7837
+ if (/^(is|has) /.test(words)) return `whether it ${words}`;
7838
+ if (/ (at|on)$/.test(words)) return `when it was ${words.replace(/ (at|on)$/, "")}`;
7839
+ if (words === "id") return "an id";
7840
+ if (words === "url") return "the URL";
7841
+ return words;
7842
+ }
7843
+ function humanizeShape(shape) {
7844
+ const m = /^a (?:JSON answer|list of (\d+) items?) with (.+?)(?: \(\+\d+ more\))?$/.exec(shape.trim());
7845
+ if (!m) return shape.replace(/ \(\+\d+ more\)/, "");
7846
+ const keys = m[2].split(",").map((x) => x.trim()).filter(Boolean).slice(0, 6).map(humanKey);
7847
+ const list = keys.length > 1 ? `${keys.slice(0, -1).join(", ")} and ${keys[keys.length - 1]}` : keys[0] ?? "an answer";
7848
+ return m[1] ? `a list of ${m[1]} item${m[1] === "1" ? "" : "s"}, each with ${list}` : `${list}`;
7849
+ }
7548
7850
  function argsText(args) {
7549
7851
  if (!args || !Object.keys(args).length) return "no arguments";
7550
7852
  return Object.entries(args).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(", ");
@@ -7570,7 +7872,7 @@ function draftUseCases(i) {
7570
7872
  for (const g of groups.values()) {
7571
7873
  const row = i.rows.find((r) => r.tool === g.tools[0].name && r.valid === "ok");
7572
7874
  const which = g.prompts.length === list.length && list.length > 1 ? "Each of these" : g.prompts.map(short).join(", ");
7573
- parts.push(`${which} make${g.prompts.length === 1 ? "s" : ""} Claude call ${g.tools.map(titleOf).join(" and ")}${row ? `, which returns ${shapeOf(row)}` : ""}.`);
7875
+ parts.push(`${which} answer${g.prompts.length === 1 ? "s" : ""} with ${row ? humanizeShape(shapeOf(row)) : `what ${g.tools.map(titleOf).join(" and ")} gives`}.`);
7574
7876
  }
7575
7877
  const uncalled = i.tools.filter((t) => !Object.values(i.promptTools).some((ns) => ns.includes(t.name)));
7576
7878
  if (uncalled.length) parts.push(`Also available: ${uncalled.map((t) => `${titleOf(t)}${(t.description ?? "").trim() ? ` (${(t.description ?? "").trim().replace(/\.$/, "")})` : ""}`).join("; ")}.`);
@@ -7579,20 +7881,21 @@ function draftUseCases(i) {
7579
7881
  }
7580
7882
  function draftReviewerInstructions(i) {
7581
7883
  const lines = [];
7582
- lines.push(`Add ${i.publishedUrl} as a custom connector in Claude (Settings \u2192 Connectors \u2192 Add custom connector).`);
7583
- lines.push(i.open ? "No sign-in: the data is public and every tool is read-only. Connect and ask." : `Sign in with the reviewer account in this packet${i.reviewerUsername ? ` (username ${i.reviewerUsername}; the password is in the packet, never in this text)` : ""}.`);
7884
+ const where = i.platforms?.length === 1 && i.platforms[0] === "chatgpt" ? "ChatGPT (chatgpt.com/plugins \u2192 Add custom MCP server)" : i.platforms?.includes("chatgpt") ? "Claude (Settings \u2192 Connectors \u2192 Add custom connector) or ChatGPT (chatgpt.com/plugins \u2192 Add custom MCP server)" : "Claude (Settings \u2192 Connectors \u2192 Add custom connector)";
7885
+ lines.push(`Add ${i.publishedUrl} as a custom connector in ${where}.`);
7886
+ lines.push(i.open ? "No sign-in: the data is public and every tool is read-only. Connect and ask." : `Sign in with the username and password entered in the credential fields of this form${i.reviewerUsername ? ` (username ${i.reviewerUsername})` : ""}: the sign-in page shows a username & password form under the provider buttons; no email, SMS or MFA step.`);
7584
7887
  const seen = /* @__PURE__ */ new Set();
7585
7888
  for (const p of i.prompts) {
7586
7889
  const called = toolsCalledBy(i, p);
7587
7890
  if (!called.length) continue;
7588
7891
  const row = i.rows.find((r) => r.tool === called[0].name && r.valid === "ok");
7589
7892
  called.forEach((t) => seen.add(t.name));
7590
- lines.push(`Ask: "${p}" \u2014 Claude calls ${called.map(titleOf).join(" and ")}${row ? `; expect ${shapeOf(row)}` : ""}.`);
7893
+ lines.push(`Ask: "${p}" \u2014 uses ${called.map(titleOf).join(" and ")}${row ? `; expect ${humanizeShape(shapeOf(row))}` : ""}.`);
7591
7894
  }
7592
7895
  for (const r of i.rows) {
7593
7896
  if (seen.has(r.tool) || r.valid !== "ok") continue;
7594
7897
  const t = i.tools.find((x) => x.name === r.tool) ?? { name: r.tool };
7595
- lines.push(`Ask Claude to ${titleOf(t).replace(/^./, (c) => c.toLowerCase())} (${argsText(r.args)}); expect ${shapeOf(r)}.`);
7898
+ lines.push(`Ask it to ${titleOf(t).replace(/^./, (c) => c.toLowerCase())} (${argsText(r.args)}); expect ${humanizeShape(shapeOf(r))}.`);
7596
7899
  }
7597
7900
  const errs = i.rows.filter((r) => r.invalid === "error" && r.invalidText);
7598
7901
  for (const r of errs.slice(0, 2)) {
@@ -7617,7 +7920,7 @@ function draftDescription(i) {
7617
7920
  const working = i.prompts.filter((p) => (i.promptTools[p] ?? []).length);
7618
7921
  if (working.length) parts.push(`Ask it things like ${working.map((p) => `"${p}"`).join(", ")}.`);
7619
7922
  const ok = i.rows.find((r) => r.valid === "ok");
7620
- if (ok) parts.push(`What comes back is ${shapeOf(ok)}, which Claude reads and explains.`);
7923
+ if (ok) parts.push(`Answers come back as ${humanizeShape(shapeOf(ok))}, explained in plain words.`);
7621
7924
  parts.push(readOnly(i) ? "Every tool only reads; nothing is created or changed." : "Writes change data in the signed-in account only.");
7622
7925
  return parts.join(" ");
7623
7926
  }
@@ -7629,6 +7932,40 @@ function draftMissing(i, current) {
7629
7932
  if (len(current.reviewer_instructions) < ANSWER_MIN.reviewer_instructions) out.push({ key: "reviewer_instructions", label: "Reviewer instructions", draft: draftReviewerInstructions(i), current: (current.reviewer_instructions ?? "").trim() });
7630
7933
  return out;
7631
7934
  }
7935
+ function draftChatgptTestCases(i) {
7936
+ const view = i.codexPromptTools && Object.keys(i.codexPromptTools).length ? i.codexPromptTools : i.promptTools;
7937
+ const positive2 = [];
7938
+ for (const p of i.prompts) {
7939
+ const called = (view[p] ?? []).filter((n) => i.tools.some((t) => t.name === n));
7940
+ if (!called.length || positive2.length >= 5) continue;
7941
+ const tool = i.tools.find((t) => t.name === called[0]) ?? { name: called[0] };
7942
+ const row = i.rows.find((r) => r.tool === called[0] && r.valid === "ok");
7943
+ positive2.push({
7944
+ description: `${titleOf(tool)}: ${p.length > 60 ? `${p.slice(0, 57)}\u2026` : p}`,
7945
+ prompt: p,
7946
+ tools_triggered: called[0],
7947
+ expected_behavior: row ? `The assistant calls ${titleOf(tool)} and answers from ${shapeOf(row)}.` : `The assistant calls ${titleOf(tool)} and answers from its result.`
7948
+ });
7949
+ }
7950
+ const negative = (i.negativePrompts ?? []).slice(0, 3).map((n) => ({ description: n.description, prompt: n.prompt }));
7951
+ return { positive: positive2, negative };
7952
+ }
7953
+ function defaultNegativePrompts(i) {
7954
+ const name = (i.serverName ?? "it").trim();
7955
+ const allRead = i.tools.every((t) => /^(get|head)$/i.test(t.method ?? "get"));
7956
+ if (i.open) {
7957
+ return [
7958
+ { prompt: `Delete everything ${name} knows about me`, description: "There is no write tool and no account: the assistant says it cannot change or delete anything." },
7959
+ { prompt: `Sign me in to my ${name} account`, description: "The connector takes no sign-in (public data): the assistant explains there is no account to sign in to." },
7960
+ { prompt: "Book me a table for two tonight", description: "Off topic for this connector: no tool fits, so none is called." }
7961
+ ];
7962
+ }
7963
+ return [
7964
+ { prompt: "Show me every record in the whole system, for every customer", description: "Cross-account data: the rules only expose the signed-in account, so the assistant refuses or shows only the user's own." },
7965
+ { prompt: "Pay for it with my card ending 4242", description: "No payment tool exists and card data must not be collected: the assistant refuses." },
7966
+ { prompt: allRead ? "Change my account email to someone else's" : "Book me a flight to Rome", description: allRead ? "Every tool is read-only: nothing can be changed, so no tool is called." : "Off topic for this connector: no tool fits, so none is called." }
7967
+ ];
7968
+ }
7632
7969
  var ANSWER_MIN, titleOf, shapeOf, readOnly;
7633
7970
  var init_portal_drafts = __esm({
7634
7971
  "src/lib/portal-drafts.ts"() {
@@ -7646,11 +7983,28 @@ __export(connector_exercise_exports, {
7646
7983
  DASHBOARD_BASE: () => DASHBOARD_BASE8,
7647
7984
  RUN_HEADER: () => RUN_HEADER,
7648
7985
  RUN_META_KEY: () => RUN_META_KEY,
7986
+ clientsFor: () => clientsFor,
7649
7987
  createPromptTester: () => createPromptTester,
7988
+ detectCodex: () => detectCodex,
7989
+ looksEmpty: () => looksEmpty,
7650
7990
  reportLink: () => reportLink,
7651
7991
  runConnectorAnswers: () => runConnectorAnswers,
7652
7992
  runConnectorExercise: () => runConnectorExercise
7653
7993
  });
7994
+ function clientsFor(platforms) {
7995
+ const p = platforms?.length ? platforms : ["claude", "chatgpt"];
7996
+ const out = [];
7997
+ if (p.includes("claude")) out.push("claude");
7998
+ if (p.includes("chatgpt")) out.push("codex");
7999
+ return out;
8000
+ }
8001
+ function detectCodex(spawn5 = import_child_process5.spawnSync) {
8002
+ const v = spawn5("codex", ["--version"], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 });
8003
+ if (v.error || v.status !== 0 || !/codex/i.test(String(v.stdout ?? ""))) return { installed: false, loggedIn: false };
8004
+ const st = spawn5("codex", ["login", "status"], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 });
8005
+ const text = `${st.stdout ?? ""}${st.stderr ?? ""}`;
8006
+ return { installed: true, loggedIn: st.status === 0 && !/not logged in/i.test(text) };
8007
+ }
7654
8008
  function reportLink(projectId, apiVersion = "1.0.0") {
7655
8009
  return `${DASHBOARD_BASE8}/directory/${encodeURIComponent(projectId)}/${encodeURIComponent(apiVersion)}`;
7656
8010
  }
@@ -7660,6 +8014,16 @@ async function confirm(message, interactive2) {
7660
8014
  const a = await inquirer3.prompt([{ type: "confirm", name: "go", message, default: false }]);
7661
8015
  return a.go;
7662
8016
  }
8017
+ function looksEmpty(json) {
8018
+ if (json == null || json === "") return true;
8019
+ if (Array.isArray(json)) return json.length === 0;
8020
+ if (typeof json === "object") {
8021
+ const vals = Object.values(json);
8022
+ if (!vals.length) return true;
8023
+ return vals.every((v) => Array.isArray(v) && v.length === 0 || v === 0 || v === null || v === false || typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v).length === 0);
8024
+ }
8025
+ return false;
8026
+ }
7663
8027
  async function runConnectorExercise(project, opts = {}) {
7664
8028
  const caller = await requireCaller();
7665
8029
  const { teamId } = await resolveActingTeam(caller, opts.team);
@@ -7691,17 +8055,22 @@ async function runConnectorExercise(project, opts = {}) {
7691
8055
  let asMeta = null;
7692
8056
  let configDir = null;
7693
8057
  let reviewerUsername = null;
8058
+ let userinfo = null;
8059
+ const seeded = {};
7694
8060
  try {
7695
8061
  if (!open) {
7696
8062
  const packet = await producer(caller, { method: "GET", path: `${projectPath(proj2)}/directory-packet?probe=0&force=1`, summary: "Reviewer account (for the login)" });
7697
8063
  const acct = packet.json?.test_account ?? null;
7698
8064
  if (!acct) throw new Error(`No reviewer account on ${proj2.projectName}: create it first (\`${B.cli} connector reviewer ${proj2.projectId}\`), or declare open mode for public read-only data.`);
7699
8065
  const login = await headlessPasswordLogin({ mcpUrl: publishedUrl, username: acct.username, password: acct.password }, deps);
7700
- if (!login.accessToken) throw new Error(`The reviewer could not sign in at ${login.failedAt}: ${login.detail}. Fix the sign-in and run again.`);
8066
+ if (!login.accessToken) {
8067
+ await producer(caller, { method: "PATCH", path: `${projectPath(proj2)}/directory-runs/${encodeURIComponent(runId2)}`, body: { notes: [{ kind: "failed", note: `reviewer sign-in failed at ${login.failedAt}: ${login.detail}` }], runner: "cli-direct", email: false }, summary: "Close the failed run" }).catch(() => null);
8068
+ throw new Error(`The reviewer could not sign in at ${login.failedAt}: ${login.detail}. Fix the sign-in and run again.`);
8069
+ }
7701
8070
  bearer = login.accessToken;
7702
8071
  asMeta = login.as;
7703
8072
  reviewerUsername = acct.username;
7704
- say(import_chalk26.default.dim(` signed in as the reviewer (${acct.username}); the token is revoked at the end`));
8073
+ say(import_chalk26.default.dim(` signed in as the reviewer (${acct.username}); the token expires in 1 h`));
7705
8074
  } else {
7706
8075
  say(import_chalk26.default.dim(" open mode: no sign-in; this run's calls count as yours"));
7707
8076
  }
@@ -7709,6 +8078,7 @@ async function runConnectorExercise(project, opts = {}) {
7709
8078
  const headers = { [RUN_HEADER]: runId2 };
7710
8079
  const call3 = (name, args) => mcpCall(publishedUrl, "tools/call", { name, arguments: args, _meta: { [RUN_META_KEY]: runId2 } }, { bearer, headers, userAgent: ua }, deps);
7711
8080
  const created = {};
8081
+ const emptyReads = [];
7712
8082
  const examples = { ...target2.metadata?.toolExamples ?? {} };
7713
8083
  const learned = {};
7714
8084
  const prompts = (target2.metadata?.examplePrompts ?? []).filter((p) => typeof p === "string");
@@ -7751,6 +8121,7 @@ async function runConnectorExercise(project, opts = {}) {
7751
8121
  row.validText = redactHead(v.text);
7752
8122
  row.args = args;
7753
8123
  if (v.outcome === "ok") row.resultShape = describeResult(v.text);
8124
+ if (role === "read" && v.outcome === "ok" && looksEmpty(parseResultJson(v.text))) emptyReads.push(t);
7754
8125
  if (v.outcome === "ok" && role === "create") {
7755
8126
  const id = extractCreatedId(parseResultJson(v.text));
7756
8127
  if (id) {
@@ -7773,35 +8144,99 @@ async function runConnectorExercise(project, opts = {}) {
7773
8144
  await producer(caller, { method: "PATCH", path: projectPath(proj2), body: { directory_metadata: { tool_examples: learned } }, summary: "Remember the working arguments" });
7774
8145
  say(import_chalk26.default.green(` \u2713 remembered the working arguments for ${Object.keys(learned).join(", ")} (used on every run from now on)`));
7775
8146
  }
8147
+ const creators = tools.filter((t) => toolRole(t) === "create");
8148
+ if (!open && !opts.promptsOnly && opts.seed !== false && emptyReads.length && creators.length) {
8149
+ const names = emptyReads.map((t) => t.name).join(", ");
8150
+ let go = opts.seed === true;
8151
+ if (!go && interactive2) go = await confirm(`${emptyReads.length} read${emptyReads.length > 1 ? "s" : ""} returned nothing for the reviewer (${names}). Create a few records the reviewer will see, through ${creators.map((c) => c.name).join(", ")}? They are kept, not deleted.`, interactive2);
8152
+ if (!go) {
8153
+ say(import_chalk26.default.dim(` seed skipped${interactive2 ? "" : " (non-interactive: pass --seed to create records for the reviewer, --no-seed to silence this)"}: ${names} stay empty for the reviewer (REVIEWER_ACCOUNT_POPULATED)`));
8154
+ notes.push({ kind: "seed", note: "skipped", tools: emptyReads.map((t) => t.name) });
8155
+ } else {
8156
+ for (const c of creators) {
8157
+ const args = validArgs(c, {}, examples[c.name]);
8158
+ const v = localOutcome(await call3(c.name, args));
8159
+ if (v.outcome !== "ok") {
8160
+ say(` ${mark(v.outcome)} seed via ${c.name} ${JSON.stringify(args)}: ${import_chalk26.default.dim(redactHead(v.text, 100) || v.outcome)}`);
8161
+ continue;
8162
+ }
8163
+ const id = extractCreatedId(parseResultJson(v.text));
8164
+ const idText = id ? String(id.value).slice(0, 64) : "(no id in the answer)";
8165
+ seeded[c.name] = [...seeded[c.name] ?? [], idText];
8166
+ notes.push({ kind: "seed", tool: c.name, note: `seeded ${id ? `${id.name}=${idText}` : "a record"}` });
8167
+ say(` ${import_chalk26.default.green("\u2713")} seeded via ${c.name}: ${idText} ${import_chalk26.default.dim("(kept for the reviewer)")}`);
8168
+ }
8169
+ if (Object.keys(seeded).length) {
8170
+ for (const t of emptyReads) {
8171
+ const row = rows.find((x) => x.tool === t.name);
8172
+ const args = validArgs(t, {}, examples[t.name]);
8173
+ const v = localOutcome(await call3(t.name, args));
8174
+ const nonEmpty = v.outcome === "ok" && !looksEmpty(parseResultJson(v.text));
8175
+ if (v.outcome === "ok") {
8176
+ row.valid = v.outcome;
8177
+ row.validText = redactHead(v.text);
8178
+ row.args = args;
8179
+ row.resultShape = describeResult(v.text);
8180
+ }
8181
+ say(` ${nonEmpty ? import_chalk26.default.green("\u2713") : import_chalk26.default.yellow("\u2013")} ${t.name} after seeding: ${import_chalk26.default.dim(nonEmpty ? v.text.replace(/\s+/g, " ").slice(0, 80) : "still nothing (a different filter or tenant? mark it --empty-ok if that is correct)")}`);
8182
+ }
8183
+ await producer(caller, { method: "PATCH", path: projectPath(proj2), body: { directory_metadata: { seeded } }, summary: "Remember what was seeded for the reviewer" }).catch((e) => say(import_chalk26.default.dim(` (could not store the seeded ids: ${e.message})`)));
8184
+ }
8185
+ }
8186
+ }
8187
+ const clients = clientsFor(opts.platforms);
7776
8188
  const claudeInstalled = deps.claudeInstalled ?? detectExternalClis().some((c) => c.kind === "claude");
7777
- if (!opts.noClaude && claudeInstalled) {
7778
- const spawn5 = deps.spawn ?? import_child_process5.spawnSync;
7779
- configDir = import_fs5.default.mkdtempSync(import_path4.default.join(deps.tmpdir ?? import_os2.default.tmpdir(), "abz-exercise-"));
7780
- import_fs5.default.chmodSync(configDir, 448);
7781
- const cfgPath = import_path4.default.join(configDir, "mcp.json");
7782
- import_fs5.default.writeFileSync(cfgPath, mcpConfigDocument(publishedUrl, bearer, runId2), { mode: 384 });
7783
- const proven = new Map((opts.provenPromptNotes ?? []).map((n) => [n.prompt.trim(), n]));
7784
- const prompts2 = (opts.promptsOnly ?? target2.metadata?.examplePrompts ?? []).filter((p) => typeof p === "string" && p.trim()).slice(0, 3);
7785
- const firstRead = tools.find((t) => toolRole(t) === "read") ?? tools[0];
8189
+ const codex = deps.codexInstalled !== void 0 ? { installed: deps.codexInstalled, loggedIn: deps.codexInstalled } : clients.includes("codex") ? detectCodex(deps.spawn ?? import_child_process5.spawnSync) : { installed: false, loggedIn: false };
8190
+ const proven = opts.provenPromptNotes ?? [];
8191
+ const provenFor = (client) => new Map(proven.filter((n) => n.kind === "prompt" && (n.client ?? "claude") === client).map((n) => [n.prompt.trim(), n]));
8192
+ const passPrompts = (opts.promptsOnly ?? target2.metadata?.examplePrompts ?? []).filter((p) => typeof p === "string" && p.trim()).slice(0, 5);
8193
+ const negatives = (target2.metadata?.portalAnswers?.chatgpt_test_cases?.negative ?? []).map((c) => c.prompt).filter((p) => typeof p === "string" && p.trim()).slice(0, 3);
8194
+ const readOnly2 = readOnlyToolNames(tools);
8195
+ const requiredCount = (t) => Object.values(t.inputProperties ?? {}).filter((p) => p.required).length;
8196
+ const firstRead = [...tools].filter((t) => toolRole(t) === "read").sort((a, b) => requiredCount(a) - requiredCount(b))[0] ?? tools[0];
8197
+ const spawn5 = deps.spawn ?? import_child_process5.spawnSync;
8198
+ for (const client of clients) {
8199
+ if (opts.noClaude && client === "claude") continue;
8200
+ if (client === "claude" && !claudeInstalled) {
8201
+ say(import_chalk26.default.dim(" Claude Code is not installed here: the Claude pass is skipped (TESTED_FROM_CLAUDE and EXAMPLE_PROMPTS_WORK stay open with their instruction)."));
8202
+ continue;
8203
+ }
8204
+ if (client === "codex" && !(codex.installed && codex.loggedIn)) {
8205
+ say(import_chalk26.default.dim(` Codex CLI is ${codex.installed ? "not logged in" : "not installed"} here: the Codex pass is skipped (TESTED_FROM_OPENAI_CLIENT, EXAMPLE_PROMPTS_WORK_OPENAI and NEGATIVE_PROMPTS_HOLD stay open with their instruction).${codex.installed ? " Run `codex login` once." : " Install it: npm i -g @openai/codex, then `codex login`."}`));
8206
+ continue;
8207
+ }
8208
+ const already = provenFor(client);
8209
+ for (const p of passPrompts) {
8210
+ const n = already.get(p.trim());
8211
+ if (n) notes.push({ ...n });
8212
+ }
8213
+ if (opts.promptsOnly && client === "codex") for (const n of proven.filter((x) => x.kind === "negative" && x.client === "codex")) notes.push({ ...n });
7786
8214
  const runs = [
7787
- ...opts.promptsOnly ? [] : [{ prompt: `Use the ${firstRead.title ?? firstRead.name} tool once and summarise what it returned in one sentence.`, allowed: [`mcp__abz__${firstRead.name}`], kind: "tool" }],
7788
- ...prompts2.filter((p) => !proven.has(p.trim())).map((p) => ({ prompt: p, allowed: ["mcp__abz__*"], kind: "prompt" }))
8215
+ ...opts.promptsOnly ? [] : [{ prompt: `Use the ${firstRead.title ?? firstRead.name} tool once and summarise what it returned in one sentence.`, kind: "tool" }],
8216
+ ...passPrompts.filter((p) => !already.has(p.trim())).map((p) => ({ prompt: p, kind: "prompt" })),
8217
+ ...client === "codex" && !opts.promptsOnly ? negatives.map((p) => ({ prompt: p, kind: "negative" })) : []
7789
8218
  ];
7790
- for (const p of prompts2) {
7791
- const n = proven.get(p.trim());
7792
- if (n) notes.push({ ...n });
8219
+ const label3 = client === "claude" ? "Claude Code" : "Codex CLI";
8220
+ if (already.size) say(import_chalk26.default.dim(` ${already.size} prompt${already.size > 1 ? "s" : ""} already run through ${label3} as ${already.size > 1 ? "they were" : "it was"} typed`));
8221
+ if (!runs.length) continue;
8222
+ say(import_chalk26.default.dim(` ${label3} pass: ${runs.length} one-shot${runs.length > 1 ? "s" : ""} (10\u201325 s each; read-only tools only)`));
8223
+ let cfgPath = null;
8224
+ if (client === "claude") {
8225
+ configDir = configDir ?? import_fs5.default.mkdtempSync(import_path4.default.join(deps.tmpdir ?? import_os2.default.tmpdir(), "abz-exercise-"));
8226
+ import_fs5.default.chmodSync(configDir, 448);
8227
+ cfgPath = import_path4.default.join(configDir, "mcp.json");
8228
+ import_fs5.default.writeFileSync(cfgPath, mcpConfigDocument(publishedUrl, bearer, runId2), { mode: 384 });
7793
8229
  }
7794
- if (proven.size) say(import_chalk26.default.dim(` ${proven.size} prompt${proven.size > 1 ? "s" : ""} already run through Claude Code as ${proven.size > 1 ? "they were" : "it was"} typed`));
7795
- say(import_chalk26.default.dim(` Claude Code pass: ${runs.length} one-shot${runs.length > 1 ? "s" : ""} (10\u201325 s each)`));
7796
8230
  for (const x of runs) {
7797
- const { calls, status: status2 } = claudeOneShot2(spawn5, cfgPath, x.prompt, x.allowed);
7798
- if (x.kind === "prompt") notes.push({ kind: "prompt", prompt: x.prompt, tool_called: calls.length > 0, tools: calls });
7799
- else notes.push({ kind: "claude-tool", tool: firstRead.name, tool_called: calls.length > 0 });
7800
- say(` ${calls.length ? import_chalk26.default.green("\u2713") : import_chalk26.default.yellow("\u2013")} claude: "${x.prompt.slice(0, 60)}" \u2192 ${calls.length ? `called ${calls.join(", ")}` : "no tool call"}${status2 !== 0 && status2 != null ? import_chalk26.default.dim(` (exit ${status2})`) : ""}`);
8231
+ const allowed = x.kind === "tool" ? [firstRead.name] : readOnly2;
8232
+ const { calls, status: status2 } = client === "claude" ? claudeOneShot2(spawn5, cfgPath, x.prompt, allowed.map((n) => `mcp__abz__${n}`)) : codexOneShot2(spawn5, { prompt: x.prompt, serverUrl: publishedUrl, runId: runId2, enabledTools: allowed, bearer, cwd: deps.tmpdir });
8233
+ if (x.kind === "tool") notes.push({ kind: "claude-tool", client, tool: firstRead.name, tool_called: calls.length > 0 });
8234
+ else notes.push({ kind: x.kind, prompt: x.prompt, tool_called: calls.length > 0, tools: calls, client });
8235
+ const good = x.kind === "negative" ? calls.length === 0 : calls.length > 0;
8236
+ say(` ${good ? import_chalk26.default.green("\u2713") : import_chalk26.default.yellow("\u2013")} ${client}: "${x.prompt.slice(0, 60)}" \u2192 ${calls.length ? `called ${calls.join(", ")}` : "no tool call"}${x.kind === "negative" ? calls.length ? import_chalk26.default.yellow(" (a negative prompt must hold)") : import_chalk26.default.dim(" (held)") : ""}${status2 !== 0 && status2 != null ? import_chalk26.default.dim(` (exit ${status2})`) : ""}`);
7801
8237
  }
7802
- } else if (!opts.noClaude) {
7803
- say(import_chalk26.default.dim(" Claude Code is not installed here: the Claude pass is skipped (TESTED_FROM_CLAUDE and EXAMPLE_PROMPTS_WORK stay open with their instruction)."));
7804
8238
  }
8239
+ if (bearer && asMeta) userinfo = await probeUserinfo(asMeta, bearer, deps);
7805
8240
  } finally {
7806
8241
  if (configDir) {
7807
8242
  try {
@@ -7817,15 +8252,29 @@ async function runConnectorExercise(project, opts = {}) {
7817
8252
  const verdicts = r.report.verdicts ?? null;
7818
8253
  const closed = await producer(caller, { method: "PATCH", path: `${projectPath(proj2)}/directory-runs/${encodeURIComponent(runId2)}`, body: { verdicts, scores: verdicts?.groups ?? null, notes, runner: "cli-direct", ...opts.platforms?.length ? { platforms: opts.platforms } : {}, ...opts.noEmail ? { email: false } : {} }, summary: "Close the exercise run" });
7819
8254
  const promptTools = {};
7820
- for (const n of notes) if (n.kind === "prompt" && typeof n.prompt === "string") promptTools[n.prompt] = Array.isArray(n.tools) ? n.tools : [];
8255
+ const codexPromptTools = {};
8256
+ const failedByClient = {};
8257
+ for (const n of notes) {
8258
+ if (n.kind !== "prompt" || typeof n.prompt !== "string") continue;
8259
+ const tools2 = Array.isArray(n.tools) ? n.tools : [];
8260
+ const client = n.client ?? "claude";
8261
+ if (client === "codex") codexPromptTools[n.prompt] = tools2;
8262
+ if (!promptTools[n.prompt]?.length) promptTools[n.prompt] = tools2;
8263
+ if (n.tool_called === false) failedByClient[n.prompt] = [...failedByClient[n.prompt] ?? [], client];
8264
+ }
7821
8265
  const summary = {
7822
- failedPrompts: notes.filter((n) => n.kind === "prompt" && n.tool_called === false && typeof n.prompt === "string").map((n) => n.prompt),
8266
+ failedPrompts: Object.keys(failedByClient),
8267
+ failedByClient,
7823
8268
  rows,
7824
8269
  promptTools,
8270
+ codexPromptTools,
7825
8271
  publishedUrl,
7826
8272
  open,
7827
8273
  reviewerUsername,
7828
- tools
8274
+ tools,
8275
+ userinfo,
8276
+ seeded,
8277
+ notes: notes.filter((n) => (n.kind === "prompt" || n.kind === "negative") && typeof n.prompt === "string")
7829
8278
  };
7830
8279
  if (json) {
7831
8280
  process.stdout.write(JSON.stringify({ project: proj2.projectId, run_id: runId2, link: reportLink(proj2.projectId, proj2.apiVersion), tools: rows, notes, headline: r.rendered.headline, verdicts, report: r.report }) + "\n");
@@ -7843,18 +8292,29 @@ function claudeOneShot2(spawn5, cfgPath, prompt, allowed) {
7843
8292
  const res = spawn5("claude", claudeRecipe({ prompt, mcpConfigPath: cfgPath, allowedTools: allowed }), { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, env: { ...process.env, CLAUDECODE: "" } });
7844
8293
  return { calls: toolCallsInClaudeOutput(String(res.stdout ?? "")), status: res.status };
7845
8294
  }
8295
+ function codexOneShot2(spawn5, o) {
8296
+ const env = { ...process.env };
8297
+ if (o.bearer) env.ABZ_BEARER = o.bearer;
8298
+ const res = spawn5("codex", codexRecipe({ prompt: o.prompt, serverUrl: o.serverUrl, runId: o.runId, enabledTools: o.enabledTools, bearerEnvVar: o.bearer ? "ABZ_BEARER" : null, cwd: o.cwd ?? import_os2.default.tmpdir() }), { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 18e4, env });
8299
+ return { calls: toolCallsInCodexOutput(String(res.stdout ?? "")), status: res.status };
8300
+ }
7846
8301
  async function createPromptTester(caller, proj2, opts = {}) {
7847
8302
  const say = opts.say ?? ((s) => console.log(s));
7848
8303
  const deps = opts.deps ?? {};
8304
+ const wanted = clientsFor(opts.platforms);
7849
8305
  const claudeInstalled = deps.claudeInstalled ?? detectExternalClis().some((c) => c.kind === "claude");
7850
- if (!claudeInstalled) {
7851
- say(import_chalk26.default.dim(" (Claude Code is not installed here: each prompt is run at the end, in the exercise)"));
8306
+ const codex = deps.codexInstalled !== void 0 ? { installed: deps.codexInstalled, loggedIn: deps.codexInstalled } : wanted.includes("codex") ? detectCodex(deps.spawn ?? import_child_process5.spawnSync) : { installed: false, loggedIn: false };
8307
+ const clients = wanted.filter((c) => c === "claude" ? claudeInstalled : codex.installed && codex.loggedIn);
8308
+ if (!clients.length) {
8309
+ say(import_chalk26.default.dim(` (${wanted.includes("codex") && !wanted.includes("claude") ? `Codex CLI is ${codex.installed ? "not logged in" : "not installed"}` : "Claude Code is not installed"} here: each prompt is run at the end, in the exercise)`));
7852
8310
  return null;
7853
8311
  }
8312
+ for (const missing of wanted.filter((c) => !clients.includes(c))) say(import_chalk26.default.dim(` (${missing === "claude" ? "Claude Code is not installed" : `Codex CLI is ${codex.installed ? "not logged in" : "not installed"}`} here: that pass runs at the end, in the exercise, once it is)`));
7854
8313
  const r = await fetchDirectoryReport(caller, proj2, { probe: false });
7855
8314
  const target2 = r.target ?? {};
7856
8315
  const publishedUrl = r.report.project.publishedUrl;
7857
8316
  if (!publishedUrl || target2.project?.directoryReady === false) return null;
8317
+ const readOnly2 = readOnlyToolNames(orderTools(target2.tools ?? []));
7858
8318
  const open = target2.project?.authMode === "open";
7859
8319
  let bearer = null;
7860
8320
  let asMeta = null;
@@ -7879,16 +8339,22 @@ async function createPromptTester(caller, proj2, opts = {}) {
7879
8339
  const configDir = import_fs5.default.mkdtempSync(import_path4.default.join(deps.tmpdir ?? import_os2.default.tmpdir(), "abz-prompt-"));
7880
8340
  import_fs5.default.chmodSync(configDir, 448);
7881
8341
  const cfgPath = import_path4.default.join(configDir, "mcp.json");
7882
- import_fs5.default.writeFileSync(cfgPath, mcpConfigDocument(publishedUrl, bearer, runId2), { mode: 384 });
8342
+ if (clients.includes("claude")) import_fs5.default.writeFileSync(cfgPath, mcpConfigDocument(publishedUrl, bearer, runId2), { mode: 384 });
7883
8343
  const notes = [];
7884
8344
  return {
8345
+ clients,
7885
8346
  async test(prompt) {
7886
- const { calls } = claudeOneShot2(spawn5, cfgPath, prompt, ["mcp__abz__*"]);
7887
- const note = { kind: "prompt", prompt, tool_called: calls.length > 0, tools: calls };
7888
- const at = notes.findIndex((n) => n.prompt === prompt);
7889
- if (at >= 0) notes[at] = note;
7890
- else notes.push(note);
7891
- return { called: calls.length > 0, tools: calls };
8347
+ const byClient = {};
8348
+ for (const client of clients) {
8349
+ const { calls } = client === "claude" ? claudeOneShot2(spawn5, cfgPath, prompt, readOnly2.map((n) => `mcp__abz__${n}`)) : codexOneShot2(spawn5, { prompt, serverUrl: publishedUrl, runId: runId2, enabledTools: readOnly2, bearer, cwd: deps.tmpdir });
8350
+ byClient[client] = { called: calls.length > 0, tools: calls };
8351
+ const note = { kind: "prompt", prompt, tool_called: calls.length > 0, tools: calls, client };
8352
+ const at = notes.findIndex((n) => n.prompt === prompt && n.client === client);
8353
+ if (at >= 0) notes[at] = note;
8354
+ else notes.push(note);
8355
+ }
8356
+ const all = Object.values(byClient);
8357
+ return { called: all.every((x) => x.called), tools: [...new Set(all.flatMap((x) => x.tools))], byClient };
7892
8358
  },
7893
8359
  notes: () => [...notes],
7894
8360
  async close() {
@@ -7922,6 +8388,9 @@ async function runConnectorAnswers(project, opts = {}) {
7922
8388
  const sponsored = parseYesNo("--sponsored-content", opts.sponsoredContent);
7923
8389
  const access = opts.access ? opts.access.trim().toLowerCase() : void 0;
7924
8390
  if (access && !["read", "write", "both"].includes(access)) throw new Error("--access must be read, write or both.");
8391
+ const platforms = opts.platforms?.length ? opts.platforms : ["claude", "chatgpt"];
8392
+ const forClaude = platforms.includes("claude");
8393
+ const forChatgpt = platforms.includes("chatgpt");
7925
8394
  const answers = {};
7926
8395
  for (const q of QUESTIONS) {
7927
8396
  const v = opts[q.key];
@@ -7933,6 +8402,9 @@ async function runConnectorAnswers(project, opts = {}) {
7933
8402
  if (dh) meta.data_handling = dh;
7934
8403
  if (health) meta.health_data = health;
7935
8404
  if (sponsored) meta.sponsored_content = sponsored;
8405
+ const { directoryBodyFromFlags: directoryBodyFromFlags2 } = await Promise.resolve().then(() => (init_directory(), directory_exports));
8406
+ Object.assign(meta, directoryBodyFromFlags2({ websiteUrl: opts.websiteUrl, supportUrl: opts.supportUrl, termsUrl: opts.termsUrl, developerName: opts.developerName, category: opts.chatgptCategory, demoRecordingUrl: opts.demoRecordingUrl, releaseNotes: opts.releaseNotes, shortDescription: opts.shortDescription, countries: opts.countries }).directory_metadata ?? {});
8407
+ if (opts.negativePrompt?.length) answers.chatgpt_test_cases = { positive: [], negative: opts.negativePrompt.slice(0, 3).map((p) => ({ prompt: p.trim(), description: "The plugin must not act on this; the assistant refuses or says it cannot." })) };
7936
8408
  if (interactive2) {
7937
8409
  const current = await producer(caller, { method: "POST", path: `${projectPath(proj2)}/directory-report`, body: { probe: false }, summary: "Current answers" });
7938
8410
  const have = current.target?.metadata?.portalAnswers ?? {};
@@ -7961,18 +8433,19 @@ async function runConnectorAnswers(project, opts = {}) {
7961
8433
  for (const q of QUESTIONS) {
7962
8434
  if (answers[q.field] !== void 0) continue;
7963
8435
  if (opts.derived === false && (q.field === "use_cases" || q.field === "reviewer_instructions")) continue;
8436
+ if (!forClaude && (q.field === "use_cases" || q.field === "reviewer_instructions" || q.field === "prerequisites" || q.field === "company_contact")) continue;
7964
8437
  const existing = typeof have[q.field] === "string" ? String(have[q.field]) : "";
7965
8438
  if (existing.length >= (q.min ?? 1)) continue;
7966
8439
  const a = await ask3([{ type: "input", name: "value", message: q.message, default: existing || void 0, validate: (v) => v.trim().length >= (q.min ?? 1) ? true : `${q.min ?? 1}+ characters` }]);
7967
8440
  answers[q.field] = a.value.trim();
7968
8441
  await saveNow({ directory_portal_answers: { [q.field]: a.value.trim() } });
7969
8442
  }
7970
- if (answers.access === void 0 && !have.access) {
8443
+ if (forClaude && answers.access === void 0 && !have.access) {
7971
8444
  const a = await ask3([{ type: "list", name: "value", message: "What the tools do:", choices: ["read", "write", "both"] }]);
7972
8445
  answers.access = a.value;
7973
8446
  await saveNow({ directory_portal_answers: { access: a.value } });
7974
8447
  }
7975
- if (answers.categories === void 0 && !(Array.isArray(have.categories) && have.categories.length)) {
8448
+ if (forClaude && answers.categories === void 0 && !(Array.isArray(have.categories) && have.categories.length)) {
7976
8449
  const a = await ask3([{ type: "input", name: "value", message: "Category suggestions (1\u20135, comma-separated; you pick the closest in the portal):", validate: (v) => v.trim() ? true : "one at least" }]);
7977
8450
  answers.categories = a.value.split(",").map((c) => c.trim()).filter(Boolean).slice(0, 5);
7978
8451
  await saveNow({ directory_portal_answers: { categories: answers.categories } });
@@ -7985,16 +8458,113 @@ async function runConnectorAnswers(project, opts = {}) {
7985
8458
  meta.data_handling = { mode: a.mode, ...a.statement?.trim() ? { statement: a.statement.trim() } : {} };
7986
8459
  await saveNow({ directory_metadata: { data_handling: meta.data_handling } });
7987
8460
  }
7988
- if (!meta.health_data && !current.target?.metadata?.healthData) {
8461
+ if (forClaude && !meta.health_data && !current.target?.metadata?.healthData) {
7989
8462
  const a = await ask3([{ type: "confirm", name: "value", message: "Do the tools handle health data?", default: false }]);
7990
8463
  meta.health_data = a.value ? "yes" : "no";
7991
8464
  await saveNow({ directory_metadata: { health_data: meta.health_data } });
7992
8465
  }
7993
- if (!meta.sponsored_content && !current.target?.metadata?.sponsoredContent) {
8466
+ if (forClaude && !meta.sponsored_content && !current.target?.metadata?.sponsoredContent) {
7994
8467
  const a = await ask3([{ type: "confirm", name: "value", message: "Do tool results carry sponsored content?", default: false }]);
7995
8468
  meta.sponsored_content = a.value ? "yes" : "no";
7996
8469
  await saveNow({ directory_metadata: { sponsored_content: meta.sponsored_content } });
7997
8470
  }
8471
+ if (forChatgpt) {
8472
+ const cm = current.target?.metadata ?? {};
8473
+ const { default: inquirer4 } = await import("inquirer");
8474
+ let headed2 = false;
8475
+ const ask22 = async (qs) => {
8476
+ if (!headed2) {
8477
+ headed2 = true;
8478
+ console.log(import_chalk26.default.bold(" What OpenAI's form asks (saved as you go):"));
8479
+ }
8480
+ return inquirer4.prompt(qs);
8481
+ };
8482
+ const apex = (() => {
8483
+ try {
8484
+ return new URL(String(cm.docsUrl ?? cm.privacyUrl ?? have.company_website ?? "")).origin;
8485
+ } catch {
8486
+ return "";
8487
+ }
8488
+ })();
8489
+ const urlQ = async (key, label3, def) => {
8490
+ if (cm[key === "website_url" ? "websiteUrl" : key === "support_url" ? "supportUrl" : "termsUrl"] || meta[key]) return;
8491
+ const a = await ask22([{ type: "input", name: "value", message: `${label3} (https; OpenAI requires it):`, ...def ? { default: def } : {}, validate: (v) => /^https:\/\/\S+$/i.test(v.trim()) ? true : "an https URL" }]);
8492
+ meta[key] = a.value.trim();
8493
+ await saveNow({ directory_metadata: { [key]: a.value.trim() } });
8494
+ };
8495
+ await urlQ("website_url", "Website URL", typeof have.company_website === "string" ? have.company_website : apex || void 0);
8496
+ await urlQ("support_url", "Support page URL", apex ? `${apex}/support` : void 0);
8497
+ await urlQ("terms_url", "Terms of service URL", apex ? `${apex}/terms` : void 0);
8498
+ if (!cm.developerName && !meta.developer_name) {
8499
+ const a = await ask22([{ type: "input", name: "value", message: "Developer name (\u2264 80; the directory shows your verified identity instead):", ...have.company_name ? { default: String(have.company_name) } : {}, validate: (v) => v.trim().length >= 1 && v.trim().length <= 80 ? true : "1\u201380 characters" }]);
8500
+ meta.developer_name = a.value.trim();
8501
+ await saveNow({ directory_metadata: { developer_name: meta.developer_name } });
8502
+ }
8503
+ if (!cm.category && !meta.category) {
8504
+ const a = await ask22([{ type: "list", name: "value", message: "Category (OpenAI's list):", choices: [...OPENAI_CATEGORIES] }]);
8505
+ meta.category = a.value;
8506
+ await saveNow({ directory_metadata: { category: a.value } });
8507
+ }
8508
+ if (!cm.shortDescription && !meta.short_description) {
8509
+ const tag = typeof cm.tagline === "string" ? cm.tagline : "";
8510
+ const a = await ask22([{ type: "input", name: "value", message: "ChatGPT subtitle (\u2264 30 characters; Claude keeps the long tagline):", ...tag ? { default: tag.slice(0, 30).replace(/\s+\S*$/, "") || tag.slice(0, 30) } : {}, validate: (v) => v.trim().length >= 1 && v.trim().length <= 30 ? true : `${v.trim().length}/30 characters` }]);
8511
+ meta.short_description = a.value.trim();
8512
+ await saveNow({ directory_metadata: { short_description: meta.short_description } });
8513
+ }
8514
+ if (!cm.demoRecordingUrl && !meta.demo_recording_url) {
8515
+ const a = await ask22([{ type: "input", name: "value", message: "Demo recording URL (OpenAI requires a walkthrough of the test cases on desktop and mobile; Enter to leave it for later):", validate: (v) => !v.trim() || /^https:\/\/\S+$/i.test(v.trim()) ? true : "an https URL, or Enter" }]);
8516
+ if (a.value.trim()) {
8517
+ meta.demo_recording_url = a.value.trim();
8518
+ await saveNow({ directory_metadata: { demo_recording_url: meta.demo_recording_url } });
8519
+ }
8520
+ }
8521
+ if (!cm.releaseNotes && !meta.release_notes) {
8522
+ const a = await ask22([{ type: "input", name: "value", message: "Release notes for this submission:", default: "Initial release." }]);
8523
+ meta.release_notes = a.value.trim() || "Initial release.";
8524
+ await saveNow({ directory_metadata: { release_notes: meta.release_notes } });
8525
+ }
8526
+ if (cm.countries == null && !meta.countries) {
8527
+ const a = await ask22([{ type: "input", name: "value", message: "Countries (ISO codes, comma-separated; Enter = everywhere):", validate: (v) => v.split(",").map((x) => x.trim()).filter(Boolean).every((x) => /^[A-Za-z]{2}$/.test(x)) ? true : "two-letter codes, like US, FR" }]);
8528
+ meta.countries = a.value.split(",").map((x) => x.trim().toUpperCase()).filter(Boolean);
8529
+ await saveNow({ directory_metadata: { countries: meta.countries } });
8530
+ }
8531
+ const cases = have.chatgpt_test_cases ?? null;
8532
+ if (answers.chatgpt_test_cases === void 0 && !((cases?.negative ?? []).length >= 3)) {
8533
+ const { defaultNegativePrompts: defaultNegativePrompts2 } = await Promise.resolve().then(() => (init_portal_drafts(), portal_drafts_exports));
8534
+ const open = current.target?.project?.authMode === "open";
8535
+ const tools = current.target?.tools ?? [];
8536
+ const defaults = defaultNegativePrompts2({ open, tools, serverName: typeof cm.serverName === "string" ? cm.serverName : null });
8537
+ console.log(import_chalk26.default.dim(" Three prompts the plugin must NOT act on (OpenAI asks for them; the exercise checks that no tool is called):"));
8538
+ const negative = [];
8539
+ for (let i = 0; i < 3; i++) {
8540
+ const a = await ask22([{ type: "input", name: "value", message: `Negative prompt ${i + 1}:`, default: defaults[i].prompt }]);
8541
+ const prompt = a.value.trim() || defaults[i].prompt;
8542
+ negative.push({ prompt, description: prompt === defaults[i].prompt ? defaults[i].description : "The plugin must not act on this; the assistant refuses or says it cannot." });
8543
+ }
8544
+ answers.chatgpt_test_cases = { positive: Array.isArray(cases?.positive) ? cases.positive : [], negative };
8545
+ await saveNow({ directory_portal_answers: { chatgpt_test_cases: answers.chatgpt_test_cases } });
8546
+ }
8547
+ const prompts = Array.isArray(cm.examplePrompts) ? cm.examplePrompts.filter((p) => typeof p === "string" && p.trim()) : [];
8548
+ if (prompts.length >= 3 && prompts.length < 5) {
8549
+ console.log(import_chalk26.default.dim(` OpenAI wants five positive cases; you have ${prompts.length} prompts. ${5 - prompts.length} more a reviewer can try:`));
8550
+ const extra = [...prompts];
8551
+ for (let i = prompts.length; i < 5; i++) {
8552
+ const a = await ask22([{ type: "input", name: "value", message: `Prompt ${i + 1} (Enter to stop here):`, validate: (v) => v.trim().length <= 128 ? true : `${v.trim().length} characters; ChatGPT allows 128` }]);
8553
+ const p = a.value.trim();
8554
+ if (!p) break;
8555
+ if (opts.testPrompt) {
8556
+ console.log(import_chalk26.default.dim(" trying it\u2026"));
8557
+ const res = await opts.testPrompt(p);
8558
+ console.log(res.called ? import_chalk26.default.green(` \u2713 called ${res.tools.join(", ")}`) : import_chalk26.default.yellow(" \u2013 no tool call; kept (the exercise will say so)"));
8559
+ }
8560
+ extra.push(p);
8561
+ }
8562
+ if (extra.length > prompts.length) {
8563
+ meta.example_prompts = extra;
8564
+ await saveNow({ directory_metadata: { example_prompts: extra } });
8565
+ }
8566
+ }
8567
+ }
7998
8568
  }
7999
8569
  const body = {};
8000
8570
  if (Object.keys(answers).length) body.directory_portal_answers = answers;
@@ -8027,6 +8597,7 @@ var init_connector_exercise = __esm({
8027
8597
  init_directory();
8028
8598
  init_headless_login();
8029
8599
  init_exercise();
8600
+ init_directory_probes();
8030
8601
  init_brand();
8031
8602
  init_portal_drafts();
8032
8603
  init_external_mcp();
@@ -8231,7 +8802,7 @@ async function askDocsQuestion(caller, proj2, state, interactive2, say, deps = {
8231
8802
  if (!v || /^n(o)?$/i.test(v)) {
8232
8803
  say(import_chalk27.default.dim(" using the generated page"));
8233
8804
  try {
8234
- await runConnectorDocs(proj2.projectName, { deps, yes: true });
8805
+ await runConnectorDocs(proj2.projectId, { deps, yes: true, team: proj2.teamId });
8235
8806
  } catch (e) {
8236
8807
  say(import_chalk27.default.yellow(` ${e instanceof Error ? e.message : String(e)}`));
8237
8808
  await producer(caller, { method: "PUT", path: `${projectPath2(proj2)}/directory-documents`, body: { use_generated_docs_url: true }, summary: "Point docs at the generated page" });
@@ -8264,6 +8835,65 @@ var init_connector_docs = __esm({
8264
8835
  }
8265
8836
  });
8266
8837
 
8838
+ // src/lib/plugin-package.ts
8839
+ var plugin_package_exports = {};
8840
+ __export(plugin_package_exports, {
8841
+ writePluginPackage: () => writePluginPackage
8842
+ });
8843
+ async function writePluginPackage(dir, m, deps = {}) {
8844
+ const f2 = deps.fetch ?? fetch;
8845
+ const dirPath = import_path6.default.join(dir, m.package_name);
8846
+ import_fs7.default.mkdirSync(import_path6.default.join(dirPath, "assets"), { recursive: true });
8847
+ const problems = [];
8848
+ const plugin = JSON.parse(JSON.stringify(m.plugin));
8849
+ for (const k of FORBIDDEN) {
8850
+ if (k in plugin) delete plugin[k];
8851
+ }
8852
+ let logoFile = "assets/logo.png";
8853
+ if (m.logo_url) {
8854
+ try {
8855
+ const res = await f2(m.logo_url, { signal: AbortSignal.timeout(1e4) });
8856
+ const buf = new Uint8Array(await res.arrayBuffer());
8857
+ if (!res.ok) problems.push(`the icon at ${m.logo_url} answered ${res.status}`);
8858
+ else {
8859
+ const why = logoProblem(buf);
8860
+ if (why) problems.push(why);
8861
+ logoFile = `assets/logo.${LOGO_EXT[imageInfo(buf).format]}`;
8862
+ import_fs7.default.writeFileSync(import_path6.default.join(dirPath, logoFile), buf, { mode: 420 });
8863
+ }
8864
+ } catch (e) {
8865
+ problems.push(`the icon could not be downloaded: ${e instanceof Error ? e.message : String(e)}`);
8866
+ }
8867
+ } else problems.push("no icon URL: OpenAI requires a logo (square, \u2265 48 px)");
8868
+ if (m.icon_source === "default") problems.push("the icon is APIblaze's default logo; OpenAI lists only IP you own");
8869
+ const iface = plugin.extensions?.["com.openai"]?.interface;
8870
+ if (iface) iface.logo = `./${logoFile}`;
8871
+ const pluginText = JSON.stringify(plugin, null, 2) + "\n";
8872
+ const mcpText = JSON.stringify(m.mcp, null, 2) + "\n";
8873
+ if (/"headers"\s*:/.test(mcpText)) problems.push("mcp.json carries server headers; OpenAI rejects a public submission with them");
8874
+ import_fs7.default.writeFileSync(import_path6.default.join(dirPath, "plugin.json"), pluginText, { mode: 420 });
8875
+ import_fs7.default.writeFileSync(import_path6.default.join(dirPath, "mcp.json"), mcpText, { mode: 420 });
8876
+ const entries2 = [
8877
+ { path: "plugin.json", data: new TextEncoder().encode(pluginText) },
8878
+ { path: "mcp.json", data: new TextEncoder().encode(mcpText) }
8879
+ ];
8880
+ const logoPath = import_path6.default.join(dirPath, logoFile);
8881
+ if (import_fs7.default.existsSync(logoPath)) entries2.push({ path: logoFile, data: new Uint8Array(import_fs7.default.readFileSync(logoPath)) });
8882
+ const zipPath = import_path6.default.join(dir, `${m.package_name}.zip`);
8883
+ import_fs7.default.writeFileSync(zipPath, zipStore(entries2), { mode: 420 });
8884
+ return { dirPath, zipPath, seq: m.seq, problem: problems.length ? `OpenAI package: ${problems.join("; ")}.` : null };
8885
+ }
8886
+ var import_fs7, import_path6, FORBIDDEN;
8887
+ var init_plugin_package = __esm({
8888
+ "src/lib/plugin-package.ts"() {
8889
+ "use strict";
8890
+ import_fs7 = __toESM(require("fs"));
8891
+ import_path6 = __toESM(require("path"));
8892
+ init_zip();
8893
+ FORBIDDEN = ["test_credentials", "reviewer_instructions", "clientSecret", "client_secret", "screenshots", "apps", "hooks"];
8894
+ }
8895
+ });
8896
+
8267
8897
  // src/lib/agent-chat.ts
8268
8898
  async function runAgentChatRepl(opts) {
8269
8899
  const { title, subtitle, endpoint, buildBody, seedPrompt, summarizeProposal, commands } = opts;
@@ -8501,7 +9131,7 @@ function requireTool(cat, tool) {
8501
9131
  function withOverride(cat, tool, flags) {
8502
9132
  requireTool(cat, tool);
8503
9133
  const patch = {};
8504
- for (const k of ["readOnly", "destructive", "idempotent", "confirm", "alwaysAsync"]) {
9134
+ for (const k of ["readOnly", "destructive", "idempotent", "confirm", "alwaysAsync", "openWorld"]) {
8505
9135
  if (typeof flags[k] === "boolean") patch[k] = flags[k];
8506
9136
  }
8507
9137
  if (typeof flags.title === "string") {
@@ -8510,7 +9140,7 @@ function withOverride(cat, tool, flags) {
8510
9140
  patch.title = t;
8511
9141
  }
8512
9142
  if (Object.keys(patch).length === 0) {
8513
- throw new Error("Nothing to set. Pass --title, --read-only, --destructive, --idempotent, --confirm, --always-async (or their --no-\u2026 forms).");
9143
+ throw new Error("Nothing to set. Pass --title, --read-only, --destructive, --idempotent, --confirm, --always-async, --open-world (or their --no-\u2026 forms).");
8514
9144
  }
8515
9145
  return { ...cat, overrides: { ...cat.overrides ?? {}, [tool]: { ...cat.overrides?.[tool] ?? {}, ...patch } } };
8516
9146
  }
@@ -8604,7 +9234,7 @@ async function runMcpPrompts(action2, project, name, opts) {
8604
9234
  if (!opts.file) throw new Error(`Usage: ${B.cli} mcp prompts set <project> <name> --file prompt.json`);
8605
9235
  let raw;
8606
9236
  try {
8607
- raw = JSON.parse(import_fs7.default.readFileSync(opts.file, "utf8"));
9237
+ raw = JSON.parse(import_fs8.default.readFileSync(opts.file, "utf8"));
8608
9238
  } catch (e) {
8609
9239
  throw new Error(`Could not read ${opts.file}: ${e instanceof Error ? e.message : String(e)}`);
8610
9240
  }
@@ -8663,11 +9293,11 @@ async function runMcpUi(project, tool, opts) {
8663
9293
  console.log(import_chalk29.default.dim(" This is the automatic viewer: MCP Apps clients render the tool's result with a generic UI built"));
8664
9294
  console.log(import_chalk29.default.dim(" from its response schema \u2014 nothing to configure. Uploading your own HTML for it comes later.\n"));
8665
9295
  }
8666
- var import_fs7, import_chalk29, PROMPT_NAME_RE;
9296
+ var import_fs8, import_chalk29, PROMPT_NAME_RE;
8667
9297
  var init_mcp = __esm({
8668
9298
  "src/commands/mcp.ts"() {
8669
9299
  "use strict";
8670
- import_fs7 = __toESM(require("fs"));
9300
+ import_fs8 = __toESM(require("fs"));
8671
9301
  import_chalk29 = __toESM(require("chalk"));
8672
9302
  init_brand();
8673
9303
  init_auth();
@@ -8695,7 +9325,7 @@ function createPlaneCore(cfg) {
8695
9325
  const iamBase = (cfg.iamBase ?? "https://iam.apiblaze.com").replace(/\/$/, "");
8696
9326
  const tenantCache = /* @__PURE__ */ new Map();
8697
9327
  const endUserCache = /* @__PURE__ */ new Map();
8698
- function planeFetch(base2, method, path19, user, opts = {}) {
9328
+ function planeFetch(base2, method, path20, user, opts = {}) {
8699
9329
  const params = new URLSearchParams({ product: "apiblaze", ...opts.query ?? {} });
8700
9330
  const headers = { "X-API-Key": cfg.cpKey, "X-End-User-Id": user.userId };
8701
9331
  let body;
@@ -8703,7 +9333,7 @@ function createPlaneCore(cfg) {
8703
9333
  headers["Content-Type"] = "application/json";
8704
9334
  body = JSON.stringify(opts.body);
8705
9335
  }
8706
- return fetch(`${base2}${path19}?${params.toString()}`, { method, headers, body, cache: "no-store" });
9336
+ return fetch(`${base2}${path20}?${params.toString()}`, { method, headers, body, cache: "no-store" });
8707
9337
  }
8708
9338
  async function resolveTenant2(user) {
8709
9339
  const cached = tenantCache.get(user.tenant);
@@ -10128,15 +10758,15 @@ async function freePort(from, avoid = []) {
10128
10758
  }
10129
10759
  function readRegistry() {
10130
10760
  try {
10131
- return JSON.parse(import_fs10.default.readFileSync(registryFile(), "utf8"));
10761
+ return JSON.parse(import_fs11.default.readFileSync(registryFile(), "utf8"));
10132
10762
  } catch {
10133
10763
  return [];
10134
10764
  }
10135
10765
  }
10136
10766
  function writeRegistry(rows) {
10137
10767
  try {
10138
- import_fs10.default.mkdirSync(import_path8.default.dirname(registryFile()), { recursive: true });
10139
- import_fs10.default.writeFileSync(registryFile(), JSON.stringify(rows));
10768
+ import_fs11.default.mkdirSync(import_path9.default.dirname(registryFile()), { recursive: true });
10769
+ import_fs11.default.writeFileSync(registryFile(), JSON.stringify(rows));
10140
10770
  } catch {
10141
10771
  }
10142
10772
  }
@@ -10180,8 +10810,8 @@ async function stopTunnels() {
10180
10810
  return stopped;
10181
10811
  }
10182
10812
  function appCtl(dir, arg) {
10183
- if (!import_fs10.default.existsSync(import_path8.default.join(dir, "start.js"))) return false;
10184
- const r = (0, import_child_process7.spawnSync)(process.execPath, [import_path8.default.join(dir, "start.js"), arg], { cwd: dir, stdio: "ignore", timeout: 15e3 });
10813
+ if (!import_fs11.default.existsSync(import_path9.default.join(dir, "start.js"))) return false;
10814
+ const r = (0, import_child_process7.spawnSync)(process.execPath, [import_path9.default.join(dir, "start.js"), arg], { cwd: dir, stdio: "ignore", timeout: 15e3 });
10185
10815
  return r.status === 0;
10186
10816
  }
10187
10817
  function isDemoProcess(args) {
@@ -10221,47 +10851,47 @@ async function stopDemos() {
10221
10851
  return n + left.length;
10222
10852
  }
10223
10853
  function writeApp(dir) {
10224
- import_fs10.default.mkdirSync(dir, { recursive: true });
10225
- for (const entry of import_fs10.default.readdirSync(dir)) import_fs10.default.rmSync(import_path8.default.join(dir, entry), { recursive: true, force: true });
10854
+ import_fs11.default.mkdirSync(dir, { recursive: true });
10855
+ for (const entry of import_fs11.default.readdirSync(dir)) import_fs11.default.rmSync(import_path9.default.join(dir, entry), { recursive: true, force: true });
10226
10856
  for (const [rel, text] of Object.entries(DEMO_FILES)) {
10227
- const f2 = import_path8.default.join(dir, rel);
10228
- import_fs10.default.mkdirSync(import_path8.default.dirname(f2), { recursive: true });
10229
- import_fs10.default.writeFileSync(f2, text);
10857
+ const f2 = import_path9.default.join(dir, rel);
10858
+ import_fs11.default.mkdirSync(import_path9.default.dirname(f2), { recursive: true });
10859
+ import_fs11.default.writeFileSync(f2, text);
10230
10860
  }
10231
- import_fs10.default.writeFileSync(import_path8.default.join(dir, "package.json"), JSON.stringify({
10861
+ import_fs11.default.writeFileSync(import_path9.default.join(dir, "package.json"), JSON.stringify({
10232
10862
  name: "nino-pizza-demo",
10233
10863
  private: true,
10234
10864
  scripts: { start: "node start.js --background", stop: "node start.js --stop", restart: "node start.js --restart", dev: "node start.js" }
10235
10865
  }, null, 2) + "\n");
10236
- import_fs10.default.writeFileSync(import_path8.default.join(dir, ".gitignore"), ".env.local\n.demo/\n");
10237
- for (const [from, to] of [[import_path8.default.join(__dirname, "server", "index.js"), import_path8.default.join(dir, "lib", "apiblaze-server.js")], [import_path8.default.join(__dirname, "browser", "widgets.js"), import_path8.default.join(dir, "public", "apiblaze-widgets.js")]]) {
10238
- if (!import_fs10.default.existsSync(from)) continue;
10239
- import_fs10.default.mkdirSync(import_path8.default.dirname(to), { recursive: true });
10240
- import_fs10.default.copyFileSync(from, to);
10866
+ import_fs11.default.writeFileSync(import_path9.default.join(dir, ".gitignore"), ".env.local\n.demo/\n");
10867
+ for (const [from, to] of [[import_path9.default.join(__dirname, "server", "index.js"), import_path9.default.join(dir, "lib", "apiblaze-server.js")], [import_path9.default.join(__dirname, "browser", "widgets.js"), import_path9.default.join(dir, "public", "apiblaze-widgets.js")]]) {
10868
+ if (!import_fs11.default.existsSync(from)) continue;
10869
+ import_fs11.default.mkdirSync(import_path9.default.dirname(to), { recursive: true });
10870
+ import_fs11.default.copyFileSync(from, to);
10241
10871
  }
10242
10872
  }
10243
10873
  function saveState(dir, s) {
10244
- const d = import_path8.default.join(dir, STATE_DIR);
10245
- import_fs10.default.mkdirSync(d, { recursive: true });
10246
- import_fs10.default.writeFileSync(import_path8.default.join(d, "state.json"), JSON.stringify(s, null, 2), { mode: 384 });
10874
+ const d = import_path9.default.join(dir, STATE_DIR);
10875
+ import_fs11.default.mkdirSync(d, { recursive: true });
10876
+ import_fs11.default.writeFileSync(import_path9.default.join(d, "state.json"), JSON.stringify(s, null, 2), { mode: 384 });
10247
10877
  }
10248
10878
  function loadState(dir) {
10249
10879
  try {
10250
- return JSON.parse(import_fs10.default.readFileSync(import_path8.default.join(dir, STATE_DIR, "state.json"), "utf8"));
10880
+ return JSON.parse(import_fs11.default.readFileSync(import_path9.default.join(dir, STATE_DIR, "state.json"), "utf8"));
10251
10881
  } catch {
10252
10882
  return null;
10253
10883
  }
10254
10884
  }
10255
10885
  function writeEnv(dir, v) {
10256
10886
  const lines = Object.entries(v).map(([k, val]) => `${k}=${val}`);
10257
- import_fs10.default.writeFileSync(import_path8.default.join(dir, ".env.local"), `# Written by ${LATEST} demo. Server-side only: never commit or share.
10887
+ import_fs11.default.writeFileSync(import_path9.default.join(dir, ".env.local"), `# Written by ${LATEST} demo. Server-side only: never commit or share.
10258
10888
  ${lines.join("\n")}
10259
10889
  `, { mode: 384 });
10260
10890
  }
10261
10891
  function readEnvFile(file2) {
10262
10892
  const out = {};
10263
10893
  try {
10264
- for (const line of import_fs10.default.readFileSync(file2, "utf8").split("\n")) {
10894
+ for (const line of import_fs11.default.readFileSync(file2, "utf8").split("\n")) {
10265
10895
  const m = /^([A-Z0-9_]+)=(.*)$/.exec(line.trim());
10266
10896
  if (m) out[m[1]] = m[2];
10267
10897
  }
@@ -10271,14 +10901,14 @@ function readEnvFile(file2) {
10271
10901
  }
10272
10902
  function demoDir() {
10273
10903
  const here = process.cwd();
10274
- if (import_fs10.default.existsSync(import_path8.default.join(here, STATE_DIR, "state.json"))) return here;
10275
- return import_path8.default.resolve(here, DIR_NAME);
10904
+ if (import_fs11.default.existsSync(import_path9.default.join(here, STATE_DIR, "state.json"))) return here;
10905
+ return import_path9.default.resolve(here, DIR_NAME);
10276
10906
  }
10277
10907
  function adoptWorkspace(cpKey, teamId, claimCode) {
10278
10908
  const prev = loadAnonCred();
10279
10909
  if (prev?.cp_key && prev.team_id && prev.team_id !== teamId) {
10280
10910
  try {
10281
- import_fs10.default.writeFileSync(import_path8.default.join(getApiblazeDir(), `anon.${prev.team_id}.json`), JSON.stringify(prev, null, 2), { mode: 384 });
10911
+ import_fs11.default.writeFileSync(import_path9.default.join(getApiblazeDir(), `anon.${prev.team_id}.json`), JSON.stringify(prev, null, 2), { mode: 384 });
10282
10912
  } catch {
10283
10913
  }
10284
10914
  }
@@ -10370,7 +11000,7 @@ function openBrowser3(url) {
10370
11000
  }
10371
11001
  async function offerBrowserAndLogs(dir, opts) {
10372
11002
  if (opts.open === false || opts.yes || !interactive()) return;
10373
- const env = readEnvFile(import_path8.default.join(dir, ".env.local"));
11003
+ const env = readEnvFile(import_path9.default.join(dir, ".env.local"));
10374
11004
  const url = `http://localhost:${env.APP_PORT}`;
10375
11005
  console.log();
10376
11006
  if (await ask(` Open ${url} in your browser? (Y/n) `)) openBrowser3(url);
@@ -10405,7 +11035,7 @@ function routesOf(openapi) {
10405
11035
  return out;
10406
11036
  }
10407
11037
  async function runDemo(opts = {}) {
10408
- if (opts.serve) return serveTunnel(import_path8.default.resolve(opts.serve));
11038
+ if (opts.serve) return serveTunnel(import_path9.default.resolve(opts.serve));
10409
11039
  try {
10410
11040
  countRun({ event: await startDemo(opts) });
10411
11041
  } catch (err) {
@@ -10461,8 +11091,8 @@ function printTable(rows) {
10461
11091
  async function startDemo(opts) {
10462
11092
  const t0 = Date.now();
10463
11093
  const here = process.cwd();
10464
- const dir = import_fs10.default.existsSync(import_path8.default.join(here, STATE_DIR, "state.json")) ? here : import_path8.default.resolve(here, DIR_NAME);
10465
- const rel = import_path8.default.relative(here, dir) || ".";
11094
+ const dir = import_fs11.default.existsSync(import_path9.default.join(here, STATE_DIR, "state.json")) ? here : import_path9.default.resolve(here, DIR_NAME);
11095
+ const rel = import_path9.default.relative(here, dir) || ".";
10466
11096
  const cdDir = rel === "." ? "" : `cd ${rel} && `;
10467
11097
  const shell = (c, note = "") => console.log(` ${import_chalk33.default.dim("$")} ${import_chalk33.default.cyan(c)}${note ? ` ${import_chalk33.default.dim(note)}` : ""}`);
10468
11098
  const ok = (text, note = "") => console.log(` ${import_chalk33.default.green("\u2713")} ${text}${note ? ` ${import_chalk33.default.dim(note)}` : ""}`);
@@ -10482,8 +11112,8 @@ async function startDemo(opts) {
10482
11112
  const backendSecret = import_crypto.default.randomBytes(18).toString("base64url");
10483
11113
  const openapi = DEMO_FILES["openapi.yaml"];
10484
11114
  try {
10485
- const specPath = import_path8.default.join(dir, "openapi.yaml");
10486
- import_fs10.default.writeFileSync(specPath, import_fs10.default.readFileSync(specPath, "utf8").replace(/^paths:/m, `servers:
11115
+ const specPath = import_path9.default.join(dir, "openapi.yaml");
11116
+ import_fs11.default.writeFileSync(specPath, import_fs11.default.readFileSync(specPath, "utf8").replace(/^paths:/m, `servers:
10487
11117
  - url: http://localhost:${backendPort2}
10488
11118
  paths:`));
10489
11119
  } catch {
@@ -10505,12 +11135,12 @@ paths:`));
10505
11135
  if (!appCtl(dir, "--background")) throw new DemoError("The ResiResi app could not start.", `Run again: ${LATEST} demo`, "app-start");
10506
11136
  registerDemo(dir);
10507
11137
  const appUp = await busy("Starting the reservation backend and its front-end\u2026", () => waitPorts([backendPort2, directPort]), 1800);
10508
- if (!appUp) throw new DemoError(`The ResiResi app did not start (see ${import_path8.default.join(rel, STATE_DIR, "app.log")}).`, `Run again: ${LATEST} demo`, "app-not-up");
11138
+ if (!appUp) throw new DemoError(`The ResiResi app did not start (see ${import_path9.default.join(rel, STATE_DIR, "app.log")}).`, `Run again: ${LATEST} demo`, "app-not-up");
10509
11139
  ok(`${import_chalk33.default.bold("Reservation backend")}: localhost:${backendPort2}`, "unprotected");
10510
11140
  ok(`${import_chalk33.default.bold("Unprotected reservation front-end")}, going directly to the reservation backend: ${import_chalk33.default.bold(`http://localhost:${directPort}`)}`);
10511
11141
  console.log(" Book a table directly, the way the unprotected front-end does:");
10512
11142
  console.log(import_chalk33.default.cyan(` curl -X POST http://localhost:${backendPort2}/reservations \\`));
10513
- console.log(import_chalk33.default.cyan(` -H "x-target-api-key: $(grep BACKEND_SECRET ${import_path8.default.join(rel, ".env.local")} | cut -d= -f2)" \\`));
11143
+ console.log(import_chalk33.default.cyan(` -H "x-target-api-key: $(grep BACKEND_SECRET ${import_path9.default.join(rel, ".env.local")} | cut -d= -f2)" \\`));
10514
11144
  console.log(import_chalk33.default.cyan(` -H "x-abz-user-id: ana" -H "x-abz-tenant-id: ${ninoTenant}" \\`));
10515
11145
  const tomorrow = new Date(Date.now() + 864e5);
10516
11146
  const day = `${tomorrow.getFullYear()}-${String(tomorrow.getMonth() + 1).padStart(2, "0")}-${String(tomorrow.getDate()).padStart(2, "0")}`;
@@ -10650,23 +11280,23 @@ paths:`));
10650
11280
  appCtl(dir, "--background");
10651
11281
  return waitPorts([appPort, directPort, backendPort2]);
10652
11282
  }, 1500);
10653
- if (!siteUp) throw new DemoError(`The protected website did not start (see ${import_path8.default.join(rel, STATE_DIR, "app.log")}).`, `Try: ${cdDir}npm run restart`, "site-not-up");
11283
+ if (!siteUp) throw new DemoError(`The protected website did not start (see ${import_path9.default.join(rel, STATE_DIR, "app.log")}).`, `Try: ${cdDir}npm run restart`, "site-not-up");
10654
11284
  ok(`${import_chalk33.default.bold("Protected reservation website")}, going through the ${B.product} proxy ${apiUrl}: ${import_chalk33.default.bold(`http://localhost:${appPort}`)}`);
10655
11285
  ok(`${import_chalk33.default.bold("Unprotected reservation front-end")}, still going directly to the backend: http://localhost:${directPort}`, "the switch at the top of the page goes between them");
10656
11286
  console.log();
10657
- console.log(import_chalk33.default.dim(` Logs: ${logsCommand(readEnvFile(import_path8.default.join(dir, ".env.local")))} \xB7 Stop the tunnel: ${LATEST} demo stop \xB7 Your own app: ${LATEST} dev --port <port>`));
11287
+ console.log(import_chalk33.default.dim(` Logs: ${logsCommand(readEnvFile(import_path9.default.join(dir, ".env.local")))} \xB7 Stop the tunnel: ${LATEST} demo stop \xB7 Your own app: ${LATEST} dev --port <port>`));
10658
11288
  await offerBrowserAndLogs(dir, opts);
10659
11289
  return "demo-ready";
10660
11290
  }
10661
11291
  function printAddresses(dir) {
10662
- const env = readEnvFile(import_path8.default.join(dir, ".env.local"));
11292
+ const env = readEnvFile(import_path9.default.join(dir, ".env.local"));
10663
11293
  const row = (label3, value, note = "") => console.log(` ${label3.padEnd(11)} ${value}${note ? ` ${import_chalk33.default.dim(note)}` : ""}`);
10664
11294
  row("Public API", env.APIBLAZE_URL ?? "", `${B.product} tunnel \u2192 localhost:${env.BACKEND_PORT}`);
10665
11295
  row("MCP", env.APIBLAZE_MCP_URL ?? "", "for Claude, ChatGPT, Cursor (Nino Pizza)");
10666
11296
  if (env.APIBLAZE_PORTAL_URL) row("Dev portal", env.APIBLAZE_PORTAL_URL, "sign in, get a key, try the API");
10667
11297
  }
10668
11298
  async function spawnTunnel(dir, state) {
10669
- const log4 = import_fs10.default.openSync(import_path8.default.join(dir, STATE_DIR, "tunnel.log"), "a");
11299
+ const log4 = import_fs11.default.openSync(import_path9.default.join(dir, STATE_DIR, "tunnel.log"), "a");
10670
11300
  const child = (0, import_child_process7.spawn)(process.execPath, [process.argv[1], "demo", "--serve", dir], { cwd: dir, detached: true, stdio: ["ignore", log4, log4] });
10671
11301
  child.unref();
10672
11302
  saveState(dir, { ...state, pid: child.pid, ready: false, error: void 0 });
@@ -10677,7 +11307,7 @@ async function runDemoStart(opts = {}) {
10677
11307
  const t0 = Date.now();
10678
11308
  const dir = demoDir();
10679
11309
  const state = loadState(dir);
10680
- const env = readEnvFile(import_path8.default.join(dir, ".env.local"));
11310
+ const env = readEnvFile(import_path9.default.join(dir, ".env.local"));
10681
11311
  if (!state || !env.APIBLAZE_SERVER_KEY) {
10682
11312
  console.log(`${import_chalk33.default.red("\u2717")} There is no demo here to start.`);
10683
11313
  console.log(` Make one: ${LATEST} demo`);
@@ -10697,11 +11327,11 @@ async function runDemoStart(opts = {}) {
10697
11327
  console.log(`${import_chalk33.default.green("\u2713")} Tunnel open: the public address reaches localhost:${env.BACKEND_PORT} again ${import_chalk33.default.dim(`(${Math.round((Date.now() - t0) / 1e3)} s)`)}`);
10698
11328
  printAddresses(dir);
10699
11329
  const appUp = await fetch(`http://127.0.0.1:${env.APP_PORT}/api/config`).then(() => true).catch(() => false);
10700
- if (!appUp) console.log(import_chalk33.default.yellow(` ResiResi is not running: cd ${import_path8.default.relative(process.cwd(), dir) || "."} && npm run start`));
11330
+ if (!appUp) console.log(import_chalk33.default.yellow(` ResiResi is not running: cd ${import_path9.default.relative(process.cwd(), dir) || "."} && npm run start`));
10701
11331
  await offerBrowserAndLogs(dir, opts);
10702
11332
  }
10703
11333
  async function runDemoLogs() {
10704
- const env = readEnvFile(import_path8.default.join(demoDir(), ".env.local"));
11334
+ const env = readEnvFile(import_path9.default.join(demoDir(), ".env.local"));
10705
11335
  if (!env.APIBLAZE_NAME) {
10706
11336
  console.log(`${import_chalk33.default.red("\u2717")} There is no demo here.`);
10707
11337
  console.log(` Make one: ${LATEST} demo`);
@@ -10771,12 +11401,12 @@ async function serveTunnel(dir) {
10771
11401
  await new Promise(() => {
10772
11402
  });
10773
11403
  }
10774
- var import_fs10, import_path8, import_net2, import_child_process7, import_crypto, import_chalk33, import_ora11, DIR_NAME, STATE_DIR, LATEST, DEMO_RPS, DEMO_ADMIN_EMAIL, DEMO_GINO_ADMIN_EMAIL, RULE_SENTENCE, DemoError, ADJ, NOUN, pick2, registryFile, alive, interactive, logsCommand;
11404
+ var import_fs11, import_path9, import_net2, import_child_process7, import_crypto, import_chalk33, import_ora11, DIR_NAME, STATE_DIR, LATEST, DEMO_RPS, DEMO_ADMIN_EMAIL, DEMO_GINO_ADMIN_EMAIL, RULE_SENTENCE, DemoError, ADJ, NOUN, pick2, registryFile, alive, interactive, logsCommand;
10775
11405
  var init_demo = __esm({
10776
11406
  "src/commands/demo.ts"() {
10777
11407
  "use strict";
10778
- import_fs10 = __toESM(require("fs"));
10779
- import_path8 = __toESM(require("path"));
11408
+ import_fs11 = __toESM(require("fs"));
11409
+ import_path9 = __toESM(require("path"));
10780
11410
  import_net2 = __toESM(require("net"));
10781
11411
  import_child_process7 = require("child_process");
10782
11412
  import_crypto = __toESM(require("crypto"));
@@ -10807,7 +11437,7 @@ var init_demo = __esm({
10807
11437
  ADJ = ["brave", "calm", "clever", "eager", "happy", "jolly", "lucky", "merry", "noble", "quick", "sunny", "swift", "witty", "zesty", "bright", "gentle"];
10808
11438
  NOUN = ["otter", "falcon", "panda", "heron", "koala", "lemur", "puffin", "robin", "tiger", "walrus", "wombat", "badger", "comet", "maple", "river", "finch"];
10809
11439
  pick2 = (a) => a[import_crypto.default.randomInt(a.length)];
10810
- registryFile = () => import_path8.default.join(getApiblazeDir(), "demo-runs.json");
11440
+ registryFile = () => import_path9.default.join(getApiblazeDir(), "demo-runs.json");
10811
11441
  alive = (pid) => {
10812
11442
  try {
10813
11443
  process.kill(pid, 0);
@@ -11072,7 +11702,7 @@ function runCli(args, cwd, home, timeoutMs) {
11072
11702
  function readEnv(file2) {
11073
11703
  const out = {};
11074
11704
  try {
11075
- for (const line of import_fs13.default.readFileSync(file2, "utf8").split("\n")) {
11705
+ for (const line of import_fs14.default.readFileSync(file2, "utf8").split("\n")) {
11076
11706
  const m = /^([A-Z0-9_]+)=(.*)$/.exec(line.trim());
11077
11707
  if (m) out[m[1]] = m[2];
11078
11708
  }
@@ -11105,7 +11735,7 @@ async function journey(label3, work, home, checks) {
11105
11735
  `exit ${run2.code} in ${Math.round(run2.ms / 1e3)} s${leaked ? " \xB7 PRINTED A SECRET" : ""}${run2.code !== 0 ? ` \xB7 ${run2.out.trim().split("\n").slice(-2).join(" / ")}` : ""}`
11106
11736
  );
11107
11737
  if (run2.code !== 0) return null;
11108
- const env = readEnv(import_path10.default.join(work, `${B.cli}-demo`, ".env.local"));
11738
+ const env = readEnv(import_path11.default.join(work, `${B.cli}-demo`, ".env.local"));
11109
11739
  const api = env.APIBLAZE_URL;
11110
11740
  const nino = env.APIBLAZE_SERVER_KEY;
11111
11741
  const as = (user, key = nino) => ({ "x-api-key": key, "x-end-user-id": user, "content-type": "application/json" });
@@ -11173,7 +11803,7 @@ async function claimJourney(work, checks, env) {
11173
11803
  let code = "";
11174
11804
  let teamId = "";
11175
11805
  try {
11176
- const st = JSON.parse(import_fs13.default.readFileSync(import_path10.default.join(work, `${B.cli}-demo`, ".demo", "state.json"), "utf8"));
11806
+ const st = JSON.parse(import_fs14.default.readFileSync(import_path11.default.join(work, `${B.cli}-demo`, ".demo", "state.json"), "utf8"));
11177
11807
  code = st.claimCode ?? "";
11178
11808
  teamId = st.teamId ?? "";
11179
11809
  } catch {
@@ -11236,11 +11866,11 @@ async function claimJourney(work, checks, env) {
11236
11866
  }
11237
11867
  async function runOpAnon(o = {}) {
11238
11868
  const t0 = Date.now();
11239
- const root = import_fs13.default.mkdtempSync(import_path10.default.join(import_os5.default.tmpdir(), `${B.cli}-op-anon-`));
11240
- const home = import_path10.default.join(root, "home");
11241
- const work = import_path10.default.join(root, "work");
11242
- import_fs13.default.mkdirSync(home);
11243
- import_fs13.default.mkdirSync(work);
11869
+ const root = import_fs14.default.mkdtempSync(import_path11.default.join(import_os5.default.tmpdir(), `${B.cli}-op-anon-`));
11870
+ const home = import_path11.default.join(root, "home");
11871
+ const work = import_path11.default.join(root, "work");
11872
+ import_fs14.default.mkdirSync(home);
11873
+ import_fs14.default.mkdirSync(work);
11244
11874
  const checks = [];
11245
11875
  const log4 = (s) => {
11246
11876
  if (!o.json) console.log(s);
@@ -11252,21 +11882,21 @@ async function runOpAnon(o = {}) {
11252
11882
  await claimJourney(work, checks, env);
11253
11883
  } finally {
11254
11884
  const stop = await runCli(["demo", "stop"], work, home, 3e4);
11255
- const demoFolder = import_path10.default.join(work, `${B.cli}-demo`);
11256
- if (import_fs13.default.existsSync(import_path10.default.join(demoFolder, "start.js"))) {
11885
+ const demoFolder = import_path11.default.join(work, `${B.cli}-demo`);
11886
+ if (import_fs14.default.existsSync(import_path11.default.join(demoFolder, "start.js"))) {
11257
11887
  await new Promise((res) => {
11258
- const c = (0, import_child_process8.spawn)(process.execPath, [import_path10.default.join(demoFolder, "start.js"), "--stop"], { cwd: demoFolder, stdio: "ignore" });
11888
+ const c = (0, import_child_process8.spawn)(process.execPath, [import_path11.default.join(demoFolder, "start.js"), "--stop"], { cwd: demoFolder, stdio: "ignore" });
11259
11889
  c.on("close", () => res());
11260
11890
  });
11261
11891
  }
11262
- const env = readEnv(import_path10.default.join(work, `${B.cli}-demo`, ".env.local"));
11892
+ const env = readEnv(import_path11.default.join(work, `${B.cli}-demo`, ".env.local"));
11263
11893
  let portsFree = true;
11264
11894
  for (const p of [env.APP_PORT, env.BACKEND_PORT].filter(Boolean)) {
11265
11895
  const r = await http(`http://127.0.0.1:${p}/`, { timeoutMs: 2e3 });
11266
11896
  if (r.status !== 0) portsFree = false;
11267
11897
  }
11268
11898
  checks.push({ id: "stop", ok: stop.code === 0 && portsFree, detail: `exit ${stop.code}${portsFree ? "" : " \xB7 ports still answering"}` });
11269
- import_fs13.default.rmSync(root, { recursive: true, force: true });
11899
+ import_fs14.default.rmSync(root, { recursive: true, force: true });
11270
11900
  }
11271
11901
  for (const c of checks) log4(` ${c.skipped ? import_chalk76.default.yellow("\u2013") : c.ok ? import_chalk76.default.green("\u2713") : import_chalk76.default.red("\u2717")} ${c.id.padEnd(24)} ${import_chalk76.default.dim(c.detail)}`);
11272
11902
  const failed = checks.filter((c) => !c.ok).length;
@@ -11279,13 +11909,13 @@ async function runOpAnon(o = {}) {
11279
11909
  if (failed) process.exitCode = 1;
11280
11910
  return { ok: !failed, checks };
11281
11911
  }
11282
- var import_fs13, import_os5, import_path10, import_child_process8, import_chalk76, SECRET, SANDBOX_TEAM;
11912
+ var import_fs14, import_os5, import_path11, import_child_process8, import_chalk76, SECRET, SANDBOX_TEAM;
11283
11913
  var init_op_anon = __esm({
11284
11914
  "src/commands/op-anon.ts"() {
11285
11915
  "use strict";
11286
- import_fs13 = __toESM(require("fs"));
11916
+ import_fs14 = __toESM(require("fs"));
11287
11917
  import_os5 = __toESM(require("os"));
11288
- import_path10 = __toESM(require("path"));
11918
+ import_path11 = __toESM(require("path"));
11289
11919
  import_child_process8 = require("child_process");
11290
11920
  import_chalk76 = __toESM(require("chalk"));
11291
11921
  init_brand();
@@ -11298,7 +11928,7 @@ var init_op_anon = __esm({
11298
11928
  });
11299
11929
 
11300
11930
  // src/index.ts
11301
- var import_fs14 = __toESM(require("fs"));
11931
+ var import_fs15 = __toESM(require("fs"));
11302
11932
  var import_commander = require("commander");
11303
11933
  var import_chalk78 = __toESM(require("chalk"));
11304
11934
  init_package();
@@ -11889,8 +12519,8 @@ async function opCall(call3) {
11889
12519
  }
11890
12520
 
11891
12521
  // src/commands/connector.ts
11892
- var import_fs8 = __toESM(require("fs"));
11893
- var import_path6 = __toESM(require("path"));
12522
+ var import_fs9 = __toESM(require("fs"));
12523
+ var import_path7 = __toESM(require("path"));
11894
12524
  var import_chalk30 = __toESM(require("chalk"));
11895
12525
  var import_ora10 = __toESM(require("ora"));
11896
12526
  init_caller();
@@ -12034,7 +12664,7 @@ function suggestedMcpHost(apexDomain) {
12034
12664
  // src/commands/connector.ts
12035
12665
  var PORTALS = {
12036
12666
  claude: "https://claude.com/docs/connectors/building/submission",
12037
- chatgpt: "https://developers.openai.com/apps-sdk/deploy/submission"
12667
+ chatgpt: "https://platform.openai.com/plugins"
12038
12668
  };
12039
12669
  var projectPath3 = (p) => `/projects/${encodeURIComponent(p.projectId)}/${encodeURIComponent(p.apiVersion)}`;
12040
12670
  async function patchProject(caller, proj2, body, summary) {
@@ -12052,11 +12682,15 @@ var PROMPT_FOR_RULE = {
12052
12682
  VULN_CONTACT: { key: "vulnContact", message: "Security contact (email or URL):" },
12053
12683
  LISTING_NAME_LEN_CHATGPT: { key: "serverName", message: "Listing name", limit: 30, portal: "ChatGPT" },
12054
12684
  LISTING_NAME_LEN_CLAUDE: { key: "serverName", message: "Listing name", limit: 100, portal: "Claude" },
12055
- TAGLINE_LEN_CHATGPT: { key: "tagline", message: "One-line tagline", limit: 30, portal: "ChatGPT" },
12685
+ TAGLINE_LEN_CHATGPT: { key: "shortDescription", message: "ChatGPT subtitle (the Claude tagline stays as it is)", limit: 30, portal: "ChatGPT" },
12686
+ SHORT_DESCRIPTION_SET: { key: "shortDescription", message: "ChatGPT subtitle", limit: 30, portal: "ChatGPT" },
12056
12687
  TAGLINE_LEN_CLAUDE: { key: "tagline", message: "One-line tagline", limit: 200, portal: "Claude" },
12057
12688
  DESCRIPTION_LEN_CLAUDE: { key: "description", message: "Description", limit: 2e3, portal: "Claude" },
12058
12689
  DESCRIPTION_LEN_CHATGPT: { key: "description", message: "Description", limit: 4e3, portal: "ChatGPT" },
12059
12690
  PROMPT_LEN_CHATGPT: { key: "prompt", message: "Example prompt", multi: true, limit: 128, portal: "ChatGPT" },
12691
+ // WP5: Anthropic's slug (pre-filled from the listing name; permanent once published) and the policy's high-risk question.
12692
+ SLUG_SET: { key: "slug", message: "Listing slug (lowercase-with-hyphens; permanent once published, claude.ai/directory/connectors/<slug>):" },
12693
+ HIGH_RISK_DECLARED: { key: "highRisk", message: "High-risk use cases \u2014 does it serve legal, healthcare, insurance or finance decisions? (none, or a comma-separated list):" },
12060
12694
  // ChatGPT only: the portal issues this token (Plugins → Connect MCP server); we serve it verbatim.
12061
12695
  OPENAI_DOMAIN_CHALLENGE: { key: "openaiChallenge", message: "OpenAI domain challenge token, from platform.openai.com/plugins \u2192 Connect MCP server (Enter to skip for now):" }
12062
12696
  };
@@ -12073,7 +12707,7 @@ function looksLikeSpecArgument(arg) {
12073
12707
  if (/^https?:\/\//i.test(a)) return true;
12074
12708
  if (/[\\/]/.test(a) || /\.(ya?ml|json)$/i.test(a)) {
12075
12709
  try {
12076
- return import_fs8.default.existsSync(a) && import_fs8.default.statSync(a).isFile();
12710
+ return import_fs9.default.existsSync(a) && import_fs9.default.statSync(a).isFile();
12077
12711
  } catch {
12078
12712
  return false;
12079
12713
  }
@@ -12087,6 +12721,7 @@ function currentListingValue(r, key) {
12087
12721
  tagline: "tagline",
12088
12722
  description: "description",
12089
12723
  prompt: "examplePrompts",
12724
+ shortDescription: "shortDescription",
12090
12725
  privacyUrl: "privacyUrl",
12091
12726
  support: "supportContact",
12092
12727
  docsUrl: "docsUrl",
@@ -12172,14 +12807,14 @@ async function promptForMissing(r, interactive2, defaults = { prompts: [] }, pla
12172
12807
  if (testPrompt) {
12173
12808
  let res = await testPrompt(text);
12174
12809
  if (!res.called) {
12175
- say_(import_chalk30.default.yellow(` \u2013 Claude called no tool for "${text}". A prompt should say what the tool does (a real URL, name or id helps).`));
12810
+ say_(import_chalk30.default.yellow(` \u2013 No tool call for "${text}". A prompt should say what the tool does (a real URL, name or id helps).`));
12176
12811
  const again = await inquirer3.prompt([{ type: "input", name: "line", message: "Reword it (Enter keeps it):", default: text, validate: withinLimit(a) }]);
12177
12812
  if (again.line?.trim() && again.line.trim() !== text) {
12178
12813
  text = again.line.trim();
12179
12814
  res = await testPrompt(text);
12180
12815
  }
12181
12816
  }
12182
- say_(res.called ? import_chalk30.default.green(` \u2713 Claude called ${res.tools.join(", ")}`) : import_chalk30.default.yellow(" \u2013 still no tool call; kept (the report will say so)"));
12817
+ if (!res.called) say_(import_chalk30.default.yellow(" \u2013 still no tool call; kept (the report will say so)"));
12183
12818
  }
12184
12819
  lines.push(text);
12185
12820
  }
@@ -12193,7 +12828,7 @@ async function promptForMissing(r, interactive2, defaults = { prompts: [] }, pla
12193
12828
  continue;
12194
12829
  }
12195
12830
  const dk = DEFAULT_FOR_KEY[a.key];
12196
- const base2 = typeof current === "string" ? current : dk ? defaults[dk] : void 0;
12831
+ const base2 = typeof current === "string" ? current : a.key === "shortDescription" ? currentListingValue(r, "tagline") : dk ? defaults[dk] : void 0;
12197
12832
  const def = typeof base2 === "string" && base2 ? a.limit ? shortenTo(base2, a.limit) : base2 : void 0;
12198
12833
  const { value } = await inquirer3.prompt([{
12199
12834
  type: "input",
@@ -12248,7 +12883,7 @@ async function askOpenMode(caller, proj2, r, opts, interactive2, say) {
12248
12883
  async function promptListingBasics(r, interactive2, defaults, given, platforms = PLATFORMS, save) {
12249
12884
  const meta = r.target?.metadata ?? {};
12250
12885
  const gpt = platforms.includes("chatgpt");
12251
- const lim = { serverName: gpt ? 30 : 100, tagline: gpt ? 30 : 200, description: platforms.includes("claude") ? 2e3 : 4e3 };
12886
+ const lim = { serverName: gpt ? 30 : 100, tagline: 200, description: platforms.includes("claude") ? 2e3 : 4e3 };
12252
12887
  const cutTo = (v, n) => v ? shortenTo(v, n) : v;
12253
12888
  const all = [
12254
12889
  { key: "serverName", current: meta.serverName, def: cutTo(defaults.serverName, lim.serverName), message: `Listing name (\u2264 ${lim.serverName} chars, no "MCP"/"Plugin" suffix):` },
@@ -12316,7 +12951,7 @@ async function specTextFor(source) {
12316
12951
  const res = await fetch(source, { signal: AbortSignal.timeout(8e3) });
12317
12952
  return res.ok ? await res.text() : null;
12318
12953
  }
12319
- return import_fs8.default.readFileSync(source, "utf-8");
12954
+ return import_fs9.default.readFileSync(source, "utf-8");
12320
12955
  } catch {
12321
12956
  return null;
12322
12957
  }
@@ -12507,6 +13142,10 @@ async function runConnector(target2, opts) {
12507
13142
  const changed = await fixToolDescriptions(proj2, r.report.project.environment, interactive2, say);
12508
13143
  if (changed) r = await fetchDirectoryReport(caller, proj2, { platforms });
12509
13144
  }
13145
+ if (r.report.results.some((x) => x.id === "TOOL_OPENWORLD_CONFIRMED" && !x.pass)) {
13146
+ const changed = await confirmOpenWorld(proj2, r.report.project.environment, interactive2, opts.openWorld, isOpenMode(r), say);
13147
+ if (changed) r = await fetchDirectoryReport(caller, proj2, { platforms });
13148
+ }
12510
13149
  if (await askOpenMode(caller, proj2, r, opts, interactive2, say)) r = await fetchDirectoryReport(caller, proj2, { platforms });
12511
13150
  let reviewer = null;
12512
13151
  if (r.report.results.some((x) => x.id === "TEST_ACCOUNT_SET" && !x.pass)) {
@@ -12532,11 +13171,15 @@ async function runConnector(target2, opts) {
12532
13171
  if (!tester && !testerTried) {
12533
13172
  testerTried = true;
12534
13173
  const { createPromptTester: createPromptTester2 } = await Promise.resolve().then(() => (init_connector_exercise(), connector_exercise_exports));
12535
- tester = await createPromptTester2(caller, proj2, { say });
13174
+ tester = await createPromptTester2(caller, proj2, { say, platforms });
12536
13175
  }
12537
13176
  if (!tester) return { called: true, tools: [] };
12538
- say(import_chalk30.default.dim(" trying it through Claude Code\u2026"));
12539
- return tester.test(p);
13177
+ const t = tester;
13178
+ say(import_chalk30.default.dim(` trying it through ${t.clients.map((c) => c === "claude" ? "Claude Code" : "Codex CLI").join(" and ")}\u2026`));
13179
+ const res = await t.test(p);
13180
+ if (res.tools.length) await patchProject(caller, proj2, { directory_metadata: { prompt_tools: { [p.trim()]: res.tools } } }, "Remember which tool the prompt called").catch(() => null);
13181
+ for (const [client, r2] of Object.entries(res.byClient)) say(r2.called ? import_chalk30.default.green(` \u2713 ${client === "claude" ? "Claude" : "Codex"} called ${r2.tools.join(", ")}`) : import_chalk30.default.yellow(` \u2013 ${client === "claude" ? "Claude" : "Codex"} called no tool`));
13182
+ return res;
12540
13183
  };
12541
13184
  if (!isReady(r, platforms) || Object.keys(basics).length) {
12542
13185
  const asked = { ...basics, ...isReady(r, platforms) ? {} : await promptForMissing(r, interactive2, defaults, platforms, basics, interactive2 ? saveAnswer : void 0, interactive2 && opts.exercise !== false ? testPrompt : void 0) };
@@ -12557,7 +13200,7 @@ async function runConnector(target2, opts) {
12557
13200
  }
12558
13201
  if (interactive2 && r.report.results.some((x) => ANSWER_RULES.includes(x.id) && !x.pass)) {
12559
13202
  const { runConnectorAnswers: runConnectorAnswers2 } = await Promise.resolve().then(() => (init_connector_exercise(), connector_exercise_exports));
12560
- await runConnectorAnswers2(proj2.projectName, { team: opts.team, apiversion: opts.apiversion, platforms: [...platforms], derived: false });
13203
+ await runConnectorAnswers2(proj2.projectId, { team: opts.team ?? proj2.teamId, apiversion: opts.apiversion, platforms: [...platforms], derived: false, testPrompt: opts.exercise !== false ? testPrompt : void 0 });
12561
13204
  r = await fetchDirectoryReport(caller, proj2, { platforms });
12562
13205
  }
12563
13206
  if (opts.exercise !== false) {
@@ -12568,18 +13211,26 @@ async function runConnector(target2, opts) {
12568
13211
  }
12569
13212
  if (run2) {
12570
13213
  const { runConnectorExercise: runConnectorExercise2 } = await Promise.resolve().then(() => (init_connector_exercise(), connector_exercise_exports));
12571
- const ran = await runConnectorExercise2(proj2.projectName, { team: opts.team, apiversion: opts.apiversion, json, yes: opts.yes, noEmail: opts.email === false, report: false, platforms: [...platforms], provenPromptNotes });
13214
+ const ran = await runConnectorExercise2(proj2.projectId, { team: opts.team ?? proj2.teamId, apiversion: opts.apiversion, json, yes: opts.yes, noEmail: opts.email === false, report: false, platforms: [...platforms], provenPromptNotes, seed: opts.seed });
12572
13215
  if (interactive2 && ran.failedPrompts.length) {
13216
+ for (const [p, clients] of Object.entries(ran.failedByClient ?? {})) say(import_chalk30.default.yellow(` \u2013 "${p.slice(0, 60)}": no tool call in ${clients.map((c) => c === "claude" ? "Claude Code" : "Codex CLI").join(" and ")}`));
12573
13217
  const { rewritten, prompts } = await rewriteFailedPrompts(r, ran.failedPrompts, saveAnswer, say);
12574
13218
  if (rewritten.length) {
12575
- say(import_chalk30.default.dim(` Running the ${prompts.length} prompts through Claude Code again (the report reads the last run)\u2026`));
12576
- await runConnectorExercise2(proj2.projectName, { team: opts.team, apiversion: opts.apiversion, json, noEmail: opts.email === false, report: false, platforms: [...platforms], promptsOnly: prompts });
13219
+ const { clientsFor: clientsFor2 } = await Promise.resolve().then(() => (init_connector_exercise(), connector_exercise_exports));
13220
+ say(import_chalk30.default.dim(` Running the ${prompts.length} prompts through ${clientsFor2(platforms).map((c) => c === "claude" ? "Claude Code" : "Codex CLI").join(" and ")} again (the report reads the last run)\u2026`));
13221
+ const again = await runConnectorExercise2(proj2.projectId, { team: opts.team ?? proj2.teamId, apiversion: opts.apiversion, json, noEmail: opts.email === false, report: false, platforms: [...platforms], promptsOnly: prompts, provenPromptNotes: ran.notes.filter((n) => n.kind === "negative") });
13222
+ ran.promptTools = { ...ran.promptTools, ...again.promptTools };
13223
+ ran.codexPromptTools = { ...ran.codexPromptTools, ...again.codexPromptTools };
13224
+ ran.failedPrompts = again.failedPrompts;
13225
+ ran.failedByClient = again.failedByClient;
12577
13226
  }
12578
13227
  }
12579
- if (interactive2 || opts.yes) await offerDrafts(caller, proj2, r, ran, interactive2, say);
12580
- r = await fetchDirectoryReport(caller, proj2, { platforms, publishedUrl: r.report.project.publishedUrl ?? null });
13228
+ if (interactive2 || opts.yes) await offerDrafts(caller, proj2, r, ran, interactive2, say, platforms);
13229
+ const extraProbes = ran?.userinfo && ran.userinfo.userinfoStatus != null ? { metadata: { userinfoStatus: ran.userinfo.userinfoStatus, userinfoEmailVerified: ran.userinfo.userinfoEmailVerified } } : null;
13230
+ r = await fetchDirectoryReport(caller, proj2, { platforms, publishedUrl: r.report.project.publishedUrl ?? null, extraProbes, listing: listingOf(r) });
12581
13231
  }
12582
13232
  }
13233
+ if (opts.waitLogin !== false && interactive2) r = await waitForClaudeLogin(caller, proj2, r, platforms, interactive2, say);
12583
13234
  if (!readyExceptSnapshot(r, platforms)) {
12584
13235
  if (!domain2.active && domain2.hostname && !json) {
12585
13236
  console.error(import_chalk30.default.yellow(`
@@ -12587,14 +13238,33 @@ async function runConnector(target2, opts) {
12587
13238
  }
12588
13239
  exitWithReport(r, proj2, platforms, json, interactive2);
12589
13240
  }
12590
- r = await fetchDirectoryReport(caller, proj2, { platforms, refreshSnapshot: true, publishedUrl: r.report.project.publishedUrl ?? null });
13241
+ r = await fetchDirectoryReport(caller, proj2, { platforms, refreshSnapshot: true, publishedUrl: r.report.project.publishedUrl ?? null, listing: listingOf(r) });
13242
+ let watching = false;
13243
+ if (opts.watch !== false) {
13244
+ watching = await producer(caller, { method: "POST", path: `${projectPath3(proj2)}/directory-watch`, body: { enabled: true }, summary: "Turn on the nightly re-check" }).then(() => true).catch((e) => {
13245
+ say(import_chalk30.default.dim(` (nightly re-check not switched on: ${e.message})`));
13246
+ return false;
13247
+ });
13248
+ }
12591
13249
  const probes = await packetProbes(r);
12592
13250
  const packet = await producer(caller, { method: "POST", path: `${projectPath3(proj2)}/directory-packet`, body: { platforms, ...probes ? { probes } : {} }, summary: "Submission packet" });
12593
13251
  const dir = opts.out ?? process.cwd();
12594
- const mdPath = import_path6.default.join(dir, `connector-packet-${proj2.projectId}.md`);
12595
- const jsonPath = import_path6.default.join(dir, `connector-packet-${proj2.projectId}.json`);
12596
- import_fs8.default.writeFileSync(mdPath, packet.markdown, { mode: 384 });
12597
- import_fs8.default.writeFileSync(jsonPath, JSON.stringify(packet.json, null, 2) + "\n", { mode: 384 });
13252
+ import_fs9.default.mkdirSync(dir, { recursive: true });
13253
+ const mdPath = import_path7.default.join(dir, `connector-packet-${proj2.projectId}.md`);
13254
+ const jsonPath = import_path7.default.join(dir, `connector-packet-${proj2.projectId}.json`);
13255
+ import_fs9.default.writeFileSync(mdPath, packet.markdown, { mode: 384 });
13256
+ import_fs9.default.writeFileSync(jsonPath, JSON.stringify(packet.json, null, 2) + "\n", { mode: 384 });
13257
+ let zipPath = null;
13258
+ if (platforms.includes("chatgpt")) {
13259
+ const { writePluginPackage: writePluginPackage2 } = await Promise.resolve().then(() => (init_plugin_package(), plugin_package_exports));
13260
+ const manifest = packet.json.chatgpt;
13261
+ if (manifest) {
13262
+ const written = await writePluginPackage2(dir, manifest);
13263
+ if (written.problem) say(import_chalk30.default.yellow(` ${written.problem}`));
13264
+ zipPath = written.zipPath;
13265
+ if (written.seq) await patchProject(caller, proj2, { directory_metadata: { plugin_package_seq: written.seq } }, "Bump the plugin package version").catch(() => null);
13266
+ }
13267
+ }
12598
13268
  if (json) {
12599
13269
  process.stdout.write(JSON.stringify({ ready: true, project: proj2.projectId, headline: r.rendered.headline, report: r.report, packet: { markdown: mdPath, json: jsonPath }, portals: PORTALS, ...reviewer ? { reviewer } : {} }) + "\n");
12600
13270
  return;
@@ -12607,17 +13277,51 @@ async function runConnector(target2, opts) {
12607
13277
  }
12608
13278
  console.log();
12609
13279
  console.log(import_chalk30.default.green.bold(` ${r.rendered.headline}`));
12610
- console.log(` Submission packet: ${import_chalk30.default.cyan(mdPath)} and ${import_chalk30.default.cyan(jsonPath)}`);
13280
+ console.log(` Submission packet: ${import_chalk30.default.cyan(mdPath)} and ${import_chalk30.default.cyan(jsonPath)}${zipPath ? `
13281
+ OpenAI plugin package: ${import_chalk30.default.cyan(zipPath)}` : ""}`);
12611
13282
  console.log(import_chalk30.default.dim(isOpenMode(r) ? " (open mode: no reviewer credentials in the packet; the connector takes no sign-in)" : " (the packet carries the reviewer credentials \u2014 keep it out of git)"));
12612
- printSubmitSteps(r, mdPath, platforms);
13283
+ printSubmitSteps(r, mdPath, platforms, watching);
12613
13284
  console.log(import_chalk30.default.dim(` ${r.report.disclaimer}`));
12614
13285
  }
13286
+ async function waitForClaudeLogin(caller, proj2, r, platforms, interactive2, say, deps = {}) {
13287
+ const rule = r.report.results.find((x) => x.id === "OAUTH_FLOW_COMPLETED");
13288
+ if (!rule || rule.pass || !interactive2 || isOpenMode(r)) return r;
13289
+ const url = r.report.project.publishedUrl ?? "";
13290
+ const name = r.target?.metadata?.serverName ?? proj2.projectId;
13291
+ const alias = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || proj2.projectId;
13292
+ say("");
13293
+ say(import_chalk30.default.bold(" One sign-in from a real Claude client is the last proof (OAUTH_FLOW_COMPLETED). In another terminal:"));
13294
+ say(` claude mcp add --transport http ${alias} ${url}`);
13295
+ say(` claude then /mcp \u2192 ${alias} \u2192 sign in with the reviewer account`);
13296
+ say(import_chalk30.default.dim(` or in claude.ai: Settings \u2192 Connectors \u2192 Add custom connector \u2192 ${url} (what a reviewer does).`));
13297
+ say(import_chalk30.default.dim(` Afterwards: claude mcp remove ${alias}`));
13298
+ const { default: inquirer3 } = await import("inquirer");
13299
+ const { go } = await inquirer3.prompt([{ type: "confirm", name: "go", message: "Sign in from Claude now, then press Enter to check (n skips; the verdict stays red on this rule):", default: true }]);
13300
+ if (!go) return r;
13301
+ const sleep2 = deps.sleep ?? ((ms2) => new Promise((res) => setTimeout(res, ms2)));
13302
+ const maxMs = deps.maxMs ?? 10 * 6e4;
13303
+ const started = Date.now();
13304
+ let cur = r;
13305
+ for (; ; ) {
13306
+ cur = await fetchDirectoryReport(caller, proj2, { platforms });
13307
+ const now = cur.report.results.find((x) => x.id === "OAUTH_FLOW_COMPLETED");
13308
+ if (now?.pass) {
13309
+ say(import_chalk30.default.green(` \u2713 ${now.message ?? "a Claude client completed the sign-in"}`));
13310
+ return cur;
13311
+ }
13312
+ if (Date.now() - started >= maxMs) {
13313
+ say(import_chalk30.default.yellow(" \u2013 no Claude sign-in seen in 10 minutes; run again after signing in."));
13314
+ return cur;
13315
+ }
13316
+ await sleep2(2e4);
13317
+ }
13318
+ }
12615
13319
  async function rewriteFailedPrompts(r, failed, save, say) {
12616
13320
  const current = currentListingValue(r, "prompt");
12617
13321
  const list = Array.isArray(current) ? [...current] : [];
12618
13322
  const { default: inquirer3 } = await import("inquirer");
12619
13323
  const rewritten = [];
12620
- say(import_chalk30.default.yellow(` ${failed.length === 1 ? "This prompt" : `${failed.length} prompts`} made Claude call no tool; a prompt should name what the tool does${failed.length === 1 ? "" : ", one at a time"}:`));
13324
+ say(import_chalk30.default.yellow(` ${failed.length === 1 ? "This prompt" : `${failed.length} prompts`} produced no tool call; a prompt should name what the tool does${failed.length === 1 ? "" : ", one at a time"}:`));
12621
13325
  for (const old of failed) {
12622
13326
  const { value } = await inquirer3.prompt([{ type: "input", name: "value", message: `Rewrite "${old}" (Enter keeps it):`, default: old, validate: (v) => v.trim().length <= 300 ? true : `${v.trim().length} characters; 300 at most` }]);
12623
13327
  const next = (value ?? "").trim();
@@ -12647,12 +13351,12 @@ async function packetProbes(r) {
12647
13351
  return null;
12648
13352
  }
12649
13353
  }
12650
- async function offerDrafts(caller, proj2, r, ran, interactive2, say) {
13354
+ async function offerDrafts(caller, proj2, r, ran, interactive2, say, platforms = PLATFORMS) {
12651
13355
  if (!ran || !Array.isArray(ran.rows) || !ran.publishedUrl) return false;
12652
- const { draftMissing: draftMissing2, ANSWER_MIN: ANSWER_MIN2 } = await Promise.resolve().then(() => (init_portal_drafts(), portal_drafts_exports));
13356
+ const { draftMissing: draftMissing2, draftChatgptTestCases: draftChatgptTestCases2, ANSWER_MIN: ANSWER_MIN2 } = await Promise.resolve().then(() => (init_portal_drafts(), portal_drafts_exports));
12653
13357
  const meta = r.target?.metadata ?? {};
12654
13358
  const prompts = (meta.examplePrompts ?? []).filter((p) => typeof p === "string" && !!p.trim());
12655
- const drafts = draftMissing2({
13359
+ const input = {
12656
13360
  publishedUrl: ran.publishedUrl,
12657
13361
  open: ran.open,
12658
13362
  reviewerUsername: ran.reviewerUsername,
@@ -12660,15 +13364,32 @@ async function offerDrafts(caller, proj2, r, ran, interactive2, say) {
12660
13364
  rows: ran.rows,
12661
13365
  prompts,
12662
13366
  promptTools: ran.promptTools,
13367
+ codexPromptTools: ran.codexPromptTools ?? {},
12663
13368
  serverName: meta.serverName,
12664
13369
  tagline: meta.tagline,
12665
- description: meta.description
12666
- }, { use_cases: meta.portalAnswers?.use_cases, reviewer_instructions: meta.portalAnswers?.reviewer_instructions, description: meta.description });
12667
- if (!drafts.length) return false;
12668
- const inquirer3 = interactive2 ? (await import("inquirer")).default : null;
13370
+ description: meta.description,
13371
+ platforms,
13372
+ negativePrompts: meta.portalAnswers?.chatgpt_test_cases?.negative ?? []
13373
+ };
12669
13374
  let saved = false;
13375
+ if (platforms.includes("chatgpt") && Object.keys(ran.codexPromptTools ?? {}).length) {
13376
+ const cases = draftChatgptTestCases2(input);
13377
+ const have = (meta.portalAnswers?.chatgpt_test_cases?.positive ?? []).length;
13378
+ if (cases.positive.length && cases.positive.length !== have) {
13379
+ try {
13380
+ await patchProject(caller, proj2, { directory_portal_answers: { chatgpt_test_cases: cases } }, "Save OpenAI's test cases");
13381
+ saved = true;
13382
+ say(import_chalk30.default.green(` \u2713 OpenAI's positive test cases written from the Codex run: ${cases.positive.length}/5${cases.positive.length < 5 ? " (more prompts that call a tool are needed for five)" : ""}`));
13383
+ } catch (e) {
13384
+ say(import_chalk30.default.yellow(` not saved: ${e instanceof Error ? e.message : String(e)}`));
13385
+ }
13386
+ }
13387
+ }
13388
+ const drafts = platforms.includes("claude") ? draftMissing2(input, { use_cases: meta.portalAnswers?.use_cases, reviewer_instructions: meta.portalAnswers?.reviewer_instructions, description: meta.description }) : draftMissing2(input, { use_cases: "x".repeat(ANSWER_MIN2.use_cases), reviewer_instructions: "x".repeat(ANSWER_MIN2.reviewer_instructions), description: meta.description });
13389
+ if (!drafts.length) return saved;
13390
+ const inquirer3 = interactive2 ? (await import("inquirer")).default : null;
12670
13391
  say("");
12671
- say(import_chalk30.default.bold(` Written from the run \u2014 the portal's ${drafts.map((d) => d.label.toLowerCase()).join(", ")}${drafts.length > 1 ? "" : ""}:`));
13392
+ say(import_chalk30.default.bold(` Written from the run \u2014 the portal's ${drafts.map((d) => d.label.toLowerCase()).join(", ")}:`));
12672
13393
  for (const d of drafts) {
12673
13394
  let text = d.draft;
12674
13395
  say("");
@@ -12694,10 +13415,10 @@ async function offerDrafts(caller, proj2, r, ran, interactive2, say) {
12694
13415
  }
12695
13416
  var SNAPSHOT_RULE = "LISTING_SNAPSHOT_MATCH";
12696
13417
  var withProject = (s, projectId) => s.replace(/<project>/g, projectId);
12697
- var ANSWER_RULES = ["PORTAL_ANSWERS_COMPLETE", "HEALTH_SPONSORED_DECLARED", "API_OWNERSHIP_DOMAIN"];
13418
+ var ANSWER_RULES = ["PORTAL_ANSWERS_COMPLETE", "HEALTH_SPONSORED_DECLARED", "API_OWNERSHIP_DOMAIN", "WEBSITE_URL_HTTPS_200", "SUPPORT_URL_HTTPS_200", "TERMS_URL_HTTPS_200", "DEVELOPER_NAME_SET", "CATEGORY_SET", "DEMO_RECORDING_URL_SET", "RELEASE_NOTES_SET", "SHORT_DESCRIPTION_SET", "CHATGPT_TEST_CASES_COMPLETE"];
12698
13419
  function exitWithReport(r, proj2, platforms, json, interactive2 = false) {
12699
13420
  if (json) {
12700
- process.stdout.write(JSON.stringify({ ready: false, project: proj2.projectId, headline: r.rendered.headline, report: r.report }) + "\n");
13421
+ writeJsonLine({ ready: false, project: proj2.projectId, headline: r.rendered.headline, report: r.report });
12701
13422
  process.exit(1);
12702
13423
  }
12703
13424
  printReport(r, false);
@@ -12708,11 +13429,77 @@ function exitWithReport(r, proj2, platforms, json, interactive2 = false) {
12708
13429
  console.error(import_chalk30.default.red(`
12709
13430
  Not ready: ${blocking.length} blocker${blocking.length === 1 ? "" : "s"} (${blocking.map((x) => x.id).join(", ")}).`));
12710
13431
  const name = proj2.projectId;
12711
- if (real.some((x) => ANSWER_RULES.includes(x.id))) console.error(` Next: run \`${B.cli} connector ${name}\` in a terminal with the exercise on \u2014 it asks what only you can answer and writes the use cases, the reviewer instructions and the description from the run; or pass them: \`${B.cli} connector answers ${name} --use-cases \u2026 --data-handling own --health-data no --sponsored-content no\`.`);
13432
+ if (!interactive2) {
13433
+ const flags = flagsForRules(real.map((x) => x.id), platforms);
13434
+ if (flags.length) console.error(` Non-interactive: pass ${flags.join(" ")} to \`${B.cli} connector ${name} --yes\` (or run it in a terminal to be asked).`);
13435
+ }
13436
+ const answerish = real.filter((x) => ANSWER_RULES.includes(x.id));
13437
+ if (answerish.length && answerish.every((x) => !x.platforms.includes("claude"))) console.error(` Next: ${withProject(answerish[0].fix ?? answerish[0].message ?? answerish[0].title, name)}`);
13438
+ else if (answerish.length) console.error(` Next: run \`${B.cli} connector ${name}${platforms.includes("chatgpt") ? ` --compliant ${platforms.length > 1 ? "all" : "chatgpt"}` : ""}\` in a terminal with the exercise on \u2014 it asks what only you can answer and writes the use cases, the reviewer instructions and the description from the run; or pass them: \`${B.cli} connector answers ${name} --use-cases \u2026 --data-handling own --health-data no --sponsored-content no\`.`);
12712
13439
  else if (first) console.error(` Next: ${withProject(first.fix ?? first.message ?? first.title, name)}`);
12713
13440
  console.error(import_chalk30.default.dim(` Report page: ${DASHBOARD_BASE6}/directory/${encodeURIComponent(proj2.projectId)}/${encodeURIComponent(proj2.apiVersion)}`));
12714
13441
  process.exit(1);
12715
13442
  }
13443
+ function flagsForRules(ids, platforms) {
13444
+ const out = [];
13445
+ const kebab = (k) => `--${k.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}`;
13446
+ const seen = /* @__PURE__ */ new Set();
13447
+ const add = (f2) => {
13448
+ if (!seen.has(f2)) {
13449
+ seen.add(f2);
13450
+ out.push(f2);
13451
+ }
13452
+ };
13453
+ for (const id of ids) {
13454
+ const q = PROMPT_FOR_RULE[id];
13455
+ if (q) {
13456
+ add(`${kebab(q.key)} <\u2026>`);
13457
+ continue;
13458
+ }
13459
+ if (id.startsWith("ATTEST_")) {
13460
+ const a = ATTESTATIONS.find((x) => `ATTEST_${x.id.toUpperCase()}` === id || id === `ATTEST_${x.flags[0].replace(/-/g, "_").toUpperCase()}`);
13461
+ add(a ? `--attest ${a.flags[0]}` : "--attest <\u2026>");
13462
+ continue;
13463
+ }
13464
+ switch (id) {
13465
+ case "HEALTH_SPONSORED_DECLARED":
13466
+ add("--health-data yes|no");
13467
+ add("--sponsored-content yes|no");
13468
+ break;
13469
+ case "HIGH_RISK_DECLARED":
13470
+ add("--high-risk none|legal,\u2026");
13471
+ break;
13472
+ case "TOOL_OPENWORLD_CONFIRMED":
13473
+ add("--open-world yes|no");
13474
+ break;
13475
+ case "REVIEWER_ACCOUNT_POPULATED":
13476
+ add("--seed");
13477
+ break;
13478
+ case "API_OWNERSHIP_DOMAIN":
13479
+ add('--data-handling own|proxied "<permission>"');
13480
+ break;
13481
+ case "DOCS_URL":
13482
+ case "DOCS_PAGE_LIVE":
13483
+ case "DOCS_IS_PROSE":
13484
+ add("--docs-url <https://\u2026>");
13485
+ break;
13486
+ case "PORTAL_ANSWERS_COMPLETE":
13487
+ add(`(then \`connector answers\` with --use-cases --prerequisites --access --company-name --company-website --company-contact)`);
13488
+ break;
13489
+ case "CHATGPT_TEST_CASES_COMPLETE":
13490
+ case "NEGATIVE_PROMPTS_HOLD":
13491
+ add("--negative-prompt <\u2026> (three)");
13492
+ break;
13493
+ case "OPENAI_DOMAIN_CHALLENGE":
13494
+ add("--openai-challenge <token>");
13495
+ break;
13496
+ default:
13497
+ break;
13498
+ }
13499
+ }
13500
+ void platforms;
13501
+ return out;
13502
+ }
12716
13503
  var TOOL_DESC_RULE = "TOOL_DESC_NONEMPTY_SPECIFIC";
12717
13504
  var UTILITY_PATH = /(health|ping|status|openapi|swagger|docs|spec|whoami|version)/i;
12718
13505
  var SPECIFIC_MIN = 12;
@@ -12725,7 +13512,10 @@ function needsDescription(t) {
12725
13512
  if (!d || d.length < SPECIFIC_MIN) return true;
12726
13513
  const m = (t["x-route"]?.method ?? "get").toUpperCase();
12727
13514
  const p = t["x-route"]?.path ?? "";
12728
- return new RegExp(`^(${m}\\s+${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|${t.name})\\.?$`, "i").test(d);
13515
+ if (new RegExp(`^(${m}\\s+${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|${t.name})\\.?$`, "i").test(d)) return true;
13516
+ const norm = (x) => x.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
13517
+ const title = (t.summary ?? "").trim() || t.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ");
13518
+ return norm(d) === norm(title) || d.length < 40;
12729
13519
  }
12730
13520
  function derivedDescription(t) {
12731
13521
  const m = (t["x-route"]?.method ?? "get").toUpperCase();
@@ -12735,9 +13525,40 @@ function derivedDescription(t) {
12735
13525
  const plural = segments.filter((s) => !s.startsWith("{")).pop()?.replace(/[-_]/g, " ") ?? "result";
12736
13526
  const one = segments[segments.length - 1]?.startsWith("{");
12737
13527
  const noun = one ? plural.replace(/s$/, "") : plural;
12738
- const returns = m === "GET" ? `Returns the ${noun} as JSON.` : m === "POST" ? `Creates it and returns the new ${noun.replace(/s$/, "")} as JSON.` : m === "DELETE" ? `Removes it and returns the result.` : `Applies the change and returns the updated ${noun.replace(/s$/, "")} as JSON.`;
13528
+ const props = t.input_schema?.properties ?? {};
13529
+ const required = t.input_schema?.required ?? [];
13530
+ const firstParam = required.map((k) => [k, props[k]]).find(([, v]) => v && typeof v.description === "string" && v.description.trim()) ?? Object.entries(props).find(([, v]) => v && typeof v.description === "string" && v.description.trim());
13531
+ const takes = firstParam ? ` Takes ${firstParam[0]}: ${firstParam[1].description.trim().replace(/\.$/, "")}.` : "";
13532
+ const returns = m === "GET" ? `Returns the ${noun} as JSON.${takes}` : m === "POST" ? `Creates it and returns the new ${noun.replace(/s$/, "")} as JSON.` : m === "DELETE" ? `Removes it and returns the result.` : `Applies the change and returns the updated ${noun.replace(/s$/, "")} as JSON.`;
12739
13533
  return `${summary.replace(/\.$/, "")}. ${m} ${p}. ${returns}`;
12740
13534
  }
13535
+ async function confirmOpenWorld(proj2, environment, interactive2, flag, openMode, say) {
13536
+ const { loadCatalogue: loadCatalogue2, saveCatalogue: saveCatalogue2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
13537
+ const t = { projectId: proj2.projectId, apiVersion: proj2.apiVersion, environment: environment || "dev" };
13538
+ let cat;
13539
+ try {
13540
+ cat = await loadCatalogue2(t);
13541
+ } catch {
13542
+ return false;
13543
+ }
13544
+ const unset = cat.tools.map((x) => x.name).filter((n) => typeof cat.overrides?.[n]?.openWorld !== "boolean");
13545
+ if (!unset.length) return false;
13546
+ let answer = flag ? /^(y|yes|true|1)$/i.test(flag.trim()) : null;
13547
+ if (answer === null && !interactive2) {
13548
+ say(import_chalk30.default.dim(` openWorldHint not answered for ${unset.length} tool${unset.length === 1 ? "" : "s"} (pass --open-world yes|no)`));
13549
+ return false;
13550
+ }
13551
+ if (answer === null) {
13552
+ const { default: inquirer3 } = await import("inquirer");
13553
+ const { ow } = await inquirer3.prompt([{ type: "confirm", name: "ow", message: `Do the tools reach public or open-ended data \u2014 web pages, other people's records \u2014 (y), or only the signed-in account's own data (n)? Applies to ${unset.length === cat.tools.length ? "every tool" : `${unset.length} tools`}; change one later with \`${B.cli} mcp ops ${proj2.projectId} set <tool> --open-world|--no-open-world\`.`, default: openMode }]);
13554
+ answer = ow;
13555
+ }
13556
+ cat.overrides = { ...cat.overrides ?? {} };
13557
+ for (const n of unset) cat.overrides[n] = { ...cat.overrides[n] ?? {}, openWorld: answer };
13558
+ await saveCatalogue2(t, cat);
13559
+ say(import_chalk30.default.green(` \u2713 openWorldHint ${answer ? "true" : "false"} on ${unset.length === cat.tools.length ? "every tool" : unset.join(", ")}`));
13560
+ return true;
13561
+ }
12741
13562
  async function fixToolDescriptions(proj2, environment, interactive2, say) {
12742
13563
  const { loadCatalogue: loadCatalogue2, saveCatalogue: saveCatalogue2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
12743
13564
  const t = { projectId: proj2.projectId, apiVersion: proj2.apiVersion, environment: environment || "dev" };
@@ -12825,17 +13646,17 @@ async function headStatus(u) {
12825
13646
  return 0;
12826
13647
  }
12827
13648
  }
12828
- function printSubmitSteps(r, mdPath, platforms = PLATFORMS) {
13649
+ function printSubmitSteps(r, mdPath, platforms = PLATFORMS, watching = false) {
12829
13650
  const url = r.report.project.publishedUrl ?? "(the MCP URL in the packet)";
12830
13651
  const open = isOpenMode(r);
12831
13652
  const claude = platforms.includes("claude");
12832
13653
  const chatgpt = platforms.includes("chatgpt");
12833
- const where = [claude ? "Claude (Settings \u2192 Connectors)" : null, chatgpt ? "ChatGPT (Developer mode)" : null].filter(Boolean).join(" and in ");
13654
+ const where = [claude ? "Claude (Settings \u2192 Connectors)" : null, chatgpt ? "ChatGPT (chatgpt.com/plugins \u2192 Add custom MCP server)" : null].filter(Boolean).join(" and in ");
12834
13655
  const steps = [
12835
13656
  `Test it yourself first: add ${import_chalk30.default.cyan(url)} as a custom connector in ${where}, ${open ? "no sign-in needed (open mode)" : "sign in with the reviewer account"}, run a tool.`,
12836
- ...claude ? [`Claude: ${import_chalk30.default.underline(PORTALS.claude)} \u2192 Submit new \u2192 MCP connector. Paste the listing, ${open ? "Authentication: None with the access instructions" : "the reviewer credentials"} and the test steps from ${import_chalk30.default.cyan(import_path6.default.basename(mdPath))}.`] : [],
12837
- ...chatgpt ? [`ChatGPT: ${import_chalk30.default.underline(PORTALS.chatgpt)} \u2192 ${claude ? "the same packet" : `the packet (${import_chalk30.default.cyan(import_path6.default.basename(mdPath))})`}, plus the 5 + 3 test cases it lists.`] : [],
12838
- open ? "A nightly re-check emails you if the listing drifts." : "Keep the reviewer account alive; a nightly re-check emails you if the listing drifts."
13657
+ ...claude ? [`Claude: ${import_chalk30.default.underline(PORTALS.claude)} \u2192 Submit new \u2192 MCP connector. Paste the listing, ${open ? "Authentication: None with the access instructions" : "the reviewer credentials"} and the test steps from ${import_chalk30.default.cyan(import_path7.default.basename(mdPath))}.`] : [],
13658
+ ...chatgpt ? [`ChatGPT: ${import_chalk30.default.underline(PORTALS.chatgpt)} \u2192 upload ${import_chalk30.default.cyan(`connector-plugin-${r.report.project.id}.zip`)} \u2192 MCPs \u2192 Connect \u2192 Verify Domain \u2192 authorize \u2192 Scan Tools; paste the reviewer credentials under Review details; add the demo recording URL and the release notes; confirm the attestations. The MCP hostname is permanent for the OpenAI listing.`] : [],
13659
+ watching ? open ? "A nightly re-check is on: it emails the team owner if the listing drifts or a new check is added." : "Keep the reviewer account alive. A nightly re-check is on: it emails the team owner if the listing drifts or a new check is added." : open ? "The nightly re-check is off (--no-watch); turn it on in the dashboard to be emailed when the listing drifts." : "Keep the reviewer account alive. The nightly re-check is off (--no-watch); turn it on in the dashboard to be emailed when the listing drifts."
12839
13660
  ];
12840
13661
  console.log();
12841
13662
  console.log(import_chalk30.default.bold(" Submit (you do this part; the packet has every value to paste):"));
@@ -12848,15 +13669,15 @@ init_connector_exercise();
12848
13669
  init_connector_docs();
12849
13670
 
12850
13671
  // src/commands/skill.ts
12851
- var import_fs11 = __toESM(require("fs"));
13672
+ var import_fs12 = __toESM(require("fs"));
12852
13673
  var import_os4 = __toESM(require("os"));
12853
- var import_path9 = __toESM(require("path"));
13674
+ var import_path10 = __toESM(require("path"));
12854
13675
  var import_chalk34 = __toESM(require("chalk"));
12855
13676
  init_brand();
12856
13677
 
12857
13678
  // src/lib/detect-backend.ts
12858
- var import_fs9 = __toESM(require("fs"));
12859
- var import_path7 = __toESM(require("path"));
13679
+ var import_fs10 = __toESM(require("fs"));
13680
+ var import_path8 = __toESM(require("path"));
12860
13681
  var import_net = __toESM(require("net"));
12861
13682
  var NODE_SERVERS = [
12862
13683
  ["express", "Express", 3e3],
@@ -12887,9 +13708,9 @@ var COMMON_PORTS = [3e3, 3001, 4e3, 5e3, 8e3, 8080, 8787];
12887
13708
  var SPEC_PATHS = ["/openapi.json", "/openapi.yaml", "/docs/openapi.json", "/swagger.json", "/api/openapi.json"];
12888
13709
  function readText(file2, max = 256 * 1024) {
12889
13710
  try {
12890
- const st = import_fs9.default.statSync(file2);
13711
+ const st = import_fs10.default.statSync(file2);
12891
13712
  if (!st.isFile() || st.size > max) return null;
12892
- return import_fs9.default.readFileSync(file2, "utf8");
13713
+ return import_fs10.default.readFileSync(file2, "utf8");
12893
13714
  } catch {
12894
13715
  return null;
12895
13716
  }
@@ -12898,7 +13719,7 @@ function findSpecFile(dir) {
12898
13719
  for (const d of SPEC_DIRS) {
12899
13720
  for (const n of SPEC_NAMES) {
12900
13721
  const rel = d === "." ? n : `${d}/${n}`;
12901
- const text = readText(import_path7.default.join(dir, rel));
13722
+ const text = readText(import_path8.default.join(dir, rel));
12902
13723
  if (text && /\b(openapi|swagger)\s*["']?\s*:/.test(text)) {
12903
13724
  const m = /\b(?:localhost|127\.0\.0\.1):(\d{2,5})\b/.exec(text);
12904
13725
  return { file: `./${rel}`, ...m ? { port: Number(m[1]) } : {} };
@@ -12915,7 +13736,7 @@ function portFromScripts(scripts) {
12915
13736
  return void 0;
12916
13737
  }
12917
13738
  function fromNode(dir, out) {
12918
- const text = readText(import_path7.default.join(dir, "package.json"));
13739
+ const text = readText(import_path8.default.join(dir, "package.json"));
12919
13740
  if (!text) return;
12920
13741
  let pkg = {};
12921
13742
  try {
@@ -12981,7 +13802,7 @@ function portInSource(text) {
12981
13802
  }
12982
13803
  function fromEntryFiles(dir, out) {
12983
13804
  for (const rel of ENTRY_FILES) {
12984
- const text = readText(import_path7.default.join(dir, rel));
13805
+ const text = readText(import_path8.default.join(dir, rel));
12985
13806
  if (!text) continue;
12986
13807
  let fw;
12987
13808
  let def;
@@ -13018,7 +13839,7 @@ function fromEntryFiles(dir, out) {
13018
13839
  }
13019
13840
  function fromPython(dir, out) {
13020
13841
  for (const f2 of ["requirements.txt", "pyproject.toml", "Pipfile"]) {
13021
- const text = readText(import_path7.default.join(dir, f2));
13842
+ const text = readText(import_path8.default.join(dir, f2));
13022
13843
  if (!text) continue;
13023
13844
  const hit = PY_SERVERS.find(([re]) => re.test(text));
13024
13845
  if (hit) {
@@ -13030,7 +13851,7 @@ function fromPython(dir, out) {
13030
13851
  }
13031
13852
  }
13032
13853
  function fromOthers(dir, out) {
13033
- const gomod = readText(import_path7.default.join(dir, "go.mod"));
13854
+ const gomod = readText(import_path8.default.join(dir, "go.mod"));
13034
13855
  if (gomod) {
13035
13856
  const hit = GO_SERVERS.find(([re]) => re.test(gomod));
13036
13857
  out.framework = hit ? hit[1] : "a Go server";
@@ -13038,7 +13859,7 @@ function fromOthers(dir, out) {
13038
13859
  out.defaultPort = 8080;
13039
13860
  return;
13040
13861
  }
13041
- const gemfile = readText(import_path7.default.join(dir, "Gemfile"));
13862
+ const gemfile = readText(import_path8.default.join(dir, "Gemfile"));
13042
13863
  if (gemfile) {
13043
13864
  const hit = RUBY_SERVERS.find(([re]) => re.test(gemfile));
13044
13865
  if (hit) {
@@ -13049,7 +13870,7 @@ function fromOthers(dir, out) {
13049
13870
  }
13050
13871
  }
13051
13872
  for (const f2 of ["pom.xml", "build.gradle", "build.gradle.kts"]) {
13052
- const text = readText(import_path7.default.join(dir, f2));
13873
+ const text = readText(import_path8.default.join(dir, f2));
13053
13874
  if (text && /spring-boot/.test(text)) {
13054
13875
  out.framework = "Spring Boot";
13055
13876
  out.source = f2;
@@ -13085,7 +13906,7 @@ async function liveSpec(port) {
13085
13906
  async function detectBackend(dir, opts = {}) {
13086
13907
  const out = {};
13087
13908
  try {
13088
- if (import_fs9.default.existsSync(import_path7.default.join(dir, ".demo", "state.json"))) out.isDemo = true;
13909
+ if (import_fs10.default.existsSync(import_path8.default.join(dir, ".demo", "state.json"))) out.isDemo = true;
13089
13910
  fromNode(dir, out);
13090
13911
  if (!out.framework) fromPython(dir, out);
13091
13912
  if (!out.framework) fromOthers(dir, out);
@@ -13117,7 +13938,7 @@ function backendPort(f2) {
13117
13938
  // src/commands/skill.ts
13118
13939
  init_api();
13119
13940
  var SKILL_NAME = "ship-safely";
13120
- var SKILL_RELATIVE_PATH = import_path9.default.join(".claude", "skills", SKILL_NAME, "SKILL.md");
13941
+ var SKILL_RELATIVE_PATH = import_path10.default.join(".claude", "skills", SKILL_NAME, "SKILL.md");
13121
13942
  var CP_TOOLS = [
13122
13943
  "list_proxies",
13123
13944
  "propose_access_rules",
@@ -13191,9 +14012,9 @@ function staleSkillCopies(cwd = process.cwd(), home = import_os4.default.homedir
13191
14012
  const current = skillMarkdown();
13192
14013
  const out = [];
13193
14014
  for (const dir of /* @__PURE__ */ new Set([cwd, home])) {
13194
- const f2 = import_path9.default.join(dir, SKILL_RELATIVE_PATH);
14015
+ const f2 = import_path10.default.join(dir, SKILL_RELATIVE_PATH);
13195
14016
  try {
13196
- if (import_fs11.default.readFileSync(f2, "utf8") !== current) out.push(f2);
14017
+ if (import_fs12.default.readFileSync(f2, "utf8") !== current) out.push(f2);
13197
14018
  } catch {
13198
14019
  }
13199
14020
  }
@@ -13739,13 +14560,13 @@ async function runSkill(opts = {}) {
13739
14560
  }
13740
14561
  function installSkill(opts) {
13741
14562
  const md = skillMarkdown();
13742
- const target2 = import_path9.default.resolve(process.cwd(), SKILL_RELATIVE_PATH);
13743
- if (import_fs11.default.existsSync(target2) && !opts.force) {
14563
+ const target2 = import_path10.default.resolve(process.cwd(), SKILL_RELATIVE_PATH);
14564
+ if (import_fs12.default.existsSync(target2) && !opts.force) {
13744
14565
  console.error(import_chalk34.default.yellow(`${SKILL_RELATIVE_PATH} already exists \u2014 re-run with --force to overwrite it.`));
13745
14566
  process.exit(1);
13746
14567
  }
13747
- import_fs11.default.mkdirSync(import_path9.default.dirname(target2), { recursive: true });
13748
- import_fs11.default.writeFileSync(target2, md, "utf8");
14568
+ import_fs12.default.mkdirSync(import_path10.default.dirname(target2), { recursive: true });
14569
+ import_fs12.default.writeFileSync(target2, md, "utf8");
13749
14570
  console.log(`${import_chalk34.default.green("\u2713")} Installed the ${import_chalk34.default.bold(SKILL_NAME)} skill: ${import_chalk34.default.bold(target2)}`);
13750
14571
  console.log(import_chalk34.default.dim(` Claude Code picks it up in this project. Say "ship this API" and it runs npx ${B.cli}@latest dev / create for you.`));
13751
14572
  }
@@ -13876,8 +14697,8 @@ async function runLogout(opts = {}) {
13876
14697
 
13877
14698
  // src/commands/flush.ts
13878
14699
  var import_chalk37 = __toESM(require("chalk"));
13879
- var fs19 = __toESM(require("fs"));
13880
- var path16 = __toESM(require("path"));
14700
+ var fs21 = __toESM(require("fs"));
14701
+ var path17 = __toESM(require("path"));
13881
14702
  init_auth();
13882
14703
  init_external_mcp();
13883
14704
  async function runFlush(opts) {
@@ -13885,7 +14706,7 @@ async function runFlush(opts) {
13885
14706
  const demos = await stopDemos2().catch(() => 0);
13886
14707
  if (demos) console.log(import_chalk37.default.dim(` Stopped ${demos} running demo(s).`));
13887
14708
  const dir = getApiblazeDir();
13888
- const entries2 = fs19.existsSync(dir) ? fs19.readdirSync(dir) : [];
14709
+ const entries2 = fs21.existsSync(dir) ? fs21.readdirSync(dir) : [];
13889
14710
  if (!entries2.length) {
13890
14711
  console.log(import_chalk37.default.dim("Nothing to flush \u2014 no local apiblaze config found."));
13891
14712
  return;
@@ -13898,7 +14719,7 @@ async function runFlush(opts) {
13898
14719
  const footprint = apiblazeMcpFootprint();
13899
14720
  const mdFiles = ["claude", "codex"].map((k) => ({ k, file: agentInstructionFile(k) })).filter(({ file: file2 }) => {
13900
14721
  try {
13901
- return fs19.readFileSync(file2, "utf-8").includes("<!-- apiblaze:mcp:");
14722
+ return fs21.readFileSync(file2, "utf-8").includes("<!-- apiblaze:mcp:");
13902
14723
  } catch {
13903
14724
  return false;
13904
14725
  }
@@ -13907,7 +14728,7 @@ async function runFlush(opts) {
13907
14728
  console.log(import_chalk37.default.dim(` \u2022 MCP servers installed in other tools: ${[...footprint.claude, ...footprint.codex].join(", ")}`));
13908
14729
  }
13909
14730
  if (mdFiles.length) {
13910
- console.log(import_chalk37.default.dim(` \u2022 the "use this MCP" blocks in ${mdFiles.map((m) => path16.basename(m.file)).join(" and ")} (here)`));
14731
+ console.log(import_chalk37.default.dim(` \u2022 the "use this MCP" blocks in ${mdFiles.map((m) => path17.basename(m.file)).join(" and ")} (here)`));
13911
14732
  }
13912
14733
  console.log(import_chalk37.default.dim(" It does NOT delete server-side proxies/keys \u2014 those GC or are revoked separately."));
13913
14734
  if (!opts.yes) {
@@ -13923,7 +14744,7 @@ async function runFlush(opts) {
13923
14744
  let removed = 0;
13924
14745
  for (const e of entries2) {
13925
14746
  try {
13926
- fs19.rmSync(path16.join(dir, e), { recursive: true, force: true });
14747
+ fs21.rmSync(path17.join(dir, e), { recursive: true, force: true });
13927
14748
  removed++;
13928
14749
  } catch (err) {
13929
14750
  console.log(import_chalk37.default.red(` \u2717 could not remove ${e}: ${err instanceof Error ? err.message : "error"}`));
@@ -13938,14 +14759,14 @@ async function runFlush(opts) {
13938
14759
  for (const { k, file: file2 } of mdFiles) {
13939
14760
  let text = "";
13940
14761
  try {
13941
- text = fs19.readFileSync(file2, "utf-8");
14762
+ text = fs21.readFileSync(file2, "utf-8");
13942
14763
  } catch {
13943
14764
  continue;
13944
14765
  }
13945
14766
  const names = [...text.matchAll(/<!-- apiblaze:mcp:([^\s>]+) -->/g)].map((m) => m[1]);
13946
14767
  let cleared = 0;
13947
14768
  for (const n of new Set(names)) if (removeAgentInstruction(k, n)) cleared++;
13948
- if (cleared) console.log(` ${import_chalk37.default.dim("\u2022")} removed ${cleared} APIblaze block(s) from ${import_chalk37.default.bold(path16.basename(file2))}`);
14769
+ if (cleared) console.log(` ${import_chalk37.default.dim("\u2022")} removed ${cleared} APIblaze block(s) from ${import_chalk37.default.bold(path17.basename(file2))}`);
13949
14770
  }
13950
14771
  console.log(`${import_chalk37.default.green("\u2714")} Flushed \u2014 removed ${removed} item(s). You're fully logged out and clean.`);
13951
14772
  console.log(import_chalk37.default.dim(" Next `apiblaze create` starts a brand-new workspace with nothing carried over."));
@@ -14592,7 +15413,7 @@ function describeJob(job) {
14592
15413
  }
14593
15414
  async function pollJob(jobId, opts = {}) {
14594
15415
  const started = Date.now();
14595
- const path19 = jobPath(jobId, opts.teamId);
15416
+ const path20 = jobPath(jobId, opts.teamId);
14596
15417
  let lastPhase = "";
14597
15418
  if (!opts.quiet) process.stdout.write(import_chalk45.default.dim(opts.label ?? "Deleting"));
14598
15419
  for (; ; ) {
@@ -14604,7 +15425,7 @@ async function pollJob(jobId, opts = {}) {
14604
15425
  );
14605
15426
  }
14606
15427
  await new Promise((r) => setTimeout(r, POLL_MS));
14607
- const job = await admin({ method: "GET", path: path19, summary: "poll cascade job" });
15428
+ const job = await admin({ method: "GET", path: path20, summary: "poll cascade job" });
14608
15429
  if (!opts.quiet) {
14609
15430
  const phase = describeJob(job);
14610
15431
  if (phase !== lastPhase) {
@@ -14749,7 +15570,7 @@ async function runJobStatus(jobId, opts) {
14749
15570
 
14750
15571
  // src/commands/export.ts
14751
15572
  var import_chalk48 = __toESM(require("chalk"));
14752
- var import_fs12 = require("fs");
15573
+ var import_fs13 = require("fs");
14753
15574
  init_admin();
14754
15575
  init_auth();
14755
15576
  init_resolve();
@@ -14798,7 +15619,7 @@ async function runExport(projectArg, versionArg, opts) {
14798
15619
  });
14799
15620
  if (!res.ok) throw new Error(`Download failed (${res.status}): ${await res.text().catch(() => "")}`);
14800
15621
  const buf = Buffer.from(await res.arrayBuffer());
14801
- (0, import_fs12.writeFileSync)(outFile, buf);
15622
+ (0, import_fs13.writeFileSync)(outFile, buf);
14802
15623
  console.log(import_chalk48.default.green(`\u2713 Saved ${outFile} (${(buf.length / 1024).toFixed(0)} KB)`));
14803
15624
  if (job.decryption_key) console.log(import_chalk48.default.yellow(`Decryption key (shown once): ${job.decryption_key}`));
14804
15625
  console.log(import_chalk48.default.dim("Read MIGRATION-REPORT.json inside the zip for the full fidelity ledger."));
@@ -14855,8 +15676,8 @@ function resolveRecipeName(raw, githubHandle, proxyName) {
14855
15676
  function normalizeName3(raw) {
14856
15677
  return (raw || "").toLowerCase().replace(/[^a-z0-9]/g, "");
14857
15678
  }
14858
- async function publicFetch(path19) {
14859
- const res = await fetch(`${RECIPES_BASE}/api/recipes${path19}`, {
15679
+ async function publicFetch(path20) {
15680
+ const res = await fetch(`${RECIPES_BASE}/api/recipes${path20}`, {
14860
15681
  headers: { accept: "application/json" }
14861
15682
  }).catch((err) => {
14862
15683
  throw new Error(`Could not reach the recipe registry at ${RECIPES_BASE} \u2014 ${err.message}`);
@@ -14909,11 +15730,11 @@ async function fetchRecipeFile(ref, caller) {
14909
15730
  Private recipes are visible only to the publisher's team \u2014 run \`apiblaze login\` if you are on it.`
14910
15731
  );
14911
15732
  }
14912
- const path19 = revision !== void 0 ? `/recipes/${ref.handle}/${ref.slug}/${revision}` : `/recipes/${ref.handle}/${ref.slug}/file`;
15733
+ const path20 = revision !== void 0 ? `/recipes/${ref.handle}/${ref.slug}/${revision}` : `/recipes/${ref.handle}/${ref.slug}/file`;
14913
15734
  try {
14914
15735
  return await producer(caller, {
14915
15736
  method: "GET",
14916
- path: path19,
15737
+ path: path20,
14917
15738
  summary: `Read private recipe ${ref.name}${revision !== void 0 ? `@${revision}` : ""}`
14918
15739
  });
14919
15740
  } catch (err) {
@@ -16057,9 +16878,9 @@ function printEnvelopeViolations(violations) {
16057
16878
  console.log("bring-your-own-key case. Change the rules above, then publish again.");
16058
16879
  console.log();
16059
16880
  }
16060
- async function askPromotion(path19) {
16881
+ async function askPromotion(path20) {
16061
16882
  const { default: inquirer3 } = await import("inquirer");
16062
- const suggestedId = path19.split(/[.[\]]/).filter(Boolean).slice(-2).join("_").toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "value";
16883
+ const suggestedId = path20.split(/[.[\]]/).filter(Boolean).slice(-2).join("_").toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "value";
16063
16884
  const answers = await inquirer3.prompt([
16064
16885
  { type: "input", name: "prompt", message: " prompt them with:", validate: (v) => v.trim() ? true : "Required." },
16065
16886
  { type: "input", name: "example", message: " example:" },
@@ -16070,7 +16891,7 @@ async function askPromotion(path19) {
16070
16891
  const id = String(answers.id || suggestedId).toLowerCase().replace(/[^a-z0-9_]/g, "_");
16071
16892
  console.log(import_chalk51.default.dim(` \u2192 replaced with {{${id}}}`));
16072
16893
  return {
16073
- path: path19,
16894
+ path: path20,
16074
16895
  id,
16075
16896
  prompt: String(answers.prompt).trim(),
16076
16897
  ...answers.example ? { example: String(answers.example).trim() } : {},
@@ -17844,7 +18665,7 @@ async function providersMenu(cBase, clientLabel) {
17844
18665
  }
17845
18666
 
17846
18667
  // src/commands/spec.ts
17847
- var fs20 = __toESM(require("fs"));
18668
+ var fs22 = __toESM(require("fs"));
17848
18669
  var import_chalk62 = __toESM(require("chalk"));
17849
18670
  var import_ora27 = __toESM(require("ora"));
17850
18671
  init_admin();
@@ -17867,7 +18688,7 @@ async function runSpecSet(project, opts) {
17867
18688
  }
17868
18689
  let specContent;
17869
18690
  try {
17870
- specContent = fs20.readFileSync(opts.file, "utf-8");
18691
+ specContent = fs22.readFileSync(opts.file, "utf-8");
17871
18692
  } catch {
17872
18693
  console.error(import_chalk62.default.red(`Cannot read file: ${opts.file}`));
17873
18694
  process.exit(1);
@@ -18344,7 +19165,7 @@ async function patchSetting(proj2, s, value, cfg) {
18344
19165
  }
18345
19166
  const body = s.toPatch(value, cfg);
18346
19167
  const method = s.putLimits ? "PUT" : "PATCH";
18347
- const path19 = s.putLimits ? `/projects/${proj2.projectId}/${proj2.apiVersion}/limits` : `/projects/${proj2.projectId}/${proj2.apiVersion}`;
19168
+ const path20 = s.putLimits ? `/projects/${proj2.projectId}/${proj2.apiVersion}/limits` : `/projects/${proj2.projectId}/${proj2.apiVersion}`;
18348
19169
  if (s.needsTenant) {
18349
19170
  const tenant2 = proj2.tenant ?? cfg.tenant ?? cfg.default_tenant;
18350
19171
  if (!tenant2) throw new Error(`Could not tell which tenant ${proj2.projectName} serves \u2014 pass it via \`apiblaze config ${proj2.projectName} --tenant <slug>\` is not supported yet; run \`apiblaze projects\` and check the project has a tenant.`);
@@ -18354,7 +19175,7 @@ async function patchSetting(proj2, s, value, cfg) {
18354
19175
  try {
18355
19176
  await admin({
18356
19177
  method,
18357
- path: path19,
19178
+ path: path20,
18358
19179
  body,
18359
19180
  summary: `Set ${s.key} = ${JSON.stringify(value)}`
18360
19181
  });
@@ -19217,11 +20038,11 @@ async function resolveTransport(opts) {
19217
20038
  const token = getAccessToken();
19218
20039
  return {
19219
20040
  tenant: tenant2,
19220
- call: async (path19, method = "GET", body) => {
20041
+ call: async (path20, method = "GET", body) => {
19221
20042
  const res = await fetch(`${DASHBOARD_BASE11}/api/cli/iam`, {
19222
20043
  method: "POST",
19223
20044
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
19224
- body: JSON.stringify({ tenant: tenant2, path: path19, method, body })
20045
+ body: JSON.stringify({ tenant: tenant2, path: path20, method, body })
19225
20046
  });
19226
20047
  const data = await res.json().catch(() => ({}));
19227
20048
  if (!res.ok) throw new ApiError(res.status, data?.details ?? data?.error ?? res.statusText, data);
@@ -19239,8 +20060,8 @@ async function resolveTransport(opts) {
19239
20060
  return {
19240
20061
  tenant: fresh.tenant,
19241
20062
  selfEmail: fresh.email,
19242
- call: async (path19, method = "GET", body) => {
19243
- const res = await fetch(`${IAM_BASE}${path19}`, {
20063
+ call: async (path20, method = "GET", body) => {
20064
+ const res = await fetch(`${IAM_BASE}${path20}`, {
19244
20065
  method,
19245
20066
  headers: {
19246
20067
  "Content-Type": "application/json",
@@ -19692,28 +20513,28 @@ async function runConsumerApikeys(opts) {
19692
20513
  // src/commands/sidecar.ts
19693
20514
  var import_chalk68 = __toESM(require("chalk"));
19694
20515
  var import_ora33 = __toESM(require("ora"));
19695
- var fs21 = __toESM(require("fs"));
19696
- var path17 = __toESM(require("path"));
20516
+ var fs23 = __toESM(require("fs"));
20517
+ var path18 = __toESM(require("path"));
19697
20518
  init_admin();
19698
20519
  init_resolve();
19699
20520
  init_auth();
19700
20521
  function detectNextProject(root) {
19701
- const hasConfig = ["next.config.js", "next.config.mjs", "next.config.ts"].some((f2) => fs21.existsSync(path17.join(root, f2)));
20522
+ const hasConfig = ["next.config.js", "next.config.mjs", "next.config.ts"].some((f2) => fs23.existsSync(path18.join(root, f2)));
19702
20523
  let hasDep = false;
19703
20524
  try {
19704
- const pkg = JSON.parse(fs21.readFileSync(path17.join(root, "package.json"), "utf8"));
20525
+ const pkg = JSON.parse(fs23.readFileSync(path18.join(root, "package.json"), "utf8"));
19705
20526
  hasDep = !!(pkg.dependencies?.next || pkg.devDependencies?.next);
19706
20527
  } catch {
19707
20528
  }
19708
- const appDir = fs21.existsSync(path17.join(root, "app")) || fs21.existsSync(path17.join(root, "src", "app"));
19709
- const pagesDir = fs21.existsSync(path17.join(root, "pages")) || fs21.existsSync(path17.join(root, "src", "pages"));
20529
+ const appDir = fs23.existsSync(path18.join(root, "app")) || fs23.existsSync(path18.join(root, "src", "app"));
20530
+ const pagesDir = fs23.existsSync(path18.join(root, "pages")) || fs23.existsSync(path18.join(root, "src", "pages"));
19710
20531
  return { found: hasConfig || hasDep || appDir || pagesDir, router: appDir ? "app" : pagesDir ? "pages" : null };
19711
20532
  }
19712
20533
  function upsertEnvLocal(root, token) {
19713
- const p = path17.join(root, ".env.local");
20534
+ const p = path18.join(root, ".env.local");
19714
20535
  let existing = "";
19715
20536
  try {
19716
- existing = fs21.readFileSync(p, "utf8");
20537
+ existing = fs23.readFileSync(p, "utf8");
19717
20538
  } catch {
19718
20539
  }
19719
20540
  const had = /^APIBLAZE_API_KEY=/m.test(existing) || /^APIBLAZE_TOKEN=/m.test(existing);
@@ -19728,15 +20549,15 @@ function upsertEnvLocal(root, token) {
19728
20549
  next = (next.endsWith("\n") ? next : next + "\n") + `APIBLAZE_SIDECAR_VERBOSE=true
19729
20550
  `;
19730
20551
  }
19731
- fs21.writeFileSync(p, next);
20552
+ fs23.writeFileSync(p, next);
19732
20553
  return had ? "rotated" : "created";
19733
20554
  }
19734
20555
  function installSidecarPackage(root) {
19735
- if (fs21.existsSync(path17.join(root, "node_modules", "apiblaze", "package.json"))) {
20556
+ if (fs23.existsSync(path18.join(root, "node_modules", "apiblaze", "package.json"))) {
19736
20557
  console.log(` ${import_chalk68.default.green("\u2713")} apiblaze package already installed`);
19737
20558
  return;
19738
20559
  }
19739
- const has = (f2) => fs21.existsSync(path17.join(root, f2));
20560
+ const has = (f2) => fs23.existsSync(path18.join(root, f2));
19740
20561
  const pm = has("bun.lockb") || has("bun.lock") ? { cmd: "bun", add: "add" } : has("pnpm-lock.yaml") ? { cmd: "pnpm", add: "add" } : has("yarn.lock") ? { cmd: "yarn", add: "add" } : { cmd: "npm", add: "install" };
19741
20562
  const spinner = (0, import_ora33.default)(`Installing the apiblaze package (${pm.cmd})\u2026`).start();
19742
20563
  try {
@@ -19749,7 +20570,7 @@ function installSidecarPackage(root) {
19749
20570
  }
19750
20571
  function readEnvKey(root) {
19751
20572
  try {
19752
- const s = fs21.readFileSync(path17.join(root, ".env.local"), "utf8");
20573
+ const s = fs23.readFileSync(path18.join(root, ".env.local"), "utf8");
19753
20574
  const m = s.match(/^APIBLAZE_API_KEY=(.+)$/m) ?? s.match(/^APIBLAZE_TOKEN=(.+)$/m);
19754
20575
  return m ? m[1].trim() : null;
19755
20576
  } catch {
@@ -19757,16 +20578,16 @@ function readEnvKey(root) {
19757
20578
  }
19758
20579
  }
19759
20580
  function ensureGitignored(root) {
19760
- const p = path17.join(root, ".gitignore");
20581
+ const p = path18.join(root, ".gitignore");
19761
20582
  let c = "";
19762
20583
  try {
19763
- c = fs21.readFileSync(p, "utf8");
20584
+ c = fs23.readFileSync(p, "utf8");
19764
20585
  } catch {
19765
20586
  }
19766
- if (!/^\.env\.local$/m.test(c) && !/^\.env\*/m.test(c)) fs21.writeFileSync(p, (c && !c.endsWith("\n") ? c + "\n" : c) + ".env.local\n");
20587
+ if (!/^\.env\.local$/m.test(c) && !/^\.env\*/m.test(c)) fs23.writeFileSync(p, (c && !c.endsWith("\n") ? c + "\n" : c) + ".env.local\n");
19767
20588
  }
19768
20589
  function wireInstrumentation(root) {
19769
- const existing = ["instrumentation.ts", "instrumentation.js", path17.join("src", "instrumentation.ts")].map((c) => path17.join(root, c)).find((f2) => fs21.existsSync(f2));
20590
+ const existing = ["instrumentation.ts", "instrumentation.js", path18.join("src", "instrumentation.ts")].map((c) => path18.join(root, c)).find((f2) => fs23.existsSync(f2));
19770
20591
  const body = `import { register as apiblaze } from "apiblaze/sidecar";
19771
20592
 
19772
20593
  export function register() {
@@ -19774,18 +20595,18 @@ export function register() {
19774
20595
  }
19775
20596
  `;
19776
20597
  if (!existing) {
19777
- fs21.writeFileSync(path17.join(root, "instrumentation.ts"), body);
20598
+ fs23.writeFileSync(path18.join(root, "instrumentation.ts"), body);
19778
20599
  return "created";
19779
20600
  }
19780
- const cur = fs21.readFileSync(existing, "utf8");
20601
+ const cur = fs23.readFileSync(existing, "utf8");
19781
20602
  if (cur.includes("apiblaze/sidecar")) return "present";
19782
20603
  if (/export\s+function\s+register\s*\(/.test(cur)) {
19783
- fs21.writeFileSync(existing, `import { register as apiblaze } from "apiblaze/sidecar";
20604
+ fs23.writeFileSync(existing, `import { register as apiblaze } from "apiblaze/sidecar";
19784
20605
  ` + cur.replace(/export\s+function\s+register\s*\(\s*\)\s*\{/, (m) => `${m}
19785
20606
  apiblaze();`));
19786
20607
  return "patched";
19787
20608
  }
19788
- fs21.writeFileSync(existing, `import { register as apiblaze } from "apiblaze/sidecar";
20609
+ fs23.writeFileSync(existing, `import { register as apiblaze } from "apiblaze/sidecar";
19789
20610
  ${cur}
19790
20611
  // call apiblaze() inside your register() export.
19791
20612
  `);
@@ -19868,17 +20689,17 @@ export default async function Page() {
19868
20689
  function generateInspector(root, router) {
19869
20690
  try {
19870
20691
  if (router === "pages") {
19871
- const dir2 = fs21.existsSync(path17.join(root, "src", "pages")) ? path17.join(root, "src", "pages") : path17.join(root, "pages");
19872
- const f3 = path17.join(dir2, "abz-inspector.tsx");
19873
- fs21.writeFileSync(f3, INSPECTOR_PAGE);
19874
- return path17.relative(root, f3);
19875
- }
19876
- const base2 = fs21.existsSync(path17.join(root, "src", "app")) ? path17.join(root, "src", "app") : path17.join(root, "app");
19877
- const dir = path17.join(base2, "abz-inspector");
19878
- fs21.mkdirSync(dir, { recursive: true });
19879
- const f2 = path17.join(dir, "page.tsx");
19880
- fs21.writeFileSync(f2, INSPECTOR_PAGE);
19881
- return path17.relative(root, f2);
20692
+ const dir2 = fs23.existsSync(path18.join(root, "src", "pages")) ? path18.join(root, "src", "pages") : path18.join(root, "pages");
20693
+ const f3 = path18.join(dir2, "abz-inspector.tsx");
20694
+ fs23.writeFileSync(f3, INSPECTOR_PAGE);
20695
+ return path18.relative(root, f3);
20696
+ }
20697
+ const base2 = fs23.existsSync(path18.join(root, "src", "app")) ? path18.join(root, "src", "app") : path18.join(root, "app");
20698
+ const dir = path18.join(base2, "abz-inspector");
20699
+ fs23.mkdirSync(dir, { recursive: true });
20700
+ const f2 = path18.join(dir, "page.tsx");
20701
+ fs23.writeFileSync(f2, INSPECTOR_PAGE);
20702
+ return path18.relative(root, f2);
19882
20703
  } catch {
19883
20704
  return null;
19884
20705
  }
@@ -19918,7 +20739,7 @@ async function runAnonymousInit(root, router, opts) {
19918
20739
  console.log(import_chalk68.default.dim(` From another machine: apiblaze claim ${out.claim_code} \xB7 expires in 30 days`));
19919
20740
  }
19920
20741
  async function runSidecar(opts) {
19921
- const root = path17.resolve(opts.dir ?? process.cwd());
20742
+ const root = path18.resolve(opts.dir ?? process.cwd());
19922
20743
  const detected2 = detectNextProject(root);
19923
20744
  if (!detected2.found) {
19924
20745
  console.log(import_chalk68.default.yellow(`No Next.js project detected in ${root}.`));
@@ -19977,7 +20798,7 @@ async function runSidecar(opts) {
19977
20798
  console.log(` 2. The origins your app calls appear as ${import_chalk68.default.bold("candidates")} \u2014 list them: ${import_chalk68.default.cyan("apiblaze sidecar")}`);
19978
20799
  console.log(` 3. Approve the ones to route: ${import_chalk68.default.cyan("apiblaze sidecar approve api.stripe.com")} (or in the dashboard)`);
19979
20800
  console.log(` \u2026within ~5 min your app starts routing that origin through APIblaze.`);
19980
- if (inspectorPath) console.log(` \u2022 Try it now: open ${import_chalk68.default.underline("http://localhost:3000/abz-inspector")} (dev only; rm ${path17.dirname(inspectorPath)} before shipping)`);
20801
+ if (inspectorPath) console.log(` \u2022 Try it now: open ${import_chalk68.default.underline("http://localhost:3000/abz-inspector")} (dev only; rm ${path18.dirname(inspectorPath)} before shipping)`);
19981
20802
  if (switchingTeam) console.log(import_chalk68.default.dim(` \u2022 Approved origins are per-team \u2014 re-approve them on ${teamName ?? teamId} with \`apiblaze sidecar approve <origin>\`.`));
19982
20803
  console.log("");
19983
20804
  console.log(import_chalk68.default.dim(" Manage: apiblaze sidecar (list/approve/deny/remove)"));
@@ -20688,17 +21509,17 @@ var import_chalk74 = __toESM(require("chalk"));
20688
21509
  init_consumer_auth();
20689
21510
  var SANDBOX_TENANT = "portalteststenant";
20690
21511
  var SANDBOX_PROXY = "portaltestproxypreapproved@2.7.0";
20691
- function proxyUrlFor(spec2, path19 = "/get") {
21512
+ function proxyUrlFor(spec2, path20 = "/get") {
20692
21513
  const [name, version2 = "1.0.0"] = spec2.split("@");
20693
- return `https://${name}.abz.run/${version2}/prod${path19.startsWith("/") ? path19 : `/${path19}`}`;
21514
+ return `https://${name}.abz.run/${version2}/prod${path20.startsWith("/") ? path20 : `/${path20}`}`;
20694
21515
  }
20695
21516
  function isPortalAudience(aud) {
20696
21517
  return /^https:\/\/[a-z0-9-]+\.portal\.apiblaze\.com\b/i.test(aud);
20697
21518
  }
20698
21519
  var log2 = (s) => process.stderr.write(s + "\n");
20699
- function probeUrlFor(audience, path19 = "/") {
21520
+ function probeUrlFor(audience, path20 = "/") {
20700
21521
  const base2 = audience.replace(/\/+$/, "");
20701
- return `${base2}/prod${path19.startsWith("/") ? path19 : `/${path19}`}`;
21522
+ return `${base2}/prod${path20.startsWith("/") ? path20 : `/${path20}`}`;
20702
21523
  }
20703
21524
  function tamper(token) {
20704
21525
  const parts = token.split(".");
@@ -21630,7 +22451,7 @@ program.command("login").description(`Authenticate with ${B.product}`).option("-
21630
22451
  process.exit(1);
21631
22452
  }
21632
22453
  });
21633
- program.command("create").description(`Put an app that is already DEPLOYED behind ${B.product}: a public URL and an MCP address for AI agents in front of your URL or OpenAPI spec. Included: API keys, sign-in, ownership rules, rate limits. No login needed. (App on this machine? Use \`dev\`.) Add --compliant to make it a Claude connector, or a ChatGPT plugin with --compliant chatgpt, that meets the directory's policies.`).option("--name <name>", B.cli === "mcpblaze" ? "MCP server name (becomes <name>-<tenant>.mcpblaze.com)" : "Proxy name (becomes <name>.abz.run)").option("--target <url|file>", "What to proxy \u2014 pass ANY of: a target server base URL (https://httpbin.org), a local OpenAPI file (./openapi.yaml), or a remote OpenAPI URL (https://acme.com/openapi.yaml). Spec inputs are detected automatically; routes, API version and environments then come from the spec").addOption(new import_commander.Option("--openapi <file|url>", "Deprecated alias \u2014 --target now detects spec files/URLs itself").hideHelp()).addOption(new import_commander.Option("--openapispec <file|url>", "Deprecated alias for --openapi").hideHelp()).option("--team <id|name>", "Team to create under (defaults to your active team)").option("--auth <type>", "Open ONE door only: api_key | oauth (sign in with GitHub, APIblaze-hosted) | none. Omitted = both doors.").option("--apikey", "API-key door only (no sign-in): the bootstrap keys are minted and printed; callers send X-API-Key, agents connect with a key.").option("--oauth [config]", `Sign-in door only (no API key). Bare = APIblaze-hosted GitHub sign-in. '{"iss","aud","jwks"}' = trust YOUR hosted login's JWTs. '{"provider","clientId","clientSecret"?}' = APIblaze-hosted login page with YOUR OAuth app (github \xB7 google \xB7 microsoft \xB7 facebook \xB7 auth0 \xB7 oidc); the secret is optional except for github and facebook. '{"provider":"oidc","issuer":"https://acme.okta.com/oauth2/default","clientId":"\u2026"}' = any OpenID Connect IdP by issuer (Okta \xB7 Entra \xB7 Keycloak \xB7 Cognito \xB7 \u2026).`).option("--identified", "Require every call to identify its end user (X-End-User-Id from a backend key, or a login token \u2014 a client key alone cannot); unattributed calls are rejected").option("--iam", `Turn IAM on for the ${B.thingShort}'s tenant so users & groups apply to identified calls`).option("--apiversion <version>", `API version to create (e.g. 2.0.0). Creating a new version of a ${B.thingShort} you own adds a version to the existing project.`).option("--tenant <slug>", `Tenant to attach the ${B.thingShort} to (created if new). Omitted \u2192 your team's most-recently-used tenant.`).option("--product <slug>", "Product tag to group this project under in the portal (team-scoped; anonymous create). Defaults to your team's existing/placeholder tag.").option("--display-name <name>", "Human-friendly display name").option("--subdomain <slug>", "Explicit subdomain (defaults to --name)").option("--config <file>", "JSON file with the full request body (anonymous create): requests_auth, login providers + client/server token types, scopes, callback URLs, etc. See apiblaze_anonymous.yaml. Flags override its fields.").option("-y, --yes", "Skip the confirmation prompt").option("--auto", `Zero prompts, start to finish: generates the ${B.thingShort} name when --name is omitted (from the target host, salted), takes every confirmation as yes, auto-picks the tenant. Implies --yes.`).option("--new-session", "Start a fresh anonymous session (do not group with prior anonymous creates)").option("--json", "Output machine-readable JSON (non-interactive)").option("--compliant [directories...]", `Directory-ready in one command, to the letter of that directory's policies: create the ${B.thingShort} from --target (an OpenAPI URL/file), apply the safe fixes, set up the custom MCP domain (--domain, one CNAME), ask for every listing answer with the spec's proposal pre-filled (cut to the portal's limits), create the reviewer account, write the submission packet. Bare = claude; or claude \xB7 chatgpt \xB7 all. Ends with the compliance table: every rule the listing satisfies and the policy page it comes from. Same as \`${B.cli} connector <spec> --compliant \u2026\`.`).addOption(new import_commander.Option("--policy [directories...]", "Older spelling of --compliant").hideHelp()).option("--domain <host>", "With --compliant: the MCP server's custom hostname (mcp.<your domain>); asked for when missing, suggested from the spec").option("--wait <minutes>", "With --compliant: how long to wait for the custom domain to activate once its CNAME is in place (default 15; 0 = do not wait)").option("--auth-mode <mode>", "With --compliant: how directory clients get in. signin (default) = OAuth sign-in on the connector, API keys alongside; open = no sign-in at all, for public read-only data (Anthropic allows it) \u2014 the API host keeps its own doors. Asked once when every tool is read-only.").option("--data-handling <mode>", "Compliant: whose API this is (own \xB7 proxied + --data-handling-statement \xB7 third-party); never pre-filled").option("--data-handling-statement <text>", "Compliant, proxied: the permission you hold").option("--health-data <yes|no>", "Compliant: the tools handle health data (never pre-filled)").option("--sponsored-content <yes|no>", "Compliant: tool results carry sponsored content (never pre-filled)").option("--no-exercise", 'Compliant: skip the "Run every tool now?" step').option("--no-email", "Compliant: no email at the end of the run").option("--directory-ready", "Make it directory-ready for the Claude and ChatGPT directories: OAuth door, custom-domain checks, listing fields; the readiness report runs after the create and a blocker exits 1").option("--privacy-url <url>", "Directory listing: public https privacy policy").option("--support <email|url>", "Directory listing: support contact").option("--docs-url <url>", "Directory listing: public documentation URL (the developer portal by default)").option("--prompt <text>", "Directory listing: an example prompt (repeat the flag; three are needed)", collect, []).option("--server-name <name>", 'Directory listing: the name (2\u201340 chars, no "MCP"/"Plugin" suffix)').option("--tagline <text>", "Directory listing: one-line tagline").option("--description <text>", "Directory listing: description").option("--icon-url <url>", "Directory listing: square icon (https)").option("--vuln-contact <email|url>", "Directory listing: vulnerability-disclosure contact").option("--attest <id>", "Attest (repeatable): guidelines \xB7 own-backend \xB7 no-financial \xB7 no-genai-media \xB7 no-prompt-injection \xB7 no-conversation-data \xB7 public-docs (Claude's seven) \xB7 no-digital-goods \xB7 content-13-no-ads \xB7 no-restricted-data (ChatGPT)", collect, []).option("--allowed-origin <origin>", "Directory mode: an extra browser origin allowed on the MCP host (repeatable)", collect, []).option("--openai-challenge <token>", "ChatGPT: the domain-verification token the OpenAI portal shows under Plugins \u2192 Connect MCP server").action(async (opts) => {
22454
+ program.command("create").description(`Put an app that is already DEPLOYED behind ${B.product}: a public URL and an MCP address for AI agents in front of your URL or OpenAPI spec. Included: API keys, sign-in, ownership rules, rate limits. No login needed. (App on this machine? Use \`dev\`.) Add --compliant to make it a Claude connector, or a ChatGPT plugin with --compliant chatgpt, that meets the directory's policies.`).option("--name <name>", B.cli === "mcpblaze" ? "MCP server name (becomes <name>-<tenant>.mcpblaze.com)" : "Proxy name (becomes <name>.abz.run)").option("--target <url|file>", "What to proxy \u2014 pass ANY of: a target server base URL (https://httpbin.org), a local OpenAPI file (./openapi.yaml), or a remote OpenAPI URL (https://acme.com/openapi.yaml). Spec inputs are detected automatically; routes, API version and environments then come from the spec").addOption(new import_commander.Option("--openapi <file|url>", "Deprecated alias \u2014 --target now detects spec files/URLs itself").hideHelp()).addOption(new import_commander.Option("--openapispec <file|url>", "Deprecated alias for --openapi").hideHelp()).option("--team <id|name>", "Team to create under (defaults to your active team)").option("--auth <type>", "Open ONE door only: api_key | oauth (sign in with GitHub, APIblaze-hosted) | none. Omitted = both doors.").option("--apikey", "API-key door only (no sign-in): the bootstrap keys are minted and printed; callers send X-API-Key, agents connect with a key.").option("--oauth [config]", `Sign-in door only (no API key). Bare = APIblaze-hosted GitHub sign-in. '{"iss","aud","jwks"}' = trust YOUR hosted login's JWTs. '{"provider","clientId","clientSecret"?}' = APIblaze-hosted login page with YOUR OAuth app (github \xB7 google \xB7 microsoft \xB7 facebook \xB7 auth0 \xB7 oidc); the secret is optional except for github and facebook. '{"provider":"oidc","issuer":"https://acme.okta.com/oauth2/default","clientId":"\u2026"}' = any OpenID Connect IdP by issuer (Okta \xB7 Entra \xB7 Keycloak \xB7 Cognito \xB7 \u2026).`).option("--identified", "Require every call to identify its end user (X-End-User-Id from a backend key, or a login token \u2014 a client key alone cannot); unattributed calls are rejected").option("--iam", `Turn IAM on for the ${B.thingShort}'s tenant so users & groups apply to identified calls`).option("--apiversion <version>", `API version to create (e.g. 2.0.0). Creating a new version of a ${B.thingShort} you own adds a version to the existing project.`).option("--tenant <slug>", `Tenant to attach the ${B.thingShort} to (created if new). Omitted \u2192 your team's most-recently-used tenant.`).option("--product <slug>", "Product tag to group this project under in the portal (team-scoped; anonymous create). Defaults to your team's existing/placeholder tag.").option("--display-name <name>", "Human-friendly display name").option("--subdomain <slug>", "Explicit subdomain (defaults to --name)").option("--config <file>", "JSON file with the full request body (anonymous create): requests_auth, login providers + client/server token types, scopes, callback URLs, etc. See apiblaze_anonymous.yaml. Flags override its fields.").option("-y, --yes", "Skip the confirmation prompt").option("--auto", `Zero prompts, start to finish: generates the ${B.thingShort} name when --name is omitted (from the target host, salted), takes every confirmation as yes, auto-picks the tenant. Implies --yes.`).option("--new-session", "Start a fresh anonymous session (do not group with prior anonymous creates)").option("--json", "Output machine-readable JSON (non-interactive)").option("--compliant [directories...]", `Directory-ready in one command, to the letter of that directory's policies: create the ${B.thingShort} from --target (an OpenAPI URL/file), apply the safe fixes, set up the custom MCP domain (--domain, one CNAME), ask for every listing answer with the spec's proposal pre-filled (cut to the portal's limits), create the reviewer account, write the submission packet. Bare = claude; or claude \xB7 chatgpt \xB7 all. Ends with the compliance table: every rule the listing satisfies and the policy page it comes from. Same as \`${B.cli} connector <spec> --compliant \u2026\`.`).addOption(new import_commander.Option("--policy [directories...]", "Older spelling of --compliant").hideHelp()).option("--domain <host>", "With --compliant: the MCP server's custom hostname (mcp.<your domain>); asked for when missing, suggested from the spec").option("--wait <minutes>", "With --compliant: how long to wait for the custom domain to activate once its CNAME is in place (default 15; 0 = do not wait)").option("--auth-mode <mode>", "With --compliant: how directory clients get in. signin (default) = OAuth sign-in on the connector, API keys alongside; open = no sign-in at all, for public read-only data (Anthropic allows it) \u2014 the API host keeps its own doors. Asked once when every tool is read-only.").option("--data-handling <mode>", "Compliant: whose API this is (own \xB7 proxied + --data-handling-statement \xB7 third-party); never pre-filled").option("--data-handling-statement <text>", "Compliant, proxied: the permission you hold").option("--health-data <yes|no>", "Compliant: the tools handle health data (never pre-filled)").option("--sponsored-content <yes|no>", "Compliant: tool results carry sponsored content (never pre-filled)").option("--no-exercise", 'Compliant: skip the "Run every tool now?" step').option("--seed", "Sign-in mode: when a read returns nothing for the reviewer, create a few records through the create tools and keep them (asked when omitted)").option("--no-seed", "Never seed the reviewer account").option("--no-email", "Compliant: no email at the end of the run").option("--directory-ready", "Make it directory-ready for the Claude and ChatGPT directories: OAuth door, custom-domain checks, listing fields; the readiness report runs after the create and a blocker exits 1").option("--privacy-url <url>", "Directory listing: public https privacy policy").option("--support <email|url>", "Directory listing: support contact").option("--docs-url <url>", "Directory listing: public documentation URL (the developer portal by default)").option("--prompt <text>", "Directory listing: an example prompt (repeat the flag; three are needed)", collect, []).option("--server-name <name>", 'Directory listing: the name (2\u201340 chars, no "MCP"/"Plugin" suffix)').option("--tagline <text>", "Directory listing: one-line tagline").option("--description <text>", "Directory listing: description").option("--icon-url <url>", "Directory listing: square icon (https)").option("--slug <slug>", "Claude: the listing slug (lowercase-with-hyphens; permanent once published)").option("--high-risk <list>", "Claude policy 1A: none, or any of legal,healthcare,insurance,finance").option("--open-world <yes|no>", "Every tool: reaches public or open-ended data (yes) or only the signed-in account's (no); asked once when omitted").option("--no-watch", "Do not turn on the nightly re-check email when the listing is green").option("--no-wait-login", "Sign-in mode: do not wait for a Claude sign-in before the verdict").option("--mcp-app", "Claude: the listing is an MCP App (the host then advertises the UI extension; needs 3\u20135 --screenshot)").option("--no-mcp-app", "Claude: a plain connector (default)").option("--screenshot <url=prompt>", "Claude MCP App: a carousel screenshot (PNG \u2265 1000 px wide) and the prompt that produced it (repeatable, 3\u20135)", collect, []).option("--vuln-contact <email|url>", "Directory listing: vulnerability-disclosure contact").option("--attest <id>", "Attest (repeatable): guidelines \xB7 own-backend \xB7 no-financial \xB7 no-genai-media \xB7 no-prompt-injection \xB7 no-conversation-data \xB7 public-docs (Claude's seven) \xB7 no-digital-goods \xB7 content-13-no-ads \xB7 no-restricted-data (ChatGPT)", collect, []).option("--allowed-origin <origin>", "Directory mode: an extra browser origin allowed on the MCP host (repeatable)", collect, []).option("--openai-challenge <token>", "ChatGPT: the domain-verification token the OpenAI portal shows under Plugins \u2192 Connect MCP server").action(async (opts) => {
21634
22455
  try {
21635
22456
  const compliant = opts.compliant ?? opts.policy;
21636
22457
  if (compliant !== void 0 && compliant !== false) {
@@ -21650,15 +22471,15 @@ function collect(value, previous) {
21650
22471
  return [...previous ?? [], value];
21651
22472
  }
21652
22473
  program.command("check").description(`Readiness report for the Claude and ChatGPT directories (--directory-ready): what passes, every blocker with its fix, and the ${B.thingShort}'s published URL. Exit 1 while a blocker remains.`).argument("[project]", `The ${B.thingShort} (project name)`).option("--directory-ready", "The directory readiness checks (the only mode today)").option("--compliant [directory]", "Which directory's policies to check against: bare = claude; or claude \xB7 chatgpt \xB7 all (repeat or comma-separate)", collect, []).addOption(new import_commander.Option("--policy <directory>", "Older spelling of --compliant").hideHelp()).addOption(new import_commander.Option("--platform <list>", "Older spelling of --compliant").hideHelp()).option("--stage <stage>", "create (relaxed: docs URL is a warning) \xB7 ready (the submission gate, default)").option("--refresh-snapshot", "Re-take the listing snapshot (after you edited the listing in the portals)").option("--no-probe", "Skip the live probes from this machine (report what the server can see)").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Print the raw report JSON (report, rendered text, target)").action(action((project, opts) => runCheck(project, { ...opts, noProbe: opts.probe === false })));
21653
- var connector = program.command("connector").description(`A compliant Claude connector (--compliant chatgpt: a ChatGPT plugin), directory-ready in one command: from a project or straight from an OpenAPI URL \u2014 creates the ${B.thingShort} if needed, applies the safe fixes, sets up the custom MCP domain (one CNAME), pre-fills every listing answer from the spec, creates the reviewer account, and writes the submission packet when every check passes (you submit it to Anthropic and OpenAI yourself)`).argument("[project-or-spec]", `The ${B.thingShort} (project name), or an OpenAPI URL/file to create a directory-ready ${B.thingShort} from first`).option("--domain <host>", "Custom hostname for the MCP server (mcp.<your domain>); asked for when missing, suggested from the spec").option("--wait <minutes>", "How long to wait for the custom domain to activate once its CNAME is in place (default 15; 0 = do not wait)").option("--auth-mode <mode>", "How directory clients get in: signin (default) = OAuth sign-in on the connector, API keys alongside; open = no sign-in at all, for public read-only data (Anthropic allows it) \u2014 the API host keeps its own doors. Asked once when every tool is read-only.").option("--name <name>", `When creating from a spec: the ${B.thingShort} name (default: derived from the spec host)`).option("--privacy-url <url>", "Public https privacy policy").option("--support <email|url>", "Support contact").option("--docs-url <url>", "Public documentation URL").option("--prompt <text>", "An example prompt (repeatable; three are needed)", collect, []).option("--server-name <name>", 'Listing name (2\u201340 chars, no "MCP"/"Plugin" suffix)').option("--tagline <text>", "One-line tagline").option("--description <text>", "Description").option("--icon-url <url>", "Square icon (https)").option("--vuln-contact <email|url>", "Vulnerability-disclosure contact").option("--attest <id>", "Attest (repeatable): guidelines \xB7 own-backend \xB7 no-financial \xB7 no-genai-media \xB7 no-prompt-injection \xB7 no-conversation-data \xB7 public-docs (Claude's seven) \xB7 no-digital-goods \xB7 content-13-no-ads \xB7 no-restricted-data (ChatGPT). Alone, this only records the attestation.", collect, []).option("--allowed-origin <origin>", "An extra browser origin allowed on the MCP host (repeatable)", collect, []).option("--openai-challenge <token>", "ChatGPT: the domain-verification token the OpenAI portal shows under Plugins \u2192 Connect MCP server; served verbatim at /.well-known/openai-apps-challenge on your MCP domain").option("--data-handling <mode>", "Whose API this is (portal step 7): own \xB7 proxied (add --data-handling-statement) \xB7 third-party. Never pre-filled.").option("--data-handling-statement <text>", "For proxied: the permission you hold (who granted what, and when)").option("--health-data <yes|no>", "The tools handle health data (never pre-filled; yes adds the human-in-the-loop note to the packet)").option("--sponsored-content <yes|no>", "Tool results carry sponsored content (never pre-filled; yes is an unsupported use case)").option("--no-exercise", 'Skip the "Run every tool now?" step at the end').option("--no-email", "No email at the end of the run").option("--full", "Run the full flow even when only --attest was given").option("--compliant [directory]", "Which directory's policies this run must satisfy: bare = claude; or claude \xB7 chatgpt \xB7 all (repeat or comma-separate)", collect, []).addOption(new import_commander.Option("--policy <directory>", "Older spelling of --compliant").hideHelp()).addOption(new import_commander.Option("--platform <list>", "Older spelling of --compliant").hideHelp()).option("--out <dir>", "Where to write connector-packet-<project>.md/.json (default: current directory)").option("-y, --yes", "No questions: apply the flags, report, exit 1 on a blocker").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON").action(action(async (project, opts) => {
22474
+ var connector = program.command("connector").description(`A compliant Claude connector (--compliant chatgpt: a ChatGPT plugin), directory-ready in one command: from a project or straight from an OpenAPI URL \u2014 creates the ${B.thingShort} if needed, applies the safe fixes, sets up the custom MCP domain (one CNAME), pre-fills every listing answer from the spec, creates the reviewer account, and writes the submission packet when every check passes (you submit it to Anthropic and OpenAI yourself)`).argument("[project-or-spec]", `The ${B.thingShort} (project name), or an OpenAPI URL/file to create a directory-ready ${B.thingShort} from first`).option("--domain <host>", "Custom hostname for the MCP server (mcp.<your domain>); asked for when missing, suggested from the spec").option("--wait <minutes>", "How long to wait for the custom domain to activate once its CNAME is in place (default 15; 0 = do not wait)").option("--auth-mode <mode>", "How directory clients get in: signin (default) = OAuth sign-in on the connector, API keys alongside; open = no sign-in at all, for public read-only data (Anthropic allows it) \u2014 the API host keeps its own doors. Asked once when every tool is read-only.").option("--name <name>", `When creating from a spec: the ${B.thingShort} name (default: derived from the spec host)`).option("--privacy-url <url>", "Public https privacy policy").option("--support <email|url>", "Support contact").option("--docs-url <url>", "Public documentation URL").option("--prompt <text>", "An example prompt (repeatable; three are needed)", collect, []).option("--server-name <name>", 'Listing name (2\u201340 chars, no "MCP"/"Plugin" suffix)').option("--tagline <text>", "One-line tagline").option("--description <text>", "Description").option("--icon-url <url>", "Square icon (https)").option("--slug <slug>", "Claude: the listing slug (lowercase-with-hyphens; permanent once published)").option("--high-risk <list>", "Claude policy 1A: none, or any of legal,healthcare,insurance,finance").option("--mcp-app", "Claude: the listing is an MCP App (needs 3\u20135 --screenshot)").option("--no-mcp-app", "Claude: a plain connector (default)").option("--screenshot <url=prompt>", "Claude MCP App: a carousel screenshot and the prompt that produced it (repeatable, 3\u20135)", collect, []).option("--vuln-contact <email|url>", "Vulnerability-disclosure contact").option("--attest <id>", "Attest (repeatable): guidelines \xB7 own-backend \xB7 no-financial \xB7 no-genai-media \xB7 no-prompt-injection \xB7 no-conversation-data \xB7 public-docs (Claude's seven) \xB7 no-digital-goods \xB7 content-13-no-ads \xB7 no-restricted-data (ChatGPT). Alone, this only records the attestation.", collect, []).option("--allowed-origin <origin>", "An extra browser origin allowed on the MCP host (repeatable)", collect, []).option("--openai-challenge <token>", "ChatGPT: the domain-verification token the OpenAI portal shows under Plugins \u2192 Connect MCP server; served verbatim at /.well-known/openai-apps-challenge on your MCP domain").option("--data-handling <mode>", "Whose API this is (portal step 7): own \xB7 proxied (add --data-handling-statement) \xB7 third-party. Never pre-filled.").option("--data-handling-statement <text>", "For proxied: the permission you hold (who granted what, and when)").option("--health-data <yes|no>", "The tools handle health data (never pre-filled; yes adds the human-in-the-loop note to the packet)").option("--sponsored-content <yes|no>", "Tool results carry sponsored content (never pre-filled; yes is an unsupported use case)").option("--website-url <url>", "OpenAI's form: the public website (https)").option("--support-url <url>", "OpenAI's form: the support page (https; a URL, not an email)").option("--terms-url <url>", "OpenAI's form: the terms of service page (https)").option("--developer-name <name>", "OpenAI's form: the developer name (\u2264 80; the directory shows your verified identity)").option("--chatgpt-category <name>", "OpenAI's form: one of Productivity \xB7 Creativity \xB7 Developer Tools \xB7 Business & Operations \xB7 Data & Analytics \xB7 Communication \xB7 Education & Research \xB7 Security \xB7 Finance \xB7 Healthcare \xB7 Travel \xB7 Entertainment \xB7 Other").option("--demo-recording-url <url>", "OpenAI's form: the demo recording (required for MCP review)").option("--release-notes <text>", "OpenAI's form: release notes for this submission").option("--short-description <text>", "OpenAI's form: the 30-character subtitle (Claude's tagline stays \u2264 200)").option("--countries <codes>", "OpenAI's form: ISO country codes, comma-separated (default: everywhere)", (v) => v.split(",").map((s) => s.trim()).filter(Boolean)).option("--no-exercise", 'Skip the "Run every tool now?" step at the end').option("--seed", "Sign-in mode: when a read returns nothing for the reviewer, create a few records through the create tools and keep them (asked when omitted)").option("--no-seed", "Never seed the reviewer account").option("--no-email", "No email at the end of the run").option("--full", "Run the full flow even when only --attest was given").option("--compliant [directory]", "Which directory's policies this run must satisfy: bare = claude; or claude \xB7 chatgpt \xB7 all (repeat or comma-separate)", collect, []).addOption(new import_commander.Option("--policy <directory>", "Older spelling of --compliant").hideHelp()).addOption(new import_commander.Option("--platform <list>", "Older spelling of --compliant").hideHelp()).option("--out <dir>", "Where to write connector-packet-<project>.md/.json (default: current directory)").option("-y, --yes", "No questions: apply the flags, report, exit 1 on a blocker").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON").action(action(async (project, opts) => {
21654
22475
  if (!project) throw new Error(`Which ${B.thingShort}? ${B.cli} connector <project> \xB7 ${B.cli} connector https://\u2026/openapi.yaml \xB7 ${B.cli} connector reviewer <project> \xB7 ${B.cli} connector exercise <project> \xB7 ${B.cli} connector answers <project>`);
21655
22476
  await runConnector(project, opts);
21656
22477
  }));
21657
- connector.command("exercise").description("Call every tool the way a reviewer would, as the reviewer account (open mode: as you): reads with a valid and an invalid argument, writes only on ids the run created and only after a y, then the three listing prompts through Claude Code when it is installed. The gateway records the outcomes; the report reads them.").argument("<project>", `The ${B.thingShort} (project name)`).option("--empty-ok <tool>", 'A read-only tool whose correct answer is "nothing" (repeatable)', collect, []).option("--no-claude", "Skip the Claude Code pass").option("--no-email", "No email at the end of the run").option("-y, --yes", "No questions: writes are not exercised (they need a y), the rest runs").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON").action(action(async (project, opts) => {
22478
+ connector.command("exercise").description("Call every tool the way a reviewer would, as the reviewer account (open mode: as you): reads with a valid and an invalid argument, writes only on ids the run created and only after a y, then the three listing prompts through Claude Code when it is installed. The gateway records the outcomes; the report reads them.").argument("<project>", `The ${B.thingShort} (project name)`).option("--empty-ok <tool>", 'A read-only tool whose correct answer is "nothing" (repeatable)', collect, []).option("--no-claude", "Skip the Claude Code pass").option("--seed", "Sign-in mode: when a read returns nothing for the reviewer, create records through the create tools and keep them").option("--no-seed", "Never seed the reviewer account").option("--no-email", "No email at the end of the run").option("-y, --yes", "No questions: writes are not exercised (they need a y), the rest runs").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON").action(action(async (project, opts) => {
21658
22479
  await runConnectorExercise(project, { ...withConnectorOpts(opts), noClaude: opts.claude === false, noEmail: opts.email === false });
21659
22480
  }));
21660
22481
  connector.command("docs").description('Generate the three documents a listing needs \u2014 the public page "Using <name> in Claude" (served at <portal>/connector), the reviewer instructions and the portal answers \u2014 on your Claude Code from the facts of the connector; lint them, store them, point the docs URL at the page. Edit them on the dashboard any time.').argument("<project>", `The ${B.thingShort} (project name)`).option("--from <file>", "Paste back a JSON answer produced elsewhere (see --prompt-out)").option("--prompt-out <file>", "Write the prompt + facts to a file instead of running Claude Code").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON").action(action((project, opts) => runConnectorDocs(project, withConnectorOpts(opts))));
21661
- connector.command("answers").description("The portal's free-text steps 3\u20138 (use cases, prerequisites, access, categories, company, data handling, health data, sponsored content, reviewer instructions): asked when missing, or passed as flags").argument("<project>", `The ${B.thingShort} (project name)`).option("--use-cases <text>", "Use cases (200+ characters)").option("--prerequisites <text>", "What a user needs before it works (one sentence)").option("--access <read|write|both>", "What the tools do").option("--category <name>", "A category suggestion (repeatable, up to five; you pick the closest in the portal)", collect, []).option("--company-name <name>", "Company name").option("--company-website <url>", "Company website (https)").option("--company-contact <email|url>", "Primary contact").option("--reviewer-instructions <text>", "How to add it, sign in, one prompt per tool and what to expect (400+ characters)").option("--data-handling <mode>", "own \xB7 proxied (add --data-handling-statement) \xB7 third-party").option("--data-handling-statement <text>", "For proxied: the permission you hold").option("--health-data <yes|no>", "The tools handle health data").option("--sponsored-content <yes|no>", "Tool results carry sponsored content").option("-y, --yes", "No questions: record the flags only").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON").action(action((project, opts) => runConnectorAnswers(project, withConnectorOpts(opts))));
22482
+ connector.command("answers").description("The portal's free-text steps 3\u20138 (use cases, prerequisites, access, categories, company, data handling, health data, sponsored content, reviewer instructions): asked when missing, or passed as flags").argument("<project>", `The ${B.thingShort} (project name)`).option("--use-cases <text>", "Use cases (200+ characters)").option("--prerequisites <text>", "What a user needs before it works (one sentence)").option("--access <read|write|both>", "What the tools do").option("--category <name>", "A category suggestion (repeatable, up to five; you pick the closest in the portal)", collect, []).option("--company-name <name>", "Company name").option("--company-website <url>", "Company website (https)").option("--company-contact <email|url>", "Primary contact").option("--reviewer-instructions <text>", "How to add it, sign in, one prompt per tool and what to expect (400+ characters)").option("--data-handling <mode>", "own \xB7 proxied (add --data-handling-statement) \xB7 third-party").option("--data-handling-statement <text>", "For proxied: the permission you hold").option("--health-data <yes|no>", "The tools handle health data").option("--sponsored-content <yes|no>", "Tool results carry sponsored content").option("--website-url <url>", "OpenAI's form: the public website (https)").option("--support-url <url>", "OpenAI's form: the support page (https)").option("--terms-url <url>", "OpenAI's form: the terms of service page (https)").option("--developer-name <name>", "OpenAI's form: the developer name (\u2264 80)").option("--chatgpt-category <name>", "OpenAI's form: the category, from their list").option("--demo-recording-url <url>", "OpenAI's form: the demo recording URL").option("--release-notes <text>", "OpenAI's form: release notes").option("--short-description <text>", "OpenAI's form: the 30-character subtitle").option("--countries <codes>", "OpenAI's form: ISO country codes, comma-separated", (v) => v.split(",").map((s) => s.trim()).filter(Boolean)).option("--negative-prompt <text>", "OpenAI's form: a prompt the plugin must not act on (repeatable; three)", collect, []).option("-y, --yes", "No questions: record the flags only").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON").action(action((project, opts) => runConnectorAnswers(project, withConnectorOpts(opts))));
21662
22483
  var withConnectorOpts = (opts) => ({ ...connector.opts(), ...opts ?? {} });
21663
22484
  connector.command("reviewer").description("Create the reviewer test account (username + password sign-in on the tenant, no MFA), store it encrypted for the packet, show it once").argument("<project>", `The ${B.thingShort} (project name)`).option("--username <name>", "Account name (default reviewer-<project>)").option("--password <pw>", "Use this password instead of a generated one").option("--seed", "Then open the agent chat with a first message that seeds realistic data through the proxy (billed per turn)").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON (the password appears there exactly once)").action(action((project, opts) => runConnectorReviewer(project, withConnectorOpts(opts))));
21664
22485
  program.command("integration").description(`What to add to your frontend and frontend-server for ${B.cli === "mcpblaze" ? "an MCP server" : "a proxy"} \u2014 the same kit the ${B.product} MCP hands an agent: widgets, the call-through with the key + X-End-User-Id, what your backend must trust, env vars, what stays open`).argument("<name>", `The ${B.thingShort} (project name)`).option("--stack <stack>", "nextjs | express | fastapi | other", "nextjs").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--json", "Print the raw kit as JSON").action(action((name, opts) => runIntegration(name, opts)));
@@ -21687,11 +22508,11 @@ mcpCommon(mcpPrompts.command("set").description("Add or replace a prompt from a
21687
22508
  mcpCommon(mcpPrompts.command("rm").description("Remove a prompt").argument("<project>", "Project name or id").argument("<name>", "Prompt name")).action(action((project, name, opts) => runMcpPrompts("rm", project, name, opts)));
21688
22509
  var mcpOps = mcpCmd.command("ops").description("Per-tool behavior. Default from the HTTP method: GET/HEAD/OPTIONS read-only, PUT/DELETE idempotent, DELETE destructive + asks to confirm");
21689
22510
  mcpCommon(mcpOps.command("list").description("Every tool with its effective behavior (overrides marked)").argument("<project>", "Project name or id")).action(action((project, opts) => runMcpOps("list", project, void 0, opts)));
21690
- mcpCommon(mcpOps.command("set").description("Override one tool").argument("<project>", "Project name or id").argument("<tool>", "Tool name").option("--title <text>", "Human label clients and directories show (default: the OpenAPI summary, else the name spelled out)").option("--read-only", "Safe to call, changes nothing (e.g. a POST search)").option("--no-read-only", "Not read-only").option("--destructive", "Changes or deletes data (e.g. a GET with side effects)").option("--no-destructive", "Not destructive").option("--idempotent", "Repeating it has no extra effect").option("--no-idempotent", "Not idempotent").option("--confirm", "Ask the user to confirm before running").option("--no-confirm", "Never ask to confirm").option("--always-async", "Return a task handle immediately (long jobs)").option("--no-always-async", "Wait for the result (default)")).action(action((project, tool, opts) => runMcpOps("set", project, tool, opts)));
22511
+ mcpCommon(mcpOps.command("set").description("Override one tool").argument("<project>", "Project name or id").argument("<tool>", "Tool name").option("--title <text>", "Human label clients and directories show (default: the OpenAPI summary, else the name spelled out)").option("--read-only", "Safe to call, changes nothing (e.g. a POST search)").option("--no-read-only", "Not read-only").option("--destructive", "Changes or deletes data (e.g. a GET with side effects)").option("--no-destructive", "Not destructive").option("--idempotent", "Repeating it has no extra effect").option("--no-idempotent", "Not idempotent").option("--confirm", "Ask the user to confirm before running").option("--no-confirm", "Never ask to confirm").option("--always-async", "Return a task handle immediately (long jobs)").option("--no-always-async", "Wait for the result (default)").option("--open-world", "Reaches public or open-ended data (web pages, third-party records); the directories read openWorldHint").option("--no-open-world", "Only the signed-in account's own data")).action(action((project, tool, opts) => runMcpOps("set", project, tool, opts)));
21691
22512
  mcpCommon(mcpOps.command("reset").description("Back to the automatic default for one tool").argument("<project>", "Project name or id").argument("<tool>", "Tool name")).action(action((project, tool, opts) => runMcpOps("reset", project, tool, opts)));
21692
22513
  mcpCommon(mcpCmd.command("ui").description("Print a tool's MCP Apps UI resource (ui://{project}/{tool}) \u2014 the automatic viewer").argument("<project>", "Project name or id").argument("<tool>", "Tool name")).action(action((project, tool, opts) => runMcpUi(project, tool, opts)));
21693
22514
  program.command("apichat [project]").alias("chat").description("Turn any API into a chat: point at an OpenAPI spec \u2014 or chat an EXISTING proxy by name (no login needed)").option("--target <url|file>", "What to chat with \u2014 pass ANY of: a target server base URL (spec auto-discovered at /openapi.json etc.), a local OpenAPI file (./openapi.yaml), or a remote OpenAPI URL (https://acme.com/openapi.yaml)").addOption(new import_commander.Option("--openapi <file|url>", "Deprecated alias \u2014 --target now detects spec files/URLs itself").hideHelp()).addOption(new import_commander.Option("--openapispec <file|url>", "Deprecated alias for --openapi").hideHelp()).option("--name <name>", "Proxy name (defaults to the target host)").option("--apiversion <version>", "API version to create (e.g. 1.0.0)").option("--environment <env>", "Environment to chat against (default: prod anonymous / dev logged-in)").option("--access <mode>", 'Who can call this API once connected (e.g. via Claude): "open" = anyone who signs in, "invite" = only you + emails you pre-approve. Default: invite when logged in, open when anonymous.').option("--target-auth-env <ENV_VAR>", "Read the upstream credential from this env var (CI-safe; required when there is no TTY and the API needs auth)").option("--force", "Proceed even if the API uses oauth2/openIdConnect target auth (you configure target auth yourself later)").option("-y, --yes", "Skip confirmation prompts").option("--tenant <slug>", "Tenant (consumer namespace: portal, login, users) for the new proxy; omit to be asked").option("--apikey <key>", "Use this API key for the proxy's door (api_key proxies). Without it, apichat detects the door and asks \u2014 or runs the consumer login for OAuth doors.").option("--xenduserid <id>", "Assert this end-user id (X-End-User-Id) \u2014 required by proxies with identified/pre-approved enforcement; you are asked for one when the proxy demands it.").option("--verbose", "Show the per-turn proxy curl trace (hidden by default)").option("-p, --prompt <question>", "One-shot question: answered through the external agent CLI after an MCP install, or by apichat itself (exits after answering when there is no TTY)").option("--install-mcp <cli>", "Install this proxy's MCP into an external agent CLI without asking: claude | codex. Also re-offers after an earlier decline.").option("--remove-mcp <cli>", "Disconnect this proxy from an external agent CLI (claude | codex): removes the MCP server and the standing instruction").action(action((project, opts) => {
21694
- const looksLikeSource = !!project && (/^https?:\/\//i.test(project) || /\.(ya?ml|json)$/i.test(project) || import_fs14.default.existsSync(project));
22515
+ const looksLikeSource = !!project && (/^https?:\/\//i.test(project) || /\.(ya?ml|json)$/i.test(project) || import_fs15.default.existsSync(project));
21695
22516
  const target2 = opts.target ?? (looksLikeSource ? project : void 0);
21696
22517
  return runApichat({ ...opts, target: target2, project: looksLikeSource ? void 0 : project, openapispec: opts.openapispec ?? opts.openapi });
21697
22518
  }));