apiblaze 0.21.52 → 0.21.54

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 +1713 -1078
  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.52";
37
+ version = "0.21.54";
38
38
  }
39
39
  });
40
40
 
@@ -5310,6 +5310,7 @@ __export(directory_exports, {
5310
5310
  isApiError: () => isApiError,
5311
5311
  isReady: () => isReady,
5312
5312
  parsePlatforms: () => parsePlatforms,
5313
+ parsePolicy: () => parsePolicy,
5313
5314
  printReport: () => printReport
5314
5315
  });
5315
5316
  function attestationId(flag) {
@@ -5330,6 +5331,7 @@ function directoryBodyFromFlags(f) {
5330
5331
  if (f.allowedOrigin?.length) m.allowed_origins = f.allowedOrigin.map((o) => o.trim()).filter(Boolean);
5331
5332
  const out = {};
5332
5333
  if (Object.keys(m).length) out.directory_metadata = m;
5334
+ if (f.openaiChallenge?.trim()) out.openai_challenge_token = f.openaiChallenge.trim();
5333
5335
  if (f.attest?.length) {
5334
5336
  const a = {};
5335
5337
  for (const raw of f.attest) {
@@ -5341,6 +5343,22 @@ function directoryBodyFromFlags(f) {
5341
5343
  }
5342
5344
  return out;
5343
5345
  }
5346
+ function parsePolicy(policy, platform) {
5347
+ if (platform && (policy === void 0 || policy === true)) return parsePlatforms(platform) ?? ["claude"];
5348
+ if (policy === void 0 || policy === false || policy === true) return ["claude"];
5349
+ const raw = (Array.isArray(policy) ? policy : [policy]).flatMap((s) => String(s).split(",")).map((s) => s.trim().toLowerCase()).filter(Boolean);
5350
+ if (!raw.length) return ["claude"];
5351
+ const out = /* @__PURE__ */ new Set();
5352
+ for (const p of raw) {
5353
+ if (p === "all" || p === "both") {
5354
+ PLATFORMS.forEach((x) => out.add(x));
5355
+ continue;
5356
+ }
5357
+ if (!PLATFORMS.includes(p)) throw new Error(`Unknown policy "${p}". Use claude, chatgpt or all.`);
5358
+ out.add(p);
5359
+ }
5360
+ return PLATFORMS.filter((p) => out.has(p));
5361
+ }
5344
5362
  function parsePlatforms(raw) {
5345
5363
  if (!raw) return void 0;
5346
5364
  const out = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
@@ -5401,11 +5419,18 @@ var init_directory = __esm({
5401
5419
  init_types();
5402
5420
  PLATFORMS = ["claude", "chatgpt"];
5403
5421
  ATTESTATIONS = [
5404
- { id: "own_backend", flags: ["own-backend", "own_backend"], label: "You own or control the backend this server calls" },
5405
- { id: "no_financial_transactions", flags: ["no-financial", "no-financial-transactions", "no_financial_transactions"], label: "No money, crypto or financial transactions through the tools" },
5406
- { 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)" },
5407
- { id: "no_genai_media_primary", flags: ["no-genai-media", "no-genai-media-primary", "no_genai_media_primary"], label: "AI image/video/audio generation is not the primary function (Claude)" },
5408
- { 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)" }
5422
+ // Claude's Compliance step: seven acknowledgments (submission page, 2026-10-16).
5423
+ { id: "directory_guidelines", flags: ["guidelines", "directory-guidelines", "directory_guidelines"], label: "You have read and will follow the directory guidelines and policies", platforms: PLATFORMS },
5424
+ { id: "own_backend", flags: ["own-backend", "own_backend"], label: "You own or control the backend this server calls (first-party API)", platforms: PLATFORMS },
5425
+ { id: "no_financial_transactions", flags: ["no-financial", "no-financial-transactions", "no_financial_transactions"], label: "No money, crypto or financial transactions through the tools", platforms: PLATFORMS },
5426
+ { id: "no_genai_media_primary", flags: ["no-genai-media", "no-genai-media-primary", "no_genai_media_primary"], label: "AI image/video/audio generation is not the primary function (Claude)", platforms: ["claude"] },
5427
+ { id: "no_prompt_injection", flags: ["no-prompt-injection", "no_prompt_injection"], label: "Tool descriptions describe the tools; nothing instructs the model, nothing is hidden or encoded", platforms: PLATFORMS },
5428
+ { id: "no_conversation_data", flags: ["no-conversation-data", "no_conversation_data"], label: "The tools take only what they need; no conversation history, memory, chat summaries or user files", platforms: PLATFORMS },
5429
+ { id: "public_documentation", flags: ["public-docs", "public-documentation", "public_documentation"], label: "Public documentation is live by the publish date (Claude)", platforms: ["claude"] },
5430
+ // ChatGPT's guidelines add these three.
5431
+ { 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"] },
5432
+ { 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"] },
5433
+ { 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"] }
5409
5434
  ];
5410
5435
  reportPath = (proj2) => `/projects/${encodeURIComponent(proj2.projectId)}/${encodeURIComponent(proj2.apiVersion)}/directory-report`;
5411
5436
  }
@@ -5602,6 +5627,26 @@ var init_iam = __esm({
5602
5627
  });
5603
5628
 
5604
5629
  // src/commands/create.ts
5630
+ var create_exports = {};
5631
+ __export(create_exports, {
5632
+ PROVIDERS: () => PROVIDERS,
5633
+ agentPrompts: () => agentPrompts,
5634
+ buildTryItCurl: () => buildTryItCurl,
5635
+ describeFamilies: () => describeFamilies,
5636
+ directoryCreateFields: () => directoryCreateFields,
5637
+ fail: () => fail4,
5638
+ finishSetup: () => finishSetup,
5639
+ firstPlainPath: () => firstPlainPath,
5640
+ loadOpenapiSource: () => loadOpenapiSource,
5641
+ normalizeName: () => normalizeName2,
5642
+ parseOauthFlag: () => parseOauthFlag,
5643
+ pickFamilies: () => pickFamilies,
5644
+ planSetup: () => planSetup,
5645
+ rememberForApichat: () => rememberForApichat,
5646
+ resolveAuth: () => resolveAuth,
5647
+ runCreate: () => runCreate,
5648
+ tenantFromPortal: () => tenantFromPortal
5649
+ });
5605
5650
  function normalizeName2(raw) {
5606
5651
  return (raw || "").toLowerCase().replace(/[^a-z0-9]/g, "");
5607
5652
  }
@@ -6424,7 +6469,8 @@ Create an ${B.thing}
6424
6469
  ownerUserId: creds?.apiblazeUserId
6425
6470
  });
6426
6471
  await applyCreateToggles(name, opts);
6427
- const directory = await directoryAfterCreate(name, version2, opts);
6472
+ opts.onCreated?.({ name, version: version2, tenant: tenant2, teamId });
6473
+ const directory = opts.skipDirectoryReport ? null : await directoryAfterCreate(name, version2, opts);
6428
6474
  if (opts.json) {
6429
6475
  process.stdout.write(JSON.stringify({
6430
6476
  project_id: result.project_id,
@@ -7091,190 +7137,603 @@ var init_agent = __esm({
7091
7137
  }
7092
7138
  });
7093
7139
 
7094
- // src/server/core.ts
7095
- async function safeErr(res) {
7096
- try {
7097
- const b = await res.json();
7098
- if (typeof b.error === "string") return b.error;
7099
- if (b.error?.message) return b.error.message;
7100
- if (b.detail || b.title) return [b.title, b.detail].filter(Boolean).join(". ").replace(/\.\. /, ". ");
7101
- return `apiblaze ${res.status}`;
7102
- } catch {
7103
- return `apiblaze ${res.status}`;
7104
- }
7105
- }
7106
- function createPlaneCore(cfg) {
7107
- if (!cfg.cpKey) throw new Error("apiblaze: cpKey is required (set APIBLAZE_CP_KEY). It is server-only \u2014 never expose it to the browser.");
7108
- const apikeysBase = (cfg.apikeysBase ?? "https://apikeys.apiblaze.com").replace(/\/$/, "");
7109
- const iamBase = (cfg.iamBase ?? "https://iam.apiblaze.com").replace(/\/$/, "");
7110
- const tenantCache = /* @__PURE__ */ new Map();
7111
- const endUserCache = /* @__PURE__ */ new Map();
7112
- function planeFetch(base2, method, path17, user, opts = {}) {
7113
- const params = new URLSearchParams({ product: "apiblaze", ...opts.query ?? {} });
7114
- const headers = { "X-API-Key": cfg.cpKey, "X-End-User-Id": user.userId };
7115
- let body;
7116
- if (opts.body !== void 0) {
7117
- headers["Content-Type"] = "application/json";
7118
- body = JSON.stringify(opts.body);
7140
+ // src/lib/agent-chat.ts
7141
+ async function runAgentChatRepl(opts) {
7142
+ const { title, subtitle, endpoint, buildBody, seedPrompt, summarizeProposal, commands } = opts;
7143
+ console.log("\n" + import_chalk26.default.cyan.bold(title));
7144
+ if (subtitle) console.log(import_chalk26.default.dim(subtitle));
7145
+ const messages = [];
7146
+ const ctx = { proposal: null, lastData: null, log: (s) => console.log(s) };
7147
+ const cmdByName = new Map(commands.map((c) => [c.name, c]));
7148
+ const footer = () => {
7149
+ const parts = [
7150
+ import_chalk26.default.dim("type to chat/refine"),
7151
+ ...commands.map((c) => import_chalk26.default.dim(`/${c.name} ${c.describe}`)),
7152
+ import_chalk26.default.dim("/show"),
7153
+ import_chalk26.default.dim("/exit when done"),
7154
+ import_chalk26.default.dim("/drop discard changes")
7155
+ ];
7156
+ return " " + import_chalk26.default.dim("[ ") + parts.join(import_chalk26.default.dim(" \xB7 ")) + import_chalk26.default.dim(" ]");
7157
+ };
7158
+ async function turn(content) {
7159
+ messages.push({ role: "user", content });
7160
+ process.stdout.write(import_chalk26.default.dim(" \u2026thinking\n"));
7161
+ const { status: status2, data } = await agentCall(endpoint, "POST", { messages, ...buildBody(messages) });
7162
+ if (status2 === 402 || status2 === 503 || parseProblem(data)) {
7163
+ await reportRefusal(status2, data, { cli: B.cli });
7164
+ console.log("");
7165
+ messages.pop();
7166
+ return;
7167
+ }
7168
+ if (status2 >= 400 || !data) {
7169
+ console.log(import_chalk26.default.red(` Error (${status2}): ${(data && data.error) ?? "request failed"}
7170
+ `));
7171
+ messages.pop();
7172
+ return;
7173
+ }
7174
+ ctx.lastData = data;
7175
+ const reply = data.reply ?? data.message ?? "(no reply)";
7176
+ messages.push({ role: "assistant", content: reply });
7177
+ console.log("\n" + import_chalk26.default.cyan("agent \u203A ") + reply + "\n");
7178
+ if (data.proposal) {
7179
+ ctx.proposal = data.proposal;
7180
+ const summary = summarizeProposal?.(data);
7181
+ if (summary) console.log(import_chalk26.default.yellow(" " + summary));
7182
+ const applies = commands.filter((c) => c.needsProposal);
7183
+ const primary = applies[applies.length - 1];
7184
+ if (primary) {
7185
+ console.log(import_chalk26.default.bold.green(" \u279C Next: type ") + import_chalk26.default.bold.cyan(`/${primary.name}`) + import_chalk26.default.bold.green(" to apply this, then ") + import_chalk26.default.bold.cyan("/exit") + import_chalk26.default.bold.green(" to leave."));
7186
+ const rest = applies.slice(0, -1).map((c) => `/${c.name} = ${c.describe}`).join(" \xB7 ");
7187
+ console.log(import_chalk26.default.dim(` (or keep chatting to refine${rest ? ` \xB7 ${rest}` : ""} \xB7 /show = inspect)`));
7188
+ }
7189
+ console.log();
7190
+ }
7191
+ const line = formatBilling(data.billing);
7192
+ if (line) {
7193
+ const llm2 = data.llm;
7194
+ console.log(import_chalk26.default.dim(` ${line}${llm2?.model ? ` \xB7 ${llm2.model}` : ""}`));
7119
7195
  }
7120
- return fetch(`${base2}${path17}?${params.toString()}`, { method, headers, body, cache: "no-store" });
7121
- }
7122
- async function resolveTenant2(user) {
7123
- const cached = tenantCache.get(user.tenant);
7124
- if (cached) return cached;
7125
- const res = await planeFetch(apikeysBase, "POST", "/tenants", user, { body: { ref: user.tenant } });
7126
- if (!res.ok) throw new PlaneError(res.status, await safeErr(res));
7127
- const { tenant: tenant2 } = await res.json();
7128
- tenantCache.set(user.tenant, tenant2);
7129
- return tenant2;
7130
7196
  }
7131
- async function ensureEndUser(user, tenant2) {
7132
- const cacheKey = `${tenant2}:${user.userId}`;
7133
- const cached = endUserCache.get(cacheKey);
7134
- if (cached) return cached;
7135
- const res = await planeFetch(apikeysBase, "POST", "/users", user, {
7136
- body: { tenant: tenant2, ref: user.userId, email: user.email ?? null, display_name: user.label ?? null }
7137
- });
7138
- if (!res.ok) throw new PlaneError(res.status, await safeErr(res));
7139
- const { abz_sub } = await res.json();
7140
- endUserCache.set(cacheKey, abz_sub);
7141
- return abz_sub;
7197
+ const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
7198
+ const ask3 = (q) => new Promise((res) => rl.question(q, res));
7199
+ await turn(seedPrompt);
7200
+ for (; ; ) {
7201
+ console.log(footer());
7202
+ const line = (await ask3(import_chalk26.default.green("you \u203A "))).trim();
7203
+ if (!line) continue;
7204
+ const lower = line.toLowerCase();
7205
+ if (line === "/exit" || line === "/quit" || lower === "exit" || lower === "quit" || lower === "q") {
7206
+ console.log(import_chalk26.default.dim(" Bye \u2014 anything you enabled or published stays in place.\n"));
7207
+ break;
7208
+ }
7209
+ if (line === "/drop") {
7210
+ console.log(import_chalk26.default.dim(" Dropped the pending proposal \u2014 rules you already enabled/published are untouched.\n"));
7211
+ break;
7212
+ }
7213
+ if (line === "/show") {
7214
+ if (!ctx.proposal) console.log(import_chalk26.default.dim(" No proposal yet \u2014 keep chatting until the agent proposes one.\n"));
7215
+ else console.log("\n" + JSON.stringify(ctx.proposal, null, 2) + "\n");
7216
+ continue;
7217
+ }
7218
+ if (line.startsWith("/")) {
7219
+ const cmd = cmdByName.get(line.slice(1));
7220
+ if (!cmd) {
7221
+ console.log(import_chalk26.default.red(` Unknown command "${line}". Try one of: ${commands.map((c) => "/" + c.name).join(", ")}, /show, /drop
7222
+ `));
7223
+ continue;
7224
+ }
7225
+ if (cmd.needsProposal && !ctx.proposal) {
7226
+ console.log(import_chalk26.default.yellow(" No proposal yet \u2014 keep chatting until the agent generates one, then try again.\n"));
7227
+ continue;
7228
+ }
7229
+ await cmd.run(ctx);
7230
+ continue;
7231
+ }
7232
+ await turn(line);
7142
7233
  }
7143
- return { planeFetch, resolveTenant: resolveTenant2, ensureEndUser, apikeysBase, iamBase };
7234
+ rl.close();
7144
7235
  }
7145
- var PlaneError;
7146
- var init_core = __esm({
7147
- "src/server/core.ts"() {
7236
+ var import_readline, import_chalk26;
7237
+ var init_agent_chat = __esm({
7238
+ "src/lib/agent-chat.ts"() {
7148
7239
  "use strict";
7149
- PlaneError = class extends Error {
7150
- constructor(status2, detail) {
7151
- super(detail);
7152
- this.status = status2;
7153
- this.detail = detail;
7154
- }
7155
- };
7240
+ import_readline = __toESM(require("readline"));
7241
+ import_chalk26 = __toESM(require("chalk"));
7242
+ init_problem();
7243
+ init_brand();
7244
+ init_api();
7245
+ init_admin();
7156
7246
  }
7157
7247
  });
7158
7248
 
7159
- // src/demo-app/files.ts
7160
- var DEMO_FILES;
7161
- var init_files = __esm({
7162
- "src/demo-app/files.ts"() {
7163
- "use strict";
7164
- DEMO_FILES = {
7165
- "README.md": "# Nino's Pizza: the APIblaze demo\n\nA tiny table-booking app for pizzerias, used by `npx apiblaze@latest demo` to show what APIblaze does.\n\n```\nnpx apiblaze@latest demo\n```\n\nThat one command writes this folder to `./apiblaze-demo`, puts APIblaze in front of the backend,\nstarts everything and opens the page. No login, no prompts, no dependencies.\n\n## What the page shows\n\n1. **Your API is live on the internet.** The backend runs on your laptop; APIblaze gives it a public\n URL and an MCP address for AI assistants. A live ticker shows each request reaching your laptop.\n2. **An AI assistant uses it.** Ask it to book a table and watch the request arrive.\n3. **Included, no code:** only the owner can change a booking, each customer (Nino Pizza,\n Gino Pizza) is separate, rate limits, sign-in, and drop-in widgets for API keys and roles.\n\n## The files\n\n| File | What it is |\n| --- | --- |\n| `backend.js` | The pizzeria's API. No login, no keys, no permission checks. |\n| `openapi.yaml` | Its endpoints. APIblaze builds the AI tools and the ownership rules from it. |\n| `app.js` | The website's server. Calls the API with one key and says who is acting. |\n| `public/index.html` | The page. |\n| `start.js` | Starts the backend (port 3001) and the website (port 3000). |\n\n`npx apiblaze@latest demo` also adds `lib/apiblaze-server.js` and `public/apiblaze-widgets.js`, the same\nhelpers you get from `npm i apiblaze`. Stop the demo with `npx apiblaze@latest demo stop`.\n",
7166
- "openapi.yaml": `openapi: 3.0.3
7167
- info:
7168
- title: ResiResi API
7169
- version: 1.0.0
7170
- description: Table bookings at restaurants (each restaurant is a tenant, like Nino Pizza and Gino Pizza). APIblaze reads this file to build the MCP tools for AI assistants and to work out who may change what.
7171
- paths:
7172
- /reservations:
7173
- get:
7174
- operationId: listReservations
7175
- summary: List my bookings
7176
- description: The signed-in person's own bookings at this restaurant, and nobody else's. Use it for "my bookings" or "the one I just made", and call it again whenever asked, since bookings change.
7177
- responses:
7178
- '200':
7179
- description: The caller's bookings
7180
- content:
7181
- application/json:
7182
- schema:
7183
- type: array
7184
- items: { $ref: '#/components/schemas/Reservation' }
7185
- post:
7186
- operationId: createReservation
7187
- summary: Book a table
7188
- description: Only the time is needed; the day is today unless a date is sent. For tomorrow or any other day, work out the date and send it. The restaurant picks a free table and books it in the signed-in person's name, so never ask for a table number or a name.
7189
- requestBody:
7190
- required: true
7191
- content:
7192
- application/json:
7193
- schema: { $ref: '#/components/schemas/NewReservation' }
7194
- responses:
7195
- '201':
7196
- description: Booked
7197
- content:
7198
- application/json:
7199
- schema: { $ref: '#/components/schemas/Reservation' }
7200
- /reservations/{id}:
7201
- parameters:
7202
- - { name: id, in: path, required: true, schema: { type: string } }
7203
- get:
7204
- operationId: getReservation
7205
- summary: Get one booking
7206
- description: Only the person who booked it (or the restaurant's admin) can see it. Anyone else is refused.
7207
- responses:
7208
- '200':
7209
- description: The booking
7210
- content:
7211
- application/json:
7212
- schema: { $ref: '#/components/schemas/Reservation' }
7213
- patch:
7214
- operationId: updateReservation
7215
- summary: Change a booking
7216
- description: Move a booking to another day, table or time, or change the number of guests. Only the person who booked it (or the restaurant's admin) can change it. Get the id from listReservations; never ask for it.
7217
- requestBody:
7218
- required: true
7219
- content:
7220
- application/json:
7221
- schema: { $ref: '#/components/schemas/ReservationChange' }
7222
- responses:
7223
- '200':
7224
- description: Changed
7225
- content:
7226
- application/json:
7227
- schema: { $ref: '#/components/schemas/Reservation' }
7228
- '409': { description: That table is already booked that day at that time }
7229
- delete:
7230
- operationId: cancelReservation
7231
- summary: Cancel a booking
7232
- description: Only the person who booked it (or the restaurant's admin) can cancel it.
7233
- responses:
7234
- '204': { description: Cancelled }
7235
- components:
7236
- schemas:
7237
- NewReservation:
7238
- type: object
7239
- required: [time]
7240
- properties:
7241
- name: { type: string, example: Ana, description: 'Optional: defaults to the signed-in person. Do not ask for it.' }
7242
- table: { type: integer, example: 4, description: 'Optional: the restaurant picks a free table. Do not ask for it.' }
7243
- date: { type: string, format: date, example: '2026-10-09', description: 'Day of the booking, YYYY-MM-DD. Optional: today if not given.' }
7244
- time: { type: string, example: '19:30', description: 'Time of the booking, 24h HH:MM' }
7245
- guests: { type: integer, example: 2, description: 'How many people. Optional, 2 if not given.' }
7246
- ReservationChange:
7247
- type: object
7248
- properties:
7249
- table: { type: integer, example: 2, description: 'Move to this table.' }
7250
- date: { type: string, format: date, example: '2026-10-10', description: 'New day, YYYY-MM-DD' }
7251
- time: { type: string, example: '20:00', description: 'New time, 24h HH:MM' }
7252
- guests: { type: integer, example: 4, description: 'New number of people' }
7253
- Reservation:
7254
- allOf:
7255
- - $ref: '#/components/schemas/NewReservation'
7256
- - type: object
7257
- properties:
7258
- id: { type: string, example: r1 }
7259
- `,
7260
- "backend.js": "// ResiResi backend: restaurants (tenants, like Nino Pizza and Gino Pizza) let their customers\n// book, list and cancel tables. It runs on YOUR laptop.\n//\n// `npx apiblaze@latest dev` (which the demo runs for you) puts it on the internet\n// at a public URL and gives AI assistants an MCP address for it. Everything else\n// (API keys, sign-in, who may change what, rate limits) happens in APIblaze, in\n// front of this file. Look at what is NOT here: no login, no keys, no permission\n// checks, no AI code.\n//\n// What APIblaze tells this backend about each call, in two headers:\n// x-abz-tenant-id which customer (pizzeria) is calling\n// x-abz-user-id which person\n// and one shared secret it adds to every call it forwards (x-target-api-key), so a\n// caller who skips APIblaze and hits localhost:3001 directly is refused.\n//\n// Zero dependencies. Run it alone with `node backend.js` (port 3001).\n\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst bookings = new Map();\nlet next = 1;\n// The people the demo provisioned up front, per pizzeria: { tenant: { handle: stable id } }.\n// Through APIblaze every call already names the stable id (x-abz-user-id); a call straight\n// to this port still says \"ana\", and must mean the same person.\nconst USER_IDS = (() => { try { return JSON.parse(process.env.DEMO_USER_IDS || '{}'); } catch { return {}; } })();\nconst idOf = (tenant, handle) => (USER_IDS[tenant] || {})[handle] || handle;\n// Bumps on every booking made or cancelled. The page asks the app (on this laptop, free)\n// whether it moved before re-listing through APIblaze, so an open, idle page spends nothing.\nlet changes = 0;\n// The last requests this laptop answered \u2014 the demo page shows them live.\nconst recent = [];\nfunction remember(entry) {\n recent.unshift({ at: Date.now(), ...entry });\n recent.length = Math.min(recent.length, 50);\n // One line per request in the terminal (`npx apiblaze@latest demo logs` follows it).\n const who = entry.who ? `${entry.who}${entry.tenant ? ` @ ${entry.tenant}` : ''}` : '-';\n console.log(`${new Date().toISOString().slice(11, 19)} ${entry.status} ${entry.method.padEnd(6)} ${entry.path.padEnd(22)} ${who}`);\n}\n\n// The restaurant's day, YYYY-MM-DD, on this laptop's clock: a booking without a date is for today.\nfunction today() {\n const d = new Date();\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n}\n// A real calendar day written YYYY-MM-DD (2026-02-30 is not one).\nfunction isDay(s) {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return false;\n const [y, m, d] = s.split('-').map(Number);\n const t = new Date(Date.UTC(y, m - 1, d));\n return t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d;\n}\n\n// The restaurant picks the table: the lowest one still free that day at that time, at that restaurant.\nfunction freeTable(tenant, date, time) {\n const taken = new Set([...bookings.values()].filter((r) => r.tenant === tenant && r.date === date && r.time === time).map((r) => r.table));\n let t = 1;\n while (taken.has(t)) t++;\n return t;\n}\n\nfunction send(res, status, body) {\n res.writeHead(status, { 'content-type': 'application/json' });\n res.end(body === undefined ? '' : JSON.stringify(body));\n}\n\nfunction handle(req, res, body) {\n const url = new URL(req.url, 'http://x');\n if (url.pathname === '/openapi.yaml') {\n res.writeHead(200, { 'content-type': 'text/yaml' });\n return res.end(fs.readFileSync(path.join(__dirname, 'openapi.yaml')));\n }\n const tenant = req.headers['x-abz-tenant-id'] || '';\n const who = idOf(tenant, req.headers['x-abz-user-id'] || '');\n // Shown in the ticker and the terminal: the name the caller gave (APIblaze forwards it as\n // x-end-user-id next to the stable id), never the opaque id.\n const shown = req.headers['x-end-user-id'] || req.headers['x-abz-user-id'] || '';\n const reply = (status, data) => { remember({ method: req.method, path: url.pathname, who: shown, tenant, status }); send(res, status, data); };\n const secret = process.env.BACKEND_SECRET || '';\n if (secret && req.headers['x-target-api-key'] !== secret) return reply(401, { error: 'missing or wrong backend secret (APIblaze adds it to every call it forwards)' });\n if (!who) return reply(401, { error: 'say who is calling (x-abz-user-id)' });\n\n const m = /^\\/reservations(?:\\/([^/]+))?$/.exec(url.pathname);\n if (!m) return reply(404, { error: 'not found' });\n const id = m[1];\n\n if (!id && req.method === 'POST') {\n let input = {};\n try { input = JSON.parse(body || '{}'); } catch { return reply(400, { error: 'body must be JSON' }); }\n const date = input.date ? String(input.date) : today();\n if (!isDay(date)) return reply(400, { error: 'date must be a day, YYYY-MM-DD' });\n const time = String(input.time || '19:30');\n const row = {\n id: `r${next++}`, name: String(input.name || who), table: Number(input.table) || freeTable(tenant, date, time),\n date, time, guests: Number(input.guests) || 2, owner: who, tenant,\n };\n bookings.set(row.id, row); changes++;\n return reply(201, row);\n }\n if (!id && req.method === 'GET') {\n // Lists are the backend's job: each person sees their own bookings, at their own pizzeria.\n return reply(200, [...bookings.values()].filter((r) => r.owner === who && r.tenant === tenant)\n .sort((a, b) => `${a.date} ${a.time}`.localeCompare(`${b.date} ${b.time}`)));\n }\n const row = bookings.get(id);\n if (!row) return reply(404, { error: 'no such booking' });\n if (req.method === 'GET') return reply(200, row);\n if (req.method === 'PATCH') {\n let input = {};\n try { input = JSON.parse(body || '{}'); } catch { return reply(400, { error: 'body must be JSON' }); }\n const date = input.date ? String(input.date) : row.date;\n if (!isDay(date)) return reply(400, { error: 'date must be a day, YYYY-MM-DD' });\n const time = input.time ? String(input.time) : row.time;\n const moved = date !== row.date || time !== row.time;\n const table = input.table ? Number(input.table) : (moved ? freeTable(row.tenant, date, time) : row.table);\n const clash = [...bookings.values()].find((r) => r.id !== row.id && r.tenant === row.tenant && r.date === date && r.time === time && r.table === table);\n if (clash) return reply(409, { error: `table ${table} is already booked on ${date} at ${time}` });\n Object.assign(row, { date, time, table, ...(input.guests ? { guests: Number(input.guests) } : {}) }); changes++;\n return reply(200, row);\n }\n if (req.method === 'DELETE') { bookings.delete(id); changes++; return reply(204); }\n return reply(405, { error: 'method not allowed' });\n}\n\nfunction startBackend(port = Number(process.env.BACKEND_PORT) || 3001) {\n const server = http.createServer((req, res) => {\n let body = '';\n req.on('data', (c) => { body += c; });\n req.on('end', () => {\n try { handle(req, res, body); } catch (e) { send(res, 500, { error: String((e && e.message) || e) }); }\n });\n });\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => resolve(server));\n });\n}\n\nmodule.exports = { startBackend, recent, changeCount: () => changes };\nif (require.main === module) startBackend().then(() => console.log('backend on http://localhost:3001'));\n",
7261
- "app.js": "// ResiResi website server: serves the page and makes its calls.\n//\n// PROTECTED (the default): every call goes to the PUBLIC API \u2014 through APIblaze \u2014 with the\n// pizzeria's server key (kept here, never in the browser) and X-End-User-Id naming the\n// person. APIblaze checks the key, applies the rules and forwards to the backend.\n//\n// DIRECT (the page's \"unprotected\" switch): the same calls go straight to the backend on\n// localhost, with the shared backend secret and whatever user the page claims. No APIblaze:\n// nobody checks who may do what. That is how most apps ship.\n//\n// The chat and the two admin widgets use apiblaze's own server helpers\n// (lib/apiblaze-server.js, copied in by `npx apiblaze@latest demo` \u2014 the same code as\n// `import \u2026 from 'apiblaze/server'`).\n\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst env = (name, fallback = '') => process.env[name] || fallback;\nconst json = (res, status, body) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); };\n\nlet lib = null;\ntry { lib = require('./lib/apiblaze-server.js'); } catch { /* run without the widgets */ }\n\n// The two pizzerias (tenants) and the people the demo lets you be. A real app takes\n// \"who is this\" from its own login.\nconst PIZZERIAS = {\n nino: { label: 'Nino Pizza', tenant: () => env('APIBLAZE_TENANT', env('DEMO_NINO_TENANT')), key: () => env('APIBLAZE_SERVER_KEY'), admin: { userId: 'ana', label: 'Ana', email: 'ana@ninopizza.example' } },\n gino: { label: 'Gino Pizza', tenant: () => env('APIBLAZE_GINO_TENANT', env('DEMO_GINO_TENANT')), key: () => env('APIBLAZE_GINO_KEY'), admin: { userId: 'gina', label: 'Gina', email: 'gina@ginopizza.example' } },\n};\n// Each pizzeria (tenant) has its own MCP address: https://{proxy}-{tenant}.mcp.\u2026\nconst portalFor = (tenant) => (env('APIBLAZE_PORTAL_URL') && env('APIBLAZE_TENANT') && tenant ? env('APIBLAZE_PORTAL_URL').replace(`${env('APIBLAZE_TENANT')}.portal`, `${tenant}.portal`) : '');\nconst mcpFor = (tenant) => (env('APIBLAZE_MCP_URL') && env('APIBLAZE_TENANT') && tenant ? env('APIBLAZE_MCP_URL').replace(`-${env('APIBLAZE_TENANT')}.`, `-${tenant}.`) : '');\nconst CUSTOMERS = { ana: 'Ana', ben: 'Ben' };\nconst pizzeriaOf = (id) => PIZZERIAS[id] || PIZZERIAS.nino;\nfunction queryOf(req) { return new URL(req.url, 'http://x').searchParams; }\n\nasync function call(op, { pizzeria = 'nino', as = 'ana', id, body, direct = false } = {}) {\n const pz = pizzeriaOf(pizzeria);\n const base = direct ? `http://127.0.0.1:${env('BACKEND_PORT', '3001')}` : env('APIBLAZE_URL').replace(/\\/$/, '');\n const headers = { 'content-type': 'application/json' };\n if (direct) {\n // Straight to localhost: the backend trusts whatever these headers say.\n headers['x-target-api-key'] = env('BACKEND_SECRET');\n headers['x-abz-user-id'] = as;\n headers['x-abz-tenant-id'] = pz.tenant();\n } else {\n headers['x-api-key'] = pz.key();\n headers['x-end-user-id'] = as;\n }\n const req = op === 'book' ? { method: 'POST', url: `${base}/reservations`, body: JSON.stringify({ name: CUSTOMERS[as] || as, ...(body || {}) }) }\n : op === 'cancel' ? { method: 'DELETE', url: `${base}/reservations/${encodeURIComponent(id || '')}` }\n : { method: 'GET', url: `${base}/reservations` };\n const r = await fetch(req.url, { method: req.method, headers, body: req.body });\n const text = await r.text();\n let data = null;\n try { data = text ? JSON.parse(text) : null; } catch { data = text; }\n return { status: r.status, body: data };\n}\n\n// Node http <-> the web-standard handlers apiblaze/server exports.\nasync function viaHandler(handler, req, res, raw) {\n const request = new Request(`http://localhost${req.url}`, {\n method: req.method,\n headers: { 'content-type': req.headers['content-type'] || 'application/json' },\n body: ['GET', 'HEAD'].includes(req.method) ? undefined : raw,\n });\n const out = await handler(request);\n const headers = {};\n out.headers.forEach((v, k) => { headers[k] = v; });\n res.writeHead(out.status, headers);\n if (!out.body) return res.end();\n const reader = out.body.getReader();\n for (;;) { const { done, value } = await reader.read(); if (done) break; res.write(Buffer.from(value)); }\n res.end();\n}\n\nfunction widgets() {\n if (!lib) return {};\n const out = { chat: {} };\n for (const [id, pz] of Object.entries(PIZZERIAS)) {\n if (!pz.tenant() || !pz.key()) continue;\n // The assistant acts as the signed-in customer, at that customer's pizzeria.\n out.chat[id] = lib.createApiblazeChat({\n project: `${env('APIBLAZE_NAME')}-${pz.tenant()}`, apiKey: pz.key(),\n host: 'tryabz.run', environment: 'dev', getUser: (req) => ({ userId: queryOf(req).get('as') || 'ana' }),\n });\n }\n if (env('APIBLAZE_CP_KEY') && env('APIBLAZE_WIDGETS') === '1') {\n // Admin widgets: the admin of the chosen pizzeria.\n const admin = (req) => { const pz = pizzeriaOf(queryOf(req).get('pizzeria')); return { tenant: pz.tenant(), ...pz.admin }; };\n out.keys = lib.createApiblazeKeys({ cpKey: env('APIBLAZE_CP_KEY'), getUser: admin, environment: 'dev' });\n out.groups = lib.createApiblazeGroups({ cpKey: env('APIBLAZE_CP_KEY'), getUser: admin });\n }\n return out;\n}\n\n/** One of the two websites: `direct` = straight to the backend (unprotected), else through APIblaze. */\nfunction startApp(port = Number(env('APP_PORT', '3000')), { direct = false } = {}) {\n const w = direct ? {} : widgets();\n const pub = path.join(__dirname, 'public');\n const server = http.createServer((req, res) => {\n let raw = '';\n req.on('data', (c) => { raw += c; });\n req.on('end', async () => {\n try {\n const url = new URL(req.url, 'http://x');\n const p = url.pathname;\n if (req.method === 'GET' && (p === '/' || p === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n return res.end(fs.readFileSync(path.join(pub, 'index.html')));\n }\n for (const asset of ['apiblaze-widgets.js', 'apiblaze-logo.svg']) {\n if (req.method === 'GET' && p === `/${asset}` && fs.existsSync(path.join(pub, asset))) {\n res.writeHead(200, { 'content-type': asset.endsWith('.svg') ? 'image/svg+xml' : 'text/javascript; charset=utf-8' });\n return res.end(fs.readFileSync(path.join(pub, asset)));\n }\n }\n if (p === '/api/config') {\n return json(res, 200, {\n direct, apiUrl: env('APIBLAZE_URL'), portalUrl: env('APIBLAZE_PORTAL_URL'),\n backendPort: env('BACKEND_PORT', '3001'),\n protectedUrl: env('APIBLAZE_URL') ? `http://localhost:${env('APP_PORT', '3000')}` : '',\n directUrl: `http://localhost:${env('DIRECT_PORT', '3002')}`,\n logsCommand: `npx apiblaze@latest logs ${env('APIBLAZE_NAME')} --tenant ${env('APIBLAZE_TENANT')}`,\n pizzerias: Object.entries(PIZZERIAS).filter(([, pz]) => pz.tenant()).map(([id, pz]) => ({ id, label: pz.label, admin: pz.admin.label, mcpUrl: mcpFor(pz.tenant()), portalUrl: portalFor(pz.tenant()) })),\n widgets: { chat: Object.keys(w.chat || {}), keys: !!w.keys, groups: !!w.groups },\n });\n }\n if (p === '/api/changes') {\n // Local and free: has any booking changed since the page last listed? The page\n // re-lists through APIblaze only when it has (the backend runs in this process).\n let n = 0;\n try { n = require('./backend').changeCount(); } catch { /* backend runs elsewhere */ }\n return json(res, 200, { n });\n }\n if (p === '/api/apiblaze/chat') {\n const h = (w.chat || {})[url.searchParams.get('pizzeria') || 'nino'];\n if (h) return viaHandler(h.handler, req, res, raw);\n }\n if (p === '/api/apiblaze/keys' && w.keys) return viaHandler(w.keys.handler, req, res, raw);\n if (p === '/api/apiblaze/groups' && w.groups) return viaHandler(w.groups.handler, req, res, raw);\n if (req.method === 'POST' && p === '/api/act') {\n const b = JSON.parse(raw || '{}');\n return json(res, 200, await call(String(b.op || 'list'), { pizzeria: b.pizzeria, as: String(b.as || 'ana'), id: b.id, body: b.body, direct }));\n }\n json(res, 404, { error: 'not found' });\n } catch (e) {\n json(res, 502, { error: `Could not reach the API: ${String((e && e.message) || e)}` });\n }\n });\n });\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => resolve(server));\n });\n}\n\nmodule.exports = { startApp };\n",
7262
- "start.js": "// Starts ResiResi on this laptop: the backend (3001), the unprotected front-end straight to it\n// (3002) and, once APIblaze is set up, the protected website through APIblaze (3000).\n// No APIblaze involved here \u2014 the tunnel is `npx apiblaze@latest demo start|stop|restart`.\n//\n// node start.js run in the foreground (Ctrl-C stops)\n// npm run start run in the background\n// npm run stop stop the background one\n// npm run restart stop, then start again\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawn } = require('child_process');\n\nconst PID = path.join(__dirname, '.demo', 'app.pid');\n\nfunction loadEnv(file = path.join(__dirname, '.env.local')) {\n if (!fs.existsSync(file)) return;\n for (const line of fs.readFileSync(file, 'utf8').split('\\n')) {\n const m = /^\\s*([A-Z0-9_]+)\\s*=\\s*(.*)\\s*$/.exec(line);\n if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^\"(.*)\"$/, '$1');\n }\n}\n\nasync function start() {\n loadEnv();\n const { startBackend } = require('./backend');\n const { startApp } = require('./app');\n const backendPort = Number(process.env.BACKEND_PORT) || 3001;\n const appPort = Number(process.env.APP_PORT) || 3000;\n const directPort = Number(process.env.DIRECT_PORT) || 3002;\n await startBackend(backendPort);\n // The unprotected front-end: straight to the backend, no APIblaze.\n await startApp(directPort, { direct: true });\n // The protected website: through APIblaze (once the demo has written its keys).\n if (process.env.APIBLAZE_URL) await startApp(appPort);\n return { backendPort, appPort, directPort };\n}\n\nconst alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };\nfunction runningPid() {\n try { const pid = Number(fs.readFileSync(PID, 'utf8')); return pid && alive(pid) ? pid : null; } catch { return null; }\n}\nasync function stop() {\n const pid = runningPid();\n if (!pid) { console.log('ResiResi is not running.'); return; }\n process.kill(pid, 'SIGTERM');\n for (let i = 0; i < 30 && alive(pid); i++) await new Promise((r) => setTimeout(r, 100));\n try { fs.unlinkSync(PID); } catch { /* gone */ }\n console.log('Stopped ResiResi (backend and websites).');\n}\nfunction background() {\n if (runningPid()) { console.log('ResiResi is already running.'); return; }\n fs.mkdirSync(path.dirname(PID), { recursive: true });\n const log = fs.openSync(path.join(__dirname, '.demo', 'app.log'), 'a');\n const child = spawn(process.execPath, [__filename], { cwd: __dirname, detached: true, stdio: ['ignore', log, log] });\n child.unref();\n fs.writeFileSync(PID, String(child.pid));\n loadEnv();\n console.log(`Started ResiResi: backend localhost:${process.env.BACKEND_PORT || 3001}, unprotected front-end http://localhost:${process.env.DIRECT_PORT || 3002}${process.env.APIBLAZE_URL ? `, protected website http://localhost:${process.env.APP_PORT || 3000}` : ''}.`);\n}\n\nmodule.exports = { start, loadEnv };\nif (require.main === module) {\n const arg = process.argv[2];\n if (arg === '--stop') stop();\n else if (arg === '--background') background();\n else if (arg === '--restart') stop().then(() => setTimeout(background, 300));\n else {\n start().then(({ appPort, backendPort, directPort }) => console.log(`ResiResi running: backend localhost:${backendPort}, unprotected front-end http://localhost:${directPort}${process.env.APIBLAZE_URL ? `, protected website http://localhost:${appPort}` : ''}. Ctrl-C stops.`))\n .catch((e) => { console.error(`Could not start: ${e.message}`); process.exit(1); });\n }\n}\n",
7263
- "public/index.html": `<!doctype html>
7264
- <html lang="en">
7265
- <head>
7266
- <meta charset="utf-8">
7267
- <meta name="viewport" content="width=device-width, initial-scale=1">
7268
- <title>ResiResi demo</title>
7269
- <style>
7270
- :root {
7271
- --bg: #faf7f2; --card: #fff; --ink: #1d1b19; --muted: #6b6560; --line: #e8e2d9; --soft: #f3eee6;
7272
- --accent: #e4572e; --accent-ink: #fff; --good: #15803d; --good-bg: #ecfdf3; --bad: #c2410c; --bad-bg: #fff1e8;
7273
- --blaze: #4f46e5; --blaze-bg: #eef2ff; --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
7274
- }
7275
- @media (prefers-color-scheme: dark) {
7276
- :root { --bg: #161412; --card: #1f1c1a; --ink: #f3efe9; --muted: #a39b92; --line: #34302c; --soft: #26221f;
7277
- --good: #4ade80; --good-bg: #10241a; --bad: #fb923c; --bad-bg: #2b1a10; --blaze: #a5b4fc; --blaze-bg: #1e1b4b; }
7249
+ // src/commands/mcp.ts
7250
+ var mcp_exports = {};
7251
+ __export(mcp_exports, {
7252
+ PROMPT_NAME_RE: () => PROMPT_NAME_RE,
7253
+ derivedBehavior: () => derivedBehavior,
7254
+ effectiveBehavior: () => effectiveBehavior,
7255
+ loadCatalogue: () => loadCatalogue,
7256
+ normalizePrompt: () => normalizePrompt,
7257
+ renderOps: () => renderOps,
7258
+ runMcp: () => runMcp,
7259
+ runMcpOps: () => runMcpOps,
7260
+ runMcpPrompts: () => runMcpPrompts,
7261
+ runMcpUi: () => runMcpUi,
7262
+ saveCatalogue: () => saveCatalogue,
7263
+ uiResourceUri: () => uiResourceUri,
7264
+ withOverride: () => withOverride,
7265
+ withPrompt: () => withPrompt,
7266
+ withoutOverride: () => withoutOverride,
7267
+ withoutPrompt: () => withoutPrompt
7268
+ });
7269
+ async function runMcp(projectArg, apiVersionArg, opts) {
7270
+ const creds = loadCredentials();
7271
+ if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
7272
+ const projects = await getProjects(creds.teamId ?? "");
7273
+ const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
7274
+ if (!match) throw new Error(`Project "${projectArg}" not found in your active team. Run \`apiblaze projects\`.`);
7275
+ const projectId = match.projectId;
7276
+ const apiVersion = apiVersionArg || match.apiVersion;
7277
+ const environment = opts.environment || "prod";
7278
+ const mcpHost2 = match.tenant ? `${projectId}-${match.tenant}` : projectId;
7279
+ async function publish(ctx) {
7280
+ const spec2 = ctx.proposal;
7281
+ const pub = await agentCall(`/projects/${projectId}/${apiVersion}/mcp/spec`, "PUT", { environment, spec: spec2 });
7282
+ if (pub.status >= 400) {
7283
+ ctx.log(import_chalk27.default.red(` Publish failed (${pub.status}): ${pub.data?.error ?? ""}
7284
+ `));
7285
+ return;
7286
+ }
7287
+ ctx.log(import_chalk27.default.green(` \u2713 Published MCP server \u2014 ${mcpHost2}.mcp.apiblaze.com/${apiVersion}/${environment}.
7288
+ `));
7289
+ }
7290
+ await runAgentChatRepl({
7291
+ title: `MCP builder \u2014 ${projectId} ${apiVersion} (${environment})`,
7292
+ subtitle: "discuss which routes to expose as tools, then make the catalogue official.",
7293
+ endpoint: `/projects/${projectId}/${apiVersion}/mcp/chat`,
7294
+ buildBody: () => ({ environment, included_sample_ids: [] }),
7295
+ seedPrompt: "Look at this API's routes and recommend which should become MCP tools, with good names and descriptions. Don't finalize yet \u2014 explain first.",
7296
+ summarizeProposal: (data) => {
7297
+ const spec2 = data.proposal;
7298
+ const tools = Array.isArray(spec2.tools) ? spec2.tools : [];
7299
+ const names = tools.slice(0, 12).map((t) => t.name ?? "(unnamed)").join(", ");
7300
+ return `Catalogue ready: ${tools.length} tool(s)${names ? ` \u2014 ${names}${tools.length > 12 ? ", \u2026" : ""}` : ""}.`;
7301
+ },
7302
+ commands: [
7303
+ { name: "publish", describe: `to ${mcpHost2}.mcp.apiblaze.com`, needsProposal: true, run: publish }
7304
+ ]
7305
+ });
7306
+ }
7307
+ function derivedBehavior(method) {
7308
+ const m = (method ?? "").toUpperCase();
7309
+ const destructive = m === "DELETE";
7310
+ return {
7311
+ readOnly: m === "GET" || m === "HEAD" || m === "OPTIONS",
7312
+ destructive,
7313
+ idempotent: m === "PUT" || m === "DELETE",
7314
+ confirm: destructive,
7315
+ alwaysAsync: false
7316
+ };
7317
+ }
7318
+ function effectiveBehavior(method, o) {
7319
+ const d = derivedBehavior(method);
7320
+ const readOnly = o?.readOnly ?? d.readOnly;
7321
+ const destructive = readOnly ? false : o?.destructive ?? d.destructive;
7322
+ return {
7323
+ readOnly,
7324
+ destructive,
7325
+ idempotent: o?.idempotent ?? d.idempotent,
7326
+ confirm: o?.confirm ?? destructive,
7327
+ alwaysAsync: o?.alwaysAsync ?? false
7328
+ };
7329
+ }
7330
+ function uiResourceUri(projectId, toolName) {
7331
+ return `ui://${projectId}/${toolName}`;
7332
+ }
7333
+ function normalizePrompt(name, raw) {
7334
+ if (!PROMPT_NAME_RE.test(name)) throw new Error(`Prompt name "${name}" must match ${PROMPT_NAME_RE} (lowercase letters, digits, _ and -).`);
7335
+ const p = raw ?? {};
7336
+ if (typeof p !== "object" || Array.isArray(p)) throw new Error("The prompt file must hold one JSON object.");
7337
+ let messages = p.messages;
7338
+ if (!Array.isArray(messages) && typeof p.text === "string") messages = [{ role: "user", text: p.text }];
7339
+ if (!Array.isArray(messages) || messages.length === 0) throw new Error('The prompt needs "messages": [{ "role": "user", "text": "\u2026" }] (or a "text" shorthand).');
7340
+ for (const m of messages) {
7341
+ if (!m || m.role !== "user" && m.role !== "assistant" || typeof m.text !== "string" || !m.text) {
7342
+ throw new Error('Each message needs role "user" | "assistant" and a non-empty "text".');
7343
+ }
7344
+ }
7345
+ if (p.arguments !== void 0 && (!Array.isArray(p.arguments) || p.arguments.some((a) => !a || typeof a.name !== "string"))) {
7346
+ throw new Error('"arguments" must be [{ "name": "\u2026", "description"?: "\u2026", "required"?: true }].');
7347
+ }
7348
+ return {
7349
+ name,
7350
+ ...typeof p.title === "string" ? { title: p.title } : {},
7351
+ ...typeof p.description === "string" ? { description: p.description } : {},
7352
+ ...Array.isArray(p.arguments) ? { arguments: p.arguments } : {},
7353
+ messages
7354
+ };
7355
+ }
7356
+ function withPrompt(cat, prompt) {
7357
+ const prompts = [...cat.prompts ?? []];
7358
+ const i = prompts.findIndex((p) => p.name === prompt.name);
7359
+ if (i >= 0) prompts[i] = prompt;
7360
+ else prompts.push(prompt);
7361
+ return { ...cat, prompts };
7362
+ }
7363
+ function withoutPrompt(cat, name) {
7364
+ const prompts = cat.prompts ?? [];
7365
+ if (!prompts.some((p) => p.name === name)) throw new Error(`No prompt named "${name}". See \`${B.cli} mcp prompts list\`.`);
7366
+ return { ...cat, prompts: prompts.filter((p) => p.name !== name) };
7367
+ }
7368
+ function requireTool(cat, tool) {
7369
+ if (!cat.tools.some((t) => t.name === tool)) {
7370
+ const names = cat.tools.slice(0, 10).map((t) => t.name).join(", ");
7371
+ throw new Error(`No tool named "${tool}" in the published catalogue${names ? ` (tools: ${names}${cat.tools.length > 10 ? ", \u2026" : ""})` : ""}.`);
7372
+ }
7373
+ }
7374
+ function withOverride(cat, tool, flags) {
7375
+ requireTool(cat, tool);
7376
+ const patch = {};
7377
+ for (const k of ["readOnly", "destructive", "idempotent", "confirm", "alwaysAsync"]) {
7378
+ if (typeof flags[k] === "boolean") patch[k] = flags[k];
7379
+ }
7380
+ if (typeof flags.title === "string") {
7381
+ const t = flags.title.trim().slice(0, 120);
7382
+ if (!t) throw new Error('--title needs some text (a short human label, e.g. "List pets").');
7383
+ patch.title = t;
7384
+ }
7385
+ if (Object.keys(patch).length === 0) {
7386
+ throw new Error("Nothing to set. Pass --title, --read-only, --destructive, --idempotent, --confirm, --always-async (or their --no-\u2026 forms).");
7387
+ }
7388
+ return { ...cat, overrides: { ...cat.overrides ?? {}, [tool]: { ...cat.overrides?.[tool] ?? {}, ...patch } } };
7389
+ }
7390
+ function withoutOverride(cat, tool) {
7391
+ requireTool(cat, tool);
7392
+ const overrides = { ...cat.overrides ?? {} };
7393
+ delete overrides[tool];
7394
+ return { ...cat, overrides };
7395
+ }
7396
+ function flagsLine(b) {
7397
+ const f = [];
7398
+ if (b.readOnly) f.push("read-only");
7399
+ if (b.idempotent) f.push("idempotent");
7400
+ if (b.destructive) f.push("destructive");
7401
+ if (b.confirm) f.push("asks to confirm");
7402
+ if (b.alwaysAsync) f.push("always async");
7403
+ return f.length ? f.join(", ") : "\u2014";
7404
+ }
7405
+ function renderOps(cat) {
7406
+ const rows = cat.tools.map((t) => {
7407
+ const method = t["x-route"]?.method?.toUpperCase() ?? "";
7408
+ const o = cat.overrides?.[t.name];
7409
+ return { name: t.name, route: `${method} ${t["x-route"]?.path ?? ""}`.trim(), eff: flagsLine(effectiveBehavior(method, o)), custom: !!o && Object.keys(o).length > 0 };
7410
+ });
7411
+ const w = Math.min(40, Math.max(4, ...rows.map((r) => r.name.length)));
7412
+ const rw = Math.min(40, Math.max(5, ...rows.map((r) => r.route.length)));
7413
+ return rows.map((r) => ` ${r.name.padEnd(w)} ${r.route.padEnd(rw)} ${r.eff}${r.custom ? import_chalk27.default.yellow(" (override)") : ""}`);
7414
+ }
7415
+ async function resolveMcpTarget(projectArg, opts) {
7416
+ const creds = loadCredentials();
7417
+ if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
7418
+ const projects = await getProjects(creds.teamId ?? "");
7419
+ const matches2 = projects.filter((p) => p.projectId === projectArg || p.projectName === projectArg);
7420
+ const wanted = opts.apiversion ?? opts.version;
7421
+ const match = (wanted ? matches2.find((p) => p.apiVersion === wanted) : matches2[0]) ?? null;
7422
+ if (!match) throw new Error(`Project "${projectArg}"${wanted ? ` version ${wanted}` : ""} not found in your active team. Run \`${B.cli} projects\`.`);
7423
+ return { projectId: match.projectId, apiVersion: wanted || match.apiVersion, environment: opts.environment || "prod" };
7424
+ }
7425
+ async function loadCatalogue(t) {
7426
+ const r = await agentCall(`/projects/${t.projectId}/${t.apiVersion}/mcp/spec?environment=${encodeURIComponent(t.environment)}`, "GET");
7427
+ if (r.status >= 400) throw new Error(`Could not read the MCP catalogue (${r.status}): ${r.data?.error ?? ""}`);
7428
+ const spec2 = r.data?.spec;
7429
+ if (!spec2 || !Array.isArray(spec2.tools)) {
7430
+ throw new Error(`No MCP catalogue published for ${t.projectId} ${t.apiVersion} (${t.environment}). Try --environment, or build one with \`${B.cli} agent mcp ${t.projectId}\`.`);
7431
+ }
7432
+ return spec2;
7433
+ }
7434
+ async function saveCatalogue(t, spec2) {
7435
+ const r = await agentCall(`/projects/${t.projectId}/${t.apiVersion}/mcp/spec`, "PUT", { environment: t.environment, spec: spec2 });
7436
+ if (r.status >= 400) {
7437
+ const d = r.data;
7438
+ throw new Error(`Save failed (${r.status}): ${d?.error ?? ""}${d?.details ? ` \u2014 ${typeof d.details === "string" ? d.details : JSON.stringify(d.details)}` : ""}`);
7439
+ }
7440
+ }
7441
+ async function runMcpPrompts(action2, project, name, opts) {
7442
+ const t = await resolveMcpTarget(project, opts);
7443
+ const cat = await loadCatalogue(t);
7444
+ const prompts = cat.prompts ?? [];
7445
+ if (action2 === "list") {
7446
+ if (opts.json) return void console.log(JSON.stringify({ environment: t.environment, prompts }, null, 2));
7447
+ if (!prompts.length) {
7448
+ console.log(import_chalk27.default.dim(`
7449
+ No prompts on ${t.projectId} (${t.environment}). Prompts are drafted by the MCP agent (\`${B.cli} agent mcp ${t.projectId}\`) or added with \`${B.cli} mcp prompts set\`.
7450
+ `));
7451
+ return;
7452
+ }
7453
+ console.log("");
7454
+ for (const p of prompts) {
7455
+ const args = (p.arguments ?? []).map((a) => a.required ? a.name : `${a.name}?`).join(", ");
7456
+ console.log(` ${import_chalk27.default.bold(p.name)}${args ? import_chalk27.default.dim(`(${args})`) : ""}${p.title || p.description ? ` ${p.title ?? p.description}` : ""}`);
7457
+ }
7458
+ console.log("");
7459
+ return;
7460
+ }
7461
+ if (!name) throw new Error(`Usage: ${B.cli} mcp prompts ${action2} <project> <name>`);
7462
+ if (action2 === "show") {
7463
+ const p = prompts.find((x) => x.name === name);
7464
+ if (!p) throw new Error(`No prompt named "${name}". See \`${B.cli} mcp prompts list ${project}\`.`);
7465
+ if (opts.json) return void console.log(JSON.stringify(p, null, 2));
7466
+ console.log(`
7467
+ ${import_chalk27.default.bold(p.name)}${p.title ? ` \u2014 ${p.title}` : ""}`);
7468
+ if (p.description) console.log(` ${p.description}`);
7469
+ for (const a of p.arguments ?? []) console.log(import_chalk27.default.dim(` ${a.name}${a.required ? " (required)" : ""}${a.description ? `: ${a.description}` : ""}`));
7470
+ for (const m of p.messages) console.log(`
7471
+ [${m.role}] ${m.text}`);
7472
+ console.log("");
7473
+ return;
7474
+ }
7475
+ let next;
7476
+ if (action2 === "set") {
7477
+ if (!opts.file) throw new Error(`Usage: ${B.cli} mcp prompts set <project> <name> --file prompt.json`);
7478
+ let raw;
7479
+ try {
7480
+ raw = JSON.parse(import_fs5.default.readFileSync(opts.file, "utf8"));
7481
+ } catch (e) {
7482
+ throw new Error(`Could not read ${opts.file}: ${e instanceof Error ? e.message : String(e)}`);
7483
+ }
7484
+ next = withPrompt(cat, normalizePrompt(name, raw));
7485
+ } else {
7486
+ next = withoutPrompt(cat, name);
7487
+ }
7488
+ await saveCatalogue(t, next);
7489
+ if (opts.json) return void console.log(JSON.stringify({ saved: true, environment: t.environment, prompts: next.prompts }, null, 2));
7490
+ console.log(import_chalk27.default.green(` \u2713 ${action2 === "set" ? "Saved" : "Removed"} prompt "${name}" on ${t.projectId} (${t.environment}).`));
7491
+ }
7492
+ async function runMcpOps(action2, project, tool, opts) {
7493
+ const t = await resolveMcpTarget(project, opts);
7494
+ const cat = await loadCatalogue(t);
7495
+ if (action2 === "list") {
7496
+ if (opts.json) {
7497
+ const tools = cat.tools.map((x) => {
7498
+ const method2 = x["x-route"]?.method?.toUpperCase();
7499
+ return {
7500
+ name: x.name,
7501
+ method: method2,
7502
+ path: x["x-route"]?.path,
7503
+ default: derivedBehavior(method2),
7504
+ override: cat.overrides?.[x.name] ?? null,
7505
+ effective: effectiveBehavior(method2, cat.overrides?.[x.name])
7506
+ };
7507
+ });
7508
+ return void console.log(JSON.stringify({ environment: t.environment, tools }, null, 2));
7509
+ }
7510
+ console.log(import_chalk27.default.dim(`
7511
+ ${cat.tools.length} tool(s) on ${t.projectId} (${t.environment}). Defaults come from the HTTP method: GET/HEAD/OPTIONS read-only, PUT/DELETE idempotent, DELETE destructive + asks to confirm.
7512
+ `));
7513
+ for (const l of renderOps(cat)) console.log(l);
7514
+ console.log(import_chalk27.default.dim(`
7515
+ Override: ${B.cli} mcp ops set ${project} <tool> --read-only | --destructive | --confirm | --no-confirm | --always-async \xB7 Reset: ${B.cli} mcp ops reset ${project} <tool>
7516
+ `));
7517
+ return;
7518
+ }
7519
+ if (!tool) throw new Error(`Usage: ${B.cli} mcp ops ${action2} <project> <tool>`);
7520
+ const next = action2 === "set" ? withOverride(cat, tool, opts) : withoutOverride(cat, tool);
7521
+ await saveCatalogue(t, next);
7522
+ const method = cat.tools.find((x) => x.name === tool)?.["x-route"]?.method;
7523
+ const eff = effectiveBehavior(method, next.overrides?.[tool]);
7524
+ if (opts.json) return void console.log(JSON.stringify({ saved: true, tool, override: next.overrides?.[tool] ?? null, effective: eff }, null, 2));
7525
+ console.log(import_chalk27.default.green(` \u2713 ${tool}: ${flagsLine(eff)}${action2 === "reset" ? " (automatic default)" : ""}`));
7526
+ }
7527
+ async function runMcpUi(project, tool, opts) {
7528
+ const t = await resolveMcpTarget(project, opts);
7529
+ const cat = await loadCatalogue(t);
7530
+ requireTool(cat, tool);
7531
+ const uri = uiResourceUri(t.projectId, tool);
7532
+ if (opts.json) return void console.log(JSON.stringify({ tool, uri, kind: "automatic" }, null, 2));
7533
+ console.log(`
7534
+ ${import_chalk27.default.bold(uri)}
7535
+ `);
7536
+ console.log(import_chalk27.default.dim(" This is the automatic viewer: MCP Apps clients render the tool's result with a generic UI built"));
7537
+ console.log(import_chalk27.default.dim(" from its response schema \u2014 nothing to configure. Uploading your own HTML for it comes later.\n"));
7538
+ }
7539
+ var import_fs5, import_chalk27, PROMPT_NAME_RE;
7540
+ var init_mcp = __esm({
7541
+ "src/commands/mcp.ts"() {
7542
+ "use strict";
7543
+ import_fs5 = __toESM(require("fs"));
7544
+ import_chalk27 = __toESM(require("chalk"));
7545
+ init_brand();
7546
+ init_auth();
7547
+ init_api();
7548
+ init_agent_chat();
7549
+ PROMPT_NAME_RE = /^[a-z0-9_-]{1,64}$/;
7550
+ }
7551
+ });
7552
+
7553
+ // src/server/core.ts
7554
+ async function safeErr(res) {
7555
+ try {
7556
+ const b = await res.json();
7557
+ if (typeof b.error === "string") return b.error;
7558
+ if (b.error?.message) return b.error.message;
7559
+ if (b.detail || b.title) return [b.title, b.detail].filter(Boolean).join(". ").replace(/\.\. /, ". ");
7560
+ return `apiblaze ${res.status}`;
7561
+ } catch {
7562
+ return `apiblaze ${res.status}`;
7563
+ }
7564
+ }
7565
+ function createPlaneCore(cfg) {
7566
+ if (!cfg.cpKey) throw new Error("apiblaze: cpKey is required (set APIBLAZE_CP_KEY). It is server-only \u2014 never expose it to the browser.");
7567
+ const apikeysBase = (cfg.apikeysBase ?? "https://apikeys.apiblaze.com").replace(/\/$/, "");
7568
+ const iamBase = (cfg.iamBase ?? "https://iam.apiblaze.com").replace(/\/$/, "");
7569
+ const tenantCache = /* @__PURE__ */ new Map();
7570
+ const endUserCache = /* @__PURE__ */ new Map();
7571
+ function planeFetch(base2, method, path17, user, opts = {}) {
7572
+ const params = new URLSearchParams({ product: "apiblaze", ...opts.query ?? {} });
7573
+ const headers = { "X-API-Key": cfg.cpKey, "X-End-User-Id": user.userId };
7574
+ let body;
7575
+ if (opts.body !== void 0) {
7576
+ headers["Content-Type"] = "application/json";
7577
+ body = JSON.stringify(opts.body);
7578
+ }
7579
+ return fetch(`${base2}${path17}?${params.toString()}`, { method, headers, body, cache: "no-store" });
7580
+ }
7581
+ async function resolveTenant2(user) {
7582
+ const cached = tenantCache.get(user.tenant);
7583
+ if (cached) return cached;
7584
+ const res = await planeFetch(apikeysBase, "POST", "/tenants", user, { body: { ref: user.tenant } });
7585
+ if (!res.ok) throw new PlaneError(res.status, await safeErr(res));
7586
+ const { tenant: tenant2 } = await res.json();
7587
+ tenantCache.set(user.tenant, tenant2);
7588
+ return tenant2;
7589
+ }
7590
+ async function ensureEndUser(user, tenant2) {
7591
+ const cacheKey = `${tenant2}:${user.userId}`;
7592
+ const cached = endUserCache.get(cacheKey);
7593
+ if (cached) return cached;
7594
+ const res = await planeFetch(apikeysBase, "POST", "/users", user, {
7595
+ body: { tenant: tenant2, ref: user.userId, email: user.email ?? null, display_name: user.label ?? null }
7596
+ });
7597
+ if (!res.ok) throw new PlaneError(res.status, await safeErr(res));
7598
+ const { abz_sub } = await res.json();
7599
+ endUserCache.set(cacheKey, abz_sub);
7600
+ return abz_sub;
7601
+ }
7602
+ return { planeFetch, resolveTenant: resolveTenant2, ensureEndUser, apikeysBase, iamBase };
7603
+ }
7604
+ var PlaneError;
7605
+ var init_core = __esm({
7606
+ "src/server/core.ts"() {
7607
+ "use strict";
7608
+ PlaneError = class extends Error {
7609
+ constructor(status2, detail) {
7610
+ super(detail);
7611
+ this.status = status2;
7612
+ this.detail = detail;
7613
+ }
7614
+ };
7615
+ }
7616
+ });
7617
+
7618
+ // src/demo-app/files.ts
7619
+ var DEMO_FILES;
7620
+ var init_files = __esm({
7621
+ "src/demo-app/files.ts"() {
7622
+ "use strict";
7623
+ DEMO_FILES = {
7624
+ "README.md": "# Nino's Pizza: the APIblaze demo\n\nA tiny table-booking app for pizzerias, used by `npx apiblaze@latest demo` to show what APIblaze does.\n\n```\nnpx apiblaze@latest demo\n```\n\nThat one command writes this folder to `./apiblaze-demo`, puts APIblaze in front of the backend,\nstarts everything and opens the page. No login, no prompts, no dependencies.\n\n## What the page shows\n\n1. **Your API is live on the internet.** The backend runs on your laptop; APIblaze gives it a public\n URL and an MCP address for AI assistants. A live ticker shows each request reaching your laptop.\n2. **An AI assistant uses it.** Ask it to book a table and watch the request arrive.\n3. **Included, no code:** only the owner can change a booking, each customer (Nino Pizza,\n Gino Pizza) is separate, rate limits, sign-in, and drop-in widgets for API keys and roles.\n\n## The files\n\n| File | What it is |\n| --- | --- |\n| `backend.js` | The pizzeria's API. No login, no keys, no permission checks. |\n| `openapi.yaml` | Its endpoints. APIblaze builds the AI tools and the ownership rules from it. |\n| `app.js` | The website's server. Calls the API with one key and says who is acting. |\n| `public/index.html` | The page. |\n| `start.js` | Starts the backend (port 3001) and the website (port 3000). |\n\n`npx apiblaze@latest demo` also adds `lib/apiblaze-server.js` and `public/apiblaze-widgets.js`, the same\nhelpers you get from `npm i apiblaze`. Stop the demo with `npx apiblaze@latest demo stop`.\n",
7625
+ "openapi.yaml": `openapi: 3.0.3
7626
+ info:
7627
+ title: ResiResi API
7628
+ version: 1.0.0
7629
+ description: Table bookings at restaurants (each restaurant is a tenant, like Nino Pizza and Gino Pizza). APIblaze reads this file to build the MCP tools for AI assistants and to work out who may change what.
7630
+ paths:
7631
+ /reservations:
7632
+ get:
7633
+ operationId: listReservations
7634
+ summary: List my bookings
7635
+ description: The signed-in person's own bookings at this restaurant, and nobody else's. Use it for "my bookings" or "the one I just made", and call it again whenever asked, since bookings change.
7636
+ responses:
7637
+ '200':
7638
+ description: The caller's bookings
7639
+ content:
7640
+ application/json:
7641
+ schema:
7642
+ type: array
7643
+ items: { $ref: '#/components/schemas/Reservation' }
7644
+ post:
7645
+ operationId: createReservation
7646
+ summary: Book a table
7647
+ description: Only the time is needed; the day is today unless a date is sent. For tomorrow or any other day, work out the date and send it. The restaurant picks a free table and books it in the signed-in person's name, so never ask for a table number or a name.
7648
+ requestBody:
7649
+ required: true
7650
+ content:
7651
+ application/json:
7652
+ schema: { $ref: '#/components/schemas/NewReservation' }
7653
+ responses:
7654
+ '201':
7655
+ description: Booked
7656
+ content:
7657
+ application/json:
7658
+ schema: { $ref: '#/components/schemas/Reservation' }
7659
+ /reservations/{id}:
7660
+ parameters:
7661
+ - { name: id, in: path, required: true, schema: { type: string } }
7662
+ get:
7663
+ operationId: getReservation
7664
+ summary: Get one booking
7665
+ description: Only the person who booked it (or the restaurant's admin) can see it. Anyone else is refused.
7666
+ responses:
7667
+ '200':
7668
+ description: The booking
7669
+ content:
7670
+ application/json:
7671
+ schema: { $ref: '#/components/schemas/Reservation' }
7672
+ patch:
7673
+ operationId: updateReservation
7674
+ summary: Change a booking
7675
+ description: Move a booking to another day, table or time, or change the number of guests. Only the person who booked it (or the restaurant's admin) can change it. Get the id from listReservations; never ask for it.
7676
+ requestBody:
7677
+ required: true
7678
+ content:
7679
+ application/json:
7680
+ schema: { $ref: '#/components/schemas/ReservationChange' }
7681
+ responses:
7682
+ '200':
7683
+ description: Changed
7684
+ content:
7685
+ application/json:
7686
+ schema: { $ref: '#/components/schemas/Reservation' }
7687
+ '409': { description: That table is already booked that day at that time }
7688
+ delete:
7689
+ operationId: cancelReservation
7690
+ summary: Cancel a booking
7691
+ description: Only the person who booked it (or the restaurant's admin) can cancel it.
7692
+ responses:
7693
+ '204': { description: Cancelled }
7694
+ components:
7695
+ schemas:
7696
+ NewReservation:
7697
+ type: object
7698
+ required: [time]
7699
+ properties:
7700
+ name: { type: string, example: Ana, description: 'Optional: defaults to the signed-in person. Do not ask for it.' }
7701
+ table: { type: integer, example: 4, description: 'Optional: the restaurant picks a free table. Do not ask for it.' }
7702
+ date: { type: string, format: date, example: '2026-10-09', description: 'Day of the booking, YYYY-MM-DD. Optional: today if not given.' }
7703
+ time: { type: string, example: '19:30', description: 'Time of the booking, 24h HH:MM' }
7704
+ guests: { type: integer, example: 2, description: 'How many people. Optional, 2 if not given.' }
7705
+ ReservationChange:
7706
+ type: object
7707
+ properties:
7708
+ table: { type: integer, example: 2, description: 'Move to this table.' }
7709
+ date: { type: string, format: date, example: '2026-10-10', description: 'New day, YYYY-MM-DD' }
7710
+ time: { type: string, example: '20:00', description: 'New time, 24h HH:MM' }
7711
+ guests: { type: integer, example: 4, description: 'New number of people' }
7712
+ Reservation:
7713
+ allOf:
7714
+ - $ref: '#/components/schemas/NewReservation'
7715
+ - type: object
7716
+ properties:
7717
+ id: { type: string, example: r1 }
7718
+ `,
7719
+ "backend.js": "// ResiResi backend: restaurants (tenants, like Nino Pizza and Gino Pizza) let their customers\n// book, list and cancel tables. It runs on YOUR laptop.\n//\n// `npx apiblaze@latest dev` (which the demo runs for you) puts it on the internet\n// at a public URL and gives AI assistants an MCP address for it. Everything else\n// (API keys, sign-in, who may change what, rate limits) happens in APIblaze, in\n// front of this file. Look at what is NOT here: no login, no keys, no permission\n// checks, no AI code.\n//\n// What APIblaze tells this backend about each call, in two headers:\n// x-abz-tenant-id which customer (pizzeria) is calling\n// x-abz-user-id which person\n// and one shared secret it adds to every call it forwards (x-target-api-key), so a\n// caller who skips APIblaze and hits localhost:3001 directly is refused.\n//\n// Zero dependencies. Run it alone with `node backend.js` (port 3001).\n\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst bookings = new Map();\nlet next = 1;\n// The people the demo provisioned up front, per pizzeria: { tenant: { handle: stable id } }.\n// Through APIblaze every call already names the stable id (x-abz-user-id); a call straight\n// to this port still says \"ana\", and must mean the same person.\nconst USER_IDS = (() => { try { return JSON.parse(process.env.DEMO_USER_IDS || '{}'); } catch { return {}; } })();\nconst idOf = (tenant, handle) => (USER_IDS[tenant] || {})[handle] || handle;\n// Bumps on every booking made or cancelled. The page asks the app (on this laptop, free)\n// whether it moved before re-listing through APIblaze, so an open, idle page spends nothing.\nlet changes = 0;\n// The last requests this laptop answered \u2014 the demo page shows them live.\nconst recent = [];\nfunction remember(entry) {\n recent.unshift({ at: Date.now(), ...entry });\n recent.length = Math.min(recent.length, 50);\n // One line per request in the terminal (`npx apiblaze@latest demo logs` follows it).\n const who = entry.who ? `${entry.who}${entry.tenant ? ` @ ${entry.tenant}` : ''}` : '-';\n console.log(`${new Date().toISOString().slice(11, 19)} ${entry.status} ${entry.method.padEnd(6)} ${entry.path.padEnd(22)} ${who}`);\n}\n\n// The restaurant's day, YYYY-MM-DD, on this laptop's clock: a booking without a date is for today.\nfunction today() {\n const d = new Date();\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n}\n// A real calendar day written YYYY-MM-DD (2026-02-30 is not one).\nfunction isDay(s) {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return false;\n const [y, m, d] = s.split('-').map(Number);\n const t = new Date(Date.UTC(y, m - 1, d));\n return t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d;\n}\n\n// The restaurant picks the table: the lowest one still free that day at that time, at that restaurant.\nfunction freeTable(tenant, date, time) {\n const taken = new Set([...bookings.values()].filter((r) => r.tenant === tenant && r.date === date && r.time === time).map((r) => r.table));\n let t = 1;\n while (taken.has(t)) t++;\n return t;\n}\n\nfunction send(res, status, body) {\n res.writeHead(status, { 'content-type': 'application/json' });\n res.end(body === undefined ? '' : JSON.stringify(body));\n}\n\nfunction handle(req, res, body) {\n const url = new URL(req.url, 'http://x');\n if (url.pathname === '/openapi.yaml') {\n res.writeHead(200, { 'content-type': 'text/yaml' });\n return res.end(fs.readFileSync(path.join(__dirname, 'openapi.yaml')));\n }\n const tenant = req.headers['x-abz-tenant-id'] || '';\n const who = idOf(tenant, req.headers['x-abz-user-id'] || '');\n // Shown in the ticker and the terminal: the name the caller gave (APIblaze forwards it as\n // x-end-user-id next to the stable id), never the opaque id.\n const shown = req.headers['x-end-user-id'] || req.headers['x-abz-user-id'] || '';\n const reply = (status, data) => { remember({ method: req.method, path: url.pathname, who: shown, tenant, status }); send(res, status, data); };\n const secret = process.env.BACKEND_SECRET || '';\n if (secret && req.headers['x-target-api-key'] !== secret) return reply(401, { error: 'missing or wrong backend secret (APIblaze adds it to every call it forwards)' });\n if (!who) return reply(401, { error: 'say who is calling (x-abz-user-id)' });\n\n const m = /^\\/reservations(?:\\/([^/]+))?$/.exec(url.pathname);\n if (!m) return reply(404, { error: 'not found' });\n const id = m[1];\n\n if (!id && req.method === 'POST') {\n let input = {};\n try { input = JSON.parse(body || '{}'); } catch { return reply(400, { error: 'body must be JSON' }); }\n const date = input.date ? String(input.date) : today();\n if (!isDay(date)) return reply(400, { error: 'date must be a day, YYYY-MM-DD' });\n const time = String(input.time || '19:30');\n const row = {\n id: `r${next++}`, name: String(input.name || who), table: Number(input.table) || freeTable(tenant, date, time),\n date, time, guests: Number(input.guests) || 2, owner: who, tenant,\n };\n bookings.set(row.id, row); changes++;\n return reply(201, row);\n }\n if (!id && req.method === 'GET') {\n // Lists are the backend's job: each person sees their own bookings, at their own pizzeria.\n return reply(200, [...bookings.values()].filter((r) => r.owner === who && r.tenant === tenant)\n .sort((a, b) => `${a.date} ${a.time}`.localeCompare(`${b.date} ${b.time}`)));\n }\n const row = bookings.get(id);\n if (!row) return reply(404, { error: 'no such booking' });\n if (req.method === 'GET') return reply(200, row);\n if (req.method === 'PATCH') {\n let input = {};\n try { input = JSON.parse(body || '{}'); } catch { return reply(400, { error: 'body must be JSON' }); }\n const date = input.date ? String(input.date) : row.date;\n if (!isDay(date)) return reply(400, { error: 'date must be a day, YYYY-MM-DD' });\n const time = input.time ? String(input.time) : row.time;\n const moved = date !== row.date || time !== row.time;\n const table = input.table ? Number(input.table) : (moved ? freeTable(row.tenant, date, time) : row.table);\n const clash = [...bookings.values()].find((r) => r.id !== row.id && r.tenant === row.tenant && r.date === date && r.time === time && r.table === table);\n if (clash) return reply(409, { error: `table ${table} is already booked on ${date} at ${time}` });\n Object.assign(row, { date, time, table, ...(input.guests ? { guests: Number(input.guests) } : {}) }); changes++;\n return reply(200, row);\n }\n if (req.method === 'DELETE') { bookings.delete(id); changes++; return reply(204); }\n return reply(405, { error: 'method not allowed' });\n}\n\nfunction startBackend(port = Number(process.env.BACKEND_PORT) || 3001) {\n const server = http.createServer((req, res) => {\n let body = '';\n req.on('data', (c) => { body += c; });\n req.on('end', () => {\n try { handle(req, res, body); } catch (e) { send(res, 500, { error: String((e && e.message) || e) }); }\n });\n });\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => resolve(server));\n });\n}\n\nmodule.exports = { startBackend, recent, changeCount: () => changes };\nif (require.main === module) startBackend().then(() => console.log('backend on http://localhost:3001'));\n",
7720
+ "app.js": "// ResiResi website server: serves the page and makes its calls.\n//\n// PROTECTED (the default): every call goes to the PUBLIC API \u2014 through APIblaze \u2014 with the\n// pizzeria's server key (kept here, never in the browser) and X-End-User-Id naming the\n// person. APIblaze checks the key, applies the rules and forwards to the backend.\n//\n// DIRECT (the page's \"unprotected\" switch): the same calls go straight to the backend on\n// localhost, with the shared backend secret and whatever user the page claims. No APIblaze:\n// nobody checks who may do what. That is how most apps ship.\n//\n// The chat and the two admin widgets use apiblaze's own server helpers\n// (lib/apiblaze-server.js, copied in by `npx apiblaze@latest demo` \u2014 the same code as\n// `import \u2026 from 'apiblaze/server'`).\n\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst env = (name, fallback = '') => process.env[name] || fallback;\nconst json = (res, status, body) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); };\n\nlet lib = null;\ntry { lib = require('./lib/apiblaze-server.js'); } catch { /* run without the widgets */ }\n\n// The two pizzerias (tenants) and the people the demo lets you be. A real app takes\n// \"who is this\" from its own login.\nconst PIZZERIAS = {\n nino: { label: 'Nino Pizza', tenant: () => env('APIBLAZE_TENANT', env('DEMO_NINO_TENANT')), key: () => env('APIBLAZE_SERVER_KEY'), admin: { userId: 'ana', label: 'Ana', email: 'ana@ninopizza.example' } },\n gino: { label: 'Gino Pizza', tenant: () => env('APIBLAZE_GINO_TENANT', env('DEMO_GINO_TENANT')), key: () => env('APIBLAZE_GINO_KEY'), admin: { userId: 'gina', label: 'Gina', email: 'gina@ginopizza.example' } },\n};\n// Each pizzeria (tenant) has its own MCP address: https://{proxy}-{tenant}.mcp.\u2026\nconst portalFor = (tenant) => (env('APIBLAZE_PORTAL_URL') && env('APIBLAZE_TENANT') && tenant ? env('APIBLAZE_PORTAL_URL').replace(`${env('APIBLAZE_TENANT')}.portal`, `${tenant}.portal`) : '');\nconst mcpFor = (tenant) => (env('APIBLAZE_MCP_URL') && env('APIBLAZE_TENANT') && tenant ? env('APIBLAZE_MCP_URL').replace(`-${env('APIBLAZE_TENANT')}.`, `-${tenant}.`) : '');\nconst CUSTOMERS = { ana: 'Ana', ben: 'Ben' };\nconst pizzeriaOf = (id) => PIZZERIAS[id] || PIZZERIAS.nino;\nfunction queryOf(req) { return new URL(req.url, 'http://x').searchParams; }\n\nasync function call(op, { pizzeria = 'nino', as = 'ana', id, body, direct = false } = {}) {\n const pz = pizzeriaOf(pizzeria);\n const base = direct ? `http://127.0.0.1:${env('BACKEND_PORT', '3001')}` : env('APIBLAZE_URL').replace(/\\/$/, '');\n const headers = { 'content-type': 'application/json' };\n if (direct) {\n // Straight to localhost: the backend trusts whatever these headers say.\n headers['x-target-api-key'] = env('BACKEND_SECRET');\n headers['x-abz-user-id'] = as;\n headers['x-abz-tenant-id'] = pz.tenant();\n } else {\n headers['x-api-key'] = pz.key();\n headers['x-end-user-id'] = as;\n }\n const req = op === 'book' ? { method: 'POST', url: `${base}/reservations`, body: JSON.stringify({ name: CUSTOMERS[as] || as, ...(body || {}) }) }\n : op === 'cancel' ? { method: 'DELETE', url: `${base}/reservations/${encodeURIComponent(id || '')}` }\n : { method: 'GET', url: `${base}/reservations` };\n const r = await fetch(req.url, { method: req.method, headers, body: req.body });\n const text = await r.text();\n let data = null;\n try { data = text ? JSON.parse(text) : null; } catch { data = text; }\n return { status: r.status, body: data };\n}\n\n// Node http <-> the web-standard handlers apiblaze/server exports.\nasync function viaHandler(handler, req, res, raw) {\n const request = new Request(`http://localhost${req.url}`, {\n method: req.method,\n headers: { 'content-type': req.headers['content-type'] || 'application/json' },\n body: ['GET', 'HEAD'].includes(req.method) ? undefined : raw,\n });\n const out = await handler(request);\n const headers = {};\n out.headers.forEach((v, k) => { headers[k] = v; });\n res.writeHead(out.status, headers);\n if (!out.body) return res.end();\n const reader = out.body.getReader();\n for (;;) { const { done, value } = await reader.read(); if (done) break; res.write(Buffer.from(value)); }\n res.end();\n}\n\nfunction widgets() {\n if (!lib) return {};\n const out = { chat: {} };\n for (const [id, pz] of Object.entries(PIZZERIAS)) {\n if (!pz.tenant() || !pz.key()) continue;\n // The assistant acts as the signed-in customer, at that customer's pizzeria.\n out.chat[id] = lib.createApiblazeChat({\n project: `${env('APIBLAZE_NAME')}-${pz.tenant()}`, apiKey: pz.key(),\n host: 'tryabz.run', environment: 'dev', getUser: (req) => ({ userId: queryOf(req).get('as') || 'ana' }),\n });\n }\n if (env('APIBLAZE_CP_KEY') && env('APIBLAZE_WIDGETS') === '1') {\n // Admin widgets: the admin of the chosen pizzeria.\n const admin = (req) => { const pz = pizzeriaOf(queryOf(req).get('pizzeria')); return { tenant: pz.tenant(), ...pz.admin }; };\n out.keys = lib.createApiblazeKeys({ cpKey: env('APIBLAZE_CP_KEY'), getUser: admin, environment: 'dev' });\n out.groups = lib.createApiblazeGroups({ cpKey: env('APIBLAZE_CP_KEY'), getUser: admin });\n }\n return out;\n}\n\n/** One of the two websites: `direct` = straight to the backend (unprotected), else through APIblaze. */\nfunction startApp(port = Number(env('APP_PORT', '3000')), { direct = false } = {}) {\n const w = direct ? {} : widgets();\n const pub = path.join(__dirname, 'public');\n const server = http.createServer((req, res) => {\n let raw = '';\n req.on('data', (c) => { raw += c; });\n req.on('end', async () => {\n try {\n const url = new URL(req.url, 'http://x');\n const p = url.pathname;\n if (req.method === 'GET' && (p === '/' || p === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n return res.end(fs.readFileSync(path.join(pub, 'index.html')));\n }\n for (const asset of ['apiblaze-widgets.js', 'apiblaze-logo.svg']) {\n if (req.method === 'GET' && p === `/${asset}` && fs.existsSync(path.join(pub, asset))) {\n res.writeHead(200, { 'content-type': asset.endsWith('.svg') ? 'image/svg+xml' : 'text/javascript; charset=utf-8' });\n return res.end(fs.readFileSync(path.join(pub, asset)));\n }\n }\n if (p === '/api/config') {\n return json(res, 200, {\n direct, apiUrl: env('APIBLAZE_URL'), portalUrl: env('APIBLAZE_PORTAL_URL'),\n backendPort: env('BACKEND_PORT', '3001'),\n protectedUrl: env('APIBLAZE_URL') ? `http://localhost:${env('APP_PORT', '3000')}` : '',\n directUrl: `http://localhost:${env('DIRECT_PORT', '3002')}`,\n logsCommand: `npx apiblaze@latest logs ${env('APIBLAZE_NAME')} --tenant ${env('APIBLAZE_TENANT')}`,\n pizzerias: Object.entries(PIZZERIAS).filter(([, pz]) => pz.tenant()).map(([id, pz]) => ({ id, label: pz.label, admin: pz.admin.label, mcpUrl: mcpFor(pz.tenant()), portalUrl: portalFor(pz.tenant()) })),\n widgets: { chat: Object.keys(w.chat || {}), keys: !!w.keys, groups: !!w.groups },\n });\n }\n if (p === '/api/changes') {\n // Local and free: has any booking changed since the page last listed? The page\n // re-lists through APIblaze only when it has (the backend runs in this process).\n let n = 0;\n try { n = require('./backend').changeCount(); } catch { /* backend runs elsewhere */ }\n return json(res, 200, { n });\n }\n if (p === '/api/apiblaze/chat') {\n const h = (w.chat || {})[url.searchParams.get('pizzeria') || 'nino'];\n if (h) return viaHandler(h.handler, req, res, raw);\n }\n if (p === '/api/apiblaze/keys' && w.keys) return viaHandler(w.keys.handler, req, res, raw);\n if (p === '/api/apiblaze/groups' && w.groups) return viaHandler(w.groups.handler, req, res, raw);\n if (req.method === 'POST' && p === '/api/act') {\n const b = JSON.parse(raw || '{}');\n return json(res, 200, await call(String(b.op || 'list'), { pizzeria: b.pizzeria, as: String(b.as || 'ana'), id: b.id, body: b.body, direct }));\n }\n json(res, 404, { error: 'not found' });\n } catch (e) {\n json(res, 502, { error: `Could not reach the API: ${String((e && e.message) || e)}` });\n }\n });\n });\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => resolve(server));\n });\n}\n\nmodule.exports = { startApp };\n",
7721
+ "start.js": "// Starts ResiResi on this laptop: the backend (3001), the unprotected front-end straight to it\n// (3002) and, once APIblaze is set up, the protected website through APIblaze (3000).\n// No APIblaze involved here \u2014 the tunnel is `npx apiblaze@latest demo start|stop|restart`.\n//\n// node start.js run in the foreground (Ctrl-C stops)\n// npm run start run in the background\n// npm run stop stop the background one\n// npm run restart stop, then start again\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawn } = require('child_process');\n\nconst PID = path.join(__dirname, '.demo', 'app.pid');\n\nfunction loadEnv(file = path.join(__dirname, '.env.local')) {\n if (!fs.existsSync(file)) return;\n for (const line of fs.readFileSync(file, 'utf8').split('\\n')) {\n const m = /^\\s*([A-Z0-9_]+)\\s*=\\s*(.*)\\s*$/.exec(line);\n if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^\"(.*)\"$/, '$1');\n }\n}\n\nasync function start() {\n loadEnv();\n const { startBackend } = require('./backend');\n const { startApp } = require('./app');\n const backendPort = Number(process.env.BACKEND_PORT) || 3001;\n const appPort = Number(process.env.APP_PORT) || 3000;\n const directPort = Number(process.env.DIRECT_PORT) || 3002;\n await startBackend(backendPort);\n // The unprotected front-end: straight to the backend, no APIblaze.\n await startApp(directPort, { direct: true });\n // The protected website: through APIblaze (once the demo has written its keys).\n if (process.env.APIBLAZE_URL) await startApp(appPort);\n return { backendPort, appPort, directPort };\n}\n\nconst alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };\nfunction runningPid() {\n try { const pid = Number(fs.readFileSync(PID, 'utf8')); return pid && alive(pid) ? pid : null; } catch { return null; }\n}\nasync function stop() {\n const pid = runningPid();\n if (!pid) { console.log('ResiResi is not running.'); return; }\n process.kill(pid, 'SIGTERM');\n for (let i = 0; i < 30 && alive(pid); i++) await new Promise((r) => setTimeout(r, 100));\n try { fs.unlinkSync(PID); } catch { /* gone */ }\n console.log('Stopped ResiResi (backend and websites).');\n}\nfunction background() {\n if (runningPid()) { console.log('ResiResi is already running.'); return; }\n fs.mkdirSync(path.dirname(PID), { recursive: true });\n const log = fs.openSync(path.join(__dirname, '.demo', 'app.log'), 'a');\n const child = spawn(process.execPath, [__filename], { cwd: __dirname, detached: true, stdio: ['ignore', log, log] });\n child.unref();\n fs.writeFileSync(PID, String(child.pid));\n loadEnv();\n console.log(`Started ResiResi: backend localhost:${process.env.BACKEND_PORT || 3001}, unprotected front-end http://localhost:${process.env.DIRECT_PORT || 3002}${process.env.APIBLAZE_URL ? `, protected website http://localhost:${process.env.APP_PORT || 3000}` : ''}.`);\n}\n\nmodule.exports = { start, loadEnv };\nif (require.main === module) {\n const arg = process.argv[2];\n if (arg === '--stop') stop();\n else if (arg === '--background') background();\n else if (arg === '--restart') stop().then(() => setTimeout(background, 300));\n else {\n start().then(({ appPort, backendPort, directPort }) => console.log(`ResiResi running: backend localhost:${backendPort}, unprotected front-end http://localhost:${directPort}${process.env.APIBLAZE_URL ? `, protected website http://localhost:${appPort}` : ''}. Ctrl-C stops.`))\n .catch((e) => { console.error(`Could not start: ${e.message}`); process.exit(1); });\n }\n}\n",
7722
+ "public/index.html": `<!doctype html>
7723
+ <html lang="en">
7724
+ <head>
7725
+ <meta charset="utf-8">
7726
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7727
+ <title>ResiResi demo</title>
7728
+ <style>
7729
+ :root {
7730
+ --bg: #faf7f2; --card: #fff; --ink: #1d1b19; --muted: #6b6560; --line: #e8e2d9; --soft: #f3eee6;
7731
+ --accent: #e4572e; --accent-ink: #fff; --good: #15803d; --good-bg: #ecfdf3; --bad: #c2410c; --bad-bg: #fff1e8;
7732
+ --blaze: #4f46e5; --blaze-bg: #eef2ff; --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
7733
+ }
7734
+ @media (prefers-color-scheme: dark) {
7735
+ :root { --bg: #161412; --card: #1f1c1a; --ink: #f3efe9; --muted: #a39b92; --line: #34302c; --soft: #26221f;
7736
+ --good: #4ade80; --good-bg: #10241a; --bad: #fb923c; --bad-bg: #2b1a10; --blaze: #a5b4fc; --blaze-bg: #1e1b4b; }
7278
7737
  }
7279
7738
  * { box-sizing: border-box; }
7280
7739
  [hidden] { display: none !important; }
@@ -7587,24 +8046,24 @@ ready.then((c) => {
7587
8046
 
7588
8047
  // src/lib/log-view.ts
7589
8048
  function statusColor(status2) {
7590
- if (status2 < 300) return import_chalk27.default.green;
7591
- if (status2 < 400) return import_chalk27.default.blue;
7592
- if (status2 < 500) return import_chalk27.default.yellow;
7593
- return import_chalk27.default.red;
8049
+ if (status2 < 300) return import_chalk29.default.green;
8050
+ if (status2 < 400) return import_chalk29.default.blue;
8051
+ if (status2 < 500) return import_chalk29.default.yellow;
8052
+ return import_chalk29.default.red;
7594
8053
  }
7595
8054
  function methodColor(method) {
7596
8055
  switch ((method || "").toUpperCase()) {
7597
8056
  case "GET":
7598
- return import_chalk27.default.green;
8057
+ return import_chalk29.default.green;
7599
8058
  case "POST":
7600
- return import_chalk27.default.blue;
8059
+ return import_chalk29.default.blue;
7601
8060
  case "PUT":
7602
8061
  case "PATCH":
7603
- return import_chalk27.default.yellow;
8062
+ return import_chalk29.default.yellow;
7604
8063
  case "DELETE":
7605
- return import_chalk27.default.red;
8064
+ return import_chalk29.default.red;
7606
8065
  default:
7607
- return import_chalk27.default.white;
8066
+ return import_chalk29.default.white;
7608
8067
  }
7609
8068
  }
7610
8069
  function clock(ts2) {
@@ -7615,12 +8074,12 @@ function routeCell(route) {
7615
8074
  return (r.length > ROUTE_W ? `\u2026${r.slice(-(ROUTE_W - 1))}` : r).padEnd(ROUTE_W);
7616
8075
  }
7617
8076
  function formatRow(r) {
7618
- const time = import_chalk27.default.dim(clock(r.ts));
8077
+ const time = import_chalk29.default.dim(clock(r.ts));
7619
8078
  const method = methodColor(r.method)(String(r.method || "\u2014").padEnd(6));
7620
8079
  const route = routeCell(String(r.route || "\u2014"));
7621
8080
  const status2 = statusColor(r.status)(String(r.status ?? "\u2014").padStart(3));
7622
- const ms2 = import_chalk27.default.dim(`${r.duration_ms}ms`.padStart(8));
7623
- const step = r.error_step ? import_chalk27.default.dim(` ${r.error_step}`) : "";
8081
+ const ms2 = import_chalk29.default.dim(`${r.duration_ms}ms`.padStart(8));
8082
+ const step = r.error_step ? import_chalk29.default.dim(` ${r.error_step}`) : "";
7624
8083
  return `${time} ${method} ${route} ${status2} ${ms2}${step}`;
7625
8084
  }
7626
8085
  function printRow(r, json) {
@@ -7674,13 +8133,13 @@ async function runTailSession(opts) {
7674
8133
  if (!held.length && !heldRowsDropped && !heldNotesDropped) return;
7675
8134
  const rows = held.filter((h) => h.kind === "row").length;
7676
8135
  const total = rows + heldRowsDropped;
7677
- if (total) console.log(import_chalk27.default.dim(`\u2500\u2500\u2500 ${total} request${total === 1 ? "" : "s"} arrived while paused \u2500\u2500\u2500`));
8136
+ if (total) console.log(import_chalk29.default.dim(`\u2500\u2500\u2500 ${total} request${total === 1 ? "" : "s"} arrived while paused \u2500\u2500\u2500`));
7678
8137
  for (const h of held) {
7679
8138
  if (h.kind === "row") printRow(h.row, opts.json);
7680
8139
  else console.error(h.msg);
7681
8140
  }
7682
8141
  if (heldRowsDropped || heldNotesDropped) {
7683
- console.log(import_chalk27.default.dim(`\u2500\u2500\u2500 ${heldRowsDropped} older one${heldRowsDropped === 1 ? "" : "s"} not shown (pause buffer full) \u2014 browse them with \`logs list\` \u2500\u2500\u2500`));
8142
+ console.log(import_chalk29.default.dim(`\u2500\u2500\u2500 ${heldRowsDropped} older one${heldRowsDropped === 1 ? "" : "s"} not shown (pause buffer full) \u2014 browse them with \`logs list\` \u2500\u2500\u2500`));
7684
8143
  }
7685
8144
  held.length = 0;
7686
8145
  heldRowsDropped = 0;
@@ -7694,7 +8153,7 @@ async function runTailSession(opts) {
7694
8153
  activeSocket?.close();
7695
8154
  } catch {
7696
8155
  }
7697
- if (!opts.json) console.log(import_chalk27.default.dim("\nStopped."));
8156
+ if (!opts.json) console.log(import_chalk29.default.dim("\nStopped."));
7698
8157
  process.exit(0);
7699
8158
  }
7700
8159
  function onKeypress(_s, key) {
@@ -7707,7 +8166,7 @@ async function runTailSession(opts) {
7707
8166
  function bindKeys() {
7708
8167
  if (!interactive2 || keysBound || paused) return;
7709
8168
  keysBound = true;
7710
- readline.emitKeypressEvents(process.stdin);
8169
+ readline2.emitKeypressEvents(process.stdin);
7711
8170
  process.stdin.setRawMode(true);
7712
8171
  process.stdin.on("keypress", onKeypress);
7713
8172
  process.stdin.resume();
@@ -7722,17 +8181,17 @@ async function runTailSession(opts) {
7722
8181
  async function openInspector() {
7723
8182
  if (paused) return;
7724
8183
  if (recent.length === 0) {
7725
- console.log(import_chalk27.default.dim(" nothing captured yet \u2014 waiting for a request\u2026"));
8184
+ console.log(import_chalk29.default.dim(" nothing captured yet \u2014 waiting for a request\u2026"));
7726
8185
  return;
7727
8186
  }
7728
8187
  paused = true;
7729
8188
  unbindKeys();
7730
8189
  try {
7731
8190
  const { default: inquirer3 } = await import("inquirer");
7732
- console.log(import_chalk27.default.dim("\n\u2500\u2500\u2500 paused \xB7 still capturing in the background \u2500\u2500\u2500"));
8191
+ console.log(import_chalk29.default.dim("\n\u2500\u2500\u2500 paused \xB7 still capturing in the background \u2500\u2500\u2500"));
7733
8192
  for (; ; ) {
7734
8193
  const choices = recent.slice(-TAIL_INSPECT_CHOICES).reverse().map((r) => ({ name: formatRow(r), value: r.request_id }));
7735
- choices.push({ name: import_chalk27.default.dim("\u2039resume tail\u203A"), value: "__resume__" });
8194
+ choices.push({ name: import_chalk29.default.dim("\u2039resume tail\u203A"), value: "__resume__" });
7736
8195
  const { picked } = await inquirer3.prompt([{
7737
8196
  type: "list",
7738
8197
  name: "picked",
@@ -7743,12 +8202,12 @@ async function runTailSession(opts) {
7743
8202
  if (picked === "__resume__") break;
7744
8203
  const row = recent.find((r) => r.request_id === picked);
7745
8204
  if (row) printDetail(row, opts.detail);
7746
- else console.log(import_chalk27.default.dim(" that request has scrolled out of the tail buffer \u2014 find it with `logs list`.\n"));
8205
+ else console.log(import_chalk29.default.dim(" that request has scrolled out of the tail buffer \u2014 find it with `logs list`.\n"));
7747
8206
  }
7748
8207
  } finally {
7749
8208
  paused = false;
7750
8209
  flushHeld();
7751
- console.log(import_chalk27.default.dim("\u2500\u2500\u2500 resumed \u2500\u2500\u2500"));
8210
+ console.log(import_chalk29.default.dim("\u2500\u2500\u2500 resumed \u2500\u2500\u2500"));
7752
8211
  bindKeys();
7753
8212
  }
7754
8213
  }
@@ -7758,12 +8217,12 @@ async function runTailSession(opts) {
7758
8217
  if (reconnects >= MAX_RECONNECTS) {
7759
8218
  paused = false;
7760
8219
  flushHeld();
7761
- console.error(import_chalk27.default.red(`Connection lost \u2014 giving up after repeated reconnects${lastFailure ? ` (last error: ${lastFailure})` : ""}.`));
8220
+ console.error(import_chalk29.default.red(`Connection lost \u2014 giving up after repeated reconnects${lastFailure ? ` (last error: ${lastFailure})` : ""}.`));
7762
8221
  process.exit(1);
7763
8222
  }
7764
8223
  const delay = Math.min(1e3 * 2 ** reconnects, 15e3);
7765
8224
  reconnects++;
7766
- diag(import_chalk27.default.dim(`disconnected${lastFailure ? ` (${lastFailure})` : ""} \u2014 reconnecting in ${Math.round(delay / 1e3)}s\u2026`));
8225
+ diag(import_chalk29.default.dim(`disconnected${lastFailure ? ` (${lastFailure})` : ""} \u2014 reconnecting in ${Math.round(delay / 1e3)}s\u2026`));
7767
8226
  setTimeout(() => {
7768
8227
  if (!closed) void connect();
7769
8228
  }, delay);
@@ -7774,7 +8233,7 @@ async function runTailSession(opts) {
7774
8233
  url = await opts.mintTicket();
7775
8234
  } catch (err) {
7776
8235
  if (!everConnected) {
7777
- console.error(import_chalk27.default.red(`Could not start tail: ${err.message}`));
8236
+ console.error(import_chalk29.default.red(`Could not start tail: ${err.message}`));
7778
8237
  process.exit(1);
7779
8238
  }
7780
8239
  lastFailure = `ticket mint failed: ${err.message}`;
@@ -7787,7 +8246,7 @@ async function runTailSession(opts) {
7787
8246
  } catch (err) {
7788
8247
  lastFailure = `bad tail URL: ${err.message}`;
7789
8248
  if (!everConnected) {
7790
- console.error(import_chalk27.default.red(`Could not start tail: ${lastFailure}`));
8249
+ console.error(import_chalk29.default.red(`Could not start tail: ${lastFailure}`));
7791
8250
  process.exit(1);
7792
8251
  }
7793
8252
  scheduleReconnect();
@@ -7802,9 +8261,9 @@ async function runTailSession(opts) {
7802
8261
  lastInbound = Date.now();
7803
8262
  lastFailure = "";
7804
8263
  if (isReconnect) {
7805
- diag(import_chalk27.default.dim("reconnected \u2014 requests during the gap were not replayed (browse them with `logs list`)."));
8264
+ diag(import_chalk29.default.dim("reconnected \u2014 requests during the gap were not replayed (browse them with `logs list`)."));
7806
8265
  } else {
7807
- diag(import_chalk27.default.dim(interactive2 ? "[Enter] inspect a request \xB7 [Ctrl+C] stop\n" : "Ctrl+C to stop."));
8266
+ diag(import_chalk29.default.dim(interactive2 ? "[Enter] inspect a request \xB7 [Ctrl+C] stop\n" : "Ctrl+C to stop."));
7808
8267
  }
7809
8268
  bindKeys();
7810
8269
  pingTimer = setInterval(() => {
@@ -7831,7 +8290,7 @@ async function runTailSession(opts) {
7831
8290
  return;
7832
8291
  }
7833
8292
  if (frame?.type === "ceiling") {
7834
- diag(import_chalk27.default.yellow(
8293
+ diag(import_chalk29.default.yellow(
7835
8294
  `\u26A0 volume ceiling \u2014 ${Number(frame.dropped_this_hour) || 0} rows dropped this hour; errors keep streaming.`
7836
8295
  ));
7837
8296
  return;
@@ -7907,10 +8366,10 @@ async function fetchPage(fetchWindow3, params, notices) {
7907
8366
  return { rows, nextCursor: res.next_cursor ?? void 0 };
7908
8367
  }
7909
8368
  if (hop >= MAX_EMPTY_HOPS) {
7910
- console.error(import_chalk27.default.yellow("\u26A0 stopped after scanning a long empty span \u2014 older rows may still exist."));
8369
+ console.error(import_chalk29.default.yellow("\u26A0 stopped after scanning a long empty span \u2014 older rows may still exist."));
7911
8370
  return { rows: [], nextCursor: res.next_cursor, fuseTripped: true };
7912
8371
  }
7913
- if (hop === 0) console.error(import_chalk27.default.dim("searching older history\u2026"));
8372
+ if (hop === 0) console.error(import_chalk29.default.dim("searching older history\u2026"));
7914
8373
  cursor = res.next_cursor;
7915
8374
  }
7916
8375
  }
@@ -7948,18 +8407,18 @@ async function runPlainList(opts, notices) {
7948
8407
  }
7949
8408
  }
7950
8409
  if (rows.length === 0) {
7951
- console.log(import_chalk27.default.dim(fuseTripped ? "Nothing found in the scanned span \u2014 older requests may still exist." : opts.errorsOnly ? "No errors logged yet." : "No requests logged yet."));
8410
+ console.log(import_chalk29.default.dim(fuseTripped ? "Nothing found in the scanned span \u2014 older requests may still exist." : opts.errorsOnly ? "No errors logged yet." : "No requests logged yet."));
7952
8411
  return;
7953
8412
  }
7954
8413
  const now = Date.now();
7955
8414
  for (const d of groupIntoDays(rows)) {
7956
- console.log(import_chalk27.default.bold(dayLabel(d.dayMs, now)));
8415
+ console.log(import_chalk29.default.bold(dayLabel(d.dayMs, now)));
7957
8416
  for (const r of d.rows) console.log(` ${formatRow(r)}`);
7958
8417
  console.log("");
7959
8418
  }
7960
8419
  const errs = rows.filter((r) => r.status >= 400).length;
7961
8420
  const tail = fuseTripped ? " \xB7 stopped early (long empty span) \u2014 older requests may still exist" : more ? " \xB7 showing the most recent \u2014 `--json` streams the rest" : "";
7962
- console.log(import_chalk27.default.dim(
8421
+ console.log(import_chalk29.default.dim(
7963
8422
  `${rows.length} request${rows.length === 1 ? "" : "s"} \xB7 ${errs} error${errs === 1 ? "" : "s"}${tail}`
7964
8423
  ));
7965
8424
  }
@@ -7978,7 +8437,7 @@ async function runHistoryBrowser(opts) {
7978
8437
  for (const r of page.rows) console.log(JSON.stringify(r));
7979
8438
  any = any || page.rows.length > 0;
7980
8439
  if (page.fuseTripped || !page.nextCursor) {
7981
- if (!any && !page.fuseTripped) console.error(import_chalk27.default.dim(opts.errorsOnly ? "No errors logged yet." : "No requests logged yet."));
8440
+ if (!any && !page.fuseTripped) console.error(import_chalk29.default.dim(opts.errorsOnly ? "No errors logged yet." : "No requests logged yet."));
7982
8441
  return;
7983
8442
  }
7984
8443
  cursor2 = page.nextCursor;
@@ -7999,7 +8458,7 @@ async function runHistoryBrowser(opts) {
7999
8458
  scanRows = first.rows;
8000
8459
  scanCursor = first.nextCursor;
8001
8460
  if (scanRows.length === 0) {
8002
- console.log(import_chalk27.default.dim(first.fuseTripped ? "Nothing found in the scanned span \u2014 older rows may exist." : opts.errorsOnly ? "No errors logged yet." : "No requests logged yet."));
8461
+ console.log(import_chalk29.default.dim(first.fuseTripped ? "Nothing found in the scanned span \u2014 older rows may exist." : opts.errorsOnly ? "No errors logged yet." : "No requests logged yet."));
8003
8462
  return;
8004
8463
  }
8005
8464
  }
@@ -8010,10 +8469,10 @@ async function runHistoryBrowser(opts) {
8010
8469
  const now2 = Date.now();
8011
8470
  const oldestKey = days[days.length - 1]?.key;
8012
8471
  const choices = days.map((d) => ({
8013
- name: `${dayLabel(d.dayMs, now2).padEnd(12)} ${String(d.rows.length).padStart(4)}${(scanCursor !== void 0 || scanFloorUnknown) && d.key === oldestKey ? "+" : ""} requests${d.errors ? import_chalk27.default.red(` ${d.errors} errors`) : ""}`,
8472
+ name: `${dayLabel(d.dayMs, now2).padEnd(12)} ${String(d.rows.length).padStart(4)}${(scanCursor !== void 0 || scanFloorUnknown) && d.key === oldestKey ? "+" : ""} requests${d.errors ? import_chalk29.default.red(` ${d.errors} errors`) : ""}`,
8014
8473
  value: d.key
8015
8474
  }));
8016
- if (scanCursor) choices.push({ name: import_chalk27.default.dim("\u2026 older days"), value: "__older__" });
8475
+ if (scanCursor) choices.push({ name: import_chalk29.default.dim("\u2026 older days"), value: "__older__" });
8017
8476
  const { chosenDay } = await inquirer3.prompt([{
8018
8477
  type: "list",
8019
8478
  name: "chosenDay",
@@ -8033,7 +8492,7 @@ async function runHistoryBrowser(opts) {
8033
8492
  scanCursor = void 0;
8034
8493
  } else {
8035
8494
  scanCursor = more.nextCursor;
8036
- if (more.rows.length === 0 && !scanCursor) console.log(import_chalk27.default.dim("That was everything \u2014 no older requests."));
8495
+ if (more.rows.length === 0 && !scanCursor) console.log(import_chalk29.default.dim("That was everything \u2014 no older requests."));
8037
8496
  }
8038
8497
  continue;
8039
8498
  }
@@ -8055,11 +8514,11 @@ async function runHistoryBrowser(opts) {
8055
8514
  cursor
8056
8515
  }, notices);
8057
8516
  if (page.rows.length === 0 && pageRows.length === 0) {
8058
- console.log(import_chalk27.default.dim(page.fuseTripped ? "Stopped scanning this day (long empty span) \u2014 older rows in it may exist." : "No requests for this day."));
8517
+ console.log(import_chalk29.default.dim(page.fuseTripped ? "Stopped scanning this day (long empty span) \u2014 older rows in it may exist." : "No requests for this day."));
8059
8518
  return;
8060
8519
  }
8061
8520
  if (page.rows.length === 0) {
8062
- console.log(import_chalk27.default.dim(page.fuseTripped ? "Stopped scanning further into this day (long empty span) \u2014 older rows in it may exist." : "That was everything for this day."));
8521
+ console.log(import_chalk29.default.dim(page.fuseTripped ? "Stopped scanning further into this day (long empty span) \u2014 older rows in it may exist." : "That was everything for this day."));
8063
8522
  moreCursor = void 0;
8064
8523
  } else {
8065
8524
  pageRows = page.rows;
@@ -8070,8 +8529,8 @@ async function runHistoryBrowser(opts) {
8070
8529
  name: formatRow(r),
8071
8530
  value: r.request_id
8072
8531
  }));
8073
- if (moreCursor) choices.push({ name: import_chalk27.default.dim("\u2026 load more"), value: "__more__" });
8074
- choices.push({ name: import_chalk27.default.dim("quit"), value: "__quit__" });
8532
+ if (moreCursor) choices.push({ name: import_chalk29.default.dim("\u2026 load more"), value: "__more__" });
8533
+ choices.push({ name: import_chalk29.default.dim("quit"), value: "__quit__" });
8075
8534
  for (; ; ) {
8076
8535
  const { picked } = await inquirer3.prompt([{
8077
8536
  type: "list",
@@ -8106,19 +8565,19 @@ function wrapCells(items, width) {
8106
8565
  ${PANE_INDENT}`);
8107
8566
  }
8108
8567
  function renderHeaders(h) {
8109
- if (!h) return import_chalk27.default.dim("\u2014");
8568
+ if (!h) return import_chalk29.default.dim("\u2014");
8110
8569
  const width = process.stdout.columns || 100;
8111
8570
  if (h.values) {
8112
8571
  const e = Object.entries(h.values);
8113
- if (!e.length) return import_chalk27.default.dim("none");
8572
+ if (!e.length) return import_chalk29.default.dim("none");
8114
8573
  const rank = (k) => {
8115
8574
  const i = HEADER_PRIORITY.indexOf(k.toLowerCase());
8116
8575
  return i === -1 ? NOISE_HEADER.test(k) ? 999 : 500 : i;
8117
8576
  };
8118
- return e.sort((a, b) => rank(a[0]) - rank(b[0])).map(([k, v]) => `${import_chalk27.default.dim(k + ":")} ${v}`).join(`
8577
+ return e.sort((a, b) => rank(a[0]) - rank(b[0])).map(([k, v]) => `${import_chalk29.default.dim(k + ":")} ${v}`).join(`
8119
8578
  ${PANE_INDENT}`);
8120
8579
  }
8121
- if (!h.names?.length) return import_chalk27.default.dim("none");
8580
+ if (!h.names?.length) return import_chalk29.default.dim("none");
8122
8581
  const names = [...h.names];
8123
8582
  const noise = names.filter((n) => NOISE_HEADER.test(n));
8124
8583
  const shown = names.filter((n) => !NOISE_HEADER.test(n)).sort((a, b) => {
@@ -8126,78 +8585,78 @@ ${PANE_INDENT}`);
8126
8585
  const ib = HEADER_PRIORITY.indexOf(b.toLowerCase());
8127
8586
  return (ia === -1 ? 500 : ia) - (ib === -1 ? 500 : ib) || a.localeCompare(b);
8128
8587
  });
8129
- const body = shown.length ? wrapCells(shown, width) : import_chalk27.default.dim("(all forwarding headers)");
8588
+ const body = shown.length ? wrapCells(shown, width) : import_chalk29.default.dim("(all forwarding headers)");
8130
8589
  const tail = noise.length ? `
8131
- ${PANE_INDENT}${import_chalk27.default.dim(`+ ${noise.length} forwarding/client-hint header${noise.length === 1 ? "" : "s"} (cf-*, sec-*)`)}` : "";
8132
- return `${body}${tail} ${import_chalk27.default.dim("(names only)")}`;
8590
+ ${PANE_INDENT}${import_chalk29.default.dim(`+ ${noise.length} forwarding/client-hint header${noise.length === 1 ? "" : "s"} (cf-*, sec-*)`)}` : "";
8591
+ return `${body}${tail} ${import_chalk29.default.dim("(names only)")}`;
8133
8592
  }
8134
8593
  function renderBody(b) {
8135
- if (!b) return import_chalk27.default.dim("\u2014");
8594
+ if (!b) return import_chalk29.default.dim("\u2014");
8136
8595
  const meta = [b.content_type, b.size != null ? `${b.size}B` : null, b.truncated ? "truncated" : null].filter(Boolean).join(" \xB7 ");
8137
- if (b.not_captured) return import_chalk27.default.dim(`not captured (${b.not_captured})${meta ? " \xB7 " + meta : ""}`);
8596
+ if (b.not_captured) return import_chalk29.default.dim(`not captured (${b.not_captured})${meta ? " \xB7 " + meta : ""}`);
8138
8597
  const isShape = b.value === void 0 && b.shape !== void 0;
8139
8598
  const content = b.value !== void 0 ? b.value : b.shape;
8140
8599
  if (content === null || content === void 0 || content === "" || content === "empty") {
8141
- return import_chalk27.default.dim(`empty${meta ? " \xB7 " + meta : ""}`);
8600
+ return import_chalk29.default.dim(`empty${meta ? " \xB7 " + meta : ""}`);
8142
8601
  }
8143
8602
  const text = typeof content === "string" ? content : JSON.stringify(content, null, 2);
8144
- const head = isShape ? import_chalk27.default.dim("shape only (values not captured)\n ") : "";
8603
+ const head = isShape ? import_chalk29.default.dim("shape only (values not captured)\n ") : "";
8145
8604
  return head + text.split("\n").join("\n ");
8146
8605
  }
8147
8606
  function pane(title, tone, view, empty) {
8148
8607
  console.log(` ${tone(title)}`);
8149
8608
  if (empty !== void 0) {
8150
- console.log(` ${import_chalk27.default.dim(empty)}`);
8609
+ console.log(` ${import_chalk29.default.dim(empty)}`);
8151
8610
  return;
8152
8611
  }
8153
8612
  const v = view ?? {};
8154
- if (v.url) console.log(` ${import_chalk27.default.dim("target")} ${v.url}`);
8613
+ if (v.url) console.log(` ${import_chalk29.default.dim("target")} ${v.url}`);
8155
8614
  if (v.status != null) {
8156
- console.log(` ${import_chalk27.default.dim("status")} ${statusColor(v.status)(String(v.status))}${v.status_text ? import_chalk27.default.dim(" " + v.status_text) : ""}`);
8615
+ console.log(` ${import_chalk29.default.dim("status")} ${statusColor(v.status)(String(v.status))}${v.status_text ? import_chalk29.default.dim(" " + v.status_text) : ""}`);
8157
8616
  }
8158
- if (v.raw_path) console.log(` ${import_chalk27.default.dim("path")} ${v.raw_path}`);
8617
+ if (v.raw_path) console.log(` ${import_chalk29.default.dim("path")} ${v.raw_path}`);
8159
8618
  if (v.query && Object.keys(v.query).length) {
8160
- console.log(` ${import_chalk27.default.dim("query")} ${Object.entries(v.query).map(([k, q]) => `${k}=${q}`).join(" ")}`);
8619
+ console.log(` ${import_chalk29.default.dim("query")} ${Object.entries(v.query).map(([k, q]) => `${k}=${q}`).join(" ")}`);
8161
8620
  }
8162
- if (v.jwt !== void 0) console.log(` ${import_chalk27.default.dim("jwt")} ${JSON.stringify(v.jwt)}`);
8163
- console.log(` ${import_chalk27.default.dim("headers")} ${renderHeaders(v.headers)}`);
8164
- console.log(` ${import_chalk27.default.dim("body")} ${renderBody(v.body)}`);
8621
+ if (v.jwt !== void 0) console.log(` ${import_chalk29.default.dim("jwt")} ${JSON.stringify(v.jwt)}`);
8622
+ console.log(` ${import_chalk29.default.dim("headers")} ${renderHeaders(v.headers)}`);
8623
+ console.log(` ${import_chalk29.default.dim("body")} ${renderBody(v.body)}`);
8165
8624
  }
8166
8625
  function printDetail(r, mode = "producer") {
8167
8626
  const v = r.views;
8168
8627
  console.log("");
8169
- console.log(import_chalk27.default.bold(`${r.method} ${r.route}`) + " " + statusColor(r.status)(String(r.status)) + import_chalk27.default.dim(` ${r.duration_ms}ms`));
8628
+ console.log(import_chalk29.default.bold(`${r.method} ${r.route}`) + " " + statusColor(r.status)(String(r.status)) + import_chalk29.default.dim(` ${r.duration_ms}ms`));
8170
8629
  const cost = typeof r.cost_cents === "number" && r.cost_cents > 0 ? ` \xB7 ${r.cost_cents}\xA2` : "";
8171
- console.log(import_chalk27.default.dim(` request_id ${r.request_id}${r.environment ? ` \xB7 env ${r.environment}` : ""}${r.consumer_id ? ` \xB7 consumer ${r.consumer_id}` : ""}${cost}`));
8630
+ console.log(import_chalk29.default.dim(` request_id ${r.request_id}${r.environment ? ` \xB7 env ${r.environment}` : ""}${r.consumer_id ? ` \xB7 consumer ${r.consumer_id}` : ""}${cost}`));
8172
8631
  console.log("");
8173
8632
  if (!v) {
8174
- console.log(import_chalk27.default.dim(" No view detail captured for this request.\n"));
8633
+ console.log(import_chalk29.default.dim(" No view detail captured for this request.\n"));
8175
8634
  return;
8176
8635
  }
8177
- pane("Request (before) \u2014 client \u2192 APIblaze", import_chalk27.default.cyan, v.request);
8636
+ pane("Request (before) \u2014 client \u2192 APIblaze", import_chalk29.default.cyan, v.request);
8178
8637
  if (mode === "producer") {
8179
8638
  pane(
8180
8639
  "Request (after) \u2014 APIblaze \u2192 upstream",
8181
- import_chalk27.default.cyan,
8640
+ import_chalk29.default.cyan,
8182
8641
  v.forwarded_request,
8183
8642
  v.forwarded_request ? void 0 : "Request did not reach an upstream."
8184
8643
  );
8185
8644
  pane(
8186
8645
  "Response (before) \u2014 upstream \u2192 APIblaze",
8187
- import_chalk27.default.magenta,
8646
+ import_chalk29.default.magenta,
8188
8647
  v.upstream_response,
8189
8648
  v.upstream_response ? void 0 : "No upstream response."
8190
8649
  );
8191
8650
  }
8192
- pane("Response (after) \u2014 APIblaze \u2192 client", import_chalk27.default.magenta, v.response);
8651
+ pane("Response (after) \u2014 APIblaze \u2192 client", import_chalk29.default.magenta, v.response);
8193
8652
  console.log("");
8194
8653
  }
8195
- var import_chalk27, readline, ROUTE_W, WindowNotices, MAX_RECONNECTS, PING_MS, LIVENESS_MS, STABLE_MS, SEEN_CAP, TAIL_INSPECT_BUFFER, TAIL_INSPECT_CHOICES, SCAN_LIMIT, PAGE_LIMIT2, JSON_PAGE_LIMIT, MAX_EMPTY_HOPS, epipeGuarded, PLAIN_MAX_ROWS, NOISE_HEADER, HEADER_PRIORITY, PANE_INDENT;
8654
+ var import_chalk29, readline2, ROUTE_W, WindowNotices, MAX_RECONNECTS, PING_MS, LIVENESS_MS, STABLE_MS, SEEN_CAP, TAIL_INSPECT_BUFFER, TAIL_INSPECT_CHOICES, SCAN_LIMIT, PAGE_LIMIT2, JSON_PAGE_LIMIT, MAX_EMPTY_HOPS, epipeGuarded, PLAIN_MAX_ROWS, NOISE_HEADER, HEADER_PRIORITY, PANE_INDENT;
8196
8655
  var init_log_view = __esm({
8197
8656
  "src/lib/log-view.ts"() {
8198
8657
  "use strict";
8199
- import_chalk27 = __toESM(require("chalk"));
8200
- readline = __toESM(require("readline"));
8658
+ import_chalk29 = __toESM(require("chalk"));
8659
+ readline2 = __toESM(require("readline"));
8201
8660
  ROUTE_W = 34;
8202
8661
  WindowNotices = class {
8203
8662
  constructor() {
@@ -8208,15 +8667,15 @@ var init_log_view = __esm({
8208
8667
  note(res) {
8209
8668
  if (res.history_unavailable && !this.saidUnavailable) {
8210
8669
  this.saidUnavailable = true;
8211
- console.error(import_chalk27.default.yellow("\u26A0 log history is unreachable right now \u2014 showing the live buffer only, older requests exist but cannot be read."));
8670
+ console.error(import_chalk29.default.yellow("\u26A0 log history is unreachable right now \u2014 showing the live buffer only, older requests exist but cannot be read."));
8212
8671
  }
8213
8672
  if ((res.history_markers?.length ?? 0) > 0 && !this.saidGap) {
8214
8673
  this.saidGap = true;
8215
- console.error(import_chalk27.default.yellow("\u26A0 this range includes a volume-ceiling gap \u2014 some rows were dropped at intake and are gone; counts are incomplete."));
8674
+ console.error(import_chalk29.default.yellow("\u26A0 this range includes a volume-ceiling gap \u2014 some rows were dropped at intake and are gone; counts are incomplete."));
8216
8675
  }
8217
8676
  if ((res.dropped_this_hour ?? 0) > 0 && !this.saidDropped) {
8218
8677
  this.saidDropped = true;
8219
- console.error(import_chalk27.default.yellow(`\u26A0 volume ceiling \u2014 ${res.dropped_this_hour} rows dropped this hour; errors keep logging.`));
8678
+ console.error(import_chalk29.default.yellow(`\u26A0 volume ceiling \u2014 ${res.dropped_this_hour} rows dropped this hour; errors keep logging.`));
8220
8679
  }
8221
8680
  }
8222
8681
  };
@@ -8303,7 +8762,7 @@ async function resolve(projectArg, versionArg, opts) {
8303
8762
  return { projectId, projectName: match.projectName, version: version2, tenant: tenant2 };
8304
8763
  }
8305
8764
  function otherPlaneHint() {
8306
- if (loadConsumer()) console.log(import_chalk28.default.dim(" \u2039consumer view: apiblaze consumer logs\u203A"));
8765
+ if (loadConsumer()) console.log(import_chalk30.default.dim(" \u2039consumer view: apiblaze consumer logs\u203A"));
8307
8766
  }
8308
8767
  function fetchWindow(projectId, version2, tenant2) {
8309
8768
  return (params) => {
@@ -8322,8 +8781,8 @@ function fetchWindow(projectId, version2, tenant2) {
8322
8781
  async function runLogsTail(projectArg, versionArg, opts) {
8323
8782
  const { projectId, projectName, version: version2, tenant: tenant2 } = await resolve(projectArg, versionArg, opts);
8324
8783
  if (!opts.json) {
8325
- console.log(import_chalk28.default.bold("API Producer logs") + import_chalk28.default.dim(` \xB7 ${projectName}@${version2} \xB7 tenant ${tenant2}${opts.errorsOnly ? " \xB7 errors only" : ""}`));
8326
- console.log(import_chalk28.default.dim("every caller's requests, with cost and upstream detail"));
8784
+ console.log(import_chalk30.default.bold("API Producer logs") + import_chalk30.default.dim(` \xB7 ${projectName}@${version2} \xB7 tenant ${tenant2}${opts.errorsOnly ? " \xB7 errors only" : ""}`));
8785
+ console.log(import_chalk30.default.dim("every caller's requests, with cost and upstream detail"));
8327
8786
  otherPlaneHint();
8328
8787
  }
8329
8788
  await runTailSession({
@@ -8344,8 +8803,8 @@ async function runLogsTail(projectArg, versionArg, opts) {
8344
8803
  async function runLogsList(projectArg, versionArg, opts) {
8345
8804
  const { projectId, projectName, version: version2, tenant: tenant2 } = await resolve(projectArg, versionArg, opts);
8346
8805
  if (!opts.json) {
8347
- console.log(import_chalk28.default.bold("API Producer logs") + import_chalk28.default.dim(` \xB7 ${projectName}@${version2} \xB7 tenant ${tenant2}${opts.errorsOnly ? " \xB7 errors only" : ""}`));
8348
- console.log(import_chalk28.default.dim("every caller's requests, with cost and upstream detail"));
8806
+ console.log(import_chalk30.default.bold("API Producer logs") + import_chalk30.default.dim(` \xB7 ${projectName}@${version2} \xB7 tenant ${tenant2}${opts.errorsOnly ? " \xB7 errors only" : ""}`));
8807
+ console.log(import_chalk30.default.dim("every caller's requests, with cost and upstream detail"));
8349
8808
  otherPlaneHint();
8350
8809
  console.log("");
8351
8810
  }
@@ -8357,11 +8816,11 @@ async function runLogsList(projectArg, versionArg, opts) {
8357
8816
  detail: "producer"
8358
8817
  });
8359
8818
  }
8360
- var import_chalk28, activeCaller, viaCaller;
8819
+ var import_chalk30, activeCaller, viaCaller;
8361
8820
  var init_logs = __esm({
8362
8821
  "src/commands/logs.ts"() {
8363
8822
  "use strict";
8364
- import_chalk28 = __toESM(require("chalk"));
8823
+ import_chalk30 = __toESM(require("chalk"));
8365
8824
  init_caller();
8366
8825
  init_auth();
8367
8826
  init_consumer_auth();
@@ -8433,15 +8892,15 @@ async function freePort(from, avoid = []) {
8433
8892
  }
8434
8893
  function readRegistry() {
8435
8894
  try {
8436
- return JSON.parse(import_fs7.default.readFileSync(registryFile(), "utf8"));
8895
+ return JSON.parse(import_fs8.default.readFileSync(registryFile(), "utf8"));
8437
8896
  } catch {
8438
8897
  return [];
8439
8898
  }
8440
8899
  }
8441
8900
  function writeRegistry(rows) {
8442
8901
  try {
8443
- import_fs7.default.mkdirSync(import_path6.default.dirname(registryFile()), { recursive: true });
8444
- import_fs7.default.writeFileSync(registryFile(), JSON.stringify(rows));
8902
+ import_fs8.default.mkdirSync(import_path6.default.dirname(registryFile()), { recursive: true });
8903
+ import_fs8.default.writeFileSync(registryFile(), JSON.stringify(rows));
8445
8904
  } catch {
8446
8905
  }
8447
8906
  }
@@ -8485,7 +8944,7 @@ async function stopTunnels() {
8485
8944
  return stopped;
8486
8945
  }
8487
8946
  function appCtl(dir, arg) {
8488
- if (!import_fs7.default.existsSync(import_path6.default.join(dir, "start.js"))) return false;
8947
+ if (!import_fs8.default.existsSync(import_path6.default.join(dir, "start.js"))) return false;
8489
8948
  const r = (0, import_child_process5.spawnSync)(process.execPath, [import_path6.default.join(dir, "start.js"), arg], { cwd: dir, stdio: "ignore", timeout: 15e3 });
8490
8949
  return r.status === 0;
8491
8950
  }
@@ -8526,47 +8985,47 @@ async function stopDemos() {
8526
8985
  return n + left.length;
8527
8986
  }
8528
8987
  function writeApp(dir) {
8529
- import_fs7.default.mkdirSync(dir, { recursive: true });
8530
- for (const entry of import_fs7.default.readdirSync(dir)) import_fs7.default.rmSync(import_path6.default.join(dir, entry), { recursive: true, force: true });
8988
+ import_fs8.default.mkdirSync(dir, { recursive: true });
8989
+ for (const entry of import_fs8.default.readdirSync(dir)) import_fs8.default.rmSync(import_path6.default.join(dir, entry), { recursive: true, force: true });
8531
8990
  for (const [rel, text] of Object.entries(DEMO_FILES)) {
8532
8991
  const f = import_path6.default.join(dir, rel);
8533
- import_fs7.default.mkdirSync(import_path6.default.dirname(f), { recursive: true });
8534
- import_fs7.default.writeFileSync(f, text);
8992
+ import_fs8.default.mkdirSync(import_path6.default.dirname(f), { recursive: true });
8993
+ import_fs8.default.writeFileSync(f, text);
8535
8994
  }
8536
- import_fs7.default.writeFileSync(import_path6.default.join(dir, "package.json"), JSON.stringify({
8995
+ import_fs8.default.writeFileSync(import_path6.default.join(dir, "package.json"), JSON.stringify({
8537
8996
  name: "nino-pizza-demo",
8538
8997
  private: true,
8539
8998
  scripts: { start: "node start.js --background", stop: "node start.js --stop", restart: "node start.js --restart", dev: "node start.js" }
8540
8999
  }, null, 2) + "\n");
8541
- import_fs7.default.writeFileSync(import_path6.default.join(dir, ".gitignore"), ".env.local\n.demo/\n");
9000
+ import_fs8.default.writeFileSync(import_path6.default.join(dir, ".gitignore"), ".env.local\n.demo/\n");
8542
9001
  for (const [from, to] of [[import_path6.default.join(__dirname, "server", "index.js"), import_path6.default.join(dir, "lib", "apiblaze-server.js")], [import_path6.default.join(__dirname, "browser", "widgets.js"), import_path6.default.join(dir, "public", "apiblaze-widgets.js")]]) {
8543
- if (!import_fs7.default.existsSync(from)) continue;
8544
- import_fs7.default.mkdirSync(import_path6.default.dirname(to), { recursive: true });
8545
- import_fs7.default.copyFileSync(from, to);
9002
+ if (!import_fs8.default.existsSync(from)) continue;
9003
+ import_fs8.default.mkdirSync(import_path6.default.dirname(to), { recursive: true });
9004
+ import_fs8.default.copyFileSync(from, to);
8546
9005
  }
8547
9006
  }
8548
9007
  function saveState(dir, s) {
8549
9008
  const d = import_path6.default.join(dir, STATE_DIR);
8550
- import_fs7.default.mkdirSync(d, { recursive: true });
8551
- import_fs7.default.writeFileSync(import_path6.default.join(d, "state.json"), JSON.stringify(s, null, 2), { mode: 384 });
9009
+ import_fs8.default.mkdirSync(d, { recursive: true });
9010
+ import_fs8.default.writeFileSync(import_path6.default.join(d, "state.json"), JSON.stringify(s, null, 2), { mode: 384 });
8552
9011
  }
8553
9012
  function loadState(dir) {
8554
9013
  try {
8555
- return JSON.parse(import_fs7.default.readFileSync(import_path6.default.join(dir, STATE_DIR, "state.json"), "utf8"));
9014
+ return JSON.parse(import_fs8.default.readFileSync(import_path6.default.join(dir, STATE_DIR, "state.json"), "utf8"));
8556
9015
  } catch {
8557
9016
  return null;
8558
9017
  }
8559
9018
  }
8560
9019
  function writeEnv(dir, v) {
8561
9020
  const lines = Object.entries(v).map(([k, val]) => `${k}=${val}`);
8562
- import_fs7.default.writeFileSync(import_path6.default.join(dir, ".env.local"), `# Written by ${LATEST} demo. Server-side only: never commit or share.
9021
+ import_fs8.default.writeFileSync(import_path6.default.join(dir, ".env.local"), `# Written by ${LATEST} demo. Server-side only: never commit or share.
8563
9022
  ${lines.join("\n")}
8564
9023
  `, { mode: 384 });
8565
9024
  }
8566
9025
  function readEnvFile(file2) {
8567
9026
  const out = {};
8568
9027
  try {
8569
- for (const line of import_fs7.default.readFileSync(file2, "utf8").split("\n")) {
9028
+ for (const line of import_fs8.default.readFileSync(file2, "utf8").split("\n")) {
8570
9029
  const m = /^([A-Z0-9_]+)=(.*)$/.exec(line.trim());
8571
9030
  if (m) out[m[1]] = m[2];
8572
9031
  }
@@ -8576,14 +9035,14 @@ function readEnvFile(file2) {
8576
9035
  }
8577
9036
  function demoDir() {
8578
9037
  const here = process.cwd();
8579
- if (import_fs7.default.existsSync(import_path6.default.join(here, STATE_DIR, "state.json"))) return here;
9038
+ if (import_fs8.default.existsSync(import_path6.default.join(here, STATE_DIR, "state.json"))) return here;
8580
9039
  return import_path6.default.resolve(here, DIR_NAME);
8581
9040
  }
8582
9041
  function adoptWorkspace(cpKey, teamId, claimCode) {
8583
9042
  const prev = loadAnonCred();
8584
9043
  if (prev?.cp_key && prev.team_id && prev.team_id !== teamId) {
8585
9044
  try {
8586
- import_fs7.default.writeFileSync(import_path6.default.join(getApiblazeDir(), `anon.${prev.team_id}.json`), JSON.stringify(prev, null, 2), { mode: 384 });
9045
+ import_fs8.default.writeFileSync(import_path6.default.join(getApiblazeDir(), `anon.${prev.team_id}.json`), JSON.stringify(prev, null, 2), { mode: 384 });
8587
9046
  } catch {
8588
9047
  }
8589
9048
  }
@@ -8642,12 +9101,12 @@ async function waitThrough(apiUrl, serverKey) {
8642
9101
  async function confirmStep(opts, question) {
8643
9102
  if (opts.yes || !interactive()) return true;
8644
9103
  console.log();
8645
- return ask(`${import_chalk29.default.bold("?")} ${question} ${import_chalk29.default.dim("(Y/n)")} `);
9104
+ return ask(`${import_chalk31.default.bold("?")} ${question} ${import_chalk31.default.dim("(Y/n)")} `);
8646
9105
  }
8647
9106
  async function busy(text, fn, minMs = 0) {
8648
9107
  if (!process.stdout.isTTY) return fn();
8649
9108
  const t = Date.now();
8650
- const spinner = (0, import_ora11.default)({ text: import_chalk29.default.dim(text), indent: 3, discardStdin: false }).start();
9109
+ const spinner = (0, import_ora11.default)({ text: import_chalk31.default.dim(text), indent: 3, discardStdin: false }).start();
8651
9110
  try {
8652
9111
  const out = await fn();
8653
9112
  const left = minMs - (Date.now() - t);
@@ -8680,7 +9139,7 @@ async function offerBrowserAndLogs(dir, opts) {
8680
9139
  console.log();
8681
9140
  if (await ask(` Open ${url} in your browser? (Y/n) `)) openBrowser3(url);
8682
9141
  if (await ask(` Show the requests reaching ${B.product} here, live? (Y/n) `)) {
8683
- console.log(import_chalk29.default.dim(` $ ${logsCommand(env)}`));
9142
+ console.log(import_chalk31.default.dim(` $ ${logsCommand(env)}`));
8684
9143
  const { runLogsTail: runLogsTail2 } = await Promise.resolve().then(() => (init_logs(), logs_exports));
8685
9144
  await runLogsTail2(env.APIBLAZE_NAME, void 0, { tenant: env.APIBLAZE_TENANT });
8686
9145
  }
@@ -8717,7 +9176,7 @@ async function runDemo(opts = {}) {
8717
9176
  const e = err instanceof DemoError ? err : new DemoError(`The demo could not start (${err.message}).`, `Run again: ${LATEST} demo`);
8718
9177
  countRun({ event: `demo-failed-${e.why}` });
8719
9178
  await stopDemos().catch(() => 0);
8720
- console.log(`${import_chalk29.default.red("\u2717")} ${e.message}`);
9179
+ console.log(`${import_chalk31.default.red("\u2717")} ${e.message}`);
8721
9180
  console.log(` ${e.fix}`);
8722
9181
  process.exit(1);
8723
9182
  }
@@ -8728,11 +9187,11 @@ function titleCard(lines) {
8728
9187
  const left = Math.floor((inner - t.length) / 2);
8729
9188
  return " ".repeat(left) + t + " ".repeat(inner - t.length - left);
8730
9189
  };
8731
- const edge = import_chalk29.default.magenta;
9190
+ const edge = import_chalk31.default.magenta;
8732
9191
  console.log();
8733
9192
  console.log(` ${edge(`\u256D${"\u2500".repeat(inner)}\u256E`)}`);
8734
9193
  console.log(` ${edge("\u2502")}${" ".repeat(inner)}${edge("\u2502")}`);
8735
- lines.forEach((l, i) => console.log(` ${edge("\u2502")}${i === 0 ? import_chalk29.default.bold(center(l)) : import_chalk29.default.dim(center(l))}${edge("\u2502")}`));
9194
+ lines.forEach((l, i) => console.log(` ${edge("\u2502")}${i === 0 ? import_chalk31.default.bold(center(l)) : import_chalk31.default.dim(center(l))}${edge("\u2502")}`));
8736
9195
  console.log(` ${edge("\u2502")}${" ".repeat(inner)}${edge("\u2502")}`);
8737
9196
  console.log(` ${edge(`\u2570${"\u2500".repeat(inner)}\u256F`)}`);
8738
9197
  }
@@ -8752,25 +9211,25 @@ function printTable(rows) {
8752
9211
  if (line) out.push(line);
8753
9212
  return out;
8754
9213
  };
8755
- const head = (l, r) => console.log(` ${import_chalk29.default.dim(l.padEnd(leftW))} ${import_chalk29.default.dim(r)}`);
9214
+ const head = (l, r) => console.log(` ${import_chalk31.default.dim(l.padEnd(leftW))} ${import_chalk31.default.dim(r)}`);
8756
9215
  head(`What ${B.product} set up`, "The command that does this");
8757
9216
  head("\u2500".repeat(leftW), "\u2500".repeat(Math.min(rightW, 60)));
8758
9217
  for (const r of rows) {
8759
- const left = `${import_chalk29.default.green("\u2713")} ${import_chalk29.default.bold(r.label.padEnd(17))} ${r.what}`;
9218
+ const left = `${import_chalk31.default.green("\u2713")} ${import_chalk31.default.bold(r.label.padEnd(17))} ${r.what}`;
8760
9219
  const leftPlain = `\u2713 ${r.label.padEnd(17)} ${r.what}`;
8761
9220
  const right = r.cmds.length ? r.cmds.flatMap(wrap2) : ["\u2014"];
8762
- console.log(` ${left}${" ".repeat(Math.max(0, leftW - leftPlain.length))} ${import_chalk29.default.cyan(right[0])}`);
8763
- for (const extra of right.slice(1)) console.log(` ${" ".repeat(leftW)} ${import_chalk29.default.cyan(extra)}`);
9221
+ console.log(` ${left}${" ".repeat(Math.max(0, leftW - leftPlain.length))} ${import_chalk31.default.cyan(right[0])}`);
9222
+ for (const extra of right.slice(1)) console.log(` ${" ".repeat(leftW)} ${import_chalk31.default.cyan(extra)}`);
8764
9223
  }
8765
9224
  }
8766
9225
  async function startDemo(opts) {
8767
9226
  const t0 = Date.now();
8768
9227
  const here = process.cwd();
8769
- const dir = import_fs7.default.existsSync(import_path6.default.join(here, STATE_DIR, "state.json")) ? here : import_path6.default.resolve(here, DIR_NAME);
9228
+ const dir = import_fs8.default.existsSync(import_path6.default.join(here, STATE_DIR, "state.json")) ? here : import_path6.default.resolve(here, DIR_NAME);
8770
9229
  const rel = import_path6.default.relative(here, dir) || ".";
8771
9230
  const cdDir = rel === "." ? "" : `cd ${rel} && `;
8772
- const shell = (c, note = "") => console.log(` ${import_chalk29.default.dim("$")} ${import_chalk29.default.cyan(c)}${note ? ` ${import_chalk29.default.dim(note)}` : ""}`);
8773
- const ok = (text, note = "") => console.log(` ${import_chalk29.default.green("\u2713")} ${text}${note ? ` ${import_chalk29.default.dim(note)}` : ""}`);
9231
+ const shell = (c, note = "") => console.log(` ${import_chalk31.default.dim("$")} ${import_chalk31.default.cyan(c)}${note ? ` ${import_chalk31.default.dim(note)}` : ""}`);
9232
+ const ok = (text, note = "") => console.log(` ${import_chalk31.default.green("\u2713")} ${text}${note ? ` ${import_chalk31.default.dim(note)}` : ""}`);
8774
9233
  const stopHere = (step, lines) => {
8775
9234
  console.log();
8776
9235
  for (const l of lines) console.log(` ${l}`);
@@ -8788,7 +9247,7 @@ async function startDemo(opts) {
8788
9247
  const openapi = DEMO_FILES["openapi.yaml"];
8789
9248
  try {
8790
9249
  const specPath = import_path6.default.join(dir, "openapi.yaml");
8791
- import_fs7.default.writeFileSync(specPath, import_fs7.default.readFileSync(specPath, "utf8").replace(/^paths:/m, `servers:
9250
+ import_fs8.default.writeFileSync(specPath, import_fs8.default.readFileSync(specPath, "utf8").replace(/^paths:/m, `servers:
8792
9251
  - url: http://localhost:${backendPort2}
8793
9252
  paths:`));
8794
9253
  } catch {
@@ -8796,14 +9255,14 @@ paths:`));
8796
9255
  const L = (c) => `${LATEST} ${c}`;
8797
9256
  writeEnv(dir, { BACKEND_SECRET: backendSecret, DEMO_NINO_TENANT: ninoTenant, APP_PORT: appPort, BACKEND_PORT: backendPort2, DIRECT_PORT: directPort });
8798
9257
  console.log();
8799
- console.log(import_chalk29.default.bold("1. The app: ResiResi, a table booking app for Tenants (Nino and Gino Pizzas) to allow end-users to book tables in their restaurants") + import_chalk29.default.dim(` (no ${B.product} yet)`));
9258
+ console.log(import_chalk31.default.bold("1. The app: ResiResi, a table booking app for Tenants (Nino and Gino Pizzas) to allow end-users to book tables in their restaurants") + import_chalk31.default.dim(` (no ${B.product} yet)`));
8800
9259
  console.log(` Written to ${rel}. Its routes, from openapi.yaml:`);
8801
- for (const r of routesOf(openapi)) console.log(` ${r.method.padEnd(7)}${r.path.padEnd(21)}${import_chalk29.default.dim(r.summary)}`);
8802
- console.log(import_chalk29.default.dim(" No login, no API keys, no permission checks in its code. It only checks one shared backend secret (in .env.local)."));
9260
+ for (const r of routesOf(openapi)) console.log(` ${r.method.padEnd(7)}${r.path.padEnd(21)}${import_chalk31.default.dim(r.summary)}`);
9261
+ console.log(import_chalk31.default.dim(" No login, no API keys, no permission checks in its code. It only checks one shared backend secret (in .env.local)."));
8803
9262
  if (!await confirmStep(opts, `Ok to serve the backend on localhost:${backendPort2}?`)) {
8804
9263
  return stopHere("backend", [
8805
9264
  `Stopped before starting anything. ResiResi is written to ${rel}.`,
8806
- `Start it any time: ${cdDir}npm run start ${import_chalk29.default.dim("\xB7")} or run the whole demo again: ${LATEST} demo`
9265
+ `Start it any time: ${cdDir}npm run start ${import_chalk31.default.dim("\xB7")} or run the whole demo again: ${LATEST} demo`
8807
9266
  ]);
8808
9267
  }
8809
9268
  shell(`${cdDir}npm run start`, "npm run stop \xB7 npm run restart");
@@ -8811,23 +9270,23 @@ paths:`));
8811
9270
  registerDemo(dir);
8812
9271
  const appUp = await busy("Starting the reservation backend and its front-end\u2026", () => waitPorts([backendPort2, directPort]), 1800);
8813
9272
  if (!appUp) throw new DemoError(`The ResiResi app did not start (see ${import_path6.default.join(rel, STATE_DIR, "app.log")}).`, `Run again: ${LATEST} demo`, "app-not-up");
8814
- ok(`${import_chalk29.default.bold("Reservation backend")}: localhost:${backendPort2}`, "unprotected");
8815
- ok(`${import_chalk29.default.bold("Unprotected reservation front-end")}, going directly to the reservation backend: ${import_chalk29.default.bold(`http://localhost:${directPort}`)}`);
9273
+ ok(`${import_chalk31.default.bold("Reservation backend")}: localhost:${backendPort2}`, "unprotected");
9274
+ ok(`${import_chalk31.default.bold("Unprotected reservation front-end")}, going directly to the reservation backend: ${import_chalk31.default.bold(`http://localhost:${directPort}`)}`);
8816
9275
  console.log(" Book a table directly, the way the unprotected front-end does:");
8817
- console.log(import_chalk29.default.cyan(` curl -X POST http://localhost:${backendPort2}/reservations \\`));
8818
- console.log(import_chalk29.default.cyan(` -H "x-target-api-key: $(grep BACKEND_SECRET ${import_path6.default.join(rel, ".env.local")} | cut -d= -f2)" \\`));
8819
- console.log(import_chalk29.default.cyan(` -H "x-abz-user-id: ana" -H "x-abz-tenant-id: ${ninoTenant}" \\`));
9276
+ console.log(import_chalk31.default.cyan(` curl -X POST http://localhost:${backendPort2}/reservations \\`));
9277
+ console.log(import_chalk31.default.cyan(` -H "x-target-api-key: $(grep BACKEND_SECRET ${import_path6.default.join(rel, ".env.local")} | cut -d= -f2)" \\`));
9278
+ console.log(import_chalk31.default.cyan(` -H "x-abz-user-id: ana" -H "x-abz-tenant-id: ${ninoTenant}" \\`));
8820
9279
  const tomorrow = new Date(Date.now() + 864e5);
8821
9280
  const day = `${tomorrow.getFullYear()}-${String(tomorrow.getMonth() + 1).padStart(2, "0")}-${String(tomorrow.getDate()).padStart(2, "0")}`;
8822
- console.log(import_chalk29.default.cyan(` -H "content-type: application/json" -d '{"date":"${day}","time":"19:30","guests":2}'`));
9281
+ console.log(import_chalk31.default.cyan(` -H "content-type: application/json" -d '{"date":"${day}","time":"19:30","guests":2}'`));
8823
9282
  if (!await confirmStep(opts, `Ok to put an ${B.product} proxy and MCP in front of the reservation backend?`)) {
8824
9283
  return stopHere("proxy", [
8825
- `Stopped before ${B.product}. ResiResi keeps running on this laptop: http://localhost:${directPort} ${import_chalk29.default.dim(`(${cdDir}npm run stop stops it)`)}`,
9284
+ `Stopped before ${B.product}. ResiResi keeps running on this laptop: http://localhost:${directPort} ${import_chalk31.default.dim(`(${cdDir}npm run stop stops it)`)}`,
8826
9285
  `Run the whole demo again any time: ${LATEST} demo`
8827
9286
  ]);
8828
9287
  }
8829
9288
  console.log();
8830
- console.log(import_chalk29.default.bold(`2. ${B.product}, in front of it`));
9289
+ console.log(import_chalk31.default.bold(`2. ${B.product}, in front of it`));
8831
9290
  const tCreate = Date.now();
8832
9291
  const setup = await busy(`Creating the proxy, its tenants, admins, rules and limits\u2026`, async () => {
8833
9292
  const r = await createProxy2(name, backendPort2, openapi, ninoTenant);
@@ -8933,13 +9392,13 @@ paths:`));
8933
9392
  ]);
8934
9393
  }
8935
9394
  console.log();
8936
- console.log(import_chalk29.default.bold("3. Live on the internet"));
9395
+ console.log(import_chalk31.default.bold("3. Live on the internet"));
8937
9396
  shell(`${LATEST} dev --port ${backendPort2} --project ${project} --auto`, "the tunnel, in the background");
8938
9397
  await busy(`Opening the tunnel to localhost:${backendPort2} and checking it with a real call\u2026`, async () => {
8939
9398
  await spawnTunnel(dir, state);
8940
9399
  await waitThrough(apiUrl, serverKey);
8941
9400
  }, 1200);
8942
- ok(`${import_chalk29.default.bold("Tunnel")}: the public address reaches localhost:${backendPort2} on this laptop`, `checked with a real call \xB7 ${Math.round((Date.now() - t0) / 1e3)} s so far`);
9401
+ ok(`${import_chalk31.default.bold("Tunnel")}: the public address reaches localhost:${backendPort2} on this laptop`, `checked with a real call \xB7 ${Math.round((Date.now() - t0) / 1e3)} s so far`);
8943
9402
  printAddresses(dir);
8944
9403
  if (!await confirmStep(opts, "Ok to launch the protected website, which will use the proxy to reach the backend?")) {
8945
9404
  return stopHere("website", [
@@ -8948,7 +9407,7 @@ paths:`));
8948
9407
  ]);
8949
9408
  }
8950
9409
  console.log();
8951
- console.log(import_chalk29.default.bold(`4. The protected website, through ${B.product}`));
9410
+ console.log(import_chalk31.default.bold(`4. The protected website, through ${B.product}`));
8952
9411
  shell(`${cdDir}npm run restart`, "picks up the keys in .env.local");
8953
9412
  const siteUp = await busy("Starting the protected website\u2026", async () => {
8954
9413
  appCtl(dir, "--stop");
@@ -8956,22 +9415,22 @@ paths:`));
8956
9415
  return waitPorts([appPort, directPort, backendPort2]);
8957
9416
  }, 1500);
8958
9417
  if (!siteUp) throw new DemoError(`The protected website did not start (see ${import_path6.default.join(rel, STATE_DIR, "app.log")}).`, `Try: ${cdDir}npm run restart`, "site-not-up");
8959
- ok(`${import_chalk29.default.bold("Protected reservation website")}, going through the ${B.product} proxy ${apiUrl}: ${import_chalk29.default.bold(`http://localhost:${appPort}`)}`);
8960
- ok(`${import_chalk29.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");
9418
+ ok(`${import_chalk31.default.bold("Protected reservation website")}, going through the ${B.product} proxy ${apiUrl}: ${import_chalk31.default.bold(`http://localhost:${appPort}`)}`);
9419
+ ok(`${import_chalk31.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");
8961
9420
  console.log();
8962
- console.log(import_chalk29.default.dim(` Logs: ${logsCommand(readEnvFile(import_path6.default.join(dir, ".env.local")))} \xB7 Stop the tunnel: ${LATEST} demo stop \xB7 Your own app: ${LATEST} dev --port <port>`));
9421
+ console.log(import_chalk31.default.dim(` Logs: ${logsCommand(readEnvFile(import_path6.default.join(dir, ".env.local")))} \xB7 Stop the tunnel: ${LATEST} demo stop \xB7 Your own app: ${LATEST} dev --port <port>`));
8963
9422
  await offerBrowserAndLogs(dir, opts);
8964
9423
  return "demo-ready";
8965
9424
  }
8966
9425
  function printAddresses(dir) {
8967
9426
  const env = readEnvFile(import_path6.default.join(dir, ".env.local"));
8968
- const row = (label3, value, note = "") => console.log(` ${label3.padEnd(11)} ${value}${note ? ` ${import_chalk29.default.dim(note)}` : ""}`);
9427
+ const row = (label3, value, note = "") => console.log(` ${label3.padEnd(11)} ${value}${note ? ` ${import_chalk31.default.dim(note)}` : ""}`);
8969
9428
  row("Public API", env.APIBLAZE_URL ?? "", `${B.product} tunnel \u2192 localhost:${env.BACKEND_PORT}`);
8970
9429
  row("MCP", env.APIBLAZE_MCP_URL ?? "", "for Claude, ChatGPT, Cursor (Nino Pizza)");
8971
9430
  if (env.APIBLAZE_PORTAL_URL) row("Dev portal", env.APIBLAZE_PORTAL_URL, "sign in, get a key, try the API");
8972
9431
  }
8973
9432
  async function spawnTunnel(dir, state) {
8974
- const log4 = import_fs7.default.openSync(import_path6.default.join(dir, STATE_DIR, "tunnel.log"), "a");
9433
+ const log4 = import_fs8.default.openSync(import_path6.default.join(dir, STATE_DIR, "tunnel.log"), "a");
8975
9434
  const child = (0, import_child_process5.spawn)(process.execPath, [process.argv[1], "demo", "--serve", dir], { cwd: dir, detached: true, stdio: ["ignore", log4, log4] });
8976
9435
  child.unref();
8977
9436
  saveState(dir, { ...state, pid: child.pid, ready: false, error: void 0 });
@@ -8984,7 +9443,7 @@ async function runDemoStart(opts = {}) {
8984
9443
  const state = loadState(dir);
8985
9444
  const env = readEnvFile(import_path6.default.join(dir, ".env.local"));
8986
9445
  if (!state || !env.APIBLAZE_SERVER_KEY) {
8987
- console.log(`${import_chalk29.default.red("\u2717")} There is no demo here to start.`);
9446
+ console.log(`${import_chalk31.default.red("\u2717")} There is no demo here to start.`);
8988
9447
  console.log(` Make one: ${LATEST} demo`);
8989
9448
  process.exit(1);
8990
9449
  }
@@ -8994,31 +9453,31 @@ async function runDemoStart(opts = {}) {
8994
9453
  } catch (err) {
8995
9454
  const e = err instanceof DemoError ? err : new DemoError(`The tunnel could not start (${err.message}).`, `Start over: ${LATEST} demo`);
8996
9455
  countRun({ event: `demo-restart-failed-${e.why}` });
8997
- console.log(`${import_chalk29.default.red("\u2717")} ${e.message}`);
9456
+ console.log(`${import_chalk31.default.red("\u2717")} ${e.message}`);
8998
9457
  console.log(` ${e.fix}`);
8999
9458
  process.exit(1);
9000
9459
  }
9001
9460
  countRun({ event: "demo-restarted" });
9002
- console.log(`${import_chalk29.default.green("\u2713")} Tunnel open: the public address reaches localhost:${env.BACKEND_PORT} again ${import_chalk29.default.dim(`(${Math.round((Date.now() - t0) / 1e3)} s)`)}`);
9461
+ console.log(`${import_chalk31.default.green("\u2713")} Tunnel open: the public address reaches localhost:${env.BACKEND_PORT} again ${import_chalk31.default.dim(`(${Math.round((Date.now() - t0) / 1e3)} s)`)}`);
9003
9462
  printAddresses(dir);
9004
9463
  const appUp = await fetch(`http://127.0.0.1:${env.APP_PORT}/api/config`).then(() => true).catch(() => false);
9005
- if (!appUp) console.log(import_chalk29.default.yellow(` ResiResi is not running: cd ${import_path6.default.relative(process.cwd(), dir) || "."} && npm run start`));
9464
+ if (!appUp) console.log(import_chalk31.default.yellow(` ResiResi is not running: cd ${import_path6.default.relative(process.cwd(), dir) || "."} && npm run start`));
9006
9465
  await offerBrowserAndLogs(dir, opts);
9007
9466
  }
9008
9467
  async function runDemoLogs() {
9009
9468
  const env = readEnvFile(import_path6.default.join(demoDir(), ".env.local"));
9010
9469
  if (!env.APIBLAZE_NAME) {
9011
- console.log(`${import_chalk29.default.red("\u2717")} There is no demo here.`);
9470
+ console.log(`${import_chalk31.default.red("\u2717")} There is no demo here.`);
9012
9471
  console.log(` Make one: ${LATEST} demo`);
9013
9472
  process.exit(1);
9014
9473
  }
9015
- console.log(import_chalk29.default.dim(`$ ${logsCommand(env)}`));
9474
+ console.log(import_chalk31.default.dim(`$ ${logsCommand(env)}`));
9016
9475
  const { runLogsTail: runLogsTail2 } = await Promise.resolve().then(() => (init_logs(), logs_exports));
9017
9476
  await runLogsTail2(env.APIBLAZE_NAME, void 0, { tenant: env.APIBLAZE_TENANT });
9018
9477
  }
9019
9478
  async function runDemoStop() {
9020
9479
  const n = await stopTunnels();
9021
- console.log(n ? `${import_chalk29.default.green("\u2713")} Stopped the tunnel. ResiResi still runs on this laptop (npm run stop stops it).` : "No demo tunnel is running.");
9480
+ console.log(n ? `${import_chalk31.default.green("\u2713")} Stopped the tunnel. ResiResi still runs on this laptop (npm run stop stops it).` : "No demo tunnel is running.");
9022
9481
  }
9023
9482
  async function serveTunnel(dir) {
9024
9483
  const state = loadState(dir);
@@ -9076,16 +9535,16 @@ async function serveTunnel(dir) {
9076
9535
  await new Promise(() => {
9077
9536
  });
9078
9537
  }
9079
- var import_fs7, import_path6, import_net2, import_child_process5, import_crypto, import_chalk29, 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;
9538
+ var import_fs8, import_path6, import_net2, import_child_process5, import_crypto, import_chalk31, 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;
9080
9539
  var init_demo = __esm({
9081
9540
  "src/commands/demo.ts"() {
9082
9541
  "use strict";
9083
- import_fs7 = __toESM(require("fs"));
9542
+ import_fs8 = __toESM(require("fs"));
9084
9543
  import_path6 = __toESM(require("path"));
9085
9544
  import_net2 = __toESM(require("net"));
9086
9545
  import_child_process5 = require("child_process");
9087
9546
  import_crypto = __toESM(require("crypto"));
9088
- import_chalk29 = __toESM(require("chalk"));
9547
+ import_chalk31 = __toESM(require("chalk"));
9089
9548
  import_ora11 = __toESM(require("ora"));
9090
9549
  init_api();
9091
9550
  init_tunnel_client();
@@ -10145,7 +10604,7 @@ async function runCheck(project, opts) {
10145
10604
  }
10146
10605
  const name = project;
10147
10606
  if (!name) throw new Error(`Which ${B.thingShort}? ${B.cli} check --directory-ready <project>`);
10148
- const platforms = parsePlatforms(opts.platform);
10607
+ const platforms = parsePolicy(opts.policy, opts.platform);
10149
10608
  const stage = opts.stage === "create" ? "create" : opts.stage === "ready" || !opts.stage ? "ready" : void 0;
10150
10609
  if (!stage) throw new Error("--stage must be create or ready");
10151
10610
  const caller = requireCaller();
@@ -10169,13 +10628,149 @@ Not ready: ${lines.length} blocker${lines.length === 1 ? "" : "s"} (${r.rendered
10169
10628
  }
10170
10629
 
10171
10630
  // src/commands/connector.ts
10172
- var import_fs5 = __toESM(require("fs"));
10631
+ var import_fs6 = __toESM(require("fs"));
10173
10632
  var import_path4 = __toESM(require("path"));
10174
- var import_chalk26 = __toESM(require("chalk"));
10633
+ var import_chalk28 = __toESM(require("chalk"));
10175
10634
  var import_ora10 = __toESM(require("ora"));
10176
10635
  init_caller();
10177
10636
  init_directory();
10178
10637
  init_brand();
10638
+
10639
+ // src/lib/listing-defaults.ts
10640
+ var import_yaml5 = __toESM(require("yaml"));
10641
+ var RESERVED_SUFFIX = /\s*\b(api|rest api|mcp|plugin|connector|app|server|service)\b\s*$/i;
10642
+ function serverNameFromTitle(title) {
10643
+ let n = (title ?? "").replace(/\s+/g, " ").trim();
10644
+ if (!n) return void 0;
10645
+ for (let i = 0; i < 3; i++) {
10646
+ const next = n.replace(RESERVED_SUFFIX, "").trim();
10647
+ if (next === n || next.length < 2) break;
10648
+ n = next;
10649
+ }
10650
+ if (n.length > 40) {
10651
+ const cut = n.slice(0, 40);
10652
+ n = (cut.includes(" ") ? cut.slice(0, cut.lastIndexOf(" ")) : cut).trim();
10653
+ }
10654
+ return n.length >= 2 ? n : void 0;
10655
+ }
10656
+ function taglineFrom(description, fallback) {
10657
+ const d = (description ?? "").replace(/\s+/g, " ").trim();
10658
+ const first = d.split(/(?<=[.!?])\s+/)[0] ?? "";
10659
+ const t = first.length > 120 ? `${first.slice(0, 117).trim()}\u2026` : first;
10660
+ if (t.length >= 10) return t;
10661
+ return fallback ? `${fallback} for AI assistants` : void 0;
10662
+ }
10663
+ function descriptionFrom(description) {
10664
+ const d = (description ?? "").trim();
10665
+ if (!d) return void 0;
10666
+ const firstPara = d.split(/\n\s*\n/)[0]?.replace(/\s+/g, " ").trim() ?? "";
10667
+ const text = firstPara.length >= 40 ? firstPara : d.replace(/\s+/g, " ").trim();
10668
+ return text.length > 600 ? `${text.slice(0, 597).trim()}\u2026` : text;
10669
+ }
10670
+ var VERB_MAP = [
10671
+ [/^(list|get all|fetch all|retrieve all|search)\b\s*(.*)$/i, (r) => `List my ${r || "items"}`],
10672
+ [/^(get|fetch|retrieve|show|read)\b\s*(.*)$/i, (r) => `Show me the ${r || "details"}`],
10673
+ [/^(create|add|make|book|register|new)\b\s*(.*)$/i, (r) => `Create a ${r || "new entry"}`],
10674
+ [/^(update|edit|change|modify|patch)\b\s*(.*)$/i, (r) => `Update the ${r || "entry"}`],
10675
+ [/^(delete|remove|cancel)\b\s*(.*)$/i, (r) => `Cancel the ${r || "entry"}`]
10676
+ ];
10677
+ function cleanRest(rest) {
10678
+ return rest.replace(/\s*\(.*?\)\s*/g, " ").replace(/\bby id or slug\b/i, "").replace(/\bby id\b/i, "").replace(/\s+/g, " ").trim().toLowerCase();
10679
+ }
10680
+ function promptFromSummary(summary, opId) {
10681
+ const s = (summary ?? "").trim() || (opId ?? "").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
10682
+ if (!s) return void 0;
10683
+ for (const [re, make] of VERB_MAP) {
10684
+ const m = s.match(re);
10685
+ if (m) {
10686
+ const rest = cleanRest(m[2] ?? "");
10687
+ const out = make(rest).replace(/\s+/g, " ").trim();
10688
+ return out.endsWith(".") ? out : `${out}.`;
10689
+ }
10690
+ }
10691
+ const t = cleanRest(s);
10692
+ return t ? `${t.charAt(0).toUpperCase()}${t.slice(1)}.` : void 0;
10693
+ }
10694
+ var SKIP_PATH = /(health|ping|status|openapi|swagger|docs|spec|whoami|version)/i;
10695
+ function promptsFromOps(ops, want = 3) {
10696
+ const usable = ops.filter((o) => !SKIP_PATH.test(o.path) && !SKIP_PATH.test(o.operationId ?? ""));
10697
+ const rank = (o) => {
10698
+ const s = (o.summary ?? o.operationId ?? "").toLowerCase();
10699
+ if (o.method === "get" && /^(list|search|get all)/.test(s)) return 0;
10700
+ if (o.method === "post" && /^(create|add|book|make|register)/.test(s)) return 1;
10701
+ if (o.method === "get") return 2;
10702
+ if (o.method === "patch" || o.method === "put") return 3;
10703
+ return 4;
10704
+ };
10705
+ const seen = /* @__PURE__ */ new Set();
10706
+ const out = [];
10707
+ for (const o of [...usable].sort((a, b) => rank(a) - rank(b))) {
10708
+ const p = promptFromSummary(o.summary, o.operationId);
10709
+ if (!p || seen.has(p.toLowerCase())) continue;
10710
+ seen.add(p.toLowerCase());
10711
+ out.push(p);
10712
+ if (out.length >= want) break;
10713
+ }
10714
+ return out;
10715
+ }
10716
+ function apexOf(host) {
10717
+ if (!host) return void 0;
10718
+ const h = host.toLowerCase().replace(/\.$/, "");
10719
+ if (/^(\d+\.){3}\d+$/.test(h) || h === "localhost") return void 0;
10720
+ const parts = h.split(".");
10721
+ return parts.length >= 2 ? parts.slice(-2).join(".") : void 0;
10722
+ }
10723
+ function hostOf(url) {
10724
+ try {
10725
+ return url ? new URL(url).hostname : void 0;
10726
+ } catch {
10727
+ return void 0;
10728
+ }
10729
+ }
10730
+ function listingDefaultsFromSpec(specText, source) {
10731
+ let doc = {};
10732
+ try {
10733
+ doc = import_yaml5.default.parse(specText) ?? {};
10734
+ } catch {
10735
+ try {
10736
+ doc = JSON.parse(specText);
10737
+ } catch {
10738
+ doc = {};
10739
+ }
10740
+ }
10741
+ const info = doc?.info ?? {};
10742
+ const servers = Array.isArray(doc?.servers) ? doc.servers : [];
10743
+ const ops = [];
10744
+ for (const [p, item] of Object.entries(doc?.paths ?? {})) {
10745
+ for (const m of ["get", "post", "put", "patch", "delete"]) {
10746
+ const o = item?.[m];
10747
+ if (o && typeof o === "object") ops.push({ method: m, path: p, summary: o.summary, operationId: o.operationId, description: o.description });
10748
+ }
10749
+ }
10750
+ const serverName = serverNameFromTitle(info.title);
10751
+ const apexDomain = apexOf(hostOf(servers[0]?.url)) ?? apexOf(hostOf(source));
10752
+ const contact = info.contact ?? {};
10753
+ const support = typeof contact.email === "string" && contact.email.includes("@") ? contact.email : typeof contact.url === "string" && /^https:\/\//i.test(contact.url) ? contact.url : apexDomain ? `support@${apexDomain}` : void 0;
10754
+ const docsUrl = typeof doc?.externalDocs?.url === "string" && /^https:\/\//i.test(doc.externalDocs.url) ? doc.externalDocs.url : void 0;
10755
+ const logo = info["x-logo"]?.url;
10756
+ const iconUrl = typeof logo === "string" && /^https:\/\//i.test(logo) ? logo : void 0;
10757
+ return {
10758
+ serverName,
10759
+ tagline: taglineFrom(info.description, serverName),
10760
+ description: descriptionFrom(info.description),
10761
+ support,
10762
+ docsUrl,
10763
+ iconUrl,
10764
+ vulnContact: apexDomain ? `security@${apexDomain}` : void 0,
10765
+ prompts: promptsFromOps(ops),
10766
+ apexDomain
10767
+ };
10768
+ }
10769
+ function suggestedMcpHost(apexDomain) {
10770
+ return apexDomain ? `mcp.${apexDomain}` : void 0;
10771
+ }
10772
+
10773
+ // src/commands/connector.ts
10179
10774
  var PORTALS = {
10180
10775
  claude: "https://claude.com/docs/connectors/building/submission",
10181
10776
  chatgpt: "https://developers.openai.com/apps-sdk/deploy/submission"
@@ -10185,55 +10780,368 @@ async function patchProject(caller, proj2, body, summary) {
10185
10780
  await producer(caller, { method: "PATCH", path: projectPath(proj2), body, summary });
10186
10781
  }
10187
10782
  var PROMPT_FOR_RULE = {
10188
- PRIVACY_URL_HTTPS_200: { key: "privacyUrl", message: "Privacy policy URL (https):" },
10783
+ PRIVACY_URL_HTTPS_200: { key: "privacyUrl", message: "Privacy policy URL (https, must answer 200):" },
10189
10784
  SUPPORT_CONTACT: { key: "support", message: "Support contact (email or URL):" },
10190
10785
  DOCS_URL: { key: "docsUrl", message: "Documentation URL (https):" },
10191
10786
  EXAMPLE_PROMPTS_3: { key: "prompt", message: "Example prompt (3 needed; one per line, empty line to finish):", multi: true },
10192
- SERVER_NAME_RULES: { key: "serverName", message: 'Listing name (2\u201340 chars, no "MCP"/"Plugin" suffix):' },
10193
- ICON_SET: { key: "iconUrl", message: "Icon URL (https, square):" },
10194
- VULN_CONTACT: { key: "vulnContact", message: "Security contact (email or URL; optional):" }
10787
+ SERVER_NAME_RULES: { key: "serverName", message: 'Listing name (no "MCP"/"Plugin" suffix):' },
10788
+ TAGLINE_SET: { key: "tagline", message: "One-line tagline:" },
10789
+ DESCRIPTION_SET: { key: "description", message: "Description (what people can do with it):" },
10790
+ ICON_SET: { key: "iconUrl", message: "Icon URL (https, square PNG):" },
10791
+ VULN_CONTACT: { key: "vulnContact", message: "Security contact (email or URL):" },
10792
+ LISTING_NAME_LEN_CHATGPT: { key: "serverName", message: "Listing name", limit: 30, portal: "ChatGPT" },
10793
+ LISTING_NAME_LEN_CLAUDE: { key: "serverName", message: "Listing name", limit: 100, portal: "Claude" },
10794
+ TAGLINE_LEN_CHATGPT: { key: "tagline", message: "One-line tagline", limit: 30, portal: "ChatGPT" },
10795
+ TAGLINE_LEN_CLAUDE: { key: "tagline", message: "One-line tagline", limit: 200, portal: "Claude" },
10796
+ DESCRIPTION_LEN_CLAUDE: { key: "description", message: "Description", limit: 2e3, portal: "Claude" },
10797
+ DESCRIPTION_LEN_CHATGPT: { key: "description", message: "Description", limit: 4e3, portal: "ChatGPT" },
10798
+ PROMPT_LEN_CHATGPT: { key: "prompt", message: "Example prompt", multi: true, limit: 128, portal: "ChatGPT" },
10799
+ // ChatGPT only: the portal issues this token (Plugins → Connect MCP server); we serve it verbatim.
10800
+ OPENAI_DOMAIN_CHALLENGE: { key: "openaiChallenge", message: "OpenAI domain challenge token, from platform.openai.com/plugins \u2192 Connect MCP server (Enter to skip for now):" }
10195
10801
  };
10196
- async function promptForMissing(r, interactive2) {
10802
+ var DEFAULT_FOR_KEY = {
10803
+ support: "support",
10804
+ docsUrl: "docsUrl",
10805
+ serverName: "serverName",
10806
+ iconUrl: "iconUrl",
10807
+ vulnContact: "vulnContact",
10808
+ privacyUrl: "privacyUrl"
10809
+ };
10810
+ function looksLikeSpecArgument(arg) {
10811
+ const a = arg.trim();
10812
+ if (/^https?:\/\//i.test(a)) return true;
10813
+ if (/[\\/]/.test(a) || /\.(ya?ml|json)$/i.test(a)) {
10814
+ try {
10815
+ return import_fs6.default.existsSync(a) && import_fs6.default.statSync(a).isFile();
10816
+ } catch {
10817
+ return false;
10818
+ }
10819
+ }
10820
+ return false;
10821
+ }
10822
+ function currentListingValue(r, key) {
10823
+ const meta = r.target?.metadata ?? {};
10824
+ const field = {
10825
+ serverName: "serverName",
10826
+ tagline: "tagline",
10827
+ description: "description",
10828
+ prompt: "examplePrompts",
10829
+ privacyUrl: "privacyUrl",
10830
+ support: "supportContact",
10831
+ docsUrl: "docsUrl",
10832
+ iconUrl: "iconUrl",
10833
+ vulnContact: "vulnContact"
10834
+ };
10835
+ const v = field[key] ? meta[field[key]] : void 0;
10836
+ if (Array.isArray(v)) return v.filter((x) => typeof x === "string");
10837
+ return typeof v === "string" && v.trim() ? v : void 0;
10838
+ }
10839
+ function shortenTo(text, limit) {
10840
+ const t = text.replace(/\s+/g, " ").trim();
10841
+ if (t.length <= limit) return t;
10842
+ const cut = t.slice(0, limit);
10843
+ const atSentence = Math.max(cut.lastIndexOf(". "), cut.lastIndexOf("! "), cut.lastIndexOf("? "));
10844
+ if (atSentence >= Math.floor(limit * 0.5)) return cut.slice(0, atSentence + 1).trim();
10845
+ const atWord = cut.lastIndexOf(" ");
10846
+ const base2 = atWord >= Math.floor(limit * 0.4) ? cut.slice(0, atWord) : cut;
10847
+ return base2.replace(/[\s,;:(\-–—]+$/g, "").trim();
10848
+ }
10849
+ async function promptForMissing(r, interactive2, defaults = { prompts: [] }, platforms = PLATFORMS) {
10197
10850
  const out = {};
10198
- if (!interactive2) return out;
10199
10851
  const failing = r.report.results.filter((x) => !x.pass && !x.unverified);
10200
- const { default: inquirer3 } = await import("inquirer");
10852
+ const asks = /* @__PURE__ */ new Map();
10201
10853
  for (const x of failing) {
10202
10854
  const p = PROMPT_FOR_RULE[x.id];
10203
10855
  if (!p) continue;
10204
- if (p.multi) {
10856
+ const cur = asks.get(p.key);
10857
+ if (!cur) {
10858
+ asks.set(p.key, { ...p });
10859
+ continue;
10860
+ }
10861
+ if (p.limit && (!cur.limit || p.limit < cur.limit)) {
10862
+ cur.limit = p.limit;
10863
+ cur.portal = p.portal;
10864
+ }
10865
+ if (!p.limit && cur.limit) cur.message = p.message;
10866
+ }
10867
+ const limitNote = (a) => a.limit ? ` (${a.portal} allows ${a.limit} characters; the proposed cut is pre-filled \u2014 edit or Enter)` : "";
10868
+ const withinLimit = (a) => (v) => {
10869
+ const n = (v ?? "").trim().length;
10870
+ return !a.limit || n <= a.limit ? true : `${n} characters; ${a.portal} allows ${a.limit}.`;
10871
+ };
10872
+ if (!interactive2) {
10873
+ for (const a of asks.values()) {
10874
+ const current = currentListingValue(r, a.key);
10875
+ if (a.multi) {
10876
+ const base2 = Array.isArray(current) && current.length >= 3 ? current : defaults.prompts;
10877
+ if (base2.length >= 3) out.prompt = base2.map((p) => a.limit ? shortenTo(p, a.limit) : p);
10878
+ continue;
10879
+ }
10880
+ const dk = DEFAULT_FOR_KEY[a.key];
10881
+ const dv = typeof current === "string" ? current : dk ? defaults[dk] : void 0;
10882
+ if (typeof dv === "string" && dv) out[a.key] = a.limit ? shortenTo(dv, a.limit) : dv;
10883
+ }
10884
+ return out;
10885
+ }
10886
+ const { default: inquirer3 } = await import("inquirer");
10887
+ for (const a of asks.values()) {
10888
+ const current = currentListingValue(r, a.key);
10889
+ if (a.multi) {
10890
+ const have = Array.isArray(current) ? current : [];
10205
10891
  const lines = [];
10206
10892
  for (let i = 0; i < 6; i++) {
10207
- const { line } = await inquirer3.prompt([{ type: "input", name: "line", message: i === 0 ? p.message : `Example prompt ${i + 1} (empty to finish):` }]);
10208
- if (!line?.trim()) break;
10893
+ const raw = have[i] ?? defaults.prompts[i];
10894
+ const def2 = raw && a.limit ? shortenTo(raw, a.limit) : raw;
10895
+ const { line } = await inquirer3.prompt([{
10896
+ type: "input",
10897
+ name: "line",
10898
+ message: (i === 0 ? `${a.message}${a.limit ? ` (3 needed, each \u2264 ${a.limit} characters for ${a.portal}; one per line, empty line to finish)` : ""}:` : `Example prompt ${i + 1}${i >= 3 ? " (optional, empty to finish)" : ""}:`).replace("::", ":"),
10899
+ ...def2 ? { default: def2 } : {},
10900
+ validate: withinLimit(a)
10901
+ }]);
10902
+ if (!line?.trim()) {
10903
+ if (i >= 3) break;
10904
+ continue;
10905
+ }
10209
10906
  lines.push(line.trim());
10210
10907
  }
10211
10908
  if (lines.length) out.prompt = lines;
10212
10909
  continue;
10213
10910
  }
10214
- const { value } = await inquirer3.prompt([{ type: "input", name: "value", message: p.message }]);
10215
- if (value?.trim()) out[p.key] = value.trim();
10911
+ const dk = DEFAULT_FOR_KEY[a.key];
10912
+ const base2 = typeof current === "string" ? current : dk ? defaults[dk] : void 0;
10913
+ const def = typeof base2 === "string" && base2 ? a.limit ? shortenTo(base2, a.limit) : base2 : void 0;
10914
+ const { value } = await inquirer3.prompt([{
10915
+ type: "input",
10916
+ name: "value",
10917
+ message: `${a.message}${limitNote(a)}${a.message.endsWith(":") ? "" : ":"}`,
10918
+ ...def ? { default: def } : {},
10919
+ // The privacy page is a hard portal requirement (https + 200): check it here, before the
10920
+ // report has to come back red for a typo.
10921
+ validate: a.key === "privacyUrl" ? validatePrivacyUrl : withinLimit(a)
10922
+ }]);
10923
+ if (value?.trim()) out[a.key] = value.trim();
10216
10924
  }
10217
10925
  const missingAttest = failing.filter((x) => x.id.startsWith("ATTEST_"));
10218
10926
  if (missingAttest.length) {
10219
- const choices = ATTESTATIONS.filter((a) => missingAttest.some((x) => x.message?.toLowerCase().includes(a.label.toLowerCase().slice(0, 20)) || true)).map((a) => ({ name: a.label, value: a.id }));
10220
- const { picked } = await inquirer3.prompt([{ type: "checkbox", name: "picked", message: "Attest (only what is true \u2014 each is recorded with your name and the time):", choices }]);
10927
+ const choices = ATTESTATIONS.filter((a) => a.platforms.some((p) => platforms.includes(p))).map((a) => ({ name: a.label, value: a.id, checked: true }));
10928
+ const { picked } = await inquirer3.prompt([{ type: "checkbox", name: "picked", message: "These are true of your server (space unticks one; each is recorded with your name and the time):", choices }]);
10221
10929
  if (picked?.length) out.attest = picked;
10222
10930
  }
10223
10931
  return out;
10224
10932
  }
10225
- async function runConnector(project, opts) {
10933
+ async function promptListingBasics(r, interactive2, defaults, given, platforms = PLATFORMS) {
10934
+ const meta = r.target?.metadata ?? {};
10935
+ const gpt = platforms.includes("chatgpt");
10936
+ const lim = { serverName: gpt ? 30 : 100, tagline: gpt ? 30 : 200, description: platforms.includes("claude") ? 2e3 : 4e3 };
10937
+ const cutTo = (v, n) => v ? shortenTo(v, n) : v;
10938
+ const all = [
10939
+ { key: "serverName", current: meta.serverName, def: cutTo(defaults.serverName, lim.serverName), message: `Listing name (\u2264 ${lim.serverName} chars, no "MCP"/"Plugin" suffix):` },
10940
+ { key: "tagline", current: meta.tagline, def: cutTo(defaults.tagline, lim.tagline), message: `One-line tagline (\u2264 ${lim.tagline} chars):` },
10941
+ { key: "description", current: meta.description, def: cutTo(defaults.description, lim.description), message: `Description (what people can do with it, \u2264 ${lim.description} chars):` }
10942
+ ];
10943
+ const want = all.filter((w) => !given[w.key] && !(w.current ?? "").trim() && w.def);
10944
+ const out = {};
10945
+ if (!want.length) return out;
10946
+ if (!interactive2) {
10947
+ for (const w of want) out[w.key] = w.def;
10948
+ return out;
10949
+ }
10950
+ const { default: inquirer3 } = await import("inquirer");
10951
+ for (const w of want) {
10952
+ const { value } = await inquirer3.prompt([{ type: "input", name: "value", message: w.message, default: w.def }]);
10953
+ if (value?.trim()) out[w.key] = value.trim();
10954
+ }
10955
+ return out;
10956
+ }
10957
+ async function createFromSpec(spec2, opts, say) {
10958
+ const { runCreate: runCreate2 } = await Promise.resolve().then(() => (init_create(), create_exports));
10959
+ let created = null;
10960
+ say(import_chalk28.default.bold(`Creating a directory-ready ${B.thingShort} from ${spec2}\u2026`));
10961
+ await runCreate2({
10962
+ openapi: spec2,
10963
+ directoryReady: true,
10964
+ ...opts.name ? { name: opts.name } : { auto: true },
10965
+ yes: true,
10966
+ json: !!opts.json,
10967
+ team: opts.team,
10968
+ ...opts.apiversion ? { apiversion: opts.apiversion } : {},
10969
+ skipDirectoryReport: true,
10970
+ onCreated: (c) => {
10971
+ created = { name: c.name, version: c.version, tenant: c.tenant };
10972
+ },
10973
+ // Listing flags given on the command line ride along on the create.
10974
+ privacyUrl: opts.privacyUrl,
10975
+ support: opts.support,
10976
+ docsUrl: opts.docsUrl,
10977
+ prompt: opts.prompt,
10978
+ serverName: opts.serverName,
10979
+ tagline: opts.tagline,
10980
+ description: opts.description,
10981
+ iconUrl: opts.iconUrl,
10982
+ vulnContact: opts.vulnContact,
10983
+ attest: opts.attest,
10984
+ allowedOrigin: opts.allowedOrigin
10985
+ });
10986
+ if (!created) throw new Error(`The create did not report a ${B.thingShort} name; run ${B.cli} connector <project> once it exists.`);
10987
+ return created;
10988
+ }
10989
+ async function specTextFor(source) {
10990
+ try {
10991
+ if (/^https?:\/\//i.test(source)) {
10992
+ const res = await fetch(source, { signal: AbortSignal.timeout(8e3) });
10993
+ return res.ok ? await res.text() : null;
10994
+ }
10995
+ return import_fs6.default.readFileSync(source, "utf-8");
10996
+ } catch {
10997
+ return null;
10998
+ }
10999
+ }
11000
+ async function projectSpecText(caller, proj2) {
11001
+ try {
11002
+ const o = await producer(caller, { method: "GET", path: `${projectPath(proj2)}/openapi`, summary: "Read the OpenAPI spec" });
11003
+ const spec2 = o?.spec ?? o?.openapi ?? o?.content ?? o?.openapi_spec ?? null;
11004
+ if (typeof spec2 === "string") return spec2;
11005
+ return spec2 && typeof spec2 === "object" ? JSON.stringify(spec2) : null;
11006
+ } catch {
11007
+ return null;
11008
+ }
11009
+ }
11010
+ var DOMAIN_RULE = "DOMAIN_CUSTOM_REQUIRED";
11011
+ async function ensureMcpDomain(caller, proj2, r, opts, defaults, interactive2, say) {
11012
+ const domainRule = r.report.results.find((x) => x.id === DOMAIN_RULE);
11013
+ if (!domainRule || domainRule.pass) return { active: true };
11014
+ const base2 = `/projects/${encodeURIComponent(proj2.projectId)}/domains`;
11015
+ const list = await producer(caller, { method: "GET", path: `${base2}?api_version=${encodeURIComponent(proj2.apiVersion)}`, summary: "List custom domains" }).catch(() => null);
11016
+ const rows = Array.isArray(list) ? list : list?.domains ?? [];
11017
+ let row = rows.find((d) => (d.kind ?? "api") === "mcp" && (!opts.domain || d.hostname === opts.domain.toLowerCase()));
11018
+ if (!row) {
11019
+ let hostname2 = opts.domain?.trim().toLowerCase();
11020
+ if (!hostname2) {
11021
+ const suggested = suggestedMcpHost(defaults.apexDomain);
11022
+ if (!interactive2) {
11023
+ if (!suggested) throw new Error(`A custom MCP hostname is required for the directories. Pass --domain mcp.<your domain>.`);
11024
+ hostname2 = suggested;
11025
+ } else {
11026
+ const { default: inquirer3 } = await import("inquirer");
11027
+ const { value } = await inquirer3.prompt([{ type: "input", name: "value", message: "Hostname for the MCP server (on a domain you control):", ...suggested ? { default: suggested } : {} }]);
11028
+ hostname2 = value?.trim().toLowerCase();
11029
+ }
11030
+ }
11031
+ if (!hostname2) throw new Error("A custom MCP hostname is required for the directories (--domain mcp.<your domain>).");
11032
+ const sp2 = opts.json ? null : (0, import_ora10.default)(`Registering ${hostname2}\u2026`).start();
11033
+ row = await producer(caller, { method: "POST", path: base2, body: { hostname: hostname2, api_version: proj2.apiVersion, kind: "mcp" }, summary: `Add custom MCP domain ${hostname2}` });
11034
+ sp2?.succeed(`Registered ${row.hostname ?? hostname2} as the MCP server's domain.`);
11035
+ }
11036
+ const hostname = row.hostname ?? opts.domain ?? "";
11037
+ if (row.status === "active") return { active: true, hostname };
11038
+ say("");
11039
+ say(import_chalk28.default.bold(" Add this DNS record at your DNS provider (one record; nothing else to add):"));
11040
+ say(` ${import_chalk28.default.cyan("CNAME")} ${row.cnameRecord?.name ?? hostname} \u2192 ${row.cnameRecord?.value ?? "proxy.abz.run"}`);
11041
+ if (row.dcv) say(` ${import_chalk28.default.cyan("TXT")} ${row.dcv.name} = ${row.dcv.value}`);
11042
+ say("");
11043
+ const minutes = opts.wait === void 0 ? 15 : Number(opts.wait);
11044
+ if (!(minutes > 0) || !row.id) return { active: false, hostname };
11045
+ const sp = opts.json ? null : (0, import_ora10.default)(`Waiting for ${hostname} to activate (Cloudflare validates it once the CNAME resolves; up to ${minutes} min, Ctrl-C to stop and re-run later)\u2026`).start();
11046
+ const until = Date.now() + minutes * 6e4;
11047
+ let status2 = row.status ?? "pending";
11048
+ while (Date.now() < until) {
11049
+ await new Promise((res) => setTimeout(res, 15e3));
11050
+ const s = await producer(caller, { method: "GET", path: `${base2}/${encodeURIComponent(row.id)}/status`, summary: "Check custom-domain status" }).catch(() => null);
11051
+ status2 = s?.status ?? status2;
11052
+ if (sp) sp.text = `Waiting for ${hostname} to activate\u2026 (${status2}, ${Math.max(0, Math.round((until - Date.now()) / 6e4))} min left)`;
11053
+ if (status2 === "active") {
11054
+ sp?.succeed(`${hostname} is active.`);
11055
+ return { active: true, hostname };
11056
+ }
11057
+ if (status2 === "suspended" || status2 === "missing") break;
11058
+ }
11059
+ sp?.fail(`${hostname} is not active yet (${status2}).`);
11060
+ return { active: false, hostname };
11061
+ }
11062
+ function generatePassword(bytes = () => crypto.getRandomValues(new Uint8Array(20))) {
11063
+ const alphabet = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
11064
+ return Array.from(bytes(), (b) => alphabet[b % alphabet.length]).join("");
11065
+ }
11066
+ async function ensureReviewerAccount(caller, teamId, proj2, opts = {}) {
11067
+ if (!proj2.tenant) throw new Error(`${proj2.projectName} has no tenant yet; attach one first (${B.cli} tenant attach ${proj2.projectName} --tenant <slug>).`);
11068
+ const base2 = `/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(proj2.tenant)}`;
11069
+ const username = (opts.username ?? `reviewer-${proj2.projectId}`).trim().toLowerCase();
11070
+ const password = opts.password ?? generatePassword();
11071
+ const { enabled, clientId } = await producer(caller, { method: "GET", path: `${base2}/password-accounts`, summary: "Check password sign-in" }).then((o) => ({ enabled: !!o?.enabled, clientId: o?.clientId ?? o?.client_id })).catch(() => ({ enabled: false, clientId: void 0 }));
11072
+ if (!enabled && clientId) {
11073
+ await producer(caller, { method: "POST", path: `${base2}/app-clients/${encodeURIComponent(clientId)}/providers`, body: { type: "password" }, summary: "Enable username & password sign-in" });
11074
+ }
11075
+ let finalUser = username;
11076
+ try {
11077
+ const created = await producer(caller, {
11078
+ method: "POST",
11079
+ path: `${base2}/password-accounts`,
11080
+ body: { username, password, purpose: "reviewer", displayName: "Directory reviewer" },
11081
+ summary: `Create reviewer account ${username}`
11082
+ });
11083
+ finalUser = created.account?.username ?? username;
11084
+ } catch (e) {
11085
+ if (!isApiError(e, 409)) throw e;
11086
+ await producer(caller, { method: "POST", path: `${base2}/password-accounts/${encodeURIComponent(username)}/rotate`, body: { password }, summary: `Rotate reviewer account ${username}` });
11087
+ }
11088
+ await producer(caller, {
11089
+ method: "PATCH",
11090
+ path: projectPath(proj2),
11091
+ body: { directory_test_account: { username: finalUser, password, tenant: proj2.tenant, ...clientId ? { app_client_id: clientId } : {} } },
11092
+ summary: "Store the reviewer account for the packet"
11093
+ });
11094
+ return { username: finalUser, password, tenant: proj2.tenant };
11095
+ }
11096
+ function printReviewer(acct, proj2) {
11097
+ console.log(import_chalk28.default.green(`Reviewer account ${import_chalk28.default.bold(acct.username)} on tenant ${acct.tenant} \u2014 stored (encrypted) for the submission packet.`));
11098
+ console.log();
11099
+ console.log(import_chalk28.default.bold(" Password (shown once):"));
11100
+ console.log(` ${import_chalk28.default.cyan(acct.username)} ${import_chalk28.default.bgWhite.black(` ${acct.password} `)}`);
11101
+ console.log();
11102
+ console.log(import_chalk28.default.dim(` Seed realistic data through the proxy (billed per turn): ${B.cli} agent --seed-for ${proj2.projectName}`));
11103
+ }
11104
+ async function runConnectorReviewer(project, opts) {
10226
11105
  const caller = requireCaller();
10227
11106
  const { teamId } = await resolveActingTeam(caller, opts.team);
10228
11107
  const proj2 = await resolveProjectVia(caller, teamId, project, opts.apiversion);
10229
- const platforms = parsePlatforms(opts.platform);
11108
+ const acct = await ensureReviewerAccount(caller, teamId, proj2, { username: opts.username, password: opts.password });
11109
+ const seedLine = `${B.cli} agent --seed-for ${proj2.projectName}`;
11110
+ if (opts.json) {
11111
+ process.stdout.write(JSON.stringify({ username: acct.username, password: acct.password, tenant: acct.tenant, project: proj2.projectId, stored: true, seed: seedLine }) + "\n");
11112
+ } else {
11113
+ printReviewer(acct, proj2);
11114
+ }
11115
+ if (opts.seed) {
11116
+ const { runAgent: runAgent2 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
11117
+ await runAgent2({ team: opts.team, first: seedMessage(proj2) });
11118
+ }
11119
+ }
11120
+ function seedMessage(proj2) {
11121
+ return `Seed realistic sample data through the proxy "${proj2.projectName}" so a directory reviewer signing in as the reviewer account on tenant ${proj2.tenant ?? ""} sees a populated account: create a handful of records with each write tool, then list them back. Never mention or use credentials.`;
11122
+ }
11123
+ async function runConnector(target2, opts) {
11124
+ const caller = requireCaller();
11125
+ const { teamId } = await resolveActingTeam(caller, opts.team);
11126
+ const platforms = parsePolicy(opts.policy, opts.platform);
10230
11127
  const json = !!opts.json;
10231
11128
  const interactive2 = !!process.stdin.isTTY && !json && !opts.yes;
10232
11129
  const say = (s) => {
10233
11130
  if (!json) console.log(s);
10234
11131
  };
11132
+ let specSource = null;
11133
+ let projectName = target2;
11134
+ let version2 = opts.apiversion;
11135
+ if (looksLikeSpecArgument(target2)) {
11136
+ specSource = target2.trim();
11137
+ const created = await createFromSpec(specSource, opts, say);
11138
+ projectName = created.name;
11139
+ version2 = created.version;
11140
+ say(import_chalk28.default.green(` ${B.thing} ${import_chalk28.default.bold(created.name)} created.`));
11141
+ }
11142
+ const proj2 = await resolveProjectVia(caller, teamId, projectName, version2);
10235
11143
  const flagBody = directoryBodyFromFlags(opts);
10236
- const onlyAttest = !!opts.attest?.length && !flagBody.directory_metadata && !opts.full;
11144
+ const onlyAttest = !!opts.attest?.length && !flagBody.directory_metadata && !opts.full && !specSource;
10237
11145
  if (onlyAttest) {
10238
11146
  await patchProject(caller, proj2, { directory_attestations: flagBody.directory_attestations }, `Attest ${Object.keys(flagBody.directory_attestations).join(", ")}`);
10239
11147
  const r2 = await fetchDirectoryReport(caller, proj2, { platforms, probe: false });
@@ -10241,14 +11149,22 @@ async function runConnector(project, opts) {
10241
11149
  process.stdout.write(JSON.stringify({ attested: Object.keys(flagBody.directory_attestations), headline: r2.rendered.headline }) + "\n");
10242
11150
  return;
10243
11151
  }
10244
- say(import_chalk26.default.green(`Attested: ${Object.keys(flagBody.directory_attestations).join(", ")}.`) + import_chalk26.default.dim(` ${r2.rendered.headline}`));
11152
+ say(import_chalk28.default.green(`Attested: ${Object.keys(flagBody.directory_attestations).join(", ")}.`) + import_chalk28.default.dim(` ${r2.rendered.headline}`));
10245
11153
  return;
10246
11154
  }
11155
+ let defaults = { prompts: [] };
11156
+ const specText = specSource ? await specTextFor(specSource) : await projectSpecText(caller, proj2);
11157
+ if (specText) defaults = listingDefaultsFromSpec(specText, specSource ?? void 0);
11158
+ if (defaults.apexDomain && (!defaults.privacyUrl || !defaults.iconUrl)) {
11159
+ const found = await probeConventionalUrls(defaults.apexDomain);
11160
+ defaults.privacyUrl ??= found.privacyUrl;
11161
+ defaults.iconUrl ??= found.iconUrl;
11162
+ }
10247
11163
  const sp = json ? null : (0, import_ora10.default)(`Turning directory-ready mode on for ${proj2.projectName}\u2026`).start();
10248
11164
  await patchProject(caller, proj2, { directory_ready: true, ...flagBody }, "Enable directory-ready mode");
10249
11165
  sp?.succeed(`Directory-ready mode is on for ${proj2.projectName} v${proj2.apiVersion}.`);
10250
11166
  let r = await fetchDirectoryReport(caller, proj2, { platforms });
10251
- say(import_chalk26.default.dim(` ${r.rendered.headline}`));
11167
+ say(import_chalk28.default.dim(` ${r.rendered.headline}`));
10252
11168
  const fixes = await producer(caller, {
10253
11169
  method: "POST",
10254
11170
  path: `${projectPath(proj2)}/directory-fixes`,
@@ -10258,19 +11174,40 @@ async function runConnector(project, opts) {
10258
11174
  if (isApiError(e, 404)) return { applied: [], skipped: [{ fix: "all", reason: "server has no auto-fixes yet" }] };
10259
11175
  throw e;
10260
11176
  });
10261
- for (const f of fixes.applied ?? []) say(import_chalk26.default.green(` \u2713 fixed: ${f}`));
10262
- for (const s of fixes.skipped ?? []) say(import_chalk26.default.dim(` \u2013 ${s.fix}: ${s.reason}`));
11177
+ for (const f of fixes.applied ?? []) say(import_chalk28.default.green(` \u2713 fixed: ${f}`));
11178
+ for (const s of fixes.skipped ?? []) say(import_chalk28.default.dim(` \u2013 ${s.fix}: ${s.reason}`));
10263
11179
  if (fixes.applied?.length) r = await fetchDirectoryReport(caller, proj2, { platforms });
10264
- if (!isReady(r, platforms)) {
10265
- const asked = await promptForMissing(r, interactive2);
11180
+ const domainWasBlocking = r.report.results.some((x) => x.id === DOMAIN_RULE && !x.pass);
11181
+ const domain2 = await ensureMcpDomain(caller, proj2, r, opts, defaults, interactive2, say);
11182
+ if (domainWasBlocking && domain2.active) r = await fetchDirectoryReport(caller, proj2, { platforms });
11183
+ if (r.report.results.some((x) => x.id === TOOL_DESC_RULE && !x.pass)) {
11184
+ const changed = await fixToolDescriptions(proj2, r.report.project.environment, interactive2, say);
11185
+ if (changed) r = await fetchDirectoryReport(caller, proj2, { platforms });
11186
+ }
11187
+ const basics = await promptListingBasics(r, interactive2, defaults, opts, platforms);
11188
+ if (!isReady(r, platforms) || Object.keys(basics).length) {
11189
+ const asked = { ...basics, ...isReady(r, platforms) ? {} : await promptForMissing(r, interactive2, defaults, platforms) };
10266
11190
  const body = directoryBodyFromFlags(asked);
10267
- if (body.directory_metadata || body.directory_attestations) {
11191
+ if (body.directory_metadata || body.directory_attestations || body.openai_challenge_token) {
10268
11192
  await patchProject(caller, proj2, { ...body }, "Save listing details");
10269
11193
  r = await fetchDirectoryReport(caller, proj2, { platforms });
10270
11194
  }
10271
11195
  }
10272
- if (!isReady(r, platforms)) {
11196
+ let reviewer = null;
11197
+ if (r.report.results.some((x) => x.id === "TEST_ACCOUNT_SET" && !x.pass)) {
11198
+ reviewer = await ensureReviewerAccount(caller, teamId, proj2);
11199
+ if (!json) {
11200
+ console.log();
11201
+ printReviewer(reviewer, proj2);
11202
+ }
11203
+ r = await fetchDirectoryReport(caller, proj2, { platforms });
11204
+ }
11205
+ if (!readyExceptSnapshot(r, platforms)) {
10273
11206
  printReport(r, json);
11207
+ if (!domain2.active && domain2.hostname && !json) {
11208
+ console.error(import_chalk28.default.yellow(`
11209
+ ${domain2.hostname} is not active yet. Once the CNAME is in place, run again: ${B.cli} connector ${proj2.projectName}`));
11210
+ }
10274
11211
  exitNotReady(r, platforms, json);
10275
11212
  }
10276
11213
  r = await fetchDirectoryReport(caller, proj2, { platforms, refreshSnapshot: true, publishedUrl: r.report.project.publishedUrl ?? null });
@@ -10278,78 +11215,141 @@ async function runConnector(project, opts) {
10278
11215
  const dir = opts.out ?? process.cwd();
10279
11216
  const mdPath = import_path4.default.join(dir, `connector-packet-${proj2.projectId}.md`);
10280
11217
  const jsonPath = import_path4.default.join(dir, `connector-packet-${proj2.projectId}.json`);
10281
- import_fs5.default.writeFileSync(mdPath, packet.markdown, { mode: 384 });
10282
- import_fs5.default.writeFileSync(jsonPath, JSON.stringify(packet.json, null, 2) + "\n", { mode: 384 });
11218
+ import_fs6.default.writeFileSync(mdPath, packet.markdown, { mode: 384 });
11219
+ import_fs6.default.writeFileSync(jsonPath, JSON.stringify(packet.json, null, 2) + "\n", { mode: 384 });
10283
11220
  if (json) {
10284
- process.stdout.write(JSON.stringify({ ready: true, headline: r.rendered.headline, report: r.report, packet: { markdown: mdPath, json: jsonPath }, portals: PORTALS }) + "\n");
11221
+ 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");
10285
11222
  return;
10286
11223
  }
10287
11224
  printReport(r, false);
10288
11225
  console.log();
10289
- console.log(import_chalk26.default.green.bold(` ${r.rendered.headline}`));
10290
- console.log(` Submission packet: ${import_chalk26.default.cyan(mdPath)} and ${import_chalk26.default.cyan(jsonPath)}`);
10291
- console.log(import_chalk26.default.dim(" (the packet carries the reviewer credentials \u2014 keep it out of git)"));
10292
- console.log(` Submit it yourself: Claude ${import_chalk26.default.underline(PORTALS.claude)} \xB7 ChatGPT ${import_chalk26.default.underline(PORTALS.chatgpt)}`);
10293
- console.log(import_chalk26.default.dim(` ${r.report.disclaimer}`));
11226
+ console.log(import_chalk28.default.green.bold(` ${r.rendered.headline}`));
11227
+ console.log(` Submission packet: ${import_chalk28.default.cyan(mdPath)} and ${import_chalk28.default.cyan(jsonPath)}`);
11228
+ console.log(import_chalk28.default.dim(" (the packet carries the reviewer credentials \u2014 keep it out of git)"));
11229
+ printSubmitSteps(r, mdPath);
11230
+ console.log(import_chalk28.default.dim(` ${r.report.disclaimer}`));
11231
+ }
11232
+ var SNAPSHOT_RULE = "LISTING_SNAPSHOT_MATCH";
11233
+ var TOOL_DESC_RULE = "TOOL_DESC_NONEMPTY_SPECIFIC";
11234
+ var UTILITY_PATH = /(health|ping|status|openapi|swagger|docs|spec|whoami|version)/i;
11235
+ var SPECIFIC_MIN = 12;
11236
+ function isUtilityTool(t) {
11237
+ const p = t["x-route"]?.path ?? "";
11238
+ return UTILITY_PATH.test(p) || UTILITY_PATH.test(t.name);
11239
+ }
11240
+ function needsDescription(t) {
11241
+ const d = (t.description ?? "").trim();
11242
+ if (!d || d.length < SPECIFIC_MIN) return true;
11243
+ const m = (t["x-route"]?.method ?? "get").toUpperCase();
11244
+ const p = t["x-route"]?.path ?? "";
11245
+ return new RegExp(`^(${m}\\s+${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|${t.name})\\.?$`, "i").test(d);
11246
+ }
11247
+ function derivedDescription(t) {
11248
+ const m = (t["x-route"]?.method ?? "get").toUpperCase();
11249
+ const p = t["x-route"]?.path ?? "";
11250
+ const summary = (t.summary ?? "").replace(/\s*\(.*?\)\s*/g, " ").replace(/\s+/g, " ").trim() || t.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/^./, (c) => c.toUpperCase());
11251
+ const segments = p.split("/").filter(Boolean);
11252
+ const plural = segments.filter((s) => !s.startsWith("{")).pop()?.replace(/[-_]/g, " ") ?? "result";
11253
+ const one = segments[segments.length - 1]?.startsWith("{");
11254
+ const noun = one ? plural.replace(/s$/, "") : plural;
11255
+ 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.`;
11256
+ return `${summary.replace(/\.$/, "")}. ${m} ${p}. ${returns}`;
11257
+ }
11258
+ async function fixToolDescriptions(proj2, environment, interactive2, say) {
11259
+ const { loadCatalogue: loadCatalogue2, saveCatalogue: saveCatalogue2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
11260
+ const t = { projectId: proj2.projectId, apiVersion: proj2.apiVersion, environment: environment || "dev" };
11261
+ let cat;
11262
+ try {
11263
+ cat = await loadCatalogue2(t);
11264
+ } catch {
11265
+ return false;
11266
+ }
11267
+ const tools = cat.tools;
11268
+ const kept = tools.filter((x) => !isUtilityTool(x));
11269
+ const dropped = tools.filter(isUtilityTool).map((x) => x.name);
11270
+ const todo = kept.filter(needsDescription);
11271
+ if (!dropped.length && !todo.length) return false;
11272
+ if (dropped.length) say(import_chalk28.default.green(` \u2713 hidden from the MCP (utility endpoints, not tools): ${dropped.join(", ")}`));
11273
+ if (todo.length) {
11274
+ say(import_chalk28.default.dim(` ${todo.length} tool${todo.length === 1 ? "" : "s"} need a description (what it does and returns):`));
11275
+ const inquirer3 = interactive2 ? (await import("inquirer")).default : null;
11276
+ for (const x of todo) {
11277
+ const def = derivedDescription(x);
11278
+ let text = def;
11279
+ if (inquirer3) {
11280
+ const { value } = await inquirer3.prompt([{ type: "input", name: "value", message: `${x.name}:`, default: def }]);
11281
+ text = value?.trim() || def;
11282
+ }
11283
+ x.description = text;
11284
+ say(import_chalk28.default.green(` \u2713 ${x.name}: ${text}`));
11285
+ }
11286
+ }
11287
+ cat.tools = kept;
11288
+ await saveCatalogue2(t, cat);
11289
+ return true;
10294
11290
  }
10295
- function generatePassword(bytes = () => crypto.getRandomValues(new Uint8Array(20))) {
10296
- const alphabet = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
10297
- return Array.from(bytes(), (b) => alphabet[b % alphabet.length]).join("");
11291
+ function readyExceptSnapshot(r, platforms) {
11292
+ if (isReady(r, platforms)) return true;
11293
+ const want = platforms ?? r.report.platformsRequested ?? ["claude", "chatgpt"];
11294
+ const blocking = r.report.results.filter((x) => !x.pass && x.severity === "block" && x.platforms.some((p) => want.includes(p)));
11295
+ return blocking.length > 0 && blocking.every((x) => x.id === SNAPSHOT_RULE && /no listing snapshot/i.test(x.message ?? ""));
10298
11296
  }
10299
- async function runConnectorReviewer(project, opts) {
10300
- const caller = requireCaller();
10301
- const { teamId } = await resolveActingTeam(caller, opts.team);
10302
- const proj2 = await resolveProjectVia(caller, teamId, project, opts.apiversion);
10303
- if (!proj2.tenant) throw new Error(`${proj2.projectName} has no tenant yet; attach one first (${B.cli} tenant attach ${proj2.projectName} --tenant <slug>).`);
10304
- const base2 = `/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(proj2.tenant)}`;
10305
- const username = (opts.username ?? `reviewer-${proj2.projectId}`).trim().toLowerCase();
10306
- const password = opts.password ?? generatePassword();
10307
- const { enabled, clientId } = await producer(caller, { method: "GET", path: `${base2}/password-accounts`, summary: "Check password sign-in" }).then((o) => ({ enabled: !!o?.enabled, clientId: o?.clientId ?? o?.client_id })).catch(() => ({ enabled: false, clientId: void 0 }));
10308
- if (!enabled && clientId) {
10309
- await producer(caller, { method: "POST", path: `${base2}/app-clients/${encodeURIComponent(clientId)}/providers`, body: { type: "password" }, summary: "Enable username & password sign-in" });
11297
+ async function probeConventionalUrls(apexDomain, fetchFn = headStatus) {
11298
+ if (!apexDomain) return {};
11299
+ const out = {};
11300
+ const hosts = [`https://${apexDomain}`, `https://www.${apexDomain}`];
11301
+ const privacyPaths = ["/privacy", "/privacy-policy", "/legal/privacy", "/privacy.html"];
11302
+ const iconPaths = ["/icon-512.png", "/icon.png", "/apple-touch-icon.png", "/logo.png", "/android-chrome-512x512.png"];
11303
+ outer: for (const h of hosts) for (const p of privacyPaths) {
11304
+ if (await fetchFn(`${h}${p}`) === 200) {
11305
+ out.privacyUrl = `${h}${p}`;
11306
+ break outer;
11307
+ }
10310
11308
  }
10311
- const created = await producer(caller, {
10312
- method: "POST",
10313
- path: `${base2}/password-accounts`,
10314
- body: { username, password, purpose: "reviewer", displayName: "Directory reviewer" },
10315
- summary: `Create reviewer account ${username}`
10316
- });
10317
- const finalUser = created.account?.username ?? username;
10318
- await producer(caller, {
10319
- method: "PATCH",
10320
- path: projectPath(proj2),
10321
- body: { directory_test_account: { username: finalUser, password, tenant: proj2.tenant, ...clientId ? { app_client_id: clientId } : {} } },
10322
- summary: "Store the reviewer account for the packet"
10323
- });
10324
- const seedLine = `${B.cli} agent --seed-for ${proj2.projectName}`;
10325
- if (opts.json) {
10326
- process.stdout.write(JSON.stringify({ username: finalUser, password, tenant: proj2.tenant, project: proj2.projectId, stored: true, seed: seedLine }) + "\n");
10327
- } else {
10328
- console.log(import_chalk26.default.green(`Reviewer account ${import_chalk26.default.bold(finalUser)} on tenant ${proj2.tenant} \u2014 stored (encrypted) for the submission packet.`));
10329
- console.log();
10330
- console.log(import_chalk26.default.bold(" Password (shown once):"));
10331
- console.log(` ${import_chalk26.default.cyan(finalUser)} ${import_chalk26.default.bgWhite.black(` ${password} `)}`);
10332
- console.log();
10333
- console.log(import_chalk26.default.dim(` Seed realistic data through the proxy (billed per turn): ${seedLine}`));
11309
+ outer2: for (const h of hosts) for (const p of iconPaths) {
11310
+ if (await fetchFn(`${h}${p}`) === 200) {
11311
+ out.iconUrl = `${h}${p}`;
11312
+ break outer2;
11313
+ }
10334
11314
  }
10335
- if (opts.seed) {
10336
- const { runAgent: runAgent2 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
10337
- await runAgent2({ team: opts.team, first: seedMessage(proj2) });
11315
+ return out;
11316
+ }
11317
+ async function validatePrivacyUrl(input, fetchFn = headStatus) {
11318
+ const u = (input ?? "").trim();
11319
+ if (!u) return "A public privacy policy URL is required by both directories.";
11320
+ if (!/^https:\/\//i.test(u)) return "The privacy URL must start with https://";
11321
+ const status2 = await fetchFn(u);
11322
+ if (status2 === 200) return true;
11323
+ return status2 === 0 ? `${u} did not answer (timeout or DNS). Publish the page, then enter its URL.` : `${u} answered ${status2}; it must answer 200.`;
11324
+ }
11325
+ async function headStatus(u) {
11326
+ try {
11327
+ const res = await fetch(u, { method: "GET", redirect: "follow", signal: AbortSignal.timeout(5e3) });
11328
+ return res.status;
11329
+ } catch {
11330
+ return 0;
10338
11331
  }
10339
11332
  }
10340
- function seedMessage(proj2) {
10341
- return `Seed realistic sample data through the proxy "${proj2.projectName}" so a directory reviewer signing in as the reviewer account on tenant ${proj2.tenant ?? ""} sees a populated account: create a handful of records with each write tool, then list them back. Never mention or use credentials.`;
11333
+ function printSubmitSteps(r, mdPath) {
11334
+ const url = r.report.project.publishedUrl ?? "(the MCP URL in the packet)";
11335
+ console.log();
11336
+ console.log(import_chalk28.default.bold(" Submit (you do this part; the packet has every value to paste):"));
11337
+ console.log(` 1. Test it yourself first: add ${import_chalk28.default.cyan(url)} as a custom connector in Claude (Settings \u2192 Connectors) and in ChatGPT (Developer mode), sign in with the reviewer account, run a tool.`);
11338
+ console.log(` 2. Claude: ${import_chalk28.default.underline(PORTALS.claude)} \u2192 Submit new \u2192 MCP connector. Paste the listing, the reviewer credentials and the test steps from ${import_chalk28.default.cyan(import_path4.default.basename(mdPath))}.`);
11339
+ console.log(` 3. ChatGPT: ${import_chalk28.default.underline(PORTALS.chatgpt)} \u2192 the same packet, plus the 5 + 3 test cases it lists.`);
11340
+ console.log(` 4. Keep the reviewer account alive; a nightly re-check emails you if the listing drifts.`);
11341
+ console.log();
10342
11342
  }
10343
11343
 
10344
11344
  // src/commands/skill.ts
10345
- var import_fs8 = __toESM(require("fs"));
11345
+ var import_fs9 = __toESM(require("fs"));
10346
11346
  var import_os2 = __toESM(require("os"));
10347
11347
  var import_path7 = __toESM(require("path"));
10348
- var import_chalk30 = __toESM(require("chalk"));
11348
+ var import_chalk32 = __toESM(require("chalk"));
10349
11349
  init_brand();
10350
11350
 
10351
11351
  // src/lib/detect-backend.ts
10352
- var import_fs6 = __toESM(require("fs"));
11352
+ var import_fs7 = __toESM(require("fs"));
10353
11353
  var import_path5 = __toESM(require("path"));
10354
11354
  var import_net = __toESM(require("net"));
10355
11355
  var NODE_SERVERS = [
@@ -10381,9 +11381,9 @@ var COMMON_PORTS = [3e3, 3001, 4e3, 5e3, 8e3, 8080, 8787];
10381
11381
  var SPEC_PATHS = ["/openapi.json", "/openapi.yaml", "/docs/openapi.json", "/swagger.json", "/api/openapi.json"];
10382
11382
  function readText(file2, max = 256 * 1024) {
10383
11383
  try {
10384
- const st = import_fs6.default.statSync(file2);
11384
+ const st = import_fs7.default.statSync(file2);
10385
11385
  if (!st.isFile() || st.size > max) return null;
10386
- return import_fs6.default.readFileSync(file2, "utf8");
11386
+ return import_fs7.default.readFileSync(file2, "utf8");
10387
11387
  } catch {
10388
11388
  return null;
10389
11389
  }
@@ -10579,7 +11579,7 @@ async function liveSpec(port) {
10579
11579
  async function detectBackend(dir, opts = {}) {
10580
11580
  const out = {};
10581
11581
  try {
10582
- if (import_fs6.default.existsSync(import_path5.default.join(dir, ".demo", "state.json"))) out.isDemo = true;
11582
+ if (import_fs7.default.existsSync(import_path5.default.join(dir, ".demo", "state.json"))) out.isDemo = true;
10583
11583
  fromNode(dir, out);
10584
11584
  if (!out.framework) fromPython(dir, out);
10585
11585
  if (!out.framework) fromOthers(dir, out);
@@ -10687,7 +11687,7 @@ function staleSkillCopies(cwd = process.cwd(), home = import_os2.default.homedir
10687
11687
  for (const dir of /* @__PURE__ */ new Set([cwd, home])) {
10688
11688
  const f = import_path7.default.join(dir, SKILL_RELATIVE_PATH);
10689
11689
  try {
10690
- if (import_fs8.default.readFileSync(f, "utf8") !== current) out.push(f);
11690
+ if (import_fs9.default.readFileSync(f, "utf8") !== current) out.push(f);
10691
11691
  } catch {
10692
11692
  }
10693
11693
  }
@@ -11007,11 +12007,13 @@ the header are not owned by ana@example.com's key.
11007
12007
 
11008
12008
  ## Listing it in the Claude or ChatGPT directory
11009
12009
 
11010
- \`npx ${cli} connector <name>\` \u2014 directory-ready in one command: it checks what the directories
11011
- require (your own domain for the MCP URL, OAuth sign-in, tool names and descriptions, a
11012
- privacy page, a reviewer account), applies the safe fixes, asks for what is missing, and
11013
- writes the submission packet when every check passes. \`npx ${cli} check --directory-ready <name>\`
11014
- is the report alone. Passing the checks means the MCP meets the requirements APIblaze can
12010
+ \`npx ${cli} create --policy --target https://yourdomain.com/openapi.yaml --domain mcp.yourdomain.com\` \u2014 directory-ready
12011
+ in one command, to the letter of the Claude directory's policies (\`--policy chatgpt\` or \`--policy all\` for OpenAI's too;
12012
+ \`npx ${cli} connector <name> --policy \u2026\` runs the same flow on an existing ${B.thingShort}): it creates the ${B.thingShort} from the spec, checks what the
12013
+ directories require, applies the safe fixes, registers the MCP hostname and waits for its ONE CNAME
12014
+ (\u2192 proxy.abz.run), hides utility endpoints and fills thin tool descriptions, asks for the rest with
12015
+ every answer pre-filled from the spec and cut to the portal's limits (Enter accepts), creates the reviewer account, and writes the
12016
+ submission packet when every check passes. \`npx ${cli} check --directory-ready <name>\` is the report alone. Passing the checks means the MCP meets the requirements APIblaze can
11015
12017
  verify; listing decisions are made by Anthropic and OpenAI, and you submit the packet yourself.
11016
12018
 
11017
12019
  ## No shell? The control-plane MCP
@@ -11119,21 +12121,21 @@ async function runForPerson(found) {
11119
12121
  const P = B.product;
11120
12122
  const run2 = `npx ${B.cli}@latest`;
11121
12123
  console.log();
11122
- console.log(` ${import_chalk30.default.bold(pitchText().split(". ")[0] + ".")}`);
12124
+ console.log(` ${import_chalk32.default.bold(pitchText().split(". ")[0] + ".")}`);
11123
12125
  console.log(` ${pitchText().split(". ").slice(1).join(". ")}`);
11124
12126
  console.log();
11125
- console.log(import_chalk30.default.dim(` With an AI assistant? Tell it: "Tell me what ${B.cli} can do for me using ${run2} skills"`));
12127
+ console.log(import_chalk32.default.dim(` With an AI assistant? Tell it: "Tell me what ${B.cli} can do for me using ${run2} skills"`));
11126
12128
  console.log();
11127
12129
  const report = folderReport(found, { forPerson: true });
11128
12130
  for (const l of report.lines) console.log(` ${l.replace(/`/g, "")}`);
11129
12131
  const port = backendPort(found);
11130
12132
  if (report.ownApi && found.listening) {
11131
12133
  const args = ["dev", "--port", String(found.listening), ...found.specFile ? ["--openapi", found.specFile] : []];
11132
- if (!await ask2(` ${import_chalk30.default.bold("?")} Put your API on port ${found.listening} behind ${P} now? ${import_chalk30.default.dim("(Y/n)")} `)) {
11133
- console.log(` Any time: ${import_chalk30.default.cyan(`${run2} ${args.join(" ")}`)}`);
12134
+ if (!await ask2(` ${import_chalk32.default.bold("?")} Put your API on port ${found.listening} behind ${P} now? ${import_chalk32.default.dim("(Y/n)")} `)) {
12135
+ console.log(` Any time: ${import_chalk32.default.cyan(`${run2} ${args.join(" ")}`)}`);
11134
12136
  return;
11135
12137
  }
11136
- console.log(import_chalk30.default.dim(` $ ${run2} ${args.join(" ")}`));
12138
+ console.log(import_chalk32.default.dim(` $ ${run2} ${args.join(" ")}`));
11137
12139
  const { spawnSync: spawnSync4 } = await import("child_process");
11138
12140
  const r = spawnSync4(process.execPath, [process.argv[1], ...args], { stdio: "inherit" });
11139
12141
  process.exitCode = r.status ?? 0;
@@ -11142,18 +12144,18 @@ async function runForPerson(found) {
11142
12144
  if (report.ownApi) {
11143
12145
  const args = ["dev", "--port", String(port ?? 3e3), ...found.specFile ? ["--openapi", found.specFile] : []];
11144
12146
  const startHint = found.start ? `, once you start it (${found.start})` : ", once it runs";
11145
- if (!await ask2(` ${import_chalk30.default.bold("?")} Put it behind ${P} now? It reaches your app on port ${port ?? 3e3}${startHint}. ${import_chalk30.default.dim("(Y/n)")} `)) {
11146
- console.log(` Any time: ${import_chalk30.default.cyan(`${run2} ${args.join(" ")}`)}`);
12147
+ if (!await ask2(` ${import_chalk32.default.bold("?")} Put it behind ${P} now? It reaches your app on port ${port ?? 3e3}${startHint}. ${import_chalk32.default.dim("(Y/n)")} `)) {
12148
+ console.log(` Any time: ${import_chalk32.default.cyan(`${run2} ${args.join(" ")}`)}`);
11147
12149
  return;
11148
12150
  }
11149
- console.log(import_chalk30.default.dim(` $ ${run2} ${args.join(" ")}`));
12151
+ console.log(import_chalk32.default.dim(` $ ${run2} ${args.join(" ")}`));
11150
12152
  const { spawnSync: spawnSync4 } = await import("child_process");
11151
12153
  const r = spawnSync4(process.execPath, [process.argv[1], ...args], { stdio: "inherit" });
11152
12154
  process.exitCode = r.status ?? 0;
11153
12155
  return;
11154
12156
  }
11155
- if (!await ask2(` ${import_chalk30.default.bold("?")} Create a demo app to try out ${P}? ${import_chalk30.default.dim("(Y/n)")} `)) {
11156
- console.log(` Your own app, when it runs: ${import_chalk30.default.cyan(`${run2} dev --port <port>`)}`);
12157
+ if (!await ask2(` ${import_chalk32.default.bold("?")} Create a demo app to try out ${P}? ${import_chalk32.default.dim("(Y/n)")} `)) {
12158
+ console.log(` Your own app, when it runs: ${import_chalk32.default.cyan(`${run2} dev --port <port>`)}`);
11157
12159
  return;
11158
12160
  }
11159
12161
  const { runDemo: runDemo2 } = await Promise.resolve().then(() => (init_demo(), demo_exports));
@@ -11186,29 +12188,29 @@ async function runSkill(opts = {}) {
11186
12188
  function installSkill(opts) {
11187
12189
  const md = skillMarkdown();
11188
12190
  const target2 = import_path7.default.resolve(process.cwd(), SKILL_RELATIVE_PATH);
11189
- if (import_fs8.default.existsSync(target2) && !opts.force) {
11190
- console.error(import_chalk30.default.yellow(`${SKILL_RELATIVE_PATH} already exists \u2014 re-run with --force to overwrite it.`));
12191
+ if (import_fs9.default.existsSync(target2) && !opts.force) {
12192
+ console.error(import_chalk32.default.yellow(`${SKILL_RELATIVE_PATH} already exists \u2014 re-run with --force to overwrite it.`));
11191
12193
  process.exit(1);
11192
12194
  }
11193
- import_fs8.default.mkdirSync(import_path7.default.dirname(target2), { recursive: true });
11194
- import_fs8.default.writeFileSync(target2, md, "utf8");
11195
- console.log(`${import_chalk30.default.green("\u2713")} Installed the ${import_chalk30.default.bold(SKILL_NAME)} skill: ${import_chalk30.default.bold(target2)}`);
11196
- console.log(import_chalk30.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.`));
12195
+ import_fs9.default.mkdirSync(import_path7.default.dirname(target2), { recursive: true });
12196
+ import_fs9.default.writeFileSync(target2, md, "utf8");
12197
+ console.log(`${import_chalk32.default.green("\u2713")} Installed the ${import_chalk32.default.bold(SKILL_NAME)} skill: ${import_chalk32.default.bold(target2)}`);
12198
+ console.log(import_chalk32.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.`));
11197
12199
  }
11198
12200
 
11199
12201
  // src/commands/integration.ts
11200
- var import_chalk31 = __toESM(require("chalk"));
12202
+ var import_chalk33 = __toESM(require("chalk"));
11201
12203
  var import_ora12 = __toESM(require("ora"));
11202
12204
  init_caller();
11203
12205
  init_api();
11204
12206
  init_brand();
11205
12207
  function file(f, indent2 = " ") {
11206
- console.log(` ${import_chalk31.default.cyan(f.file)}`);
12208
+ console.log(` ${import_chalk33.default.cyan(f.file)}`);
11207
12209
  for (const line of f.code.split("\n")) console.log(`${indent2}${line}`);
11208
12210
  console.log();
11209
12211
  }
11210
12212
  function bullets(lines, indent2 = " ") {
11211
- for (const l of lines) console.log(`${indent2}${import_chalk31.default.dim("\u2022")} ${l}`);
12213
+ for (const l of lines) console.log(`${indent2}${import_chalk33.default.dim("\u2022")} ${l}`);
11212
12214
  }
11213
12215
  async function runIntegration(name, opts = {}) {
11214
12216
  const stackRaw = (opts.stack ?? "nextjs").trim().toLowerCase();
@@ -11233,44 +12235,44 @@ async function runIntegration(name, opts = {}) {
11233
12235
  return;
11234
12236
  }
11235
12237
  console.log();
11236
- console.log(import_chalk31.default.bold(`Integration kit \u2014 ${kit.project} (${kit.stack})`));
11237
- console.log(import_chalk31.default.dim(` Edit only the frontend and the frontend-server. Your backend changes in one way: it trusts the ${B.product} identity headers.`));
12238
+ console.log(import_chalk33.default.bold(`Integration kit \u2014 ${kit.project} (${kit.stack})`));
12239
+ console.log(import_chalk33.default.dim(` Edit only the frontend and the frontend-server. Your backend changes in one way: it trusts the ${B.product} identity headers.`));
11238
12240
  console.log();
11239
- console.log(` ${import_chalk31.default.dim("Proxy: ")} ${import_chalk31.default.bold(kit.urls.proxy)}`);
11240
- console.log(` ${import_chalk31.default.dim("MCP: ")} ${kit.urls.mcp.join(import_chalk31.default.dim(" \xB7 "))}`);
11241
- console.log(` ${import_chalk31.default.dim("Portal: ")} ${kit.urls.portal}`);
11242
- console.log(` ${import_chalk31.default.dim("Spec: ")} ${kit.urls.openapi}`);
12241
+ console.log(` ${import_chalk33.default.dim("Proxy: ")} ${import_chalk33.default.bold(kit.urls.proxy)}`);
12242
+ console.log(` ${import_chalk33.default.dim("MCP: ")} ${kit.urls.mcp.join(import_chalk33.default.dim(" \xB7 "))}`);
12243
+ console.log(` ${import_chalk33.default.dim("Portal: ")} ${kit.urls.portal}`);
12244
+ console.log(` ${import_chalk33.default.dim("Spec: ")} ${kit.urls.openapi}`);
11243
12245
  const w = kit["1_frontend_widgets"];
11244
12246
  console.log();
11245
- console.log(import_chalk31.default.bold("1. Frontend \u2014 the widgets (keys + chat)"));
11246
- console.log(import_chalk31.default.dim(` ${w.note}`));
12247
+ console.log(import_chalk33.default.bold("1. Frontend \u2014 the widgets (keys + chat)"));
12248
+ console.log(import_chalk33.default.dim(` ${w.note}`));
11247
12249
  console.log();
11248
12250
  file(w.frontend);
11249
12251
  for (const r of w.server_routes) file(r);
11250
12252
  const c = kit["2_frontend_server_call_through"];
11251
- console.log(import_chalk31.default.bold("2. Frontend-server \u2014 call the proxy with the server key and say who is calling"));
12253
+ console.log(import_chalk33.default.bold("2. Frontend-server \u2014 call the proxy with the server key and say who is calling"));
11252
12254
  console.log();
11253
12255
  file(c);
11254
12256
  const t = kit["3_backend_trust_contract"];
11255
- console.log(import_chalk31.default.bold("3. Backend \u2014 trust the identity headers, and nothing else"));
11256
- for (const [h, why] of Object.entries(t.headers_you_receive)) console.log(` ${import_chalk31.default.cyan(h)} \u2014 ${why}`);
12257
+ console.log(import_chalk33.default.bold("3. Backend \u2014 trust the identity headers, and nothing else"));
12258
+ for (const [h, why] of Object.entries(t.headers_you_receive)) console.log(` ${import_chalk33.default.cyan(h)} \u2014 ${why}`);
11257
12259
  console.log();
11258
- console.log(` ${import_chalk31.default.dim("How to verify:")}`);
12260
+ console.log(` ${import_chalk33.default.dim("How to verify:")}`);
11259
12261
  bullets(t.how_to_verify);
11260
12262
  console.log();
11261
- console.log(` ${import_chalk31.default.dim("Never:")}`);
12263
+ console.log(` ${import_chalk33.default.dim("Never:")}`);
11262
12264
  bullets(t.what_not_to_do);
11263
12265
  console.log();
11264
- console.log(import_chalk31.default.bold("4. Env vars (server only)"));
11265
- for (const [k, v] of Object.entries(kit["4_env_vars"])) console.log(` ${import_chalk31.default.cyan(k)} ${import_chalk31.default.dim("=")} ${v}`);
12266
+ console.log(import_chalk33.default.bold("4. Env vars (server only)"));
12267
+ for (const [k, v] of Object.entries(kit["4_env_vars"])) console.log(` ${import_chalk33.default.cyan(k)} ${import_chalk33.default.dim("=")} ${v}`);
11266
12268
  const o = kit["5_what_stays_open"];
11267
12269
  console.log();
11268
- console.log(import_chalk31.default.bold("5. What stays open"));
11269
- console.log(` ${import_chalk31.default.dim("Protected by a rule:")} ${o.protected.length ? o.protected.join(", ") : import_chalk31.default.dim("nothing")}`);
11270
- console.log(` ${import_chalk31.default.dim("Open to any identified caller:")} ${o.open.length ? o.open.join(", ") : import_chalk31.default.dim("nothing")}`);
11271
- console.log(import_chalk31.default.dim(` ${o.note}`));
12270
+ console.log(import_chalk33.default.bold("5. What stays open"));
12271
+ console.log(` ${import_chalk33.default.dim("Protected by a rule:")} ${o.protected.length ? o.protected.join(", ") : import_chalk33.default.dim("nothing")}`);
12272
+ console.log(` ${import_chalk33.default.dim("Open to any identified caller:")} ${o.open.length ? o.open.join(", ") : import_chalk33.default.dim("nothing")}`);
12273
+ console.log(import_chalk33.default.dim(` ${o.note}`));
11272
12274
  console.log();
11273
- console.log(import_chalk31.default.dim(` Raw JSON: npx ${B.cli} integration ${kit.project} --stack ${kit.stack} --json`));
12275
+ console.log(import_chalk33.default.dim(` Raw JSON: npx ${B.cli} integration ${kit.project} --stack ${kit.stack} --json`));
11274
12276
  console.log();
11275
12277
  }
11276
12278
 
@@ -11278,14 +12280,14 @@ async function runIntegration(name, opts = {}) {
11278
12280
  init_claim();
11279
12281
 
11280
12282
  // src/commands/logout.ts
11281
- var import_chalk32 = __toESM(require("chalk"));
12283
+ var import_chalk34 = __toESM(require("chalk"));
11282
12284
  init_auth();
11283
12285
  init_consumer_auth();
11284
12286
  async function runLogout(opts = {}) {
11285
12287
  const creds = loadCredentials();
11286
12288
  const consumer2 = loadConsumer();
11287
12289
  if (!creds && !consumer2) {
11288
- console.log(import_chalk32.default.yellow("You were not logged in."));
12290
+ console.log(import_chalk34.default.yellow("You were not logged in."));
11289
12291
  return;
11290
12292
  }
11291
12293
  let target2;
@@ -11312,96 +12314,96 @@ async function runLogout(opts = {}) {
11312
12314
  }
11313
12315
  if (target2 === "producer" || target2 === "all") {
11314
12316
  clearCredentials();
11315
- console.log(import_chalk32.default.green(`\u2714 Logged out of API Producer${creds?.githubHandle ? ` (${creds.githubHandle})` : ""}.`));
12317
+ console.log(import_chalk34.default.green(`\u2714 Logged out of API Producer${creds?.githubHandle ? ` (${creds.githubHandle})` : ""}.`));
11316
12318
  }
11317
12319
  if (target2 === "consumer" || target2 === "all") {
11318
12320
  clearConsumer();
11319
- console.log(import_chalk32.default.green(`\u2714 Logged out of API Consumer${consumer2?.tenant ? ` (${consumer2.tenant})` : ""}.`));
12321
+ console.log(import_chalk34.default.green(`\u2714 Logged out of API Consumer${consumer2?.tenant ? ` (${consumer2.tenant})` : ""}.`));
11320
12322
  }
11321
12323
  }
11322
12324
 
11323
12325
  // src/commands/flush.ts
11324
- var import_chalk33 = __toESM(require("chalk"));
11325
- var fs16 = __toESM(require("fs"));
12326
+ var import_chalk35 = __toESM(require("chalk"));
12327
+ var fs17 = __toESM(require("fs"));
11326
12328
  var path14 = __toESM(require("path"));
11327
12329
  init_auth();
11328
12330
  init_external_mcp();
11329
12331
  async function runFlush(opts) {
11330
12332
  const { stopDemos: stopDemos2 } = await Promise.resolve().then(() => (init_demo(), demo_exports));
11331
12333
  const demos = await stopDemos2().catch(() => 0);
11332
- if (demos) console.log(import_chalk33.default.dim(` Stopped ${demos} running demo(s).`));
12334
+ if (demos) console.log(import_chalk35.default.dim(` Stopped ${demos} running demo(s).`));
11333
12335
  const dir = getApiblazeDir();
11334
- const entries2 = fs16.existsSync(dir) ? fs16.readdirSync(dir) : [];
12336
+ const entries2 = fs17.existsSync(dir) ? fs17.readdirSync(dir) : [];
11335
12337
  if (!entries2.length) {
11336
- console.log(import_chalk33.default.dim("Nothing to flush \u2014 no local apiblaze config found."));
12338
+ console.log(import_chalk35.default.dim("Nothing to flush \u2014 no local apiblaze config found."));
11337
12339
  return;
11338
12340
  }
11339
- console.log(import_chalk33.default.yellow("This removes ALL local apiblaze state from this machine:"));
11340
- console.log(import_chalk33.default.dim(" \u2022 login credentials & anonymous-workspace keys"));
11341
- console.log(import_chalk33.default.dim(" \u2022 saved apichats \u2014 their DP API keys and transcripts"));
11342
- console.log(import_chalk33.default.dim(" \u2022 your BYO LLM key and any cached secrets"));
11343
- console.log(import_chalk33.default.dim(` (${entries2.length} item(s) in ${dir})`));
12341
+ console.log(import_chalk35.default.yellow("This removes ALL local apiblaze state from this machine:"));
12342
+ console.log(import_chalk35.default.dim(" \u2022 login credentials & anonymous-workspace keys"));
12343
+ console.log(import_chalk35.default.dim(" \u2022 saved apichats \u2014 their DP API keys and transcripts"));
12344
+ console.log(import_chalk35.default.dim(" \u2022 your BYO LLM key and any cached secrets"));
12345
+ console.log(import_chalk35.default.dim(` (${entries2.length} item(s) in ${dir})`));
11344
12346
  const footprint = apiblazeMcpFootprint();
11345
12347
  const mdFiles = ["claude", "codex"].map((k) => ({ k, file: agentInstructionFile(k) })).filter(({ file: file2 }) => {
11346
12348
  try {
11347
- return fs16.readFileSync(file2, "utf-8").includes("<!-- apiblaze:mcp:");
12349
+ return fs17.readFileSync(file2, "utf-8").includes("<!-- apiblaze:mcp:");
11348
12350
  } catch {
11349
12351
  return false;
11350
12352
  }
11351
12353
  });
11352
12354
  if (footprint.claude.length || footprint.codex.length) {
11353
- console.log(import_chalk33.default.dim(` \u2022 MCP servers installed in other tools: ${[...footprint.claude, ...footprint.codex].join(", ")}`));
12355
+ console.log(import_chalk35.default.dim(` \u2022 MCP servers installed in other tools: ${[...footprint.claude, ...footprint.codex].join(", ")}`));
11354
12356
  }
11355
12357
  if (mdFiles.length) {
11356
- console.log(import_chalk33.default.dim(` \u2022 the "use this MCP" blocks in ${mdFiles.map((m) => path14.basename(m.file)).join(" and ")} (here)`));
12358
+ console.log(import_chalk35.default.dim(` \u2022 the "use this MCP" blocks in ${mdFiles.map((m) => path14.basename(m.file)).join(" and ")} (here)`));
11357
12359
  }
11358
- console.log(import_chalk33.default.dim(" It does NOT delete server-side proxies/keys \u2014 those GC or are revoked separately."));
12360
+ console.log(import_chalk35.default.dim(" It does NOT delete server-side proxies/keys \u2014 those GC or are revoked separately."));
11359
12361
  if (!opts.yes) {
11360
12362
  const { default: inquirer3 } = await import("inquirer");
11361
12363
  const { ok } = await inquirer3.prompt([
11362
12364
  { type: "confirm", name: "ok", message: "Delete all local state?", default: false }
11363
12365
  ]);
11364
12366
  if (!ok) {
11365
- console.log(import_chalk33.default.dim("Cancelled \u2014 nothing removed."));
12367
+ console.log(import_chalk35.default.dim("Cancelled \u2014 nothing removed."));
11366
12368
  return;
11367
12369
  }
11368
12370
  }
11369
12371
  let removed = 0;
11370
12372
  for (const e of entries2) {
11371
12373
  try {
11372
- fs16.rmSync(path14.join(dir, e), { recursive: true, force: true });
12374
+ fs17.rmSync(path14.join(dir, e), { recursive: true, force: true });
11373
12375
  removed++;
11374
12376
  } catch (err) {
11375
- console.log(import_chalk33.default.red(` \u2717 could not remove ${e}: ${err instanceof Error ? err.message : "error"}`));
12377
+ console.log(import_chalk35.default.red(` \u2717 could not remove ${e}: ${err instanceof Error ? err.message : "error"}`));
11376
12378
  }
11377
12379
  }
11378
12380
  for (const name of footprint.claude) {
11379
- console.log(removeClaudeServer(name) ? ` ${import_chalk33.default.dim("\u2022")} removed MCP ${import_chalk33.default.bold(name)} from Claude CLI` : import_chalk33.default.yellow(` \u2717 could not remove MCP ${name} from Claude CLI`));
12381
+ console.log(removeClaudeServer(name) ? ` ${import_chalk35.default.dim("\u2022")} removed MCP ${import_chalk35.default.bold(name)} from Claude CLI` : import_chalk35.default.yellow(` \u2717 could not remove MCP ${name} from Claude CLI`));
11380
12382
  }
11381
12383
  for (const name of footprint.codex) {
11382
- console.log(removeCodexServer(name) ? ` ${import_chalk33.default.dim("\u2022")} removed MCP ${import_chalk33.default.bold(name)} from Codex` : import_chalk33.default.yellow(` \u2717 could not remove MCP ${name} from Codex`));
12384
+ console.log(removeCodexServer(name) ? ` ${import_chalk35.default.dim("\u2022")} removed MCP ${import_chalk35.default.bold(name)} from Codex` : import_chalk35.default.yellow(` \u2717 could not remove MCP ${name} from Codex`));
11383
12385
  }
11384
12386
  for (const { k, file: file2 } of mdFiles) {
11385
12387
  let text = "";
11386
12388
  try {
11387
- text = fs16.readFileSync(file2, "utf-8");
12389
+ text = fs17.readFileSync(file2, "utf-8");
11388
12390
  } catch {
11389
12391
  continue;
11390
12392
  }
11391
12393
  const names = [...text.matchAll(/<!-- apiblaze:mcp:([^\s>]+) -->/g)].map((m) => m[1]);
11392
12394
  let cleared = 0;
11393
12395
  for (const n of new Set(names)) if (removeAgentInstruction(k, n)) cleared++;
11394
- if (cleared) console.log(` ${import_chalk33.default.dim("\u2022")} removed ${cleared} APIblaze block(s) from ${import_chalk33.default.bold(path14.basename(file2))}`);
12396
+ if (cleared) console.log(` ${import_chalk35.default.dim("\u2022")} removed ${cleared} APIblaze block(s) from ${import_chalk35.default.bold(path14.basename(file2))}`);
11395
12397
  }
11396
- console.log(`${import_chalk33.default.green("\u2714")} Flushed \u2014 removed ${removed} item(s). You're fully logged out and clean.`);
11397
- console.log(import_chalk33.default.dim(" Next `apiblaze create` starts a brand-new workspace with nothing carried over."));
12398
+ console.log(`${import_chalk35.default.green("\u2714")} Flushed \u2014 removed ${removed} item(s). You're fully logged out and clean.`);
12399
+ console.log(import_chalk35.default.dim(" Next `apiblaze create` starts a brand-new workspace with nothing carried over."));
11398
12400
  }
11399
12401
 
11400
12402
  // src/index.ts
11401
12403
  init_demo();
11402
12404
 
11403
12405
  // src/commands/whoami.ts
11404
- var import_chalk34 = __toESM(require("chalk"));
12406
+ var import_chalk36 = __toESM(require("chalk"));
11405
12407
  init_auth();
11406
12408
  init_consumer_auth();
11407
12409
  init_anon_cred();
@@ -11425,54 +12427,54 @@ async function runWhoami(opts = {}) {
11425
12427
  }, null, 2));
11426
12428
  return;
11427
12429
  }
11428
- console.log(import_chalk34.default.bold("API Producer"));
12430
+ console.log(import_chalk36.default.bold("API Producer"));
11429
12431
  if (!creds) {
11430
- console.log(import_chalk34.default.dim(" Not logged in. Run `apiblaze login`."));
12432
+ console.log(import_chalk36.default.dim(" Not logged in. Run `apiblaze login`."));
11431
12433
  } else {
11432
12434
  const who = creds.githubHandle ?? creds.email ?? creds.apiblazeUserId ?? "unknown";
11433
- console.log(` ${import_chalk34.default.cyan("Signed in as")} ${import_chalk34.default.bold(who)}`);
12435
+ console.log(` ${import_chalk36.default.cyan("Signed in as")} ${import_chalk36.default.bold(who)}`);
11434
12436
  if (creds.email && creds.email !== who) console.log(` Email: ${creds.email}`);
11435
- if (creds.teamName || creds.teamId) console.log(` Team: ${import_chalk34.default.bold(creds.teamName ?? creds.teamId)}`);
11436
- if (creds.activeTenant) console.log(` Tenant scope: ${import_chalk34.default.bold(creds.activeTenant)} ${import_chalk34.default.dim("(clear: `apiblaze tenant use --clear`)")}`);
11437
- if (Date.now() >= creds.expiresAt) console.log(import_chalk34.default.yellow(" \u26A0 Session expired \u2014 run `apiblaze login`."));
12437
+ if (creds.teamName || creds.teamId) console.log(` Team: ${import_chalk36.default.bold(creds.teamName ?? creds.teamId)}`);
12438
+ if (creds.activeTenant) console.log(` Tenant scope: ${import_chalk36.default.bold(creds.activeTenant)} ${import_chalk36.default.dim("(clear: `apiblaze tenant use --clear`)")}`);
12439
+ if (Date.now() >= creds.expiresAt) console.log(import_chalk36.default.yellow(" \u26A0 Session expired \u2014 run `apiblaze login`."));
11438
12440
  }
11439
12441
  if (anon) {
11440
12442
  const masked = `${anon.cp_key.slice(0, 11)}\u2026${anon.cp_key.slice(-4)}`;
11441
- console.log(import_chalk34.default.bold("\nAnonymous workspace") + import_chalk34.default.dim(" (created without an account \u2014 unclaimed)"));
11442
- console.log(` Team: ${import_chalk34.default.bold(anon.team_id)}`);
11443
- console.log(` CP key: ${import_chalk34.default.bold(masked)} ${import_chalk34.default.dim("controls the proxies you created anonymously")}`);
12443
+ console.log(import_chalk36.default.bold("\nAnonymous workspace") + import_chalk36.default.dim(" (created without an account \u2014 unclaimed)"));
12444
+ console.log(` Team: ${import_chalk36.default.bold(anon.team_id)}`);
12445
+ console.log(` CP key: ${import_chalk36.default.bold(masked)} ${import_chalk36.default.dim("controls the proxies you created anonymously")}`);
11444
12446
  console.log(
11445
- import_chalk34.default.dim(` Keep it: ${import_chalk34.default.cyan("apiblaze login")} then ${import_chalk34.default.cyan("apiblaze claim")}`) + (anon.claim_code ? import_chalk34.default.dim(` \xB7 from another machine: ${import_chalk34.default.cyan(`apiblaze claim ${anon.claim_code}`)}`) : "")
12447
+ import_chalk36.default.dim(` Keep it: ${import_chalk36.default.cyan("apiblaze login")} then ${import_chalk36.default.cyan("apiblaze claim")}`) + (anon.claim_code ? import_chalk36.default.dim(` \xB7 from another machine: ${import_chalk36.default.cyan(`apiblaze claim ${anon.claim_code}`)}`) : "")
11446
12448
  );
11447
- if (creds) console.log(import_chalk34.default.dim(" (you're logged in \u2014 `apiblaze claim` folds this workspace into your account)"));
12449
+ if (creds) console.log(import_chalk36.default.dim(" (you're logged in \u2014 `apiblaze claim` folds this workspace into your account)"));
11448
12450
  }
11449
- console.log(import_chalk34.default.bold("\nAPI Consumer"));
12451
+ console.log(import_chalk36.default.bold("\nAPI Consumer"));
11450
12452
  if (!consumer2) {
11451
- console.log(import_chalk34.default.dim(" Not logged in. Run `apiblaze consumer login`."));
12453
+ console.log(import_chalk36.default.dim(" Not logged in. Run `apiblaze consumer login`."));
11452
12454
  } else {
11453
- console.log(` ${import_chalk34.default.cyan("Signed in as")} ${import_chalk34.default.bold(consumer2.email ?? consumer2.tenant)}`);
11454
- console.log(` Tenant: ${import_chalk34.default.bold(consumer2.tenant)}`);
11455
- if (Date.now() >= consumer2.expiresAt) console.log(import_chalk34.default.yellow(" \u26A0 Token expired \u2014 it will refresh on next use, or re-run `apiblaze consumer login`."));
12455
+ console.log(` ${import_chalk36.default.cyan("Signed in as")} ${import_chalk36.default.bold(consumer2.email ?? consumer2.tenant)}`);
12456
+ console.log(` Tenant: ${import_chalk36.default.bold(consumer2.tenant)}`);
12457
+ if (Date.now() >= consumer2.expiresAt) console.log(import_chalk36.default.yellow(" \u26A0 Token expired \u2014 it will refresh on next use, or re-run `apiblaze consumer login`."));
11456
12458
  }
11457
12459
  }
11458
12460
 
11459
12461
  // src/commands/team.ts
11460
- var import_chalk35 = __toESM(require("chalk"));
12462
+ var import_chalk37 = __toESM(require("chalk"));
11461
12463
  var import_ora13 = __toESM(require("ora"));
11462
12464
  init_auth();
11463
12465
  init_api();
11464
12466
  init_admin();
11465
12467
  function fail5(message, hint) {
11466
- console.error(import_chalk35.default.red(`Error: ${message}`));
11467
- if (hint) console.error(import_chalk35.default.dim(hint));
12468
+ console.error(import_chalk37.default.red(`Error: ${message}`));
12469
+ if (hint) console.error(import_chalk37.default.dim(hint));
11468
12470
  process.exit(1);
11469
12471
  }
11470
12472
  function activate(creds, team) {
11471
12473
  const next = { ...creds, teamId: team.teamId, teamName: team.name };
11472
12474
  if (creds.teamId !== team.teamId) delete next.activeTenant;
11473
12475
  saveCredentials(next);
11474
- console.log(import_chalk35.default.green(`
11475
- \u2714 Active team: ${import_chalk35.default.bold(team.name)}`));
12476
+ console.log(import_chalk37.default.green(`
12477
+ \u2714 Active team: ${import_chalk37.default.bold(team.name)}`));
11476
12478
  }
11477
12479
  async function promptName(message) {
11478
12480
  const { default: inquirer3 } = await import("inquirer");
@@ -11480,7 +12482,7 @@ async function promptName(message) {
11480
12482
  const { name } = await inquirer3.prompt([{ type: "input", name: "name", message }]);
11481
12483
  const trimmed = (name ?? "").trim();
11482
12484
  if (trimmed.length >= 2) return trimmed;
11483
- console.log(import_chalk35.default.yellow(" Team name must be at least 2 characters.\n"));
12485
+ console.log(import_chalk37.default.yellow(" Team name must be at least 2 characters.\n"));
11484
12486
  }
11485
12487
  }
11486
12488
  async function createAndActivate(creds, name) {
@@ -11502,7 +12504,7 @@ async function createAndActivate(creds, name) {
11502
12504
  }
11503
12505
  spinner.stop();
11504
12506
  if (!created?.team_id) fail5("The server created no team id.");
11505
- console.log(`${import_chalk35.default.green("\u2714")} Team created: ${import_chalk35.default.bold(created.name ?? name)} ${import_chalk35.default.dim(`(${created.team_id})`)}`);
12507
+ console.log(`${import_chalk37.default.green("\u2714")} Team created: ${import_chalk37.default.bold(created.name ?? name)} ${import_chalk37.default.dim(`(${created.team_id})`)}`);
11506
12508
  activate(creds, { teamId: created.team_id, name: created.name ?? name });
11507
12509
  }
11508
12510
  async function runTeam(arg, opts = {}) {
@@ -11559,11 +12561,11 @@ async function runTeam(arg, opts = {}) {
11559
12561
  default: creds.teamId,
11560
12562
  choices: [
11561
12563
  ...teams.map((t) => ({
11562
- name: t.teamId === creds.teamId ? `${t.name} ${import_chalk35.default.dim("(current)")}` : t.name,
12564
+ name: t.teamId === creds.teamId ? `${t.name} ${import_chalk37.default.dim("(current)")}` : t.name,
11563
12565
  value: t.teamId
11564
12566
  })),
11565
12567
  new inquirer3.Separator(),
11566
- { name: import_chalk35.default.green("\uFF0B Create a new team"), value: CREATE }
12568
+ { name: import_chalk37.default.green("\uFF0B Create a new team"), value: CREATE }
11567
12569
  ]
11568
12570
  }]);
11569
12571
  if (picked === CREATE) {
@@ -11578,7 +12580,7 @@ async function runTeam(arg, opts = {}) {
11578
12580
  init_allow_oauth_registration();
11579
12581
 
11580
12582
  // src/commands/docs.ts
11581
- var import_chalk36 = __toESM(require("chalk"));
12583
+ var import_chalk38 = __toESM(require("chalk"));
11582
12584
  init_apichat();
11583
12585
  var CONTROL_PLANE_TARGET = "https://api.mcp.apiblaze.com/openapi.json";
11584
12586
  function opTenantForTeam3(teamId) {
@@ -11589,168 +12591,66 @@ async function runDocsWeb() {
11589
12591
  const { openRegistrationAuthorization: openRegistrationAuthorization2 } = await Promise.resolve().then(() => (init_allow_oauth_registration(), allow_oauth_registration_exports));
11590
12592
  const creds = loadCredentials2();
11591
12593
  if (!creds?.teamId) {
11592
- console.error(import_chalk36.default.red("Error: Connecting a web chatbot needs an account."));
11593
- console.error(import_chalk36.default.dim("Run `apiblaze login` first (free)."));
12594
+ console.error(import_chalk38.default.red("Error: Connecting a web chatbot needs an account."));
12595
+ console.error(import_chalk38.default.dim("Run `apiblaze login` first (free)."));
11594
12596
  process.exit(1);
11595
12597
  }
11596
12598
  const tenant2 = opTenantForTeam3(creds.teamId);
11597
12599
  const url = `https://externalauth-${tenant2}.mcp.apiblaze.com/1.0.0/prod`;
11598
- console.log(import_chalk36.default.bold("\n Your personal APIblaze MCP URL \u2014 paste it into claude.ai or ChatGPT:\n"));
11599
- console.log(" " + import_chalk36.default.bgCyan.black.bold(` ${url} `));
11600
- console.log(import_chalk36.default.dim(`
12600
+ console.log(import_chalk38.default.bold("\n Your personal APIblaze MCP URL \u2014 paste it into claude.ai or ChatGPT:\n"));
12601
+ console.log(" " + import_chalk38.default.bgCyan.black.bold(` ${url} `));
12602
+ console.log(import_chalk38.default.dim(`
11601
12603
  It is scoped to your team (${creds.teamName ?? creds.teamId}) \u2014 nobody else's account is reachable through it.`));
11602
12604
  const ok = await openRegistrationAuthorization2(creds.teamId, tenant2, 900);
11603
- console.log(ok ? `
11604
- ${import_chalk36.default.green("\u2714")} Connection pre-authorized (single use, 15 min) \u2014 connect now and it won't ask you to approve the app.` : import_chalk36.default.yellow("\n Could not pre-authorize the connection. If the sign-in says the app needs verifying, run:\n apiblaze allowoauthregistration --apiblaze"));
11605
- console.log(import_chalk36.default.dim("\n Re-run this command any time to authorize another client."));
11606
- }
11607
- async function runDocs(opts) {
11608
- console.log("");
11609
- console.log(import_chalk36.default.bgYellow.black.bold(" This is not a docs search \u2014 the chatbot can CHANGE your account. "));
11610
- console.log(import_chalk36.default.yellow(` It connects ${import_chalk36.default.bold("Claude Code")} or ${import_chalk36.default.bold("Codex")} to the real APIblaze API, acting as you:`));
11611
- console.log(import_chalk36.default.yellow(" it can create proxies, add logins, invite teammates and point domains for real."));
11612
- console.log(import_chalk36.default.yellow(" Destructive actions (deleting proxies, teams or products) are deliberately not exposed."));
11613
- console.log(import_chalk36.default.dim(' Read-only questions ("what proxies do I have?") are of course fine.'));
11614
- console.log("");
11615
- if (!opts.yes && !opts.prompt && process.stdin.isTTY) {
11616
- const { default: inquirer3 } = await import("inquirer");
11617
- const { ok } = await inquirer3.prompt([{
11618
- type: "confirm",
11619
- name: "ok",
11620
- default: true,
11621
- message: "Continue, knowing the chatbot can act on your account?"
11622
- }]);
11623
- if (!ok) {
11624
- console.log(import_chalk36.default.dim("\n Nothing connected. The written docs are at https://www.apiblaze.com/docs"));
11625
- return;
11626
- }
11627
- }
11628
- if (opts.web) {
11629
- await runDocsWeb();
11630
- return;
11631
- }
11632
- const forwarded = {
11633
- target: CONTROL_PLANE_TARGET,
11634
- prompt: opts.prompt,
11635
- installMcp: opts.installMcp,
11636
- removeMcp: opts.removeMcp,
11637
- verbose: opts.verbose,
11638
- // `--remove-mcp` needs the project name; the control-plane proxy is `externalauth`.
11639
- ...opts.removeMcp ? { project: "externalauth" } : {}
11640
- };
11641
- await runApichat(forwarded);
11642
- }
11643
-
11644
- // src/commands/authz.ts
11645
- var import_chalk39 = __toESM(require("chalk"));
11646
- init_auth();
11647
- init_api();
11648
-
11649
- // src/lib/agent-chat.ts
11650
- var import_readline = __toESM(require("readline"));
11651
- var import_chalk37 = __toESM(require("chalk"));
11652
- init_problem();
11653
- init_brand();
11654
- init_api();
11655
- init_admin();
11656
- async function runAgentChatRepl(opts) {
11657
- const { title, subtitle, endpoint, buildBody, seedPrompt, summarizeProposal, commands } = opts;
11658
- console.log("\n" + import_chalk37.default.cyan.bold(title));
11659
- if (subtitle) console.log(import_chalk37.default.dim(subtitle));
11660
- const messages = [];
11661
- const ctx = { proposal: null, lastData: null, log: (s) => console.log(s) };
11662
- const cmdByName = new Map(commands.map((c) => [c.name, c]));
11663
- const footer = () => {
11664
- const parts = [
11665
- import_chalk37.default.dim("type to chat/refine"),
11666
- ...commands.map((c) => import_chalk37.default.dim(`/${c.name} ${c.describe}`)),
11667
- import_chalk37.default.dim("/show"),
11668
- import_chalk37.default.dim("/exit when done"),
11669
- import_chalk37.default.dim("/drop discard changes")
11670
- ];
11671
- return " " + import_chalk37.default.dim("[ ") + parts.join(import_chalk37.default.dim(" \xB7 ")) + import_chalk37.default.dim(" ]");
11672
- };
11673
- async function turn(content) {
11674
- messages.push({ role: "user", content });
11675
- process.stdout.write(import_chalk37.default.dim(" \u2026thinking\n"));
11676
- const { status: status2, data } = await agentCall(endpoint, "POST", { messages, ...buildBody(messages) });
11677
- if (status2 === 402 || status2 === 503 || parseProblem(data)) {
11678
- await reportRefusal(status2, data, { cli: B.cli });
11679
- console.log("");
11680
- messages.pop();
11681
- return;
11682
- }
11683
- if (status2 >= 400 || !data) {
11684
- console.log(import_chalk37.default.red(` Error (${status2}): ${(data && data.error) ?? "request failed"}
11685
- `));
11686
- messages.pop();
11687
- return;
11688
- }
11689
- ctx.lastData = data;
11690
- const reply = data.reply ?? data.message ?? "(no reply)";
11691
- messages.push({ role: "assistant", content: reply });
11692
- console.log("\n" + import_chalk37.default.cyan("agent \u203A ") + reply + "\n");
11693
- if (data.proposal) {
11694
- ctx.proposal = data.proposal;
11695
- const summary = summarizeProposal?.(data);
11696
- if (summary) console.log(import_chalk37.default.yellow(" " + summary));
11697
- const applies = commands.filter((c) => c.needsProposal);
11698
- const primary = applies[applies.length - 1];
11699
- if (primary) {
11700
- console.log(import_chalk37.default.bold.green(" \u279C Next: type ") + import_chalk37.default.bold.cyan(`/${primary.name}`) + import_chalk37.default.bold.green(" to apply this, then ") + import_chalk37.default.bold.cyan("/exit") + import_chalk37.default.bold.green(" to leave."));
11701
- const rest = applies.slice(0, -1).map((c) => `/${c.name} = ${c.describe}`).join(" \xB7 ");
11702
- console.log(import_chalk37.default.dim(` (or keep chatting to refine${rest ? ` \xB7 ${rest}` : ""} \xB7 /show = inspect)`));
11703
- }
11704
- console.log();
11705
- }
11706
- const line = formatBilling(data.billing);
11707
- if (line) {
11708
- const llm2 = data.llm;
11709
- console.log(import_chalk37.default.dim(` ${line}${llm2?.model ? ` \xB7 ${llm2.model}` : ""}`));
12605
+ console.log(ok ? `
12606
+ ${import_chalk38.default.green("\u2714")} Connection pre-authorized (single use, 15 min) \u2014 connect now and it won't ask you to approve the app.` : import_chalk38.default.yellow("\n Could not pre-authorize the connection. If the sign-in says the app needs verifying, run:\n apiblaze allowoauthregistration --apiblaze"));
12607
+ console.log(import_chalk38.default.dim("\n Re-run this command any time to authorize another client."));
12608
+ }
12609
+ async function runDocs(opts) {
12610
+ console.log("");
12611
+ console.log(import_chalk38.default.bgYellow.black.bold(" This is not a docs search \u2014 the chatbot can CHANGE your account. "));
12612
+ console.log(import_chalk38.default.yellow(` It connects ${import_chalk38.default.bold("Claude Code")} or ${import_chalk38.default.bold("Codex")} to the real APIblaze API, acting as you:`));
12613
+ console.log(import_chalk38.default.yellow(" it can create proxies, add logins, invite teammates and point domains for real."));
12614
+ console.log(import_chalk38.default.yellow(" Destructive actions (deleting proxies, teams or products) are deliberately not exposed."));
12615
+ console.log(import_chalk38.default.dim(' Read-only questions ("what proxies do I have?") are of course fine.'));
12616
+ console.log("");
12617
+ if (!opts.yes && !opts.prompt && process.stdin.isTTY) {
12618
+ const { default: inquirer3 } = await import("inquirer");
12619
+ const { ok } = await inquirer3.prompt([{
12620
+ type: "confirm",
12621
+ name: "ok",
12622
+ default: true,
12623
+ message: "Continue, knowing the chatbot can act on your account?"
12624
+ }]);
12625
+ if (!ok) {
12626
+ console.log(import_chalk38.default.dim("\n Nothing connected. The written docs are at https://www.apiblaze.com/docs"));
12627
+ return;
11710
12628
  }
11711
12629
  }
11712
- const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
11713
- const ask3 = (q) => new Promise((res) => rl.question(q, res));
11714
- await turn(seedPrompt);
11715
- for (; ; ) {
11716
- console.log(footer());
11717
- const line = (await ask3(import_chalk37.default.green("you \u203A "))).trim();
11718
- if (!line) continue;
11719
- const lower = line.toLowerCase();
11720
- if (line === "/exit" || line === "/quit" || lower === "exit" || lower === "quit" || lower === "q") {
11721
- console.log(import_chalk37.default.dim(" Bye \u2014 anything you enabled or published stays in place.\n"));
11722
- break;
11723
- }
11724
- if (line === "/drop") {
11725
- console.log(import_chalk37.default.dim(" Dropped the pending proposal \u2014 rules you already enabled/published are untouched.\n"));
11726
- break;
11727
- }
11728
- if (line === "/show") {
11729
- if (!ctx.proposal) console.log(import_chalk37.default.dim(" No proposal yet \u2014 keep chatting until the agent proposes one.\n"));
11730
- else console.log("\n" + JSON.stringify(ctx.proposal, null, 2) + "\n");
11731
- continue;
11732
- }
11733
- if (line.startsWith("/")) {
11734
- const cmd = cmdByName.get(line.slice(1));
11735
- if (!cmd) {
11736
- console.log(import_chalk37.default.red(` Unknown command "${line}". Try one of: ${commands.map((c) => "/" + c.name).join(", ")}, /show, /drop
11737
- `));
11738
- continue;
11739
- }
11740
- if (cmd.needsProposal && !ctx.proposal) {
11741
- console.log(import_chalk37.default.yellow(" No proposal yet \u2014 keep chatting until the agent generates one, then try again.\n"));
11742
- continue;
11743
- }
11744
- await cmd.run(ctx);
11745
- continue;
11746
- }
11747
- await turn(line);
12630
+ if (opts.web) {
12631
+ await runDocsWeb();
12632
+ return;
11748
12633
  }
11749
- rl.close();
12634
+ const forwarded = {
12635
+ target: CONTROL_PLANE_TARGET,
12636
+ prompt: opts.prompt,
12637
+ installMcp: opts.installMcp,
12638
+ removeMcp: opts.removeMcp,
12639
+ verbose: opts.verbose,
12640
+ // `--remove-mcp` needs the project name; the control-plane proxy is `externalauth`.
12641
+ ...opts.removeMcp ? { project: "externalauth" } : {}
12642
+ };
12643
+ await runApichat(forwarded);
11750
12644
  }
11751
12645
 
12646
+ // src/commands/authz.ts
12647
+ var import_chalk40 = __toESM(require("chalk"));
12648
+ init_auth();
12649
+ init_api();
12650
+ init_agent_chat();
12651
+
11752
12652
  // src/lib/authz-apply.ts
11753
- var import_chalk38 = __toESM(require("chalk"));
12653
+ var import_chalk39 = __toESM(require("chalk"));
11754
12654
  init_api();
11755
12655
  async function gatherSampleIds(projectId, apiVersion, cap2 = 50) {
11756
12656
  try {
@@ -11775,10 +12675,10 @@ async function gatherSampleIds(projectId, apiVersion, cap2 = 50) {
11775
12675
  }
11776
12676
  function maybeWarnNoTraffic(total, projectArg) {
11777
12677
  if (total >= 3) return;
11778
- console.log(import_chalk38.default.yellow(total === 0 ? " \u26A0 No captured traffic for this API yet." : ` \u26A0 Only ${total} captured request(s) so far.`));
11779
- console.log(import_chalk38.default.dim(" The authorization agent designs much better models & rules from REAL requests"));
11780
- console.log(import_chalk38.default.dim(" (it sees who is allowed vs denied). Send some through your proxy first \u2014"));
11781
- console.log(import_chalk38.default.dim(` curl a few endpoints, or `) + import_chalk38.default.cyan("npx apiblaze dev") + import_chalk38.default.dim(` \u2014 then re-run.
12678
+ console.log(import_chalk39.default.yellow(total === 0 ? " \u26A0 No captured traffic for this API yet." : ` \u26A0 Only ${total} captured request(s) so far.`));
12679
+ console.log(import_chalk39.default.dim(" The authorization agent designs much better models & rules from REAL requests"));
12680
+ console.log(import_chalk39.default.dim(" (it sees who is allowed vs denied). Send some through your proxy first \u2014"));
12681
+ console.log(import_chalk39.default.dim(` curl a few endpoints, or `) + import_chalk39.default.cyan("npx apiblaze dev") + import_chalk39.default.dim(` \u2014 then re-run.
11782
12682
  `));
11783
12683
  }
11784
12684
  async function applyProposal(opts) {
@@ -11787,12 +12687,12 @@ async function applyProposal(opts) {
11787
12687
  {
11788
12688
  const m = await agentCall(`/projects/${projectId}/${apiVersion}/policies/model?tenantId=${encodeURIComponent(tenant2)}`, "POST", proposal.model);
11789
12689
  if (m.status === 404) {
11790
- log4(import_chalk38.default.red(` Project ${projectId} ${apiVersion} not found on the authorization service \u2014 check \`apiblaze projects\`.
12690
+ log4(import_chalk39.default.red(` Project ${projectId} ${apiVersion} not found on the authorization service \u2014 check \`apiblaze projects\`.
11791
12691
  `));
11792
12692
  return { ok: 0, fail: 0, enabled: false, aborted: true };
11793
12693
  }
11794
12694
  if (m.status >= 400) {
11795
- log4(import_chalk38.default.red(` Model save failed (${m.status}): ${m.data?.error ?? ""}
12695
+ log4(import_chalk39.default.red(` Model save failed (${m.status}): ${m.data?.error ?? ""}
11796
12696
  `));
11797
12697
  return { ok: 0, fail: 0, enabled: false, aborted: true };
11798
12698
  }
@@ -11824,17 +12724,17 @@ async function applyProposal(opts) {
11824
12724
  }
11825
12725
  }
11826
12726
  if (fail7 > 0) {
11827
- log4(import_chalk38.default.red(` ${fail7} of ${proposal.routes.length} route rule(s) could not be saved:`));
11828
- for (const f of failed) log4(import_chalk38.default.red(` ${f}`));
12727
+ log4(import_chalk39.default.red(` ${fail7} of ${proposal.routes.length} route rule(s) could not be saved:`));
12728
+ for (const f of failed) log4(import_chalk39.default.red(` ${f}`));
11829
12729
  if (enable) {
11830
- log4(import_chalk38.default.red(" Enforcement NOT turned on \u2014 a route without a saved rule is left open by the gateway. Fix the rule(s) above and re-run with --enforce.\n"));
12730
+ log4(import_chalk39.default.red(" Enforcement NOT turned on \u2014 a route without a saved rule is left open by the gateway. Fix the rule(s) above and re-run with --enforce.\n"));
11831
12731
  }
11832
12732
  return { ok, fail: fail7, enabled: false, aborted: true, failed };
11833
12733
  }
11834
12734
  if (enable) {
11835
12735
  const e = await agentCall(`/projects/${projectId}/${apiVersion}`, "PATCH", { authorization: { enforce_authorization: true }, tenant: tenant2 });
11836
12736
  if (e.status >= 400) {
11837
- log4(import_chalk38.default.red(` Enforced ${ok} route(s) but turning on the project-level switch failed (${e.status}). Toggle "Enforce Authorization" on in the dashboard.
12737
+ log4(import_chalk39.default.red(` Enforced ${ok} route(s) but turning on the project-level switch failed (${e.status}). Toggle "Enforce Authorization" on in the dashboard.
11838
12738
  `));
11839
12739
  return { ok, fail: fail7, enabled: false, failed };
11840
12740
  }
@@ -11859,19 +12759,19 @@ async function runAuthz(projectArg, apiVersionArg) {
11859
12759
  const proposal = ctx.proposal;
11860
12760
  const r = await applyProposal({ projectId, apiVersion, tenant: tenant2, proposal, enable, log: ctx.log });
11861
12761
  if (r.aborted) {
11862
- ctx.log(import_chalk39.default.red(enable ? ` Enforcement NOT on \u2014 ${r.fail || "the"} route rule(s) failed. Fix them and /enable again.
12762
+ ctx.log(import_chalk40.default.red(enable ? ` Enforcement NOT on \u2014 ${r.fail || "the"} route rule(s) failed. Fix them and /enable again.
11863
12763
  ` : ` Not published \u2014 ${r.fail || "the"} route rule(s) failed. Fix them and /publish again.
11864
12764
  `));
11865
12765
  return;
11866
12766
  }
11867
12767
  if (enable && r.enabled) {
11868
- ctx.log(import_chalk39.default.green(` \u2713 Enforcement ON \u2014 ${r.ok} route(s) + project switch on.`));
11869
- ctx.log(import_chalk39.default.dim(" Route config takes up to ~90 s to reach every edge. Undo any time: ") + import_chalk39.default.cyan(`apiblaze config ${projectArg} authorization.enforce_authorization false`));
11870
- ctx.log(import_chalk39.default.dim(" Done here? Type /exit to leave the chat (your rule stays on).\n"));
12768
+ ctx.log(import_chalk40.default.green(` \u2713 Enforcement ON \u2014 ${r.ok} route(s) + project switch on.`));
12769
+ ctx.log(import_chalk40.default.dim(" Route config takes up to ~90 s to reach every edge. Undo any time: ") + import_chalk40.default.cyan(`apiblaze config ${projectArg} authorization.enforce_authorization false`));
12770
+ ctx.log(import_chalk40.default.dim(" Done here? Type /exit to leave the chat (your rule stays on).\n"));
11871
12771
  } else if (enable) {
11872
- ctx.log(import_chalk39.default.red(" Enforcement NOT on \u2014 the routes are set but the project switch did not flip (see above).\n"));
12772
+ ctx.log(import_chalk40.default.red(" Enforcement NOT on \u2014 the routes are set but the project switch did not flip (see above).\n"));
11873
12773
  } else {
11874
- ctx.log(import_chalk39.default.green(` \u2713 Published (shadow): model + ${r.ok} route(s). Review, then /enable.
12774
+ ctx.log(import_chalk40.default.green(` \u2713 Published (shadow): model + ${r.ok} route(s). Review, then /enable.
11875
12775
  `));
11876
12776
  }
11877
12777
  }
@@ -11898,7 +12798,7 @@ async function runAuthz(projectArg, apiVersionArg) {
11898
12798
  }
11899
12799
 
11900
12800
  // src/commands/rule.ts
11901
- var import_chalk40 = __toESM(require("chalk"));
12801
+ var import_chalk41 = __toESM(require("chalk"));
11902
12802
  var import_ora14 = __toESM(require("ora"));
11903
12803
  init_auth();
11904
12804
  init_api();
@@ -11938,12 +12838,12 @@ async function runRule(projectArg, ruleText, opts) {
11938
12838
  const proposal = data?.proposal;
11939
12839
  if (!proposal || !Array.isArray(proposal.routes) || proposal.routes.length === 0) {
11940
12840
  spinner.fail("The agent couldn\u2019t turn that into a rule in one shot.");
11941
- if (data?.reply) console.log(import_chalk40.default.dim("\n " + String(data.reply).slice(0, 500) + "\n"));
11942
- console.log(import_chalk40.default.dim(" Try an interactive session for anything nuanced: ") + import_chalk40.default.cyan(`${B.cli} agent authz ${projectArg}`));
12841
+ if (data?.reply) console.log(import_chalk41.default.dim("\n " + String(data.reply).slice(0, 500) + "\n"));
12842
+ console.log(import_chalk41.default.dim(" Try an interactive session for anything nuanced: ") + import_chalk41.default.cyan(`${B.cli} agent authz ${projectArg}`));
11943
12843
  return;
11944
12844
  }
11945
12845
  spinner.succeed(`Rule designed \u2014 ${proposal.routes.length} route(s) affected.`);
11946
- if (data?.reply) console.log(import_chalk40.default.dim(" " + String(data.reply).slice(0, 240)));
12846
+ if (data?.reply) console.log(import_chalk41.default.dim(" " + String(data.reply).slice(0, 240)));
11947
12847
  await shadowThenEnforce(ctx, proposal, opts.enforce ? "yes" : "no", `${B.cli} rule ${projectArg} "${rule}" --enforce`);
11948
12848
  }
11949
12849
  async function proposeFromSpecBranch(ctx, opts) {
@@ -11958,417 +12858,143 @@ async function proposeFromSpecBranch(ctx, opts) {
11958
12858
  const families = first.data?.families ?? [];
11959
12859
  const protectable = families.filter((f) => !f.skipped_reason);
11960
12860
  if (!protectable.length) {
11961
- spinner.stop();
11962
- console.log(import_chalk40.default.dim(` Nothing to lock: the spec has no resource with a POST collection plus item routes${families.length ? ` (${families.length} candidate(s) not protectable)` : ""}.`));
11963
- console.log(import_chalk40.default.dim(" Describe the rule instead: ") + import_chalk40.default.cyan(`${B.cli} rule ${projectArg} "users see only their own rows"`));
11964
- return;
11965
- }
11966
- spinner.stop();
11967
- const chosen = interactive2 ? await pickFamilies(families) : protectable.map((f) => f.key);
11968
- if (!chosen.length) {
11969
- console.log(import_chalk40.default.dim(" Nothing selected \u2014 no rules written."));
11970
- return;
11971
- }
11972
- let data = first.data;
11973
- if (chosen.length !== protectable.length) {
11974
- const second = await agentCall(`/projects/${projectId}/${apiVersion}/authz/propose`, "POST", { families: chosen });
11975
- if (second.status >= 400) {
11976
- console.log(import_chalk40.default.red(` Could not build the rules for that selection (${second.status}): ${second.data?.error ?? ""}`));
11977
- return;
11978
- }
11979
- data = second.data;
11980
- }
11981
- const proposal = data?.proposal;
11982
- if (!proposal || !Array.isArray(proposal.routes) || !proposal.routes.length) {
11983
- console.log(import_chalk40.default.red(" The authorization service returned no routes for that selection."));
11984
- return;
11985
- }
11986
- const sim = data?.simulation;
11987
- if (sim?.blocked_without_backfill) {
11988
- console.log(import_chalk40.default.yellow(` \u26A0 ${sim.blocked_without_backfill} request(s) that succeed today would be denied once enforced \u2014`));
11989
- console.log(import_chalk40.default.yellow(" rows created before the rules have no recorded owner. Backfill them or accept it."));
11990
- }
11991
- console.log(import_chalk40.default.green(` \u2713 ${chosen.map((k) => k.replace(/^\//, "")).join(" + ")} \u2014 ${proposal.routes.length} route rule(s).`));
11992
- let enforce = opts.enforce ? "yes" : interactive2 ? "ask" : "no";
11993
- await shadowThenEnforce(ctx, proposal, enforce, `${B.cli} rule ${projectArg} --enforce`);
11994
- }
11995
- async function shadowThenEnforce(ctx, proposal, enforce, enforceCmd) {
11996
- const { projectId, apiVersion, tenant: tenant2, projectArg } = ctx;
11997
- const pub = await applyProposal({ projectId, apiVersion, tenant: tenant2, proposal, enable: false });
11998
- if (pub.aborted) {
11999
- console.log(import_chalk40.default.red(` Nothing saved${pub.ok ? ` beyond ${pub.ok} route(s)` : ""} \u2014 fix the error above and re-run.`));
12000
- return;
12001
- }
12002
- console.log(import_chalk40.default.green(` \u2713 Saved in shadow mode \u2014 model + ${pub.ok} route(s). Nothing is blocked yet.`));
12003
- if (enforce === "ask") {
12004
- const { default: inquirer3 } = await import("inquirer");
12005
- const { on } = await inquirer3.prompt([{ type: "confirm", name: "on", message: "Turn enforcement on now?", default: true }]);
12006
- enforce = on ? "yes" : "no";
12007
- }
12008
- if (enforce !== "yes") {
12009
- console.log(import_chalk40.default.dim(" Review it (shadow logs), then enforce: ") + import_chalk40.default.cyan(enforceCmd));
12010
- return;
12011
- }
12012
- const enf = await applyProposal({ projectId, apiVersion, tenant: tenant2, proposal, enable: true });
12013
- if (enf.aborted) {
12014
- console.log(import_chalk40.default.red(` Enforcement NOT on \u2014 ${enf.fail} route rule(s) failed (listed above). Rules stay in shadow mode.`));
12015
- return;
12016
- }
12017
- if (!enf.enabled) {
12018
- console.log(import_chalk40.default.red(" Enforcement NOT on \u2014 the routes are set but the project switch did not flip (see above)."));
12019
- return;
12020
- }
12021
- console.log(import_chalk40.default.green(` \u2713 Enforcement ON \u2014 ${enf.ok} route(s) + project switch on.`));
12022
- console.log(import_chalk40.default.dim(" Route config takes up to ~90 s to reach every edge."));
12023
- console.log(import_chalk40.default.dim(" Testing a locked route? Use the sign-in door, or send X-End-User-Id / a key minted with --for \u2014"));
12024
- console.log(import_chalk40.default.dim(" a bare key has no person to check."));
12025
- console.log(import_chalk40.default.dim(` Undo: `) + import_chalk40.default.cyan(`${B.cli} config ${projectArg} authorization.enforce_authorization false`));
12026
- }
12027
-
12028
- // src/commands/openapi.ts
12029
- var import_chalk41 = __toESM(require("chalk"));
12030
- init_auth();
12031
- init_api();
12032
- async function runOpenapi(projectArg, apiVersionArg) {
12033
- const creds = loadCredentials();
12034
- if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
12035
- const projects = await getProjects(creds.teamId ?? "");
12036
- const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
12037
- if (!match) throw new Error(`Project "${projectArg}" not found in your active team. Run \`apiblaze projects\`.`);
12038
- const projectId = match.projectId;
12039
- const apiVersion = apiVersionArg || match.apiVersion;
12040
- console.log(import_chalk41.default.dim("Gathering captured traffic samples\u2026"));
12041
- const routesRes = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes`, "GET");
12042
- if (routesRes.status >= 400) {
12043
- console.log(import_chalk41.default.red(`Could not list traffic (${routesRes.status}): ${routesRes.data?.error ?? ""}`));
12044
- return;
12045
- }
12046
- const routes = routesRes.data?.routes ?? (Array.isArray(routesRes.data) ? routesRes.data : []);
12047
- const hashes = routes.map((r) => r.route_hash).filter((h) => !!h);
12048
- const ids = /* @__PURE__ */ new Set();
12049
- for (const h of hashes) {
12050
- const s = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes/${encodeURIComponent(h)}/samples`, "GET");
12051
- const arr = s.data?.samples ?? [];
12052
- for (const x of arr) if (x.sample_id) ids.add(x.sample_id);
12053
- }
12054
- const sampleIds = [...ids];
12055
- if (sampleIds.length === 0) {
12056
- console.log(import_chalk41.default.yellow("No captured traffic samples found. Hit the dev environment of this proxy to capture some traces, then retry."));
12057
- return;
12058
- }
12059
- async function publish(ctx) {
12060
- const patch = ctx.proposal.patch;
12061
- if (!Array.isArray(patch) || patch.length === 0) {
12062
- ctx.log(import_chalk41.default.green(" Nothing to publish \u2014 the spec already covers the observed traffic.\n"));
12063
- return;
12064
- }
12065
- const specSource = ctx.lastData?.spec_source ?? "unknown";
12066
- const endpoint = specSource === "github" ? "open-pr" : "publish-openapi";
12067
- const pub = await agentCall(`/projects/${projectId}/${apiVersion}/samples/${endpoint}`, "POST", { patch, sample_ids_used: sampleIds });
12068
- if (pub.status >= 400) {
12069
- ctx.log(import_chalk41.default.red(` Publish failed (${pub.status}): ${pub.data?.error ?? ""}
12070
- `));
12071
- return;
12072
- }
12073
- const prUrl = pub.data?.pr_url;
12074
- ctx.log(import_chalk41.default.green(prUrl ? ` \u2713 Pull request opened: ${prUrl}
12075
- ` : " \u2713 Published updated OpenAPI spec.\n"));
12076
- }
12077
- await runAgentChatRepl({
12078
- title: `API-docs assistant \u2014 ${projectId} ${apiVersion}`,
12079
- subtitle: `${sampleIds.length} captured sample(s) \xB7 discuss what to document, then make it official.`,
12080
- endpoint: `/projects/${projectId}/${apiVersion}/openapi/chat`,
12081
- buildBody: () => ({ included_sample_ids: sampleIds }),
12082
- seedPrompt: "Compare the captured samples against the current spec and tell me what's missing or wrong. List the changes you'd make \u2014 don't produce a patch yet.",
12083
- summarizeProposal: (data) => {
12084
- const patch = data.proposal.patch;
12085
- const n = Array.isArray(patch) ? patch.length : 0;
12086
- return n === 0 ? "The spec already covers the observed traffic \u2014 nothing to publish." : `Patch ready: ${n} additive change(s) to the OpenAPI spec.`;
12087
- },
12088
- commands: [
12089
- { name: "publish", describe: "publish / open PR", needsProposal: true, run: publish }
12090
- ]
12091
- });
12092
- }
12093
-
12094
- // src/commands/mcp.ts
12095
- var import_fs9 = __toESM(require("fs"));
12096
- var import_chalk42 = __toESM(require("chalk"));
12097
- init_brand();
12098
- init_auth();
12099
- init_api();
12100
- async function runMcp(projectArg, apiVersionArg, opts) {
12101
- const creds = loadCredentials();
12102
- if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
12103
- const projects = await getProjects(creds.teamId ?? "");
12104
- const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
12105
- if (!match) throw new Error(`Project "${projectArg}" not found in your active team. Run \`apiblaze projects\`.`);
12106
- const projectId = match.projectId;
12107
- const apiVersion = apiVersionArg || match.apiVersion;
12108
- const environment = opts.environment || "prod";
12109
- const mcpHost2 = match.tenant ? `${projectId}-${match.tenant}` : projectId;
12110
- async function publish(ctx) {
12111
- const spec2 = ctx.proposal;
12112
- const pub = await agentCall(`/projects/${projectId}/${apiVersion}/mcp/spec`, "PUT", { environment, spec: spec2 });
12113
- if (pub.status >= 400) {
12114
- ctx.log(import_chalk42.default.red(` Publish failed (${pub.status}): ${pub.data?.error ?? ""}
12115
- `));
12116
- return;
12117
- }
12118
- ctx.log(import_chalk42.default.green(` \u2713 Published MCP server \u2014 ${mcpHost2}.mcp.apiblaze.com/${apiVersion}/${environment}.
12119
- `));
12120
- }
12121
- await runAgentChatRepl({
12122
- title: `MCP builder \u2014 ${projectId} ${apiVersion} (${environment})`,
12123
- subtitle: "discuss which routes to expose as tools, then make the catalogue official.",
12124
- endpoint: `/projects/${projectId}/${apiVersion}/mcp/chat`,
12125
- buildBody: () => ({ environment, included_sample_ids: [] }),
12126
- seedPrompt: "Look at this API's routes and recommend which should become MCP tools, with good names and descriptions. Don't finalize yet \u2014 explain first.",
12127
- summarizeProposal: (data) => {
12128
- const spec2 = data.proposal;
12129
- const tools = Array.isArray(spec2.tools) ? spec2.tools : [];
12130
- const names = tools.slice(0, 12).map((t) => t.name ?? "(unnamed)").join(", ");
12131
- return `Catalogue ready: ${tools.length} tool(s)${names ? ` \u2014 ${names}${tools.length > 12 ? ", \u2026" : ""}` : ""}.`;
12132
- },
12133
- commands: [
12134
- { name: "publish", describe: `to ${mcpHost2}.mcp.apiblaze.com`, needsProposal: true, run: publish }
12135
- ]
12136
- });
12137
- }
12138
- var PROMPT_NAME_RE = /^[a-z0-9_-]{1,64}$/;
12139
- function derivedBehavior(method) {
12140
- const m = (method ?? "").toUpperCase();
12141
- const destructive = m === "DELETE";
12142
- return {
12143
- readOnly: m === "GET" || m === "HEAD" || m === "OPTIONS",
12144
- destructive,
12145
- idempotent: m === "PUT" || m === "DELETE",
12146
- confirm: destructive,
12147
- alwaysAsync: false
12148
- };
12149
- }
12150
- function effectiveBehavior(method, o) {
12151
- const d = derivedBehavior(method);
12152
- const readOnly = o?.readOnly ?? d.readOnly;
12153
- const destructive = readOnly ? false : o?.destructive ?? d.destructive;
12154
- return {
12155
- readOnly,
12156
- destructive,
12157
- idempotent: o?.idempotent ?? d.idempotent,
12158
- confirm: o?.confirm ?? destructive,
12159
- alwaysAsync: o?.alwaysAsync ?? false
12160
- };
12161
- }
12162
- function uiResourceUri(projectId, toolName) {
12163
- return `ui://${projectId}/${toolName}`;
12164
- }
12165
- function normalizePrompt(name, raw) {
12166
- if (!PROMPT_NAME_RE.test(name)) throw new Error(`Prompt name "${name}" must match ${PROMPT_NAME_RE} (lowercase letters, digits, _ and -).`);
12167
- const p = raw ?? {};
12168
- if (typeof p !== "object" || Array.isArray(p)) throw new Error("The prompt file must hold one JSON object.");
12169
- let messages = p.messages;
12170
- if (!Array.isArray(messages) && typeof p.text === "string") messages = [{ role: "user", text: p.text }];
12171
- if (!Array.isArray(messages) || messages.length === 0) throw new Error('The prompt needs "messages": [{ "role": "user", "text": "\u2026" }] (or a "text" shorthand).');
12172
- for (const m of messages) {
12173
- if (!m || m.role !== "user" && m.role !== "assistant" || typeof m.text !== "string" || !m.text) {
12174
- throw new Error('Each message needs role "user" | "assistant" and a non-empty "text".');
12861
+ spinner.stop();
12862
+ console.log(import_chalk41.default.dim(` Nothing to lock: the spec has no resource with a POST collection plus item routes${families.length ? ` (${families.length} candidate(s) not protectable)` : ""}.`));
12863
+ console.log(import_chalk41.default.dim(" Describe the rule instead: ") + import_chalk41.default.cyan(`${B.cli} rule ${projectArg} "users see only their own rows"`));
12864
+ return;
12865
+ }
12866
+ spinner.stop();
12867
+ const chosen = interactive2 ? await pickFamilies(families) : protectable.map((f) => f.key);
12868
+ if (!chosen.length) {
12869
+ console.log(import_chalk41.default.dim(" Nothing selected \u2014 no rules written."));
12870
+ return;
12871
+ }
12872
+ let data = first.data;
12873
+ if (chosen.length !== protectable.length) {
12874
+ const second = await agentCall(`/projects/${projectId}/${apiVersion}/authz/propose`, "POST", { families: chosen });
12875
+ if (second.status >= 400) {
12876
+ console.log(import_chalk41.default.red(` Could not build the rules for that selection (${second.status}): ${second.data?.error ?? ""}`));
12877
+ return;
12175
12878
  }
12879
+ data = second.data;
12176
12880
  }
12177
- if (p.arguments !== void 0 && (!Array.isArray(p.arguments) || p.arguments.some((a) => !a || typeof a.name !== "string"))) {
12178
- throw new Error('"arguments" must be [{ "name": "\u2026", "description"?: "\u2026", "required"?: true }].');
12881
+ const proposal = data?.proposal;
12882
+ if (!proposal || !Array.isArray(proposal.routes) || !proposal.routes.length) {
12883
+ console.log(import_chalk41.default.red(" The authorization service returned no routes for that selection."));
12884
+ return;
12179
12885
  }
12180
- return {
12181
- name,
12182
- ...typeof p.title === "string" ? { title: p.title } : {},
12183
- ...typeof p.description === "string" ? { description: p.description } : {},
12184
- ...Array.isArray(p.arguments) ? { arguments: p.arguments } : {},
12185
- messages
12186
- };
12187
- }
12188
- function withPrompt(cat, prompt) {
12189
- const prompts = [...cat.prompts ?? []];
12190
- const i = prompts.findIndex((p) => p.name === prompt.name);
12191
- if (i >= 0) prompts[i] = prompt;
12192
- else prompts.push(prompt);
12193
- return { ...cat, prompts };
12194
- }
12195
- function withoutPrompt(cat, name) {
12196
- const prompts = cat.prompts ?? [];
12197
- if (!prompts.some((p) => p.name === name)) throw new Error(`No prompt named "${name}". See \`${B.cli} mcp prompts list\`.`);
12198
- return { ...cat, prompts: prompts.filter((p) => p.name !== name) };
12199
- }
12200
- function requireTool(cat, tool) {
12201
- if (!cat.tools.some((t) => t.name === tool)) {
12202
- const names = cat.tools.slice(0, 10).map((t) => t.name).join(", ");
12203
- throw new Error(`No tool named "${tool}" in the published catalogue${names ? ` (tools: ${names}${cat.tools.length > 10 ? ", \u2026" : ""})` : ""}.`);
12886
+ const sim = data?.simulation;
12887
+ if (sim?.blocked_without_backfill) {
12888
+ console.log(import_chalk41.default.yellow(` \u26A0 ${sim.blocked_without_backfill} request(s) that succeed today would be denied once enforced \u2014`));
12889
+ console.log(import_chalk41.default.yellow(" rows created before the rules have no recorded owner. Backfill them or accept it."));
12204
12890
  }
12891
+ console.log(import_chalk41.default.green(` \u2713 ${chosen.map((k) => k.replace(/^\//, "")).join(" + ")} \u2014 ${proposal.routes.length} route rule(s).`));
12892
+ let enforce = opts.enforce ? "yes" : interactive2 ? "ask" : "no";
12893
+ await shadowThenEnforce(ctx, proposal, enforce, `${B.cli} rule ${projectArg} --enforce`);
12205
12894
  }
12206
- function withOverride(cat, tool, flags) {
12207
- requireTool(cat, tool);
12208
- const patch = {};
12209
- for (const k of ["readOnly", "destructive", "idempotent", "confirm", "alwaysAsync"]) {
12210
- if (typeof flags[k] === "boolean") patch[k] = flags[k];
12895
+ async function shadowThenEnforce(ctx, proposal, enforce, enforceCmd) {
12896
+ const { projectId, apiVersion, tenant: tenant2, projectArg } = ctx;
12897
+ const pub = await applyProposal({ projectId, apiVersion, tenant: tenant2, proposal, enable: false });
12898
+ if (pub.aborted) {
12899
+ console.log(import_chalk41.default.red(` Nothing saved${pub.ok ? ` beyond ${pub.ok} route(s)` : ""} \u2014 fix the error above and re-run.`));
12900
+ return;
12211
12901
  }
12212
- if (typeof flags.title === "string") {
12213
- const t = flags.title.trim().slice(0, 120);
12214
- if (!t) throw new Error('--title needs some text (a short human label, e.g. "List pets").');
12215
- patch.title = t;
12902
+ console.log(import_chalk41.default.green(` \u2713 Saved in shadow mode \u2014 model + ${pub.ok} route(s). Nothing is blocked yet.`));
12903
+ if (enforce === "ask") {
12904
+ const { default: inquirer3 } = await import("inquirer");
12905
+ const { on } = await inquirer3.prompt([{ type: "confirm", name: "on", message: "Turn enforcement on now?", default: true }]);
12906
+ enforce = on ? "yes" : "no";
12216
12907
  }
12217
- if (Object.keys(patch).length === 0) {
12218
- throw new Error("Nothing to set. Pass --title, --read-only, --destructive, --idempotent, --confirm, --always-async (or their --no-\u2026 forms).");
12908
+ if (enforce !== "yes") {
12909
+ console.log(import_chalk41.default.dim(" Review it (shadow logs), then enforce: ") + import_chalk41.default.cyan(enforceCmd));
12910
+ return;
12219
12911
  }
12220
- return { ...cat, overrides: { ...cat.overrides ?? {}, [tool]: { ...cat.overrides?.[tool] ?? {}, ...patch } } };
12221
- }
12222
- function withoutOverride(cat, tool) {
12223
- requireTool(cat, tool);
12224
- const overrides = { ...cat.overrides ?? {} };
12225
- delete overrides[tool];
12226
- return { ...cat, overrides };
12227
- }
12228
- function flagsLine(b) {
12229
- const f = [];
12230
- if (b.readOnly) f.push("read-only");
12231
- if (b.idempotent) f.push("idempotent");
12232
- if (b.destructive) f.push("destructive");
12233
- if (b.confirm) f.push("asks to confirm");
12234
- if (b.alwaysAsync) f.push("always async");
12235
- return f.length ? f.join(", ") : "\u2014";
12236
- }
12237
- function renderOps(cat) {
12238
- const rows = cat.tools.map((t) => {
12239
- const method = t["x-route"]?.method?.toUpperCase() ?? "";
12240
- const o = cat.overrides?.[t.name];
12241
- return { name: t.name, route: `${method} ${t["x-route"]?.path ?? ""}`.trim(), eff: flagsLine(effectiveBehavior(method, o)), custom: !!o && Object.keys(o).length > 0 };
12242
- });
12243
- const w = Math.min(40, Math.max(4, ...rows.map((r) => r.name.length)));
12244
- const rw = Math.min(40, Math.max(5, ...rows.map((r) => r.route.length)));
12245
- return rows.map((r) => ` ${r.name.padEnd(w)} ${r.route.padEnd(rw)} ${r.eff}${r.custom ? import_chalk42.default.yellow(" (override)") : ""}`);
12912
+ const enf = await applyProposal({ projectId, apiVersion, tenant: tenant2, proposal, enable: true });
12913
+ if (enf.aborted) {
12914
+ console.log(import_chalk41.default.red(` Enforcement NOT on \u2014 ${enf.fail} route rule(s) failed (listed above). Rules stay in shadow mode.`));
12915
+ return;
12916
+ }
12917
+ if (!enf.enabled) {
12918
+ console.log(import_chalk41.default.red(" Enforcement NOT on \u2014 the routes are set but the project switch did not flip (see above)."));
12919
+ return;
12920
+ }
12921
+ console.log(import_chalk41.default.green(` \u2713 Enforcement ON \u2014 ${enf.ok} route(s) + project switch on.`));
12922
+ console.log(import_chalk41.default.dim(" Route config takes up to ~90 s to reach every edge."));
12923
+ console.log(import_chalk41.default.dim(" Testing a locked route? Use the sign-in door, or send X-End-User-Id / a key minted with --for \u2014"));
12924
+ console.log(import_chalk41.default.dim(" a bare key has no person to check."));
12925
+ console.log(import_chalk41.default.dim(` Undo: `) + import_chalk41.default.cyan(`${B.cli} config ${projectArg} authorization.enforce_authorization false`));
12246
12926
  }
12247
- async function resolveMcpTarget(projectArg, opts) {
12927
+
12928
+ // src/commands/openapi.ts
12929
+ var import_chalk42 = __toESM(require("chalk"));
12930
+ init_auth();
12931
+ init_api();
12932
+ init_agent_chat();
12933
+ async function runOpenapi(projectArg, apiVersionArg) {
12248
12934
  const creds = loadCredentials();
12249
12935
  if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
12250
12936
  const projects = await getProjects(creds.teamId ?? "");
12251
- const matches2 = projects.filter((p) => p.projectId === projectArg || p.projectName === projectArg);
12252
- const wanted = opts.apiversion ?? opts.version;
12253
- const match = (wanted ? matches2.find((p) => p.apiVersion === wanted) : matches2[0]) ?? null;
12254
- if (!match) throw new Error(`Project "${projectArg}"${wanted ? ` version ${wanted}` : ""} not found in your active team. Run \`${B.cli} projects\`.`);
12255
- return { projectId: match.projectId, apiVersion: wanted || match.apiVersion, environment: opts.environment || "prod" };
12256
- }
12257
- async function loadCatalogue(t) {
12258
- const r = await agentCall(`/projects/${t.projectId}/${t.apiVersion}/mcp/spec?environment=${encodeURIComponent(t.environment)}`, "GET");
12259
- if (r.status >= 400) throw new Error(`Could not read the MCP catalogue (${r.status}): ${r.data?.error ?? ""}`);
12260
- const spec2 = r.data?.spec;
12261
- if (!spec2 || !Array.isArray(spec2.tools)) {
12262
- throw new Error(`No MCP catalogue published for ${t.projectId} ${t.apiVersion} (${t.environment}). Try --environment, or build one with \`${B.cli} agent mcp ${t.projectId}\`.`);
12263
- }
12264
- return spec2;
12265
- }
12266
- async function saveCatalogue(t, spec2) {
12267
- const r = await agentCall(`/projects/${t.projectId}/${t.apiVersion}/mcp/spec`, "PUT", { environment: t.environment, spec: spec2 });
12268
- if (r.status >= 400) {
12269
- const d = r.data;
12270
- throw new Error(`Save failed (${r.status}): ${d?.error ?? ""}${d?.details ? ` \u2014 ${typeof d.details === "string" ? d.details : JSON.stringify(d.details)}` : ""}`);
12271
- }
12272
- }
12273
- async function runMcpPrompts(action2, project, name, opts) {
12274
- const t = await resolveMcpTarget(project, opts);
12275
- const cat = await loadCatalogue(t);
12276
- const prompts = cat.prompts ?? [];
12277
- if (action2 === "list") {
12278
- if (opts.json) return void console.log(JSON.stringify({ environment: t.environment, prompts }, null, 2));
12279
- if (!prompts.length) {
12280
- console.log(import_chalk42.default.dim(`
12281
- No prompts on ${t.projectId} (${t.environment}). Prompts are drafted by the MCP agent (\`${B.cli} agent mcp ${t.projectId}\`) or added with \`${B.cli} mcp prompts set\`.
12282
- `));
12283
- return;
12284
- }
12285
- console.log("");
12286
- for (const p of prompts) {
12287
- const args = (p.arguments ?? []).map((a) => a.required ? a.name : `${a.name}?`).join(", ");
12288
- console.log(` ${import_chalk42.default.bold(p.name)}${args ? import_chalk42.default.dim(`(${args})`) : ""}${p.title || p.description ? ` ${p.title ?? p.description}` : ""}`);
12289
- }
12290
- console.log("");
12937
+ const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
12938
+ if (!match) throw new Error(`Project "${projectArg}" not found in your active team. Run \`apiblaze projects\`.`);
12939
+ const projectId = match.projectId;
12940
+ const apiVersion = apiVersionArg || match.apiVersion;
12941
+ console.log(import_chalk42.default.dim("Gathering captured traffic samples\u2026"));
12942
+ const routesRes = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes`, "GET");
12943
+ if (routesRes.status >= 400) {
12944
+ console.log(import_chalk42.default.red(`Could not list traffic (${routesRes.status}): ${routesRes.data?.error ?? ""}`));
12291
12945
  return;
12292
12946
  }
12293
- if (!name) throw new Error(`Usage: ${B.cli} mcp prompts ${action2} <project> <name>`);
12294
- if (action2 === "show") {
12295
- const p = prompts.find((x) => x.name === name);
12296
- if (!p) throw new Error(`No prompt named "${name}". See \`${B.cli} mcp prompts list ${project}\`.`);
12297
- if (opts.json) return void console.log(JSON.stringify(p, null, 2));
12298
- console.log(`
12299
- ${import_chalk42.default.bold(p.name)}${p.title ? ` \u2014 ${p.title}` : ""}`);
12300
- if (p.description) console.log(` ${p.description}`);
12301
- for (const a of p.arguments ?? []) console.log(import_chalk42.default.dim(` ${a.name}${a.required ? " (required)" : ""}${a.description ? `: ${a.description}` : ""}`));
12302
- for (const m of p.messages) console.log(`
12303
- [${m.role}] ${m.text}`);
12304
- console.log("");
12305
- return;
12947
+ const routes = routesRes.data?.routes ?? (Array.isArray(routesRes.data) ? routesRes.data : []);
12948
+ const hashes = routes.map((r) => r.route_hash).filter((h) => !!h);
12949
+ const ids = /* @__PURE__ */ new Set();
12950
+ for (const h of hashes) {
12951
+ const s = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes/${encodeURIComponent(h)}/samples`, "GET");
12952
+ const arr = s.data?.samples ?? [];
12953
+ for (const x of arr) if (x.sample_id) ids.add(x.sample_id);
12306
12954
  }
12307
- let next;
12308
- if (action2 === "set") {
12309
- if (!opts.file) throw new Error(`Usage: ${B.cli} mcp prompts set <project> <name> --file prompt.json`);
12310
- let raw;
12311
- try {
12312
- raw = JSON.parse(import_fs9.default.readFileSync(opts.file, "utf8"));
12313
- } catch (e) {
12314
- throw new Error(`Could not read ${opts.file}: ${e instanceof Error ? e.message : String(e)}`);
12315
- }
12316
- next = withPrompt(cat, normalizePrompt(name, raw));
12317
- } else {
12318
- next = withoutPrompt(cat, name);
12955
+ const sampleIds = [...ids];
12956
+ if (sampleIds.length === 0) {
12957
+ console.log(import_chalk42.default.yellow("No captured traffic samples found. Hit the dev environment of this proxy to capture some traces, then retry."));
12958
+ return;
12319
12959
  }
12320
- await saveCatalogue(t, next);
12321
- if (opts.json) return void console.log(JSON.stringify({ saved: true, environment: t.environment, prompts: next.prompts }, null, 2));
12322
- console.log(import_chalk42.default.green(` \u2713 ${action2 === "set" ? "Saved" : "Removed"} prompt "${name}" on ${t.projectId} (${t.environment}).`));
12323
- }
12324
- async function runMcpOps(action2, project, tool, opts) {
12325
- const t = await resolveMcpTarget(project, opts);
12326
- const cat = await loadCatalogue(t);
12327
- if (action2 === "list") {
12328
- if (opts.json) {
12329
- const tools = cat.tools.map((x) => {
12330
- const method2 = x["x-route"]?.method?.toUpperCase();
12331
- return {
12332
- name: x.name,
12333
- method: method2,
12334
- path: x["x-route"]?.path,
12335
- default: derivedBehavior(method2),
12336
- override: cat.overrides?.[x.name] ?? null,
12337
- effective: effectiveBehavior(method2, cat.overrides?.[x.name])
12338
- };
12339
- });
12340
- return void console.log(JSON.stringify({ environment: t.environment, tools }, null, 2));
12960
+ async function publish(ctx) {
12961
+ const patch = ctx.proposal.patch;
12962
+ if (!Array.isArray(patch) || patch.length === 0) {
12963
+ ctx.log(import_chalk42.default.green(" Nothing to publish \u2014 the spec already covers the observed traffic.\n"));
12964
+ return;
12341
12965
  }
12342
- console.log(import_chalk42.default.dim(`
12343
- ${cat.tools.length} tool(s) on ${t.projectId} (${t.environment}). Defaults come from the HTTP method: GET/HEAD/OPTIONS read-only, PUT/DELETE idempotent, DELETE destructive + asks to confirm.
12344
- `));
12345
- for (const l of renderOps(cat)) console.log(l);
12346
- console.log(import_chalk42.default.dim(`
12347
- Override: ${B.cli} mcp ops set ${project} <tool> --read-only | --destructive | --confirm | --no-confirm | --always-async \xB7 Reset: ${B.cli} mcp ops reset ${project} <tool>
12966
+ const specSource = ctx.lastData?.spec_source ?? "unknown";
12967
+ const endpoint = specSource === "github" ? "open-pr" : "publish-openapi";
12968
+ const pub = await agentCall(`/projects/${projectId}/${apiVersion}/samples/${endpoint}`, "POST", { patch, sample_ids_used: sampleIds });
12969
+ if (pub.status >= 400) {
12970
+ ctx.log(import_chalk42.default.red(` Publish failed (${pub.status}): ${pub.data?.error ?? ""}
12348
12971
  `));
12349
- return;
12972
+ return;
12973
+ }
12974
+ const prUrl = pub.data?.pr_url;
12975
+ ctx.log(import_chalk42.default.green(prUrl ? ` \u2713 Pull request opened: ${prUrl}
12976
+ ` : " \u2713 Published updated OpenAPI spec.\n"));
12350
12977
  }
12351
- if (!tool) throw new Error(`Usage: ${B.cli} mcp ops ${action2} <project> <tool>`);
12352
- const next = action2 === "set" ? withOverride(cat, tool, opts) : withoutOverride(cat, tool);
12353
- await saveCatalogue(t, next);
12354
- const method = cat.tools.find((x) => x.name === tool)?.["x-route"]?.method;
12355
- const eff = effectiveBehavior(method, next.overrides?.[tool]);
12356
- if (opts.json) return void console.log(JSON.stringify({ saved: true, tool, override: next.overrides?.[tool] ?? null, effective: eff }, null, 2));
12357
- console.log(import_chalk42.default.green(` \u2713 ${tool}: ${flagsLine(eff)}${action2 === "reset" ? " (automatic default)" : ""}`));
12358
- }
12359
- async function runMcpUi(project, tool, opts) {
12360
- const t = await resolveMcpTarget(project, opts);
12361
- const cat = await loadCatalogue(t);
12362
- requireTool(cat, tool);
12363
- const uri = uiResourceUri(t.projectId, tool);
12364
- if (opts.json) return void console.log(JSON.stringify({ tool, uri, kind: "automatic" }, null, 2));
12365
- console.log(`
12366
- ${import_chalk42.default.bold(uri)}
12367
- `);
12368
- console.log(import_chalk42.default.dim(" This is the automatic viewer: MCP Apps clients render the tool's result with a generic UI built"));
12369
- console.log(import_chalk42.default.dim(" from its response schema \u2014 nothing to configure. Uploading your own HTML for it comes later.\n"));
12978
+ await runAgentChatRepl({
12979
+ title: `API-docs assistant \u2014 ${projectId} ${apiVersion}`,
12980
+ subtitle: `${sampleIds.length} captured sample(s) \xB7 discuss what to document, then make it official.`,
12981
+ endpoint: `/projects/${projectId}/${apiVersion}/openapi/chat`,
12982
+ buildBody: () => ({ included_sample_ids: sampleIds }),
12983
+ seedPrompt: "Compare the captured samples against the current spec and tell me what's missing or wrong. List the changes you'd make \u2014 don't produce a patch yet.",
12984
+ summarizeProposal: (data) => {
12985
+ const patch = data.proposal.patch;
12986
+ const n = Array.isArray(patch) ? patch.length : 0;
12987
+ return n === 0 ? "The spec already covers the observed traffic \u2014 nothing to publish." : `Patch ready: ${n} additive change(s) to the OpenAPI spec.`;
12988
+ },
12989
+ commands: [
12990
+ { name: "publish", describe: "publish / open PR", needsProposal: true, run: publish }
12991
+ ]
12992
+ });
12370
12993
  }
12371
12994
 
12995
+ // src/index.ts
12996
+ init_mcp();
12997
+
12372
12998
  // src/commands/delete.ts
12373
12999
  var import_chalk44 = __toESM(require("chalk"));
12374
13000
  var import_ora15 = __toESM(require("ora"));
@@ -15770,6 +16396,7 @@ async function runSpecDeleteRule(project, opts) {
15770
16396
 
15771
16397
  // src/commands/config-browse.ts
15772
16398
  init_agent();
16399
+ init_mcp();
15773
16400
  var limitsQuota = (cfg, patch) => ({
15774
16401
  quota: { limit: cfg.limits?.quota?.limit ?? 3e3, period: cfg.limits?.quota?.period ?? "daily", ...patch }
15775
16402
  });
@@ -18502,7 +19129,7 @@ async function runOpTest(o = {}) {
18502
19129
  for (const g of guts) {
18503
19130
  let r;
18504
19131
  try {
18505
- r = await call2({ action: "gut", id: g.id }, timeoutS);
19132
+ r = await call2({ action: "gut", id: g.id, ...o.commit ? { commit: true } : {} }, timeoutS);
18506
19133
  } catch (e) {
18507
19134
  r = { kind: "gut", id: g.id, label: g.label, ok: false, results: [], error: e instanceof Error ? e.message : String(e), duration_ms: 0 };
18508
19135
  }
@@ -18958,7 +19585,7 @@ var OP_COMMANDS = [
18958
19585
  { cmd: "op ceiling deny", blurb: "refuse a maximum-rate request; the producer is told why", extra: '<id> --note "why" (required) --json' },
18959
19586
  { cmd: "op incident status", blurb: "the degraded-mode switch: allow | deny | extend, who set it and why" },
18960
19587
  { cmd: "op incident allow|deny|extend", blurb: "dashboard-only \u2014 prints the page (reason required)" },
18961
- { cmd: "op test", blurb: "run the operator test suite against production (sandbox team only): contract specs + gut checks", extra: "--json --only <ids> --group <g> --skip-guts --guts-only --fail-fast" },
19588
+ { cmd: "op test", blurb: "run the operator test suite against production (sandbox team only): contract specs + gut checks", extra: "--json --only <ids> --group <g> --skip-guts --guts-only --fail-fast --commit" },
18962
19589
  { cmd: "op test --perf", blurb: "performance gate: fixed-rate load on a sandbox proxy, client + gateway latency vs thresholds (alias: op perf)", extra: "--rps 100 --seconds 60 --json --max-p50 --max-p95 --max-error-rate --max-gateway-p50 --max-gateway-p95 --url --key" },
18963
19590
  { cmd: "op test --consumer", blurb: "the data plane as a consumer: the stored `consumer login` token opens exactly its own proxy (401 without, 401 tampered)", extra: "--path /get --json" },
18964
19591
  { cmd: "op residue", blurb: "external-store residue report (Neon/OpenFGA) \u2014 dry run, deletes nothing" },
@@ -19069,6 +19696,7 @@ async function runOp(sub, opts = {}, view, arg) {
19069
19696
  skipGuts: !!opts.skipGuts,
19070
19697
  gutsOnly: !!opts.gutsOnly,
19071
19698
  failFast: !!opts.failFast,
19699
+ commit: !!opts.commit,
19072
19700
  timeout: typeof opts.timeout === "string" ? Number(opts.timeout) : void 0
19073
19701
  });
19074
19702
  if (!opts.only && !opts.group && !opts.gutsOnly && !opts.skipAnon) {
@@ -19466,8 +20094,15 @@ program.command("login").description(`Authenticate with ${B.product}`).option("-
19466
20094
  process.exit(1);
19467
20095
  }
19468
20096
  });
19469
- 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\`.)`).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("--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): own-backend \xB7 no-financial \xB7 no-digital-goods \xB7 no-genai-media \xB7 content-13-no-ads", collect, []).option("--allowed-origin <origin>", "Directory mode: an extra browser origin allowed on the MCP host (repeatable)", collect, []).action(async (opts) => {
20097
+ 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\`.)`).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("--policy [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. Same as \`${B.cli} connector <spec> --policy \u2026\`.`).option("--domain <host>", "With --policy: the MCP server's custom hostname (mcp.<your domain>); asked for when missing, suggested from the spec").option("--wait <minutes>", "With --policy: how long to wait for the custom domain to activate once its CNAME is in place (default 15; 0 = do not wait)").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) => {
19470
20098
  try {
20099
+ if (opts.policy !== void 0 && opts.policy !== false) {
20100
+ const spec2 = opts.target ?? opts.openapi ?? opts.openapispec;
20101
+ if (!spec2) throw new Error(`--policy needs the OpenAPI document: ${B.cli} create --policy --target https://yourdomain.com/openapi.yaml --domain mcp.yourdomain.com`);
20102
+ const { auth: _auth, target: _target, openapi: _openapi, openapispec: _spec, directoryReady: _dr, ...rest } = opts;
20103
+ await runConnector(spec2, { ...rest, full: true });
20104
+ return;
20105
+ }
19471
20106
  await runCreate({ ...opts, openapi: opts.openapi ?? opts.openapispec });
19472
20107
  } catch (err) {
19473
20108
  await printError(err);
@@ -19477,9 +20112,9 @@ program.command("create").description(`Put an app that is already DEPLOYED behin
19477
20112
  function collect(value, previous) {
19478
20113
  return [...previous ?? [], value];
19479
20114
  }
19480
- 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("--platform <list>", "claude \xB7 chatgpt \xB7 claude,chatgpt (default both)").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 })));
19481
- var connector = program.command("connector").description(`Directory-ready in one command: checks the ${B.thingShort}, applies the safe fixes, asks for what is missing, and writes the submission packet when every check passes (you submit it to Anthropic and OpenAI yourself)`).argument("[project]", `The ${B.thingShort} (project name)`).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): own-backend \xB7 no-financial \xB7 no-digital-goods \xB7 no-genai-media \xB7 content-13-no-ads. Alone, this only records the attestation.", collect, []).option("--allowed-origin <origin>", "An extra browser origin allowed on the MCP host (repeatable)", collect, []).option("--full", "Run the full flow even when only --attest was given").option("--platform <list>", "claude \xB7 chatgpt \xB7 claude,chatgpt (default both)").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) => {
19482
- if (!project) throw new Error(`Which ${B.thingShort}? ${B.cli} connector <project> \xB7 ${B.cli} connector reviewer <project>`);
20115
+ 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("--policy <directory>", "Which directory's policies to check against: claude \xB7 chatgpt \xB7 all (default claude; repeat or comma-separate)", collect, []).addOption(new import_commander.Option("--platform <list>", "Older spelling of --policy").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 })));
20116
+ var connector = program.command("connector").description(`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("--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("--full", "Run the full flow even when only --attest was given").option("--policy <directory>", "Which directory's policies this run must satisfy: claude \xB7 chatgpt \xB7 all (default claude; repeat or comma-separate)", collect, []).addOption(new import_commander.Option("--platform <list>", "Older spelling of --policy").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) => {
20117
+ 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>`);
19483
20118
  await runConnector(project, opts);
19484
20119
  }));
19485
20120
  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, opts)));
@@ -19676,7 +20311,7 @@ apikeys.command("list").description("List control-plane API keys in your team").
19676
20311
  apikeys.command("mint").description("Create a control-plane API key (secret shown once) \u2014 or, with --tenant, a consumer key in that tenant: a backend key (default), a client key (--client) or a person-bound key (--for)").option("--desc <text>", "Description").option("--expires-days <n>", "Expiry in days (default 90 server-side)").option("--tenant <slug>", "Mint a CONSUMER key in this tenant instead of a control-plane key").option("--backend", `Backend key (default). ${HOLDER_HELP.backend}`).option("--client", `Client key. ${HOLDER_HELP.client} Needs --tenant.`).option("--for <email|id>", "Issue this key to a named person \u2014 calls made with it count as that person, and ownership rules trust it. The Users & Groups widget's admin lane accepts only such a key, one per admin. Needs --tenant.").option("--admin", "With --for: a key that acts as that tenant admin on the IAM API (users & groups) \u2014 adds manage-access; env defaults to prod").option("--env <name>", "Environment the consumer key is valid for (with --tenant; default dev, prod with --admin)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts, cmd) => runKeyMint({ ...cmd.parent?.opts(), ...opts })));
19677
20312
  apikeys.command("revoke").description("Revoke a control-plane API key").argument("<keyId>", "Key id (see `apikeys list`)").option("--team <id|name>", "Team (defaults to active team)").action(action((keyId, opts, cmd) => runKeyRevoke(keyId, { ...cmd.parent?.opts(), ...opts })));
19678
20313
  program.addCommand(apikeys);
19679
- var op = new import_commander.Command("op").description("Operator menu (platform operators only)").argument("[action]", "latency | mark | wallet | money | ledger | incident | residue | sweep | test | anon | perf | cli-policy (omit for the menu)").argument("[view]", "for `latency`: slow | llm | compare | grades | distance \xB7 for `wallet`: list | inspect | freeze \u2026 \xB7 for `money`: exposure \xB7 for `ledger`: show | lag | quarantine | cut | backfill \xB7 for `incident`: status | allow | deny | extend \xB7 for `mark`: the change name, quoted").argument("[arg]", "for `wallet inspect` / `ledger show|cut`: the wallet id (or a team id)").option("-y, --yes", "Skip the sweep confirmation prompt").option("--json", "Output machine-readable JSON").option("--hours <n>", "Look-back window in hours (default 1)").option("--team <id>", "Filter to one team").option("--only <ids>", "for `test`: comma-separated spec/gut ids or prefixes").option("--group <name>", "for `test`: only specs whose group matches").option("--skip-guts", "for `test`: contract specs only").option("--guts-only", "for `test`: server-side gut checks only").option("--fail-fast", "for `test`: stop at the first failure").option("--skip-anon", "for `test`: skip the first-run journey (`op anon`) that runs after the suite").option("--timeout <s>", "for `test`: seconds to wait per check (default 150)").option("--perf", "for `test`: run the performance gate instead of the suite (fixed-rate load on a sandbox proxy)").option("--consumer", "for `test`: probe the data plane with the stored `consumer login` token instead of the suite").option("--path <p>", "for `test --consumer`: request path on the proxy (default /get)").option("--proxy <name@version>", "for `test --consumer`: the OAuth-door proxy to knock on (default: the sandbox proxy for a sandbox-tenant session)").option("--rps <n>", "for `test --perf`: requests per second (default 100, max 300); for `ceiling approve`: the maximum rate to grant (default: what was asked)").option("--seconds <n>", "for `test --perf`: how long to send for (default 60, max 300)").option("--url <url>", "for `test --perf`: load this fixture URL instead of provisioning one (with --key; nothing is torn down)").option("--max-p50 <ms>", "for `test --perf`: client p50 threshold (default 250)").option("--max-p95 <ms>", "for `test --perf`: client p95 threshold (default 800)").option("--max-error-rate <pct>", "for `test --perf`: error-rate threshold in percent (default 0.5)").option("--max-gateway-p50 <ms>", "for `test --perf`: apiblaze-overhead p50 threshold (default 60)").option("--max-gateway-p95 <ms>", "for `test --perf`: apiblaze-overhead p95 threshold (default 250)").option("--project <name>", "Filter to one proxy").option("--api-version <v>", "Filter to one API version").option("--tenant <name>", "Filter to one tenant").option("--route <path>", "Filter to one route").option("--environment <env>", "Filter to one environment").option("--status-class <c>", "Filter to a status class (2xx/4xx/5xx)").option("--min-ms <n>", "For `latency slow`: minimum duration (default 1000)").option("--min <version>", "for `cli-policy`: minimum CLI version (x.y.z; 0.0.0 = none)").option("--reject-unversioned", "for `cli-policy`: also refuse CLIs that send no version (released before 2026-09-27)").option("--allow-unversioned", "for `cli-policy`: stop refusing unversioned CLIs").option("--message <text>", "for `cli-policy`: message shown with the upgrade instruction").option("--reason <text>", "for `cli-policy`: why the policy changes (required for a change; recorded with it); for `ledger cut|backfill`: written to operator_audit").option("--force", "for `cli-policy`: allow a minimum above the latest published npm version (locks out every CLI until it ships)").option("--days <n>", "For `latency grades`: how many days back (default 7)").option("--threshold-ms <n>", "For `latency distance`: network ms above which distance counts as a problem (default 50)").option("--excellent <ms>", "Grade threshold: under this is excellent (default 250)").option("--okay <ms>", "Grade threshold: under this is okay (default 500)").option("--bad <ms>", "Grade threshold: under this is bad, at/over is terrible (default 1000)").option("--key <key>", "for `test --perf --url`: the fixture's API key").option("--since <date>", "for `ledger show`: start of the window; for `ledger backfill`: wallets active since (ISO date or epoch ms)").option("--kind <k>", "for `ledger show`: one entry kind only (or `flow`)").option("--limit <n>", "for `money exposure` / `ledger show|quarantine|backfill`: row / wallet cap").option("--ba <wallet>", "for `ledger quarantine`: one wallet only").option("--status <s>", "for `ceiling list`: pending (default) | decided | all").option("--note <text>", "for `ceiling approve` / `ceiling deny`: the note the producer reads (required to deny)").action(action((sub, view, arg, opts) => runOp(sub, opts, view, arg))).addHelpText("after", () => `
20314
+ var op = new import_commander.Command("op").description("Operator menu (platform operators only)").argument("[action]", "latency | mark | wallet | money | ledger | incident | residue | sweep | test | anon | perf | cli-policy (omit for the menu)").argument("[view]", "for `latency`: slow | llm | compare | grades | distance \xB7 for `wallet`: list | inspect | freeze \u2026 \xB7 for `money`: exposure \xB7 for `ledger`: show | lag | quarantine | cut | backfill \xB7 for `incident`: status | allow | deny | extend \xB7 for `mark`: the change name, quoted").argument("[arg]", "for `wallet inspect` / `ledger show|cut`: the wallet id (or a team id)").option("-y, --yes", "Skip the sweep confirmation prompt").option("--json", "Output machine-readable JSON").option("--hours <n>", "Look-back window in hours (default 1)").option("--team <id>", "Filter to one team").option("--only <ids>", "for `test`: comma-separated spec/gut ids or prefixes").option("--group <name>", "for `test`: only specs whose group matches").option("--skip-guts", "for `test`: contract specs only").option("--guts-only", "for `test`: server-side gut checks only").option("--fail-fast", "for `test`: stop at the first failure").option("--commit", "for `test --only openapi-drift --guts-only`: accept the current producer OpenAPI as the new drift baseline").option("--skip-anon", "for `test`: skip the first-run journey (`op anon`) that runs after the suite").option("--timeout <s>", "for `test`: seconds to wait per check (default 150)").option("--perf", "for `test`: run the performance gate instead of the suite (fixed-rate load on a sandbox proxy)").option("--consumer", "for `test`: probe the data plane with the stored `consumer login` token instead of the suite").option("--path <p>", "for `test --consumer`: request path on the proxy (default /get)").option("--proxy <name@version>", "for `test --consumer`: the OAuth-door proxy to knock on (default: the sandbox proxy for a sandbox-tenant session)").option("--rps <n>", "for `test --perf`: requests per second (default 100, max 300); for `ceiling approve`: the maximum rate to grant (default: what was asked)").option("--seconds <n>", "for `test --perf`: how long to send for (default 60, max 300)").option("--url <url>", "for `test --perf`: load this fixture URL instead of provisioning one (with --key; nothing is torn down)").option("--max-p50 <ms>", "for `test --perf`: client p50 threshold (default 250)").option("--max-p95 <ms>", "for `test --perf`: client p95 threshold (default 800)").option("--max-error-rate <pct>", "for `test --perf`: error-rate threshold in percent (default 0.5)").option("--max-gateway-p50 <ms>", "for `test --perf`: apiblaze-overhead p50 threshold (default 60)").option("--max-gateway-p95 <ms>", "for `test --perf`: apiblaze-overhead p95 threshold (default 250)").option("--project <name>", "Filter to one proxy").option("--api-version <v>", "Filter to one API version").option("--tenant <name>", "Filter to one tenant").option("--route <path>", "Filter to one route").option("--environment <env>", "Filter to one environment").option("--status-class <c>", "Filter to a status class (2xx/4xx/5xx)").option("--min-ms <n>", "For `latency slow`: minimum duration (default 1000)").option("--min <version>", "for `cli-policy`: minimum CLI version (x.y.z; 0.0.0 = none)").option("--reject-unversioned", "for `cli-policy`: also refuse CLIs that send no version (released before 2026-09-27)").option("--allow-unversioned", "for `cli-policy`: stop refusing unversioned CLIs").option("--message <text>", "for `cli-policy`: message shown with the upgrade instruction").option("--reason <text>", "for `cli-policy`: why the policy changes (required for a change; recorded with it); for `ledger cut|backfill`: written to operator_audit").option("--force", "for `cli-policy`: allow a minimum above the latest published npm version (locks out every CLI until it ships)").option("--days <n>", "For `latency grades`: how many days back (default 7)").option("--threshold-ms <n>", "For `latency distance`: network ms above which distance counts as a problem (default 50)").option("--excellent <ms>", "Grade threshold: under this is excellent (default 250)").option("--okay <ms>", "Grade threshold: under this is okay (default 500)").option("--bad <ms>", "Grade threshold: under this is bad, at/over is terrible (default 1000)").option("--key <key>", "for `test --perf --url`: the fixture's API key").option("--since <date>", "for `ledger show`: start of the window; for `ledger backfill`: wallets active since (ISO date or epoch ms)").option("--kind <k>", "for `ledger show`: one entry kind only (or `flow`)").option("--limit <n>", "for `money exposure` / `ledger show|quarantine|backfill`: row / wallet cap").option("--ba <wallet>", "for `ledger quarantine`: one wallet only").option("--status <s>", "for `ceiling list`: pending (default) | decided | all").option("--note <text>", "for `ceiling approve` / `ceiling deny`: the note the producer reads (required to deny)").action(action((sub, view, arg, opts) => runOp(sub, opts, view, arg))).addHelpText("after", () => `
19680
20315
  ${renderOpCommands()}
19681
20316
  `);
19682
20317
  program.addCommand(op, { hidden: true });