apiblaze 0.21.53 → 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.
- package/dist/index.js +1709 -1075
- 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.
|
|
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
|
-
|
|
5405
|
-
{ id: "
|
|
5406
|
-
{ id: "
|
|
5407
|
-
{ id: "
|
|
5408
|
-
{ id: "
|
|
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
|
-
|
|
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/
|
|
7095
|
-
async function
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
}
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
function
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
if (
|
|
7117
|
-
|
|
7118
|
-
|
|
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
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
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
|
-
|
|
7234
|
+
rl.close();
|
|
7144
7235
|
}
|
|
7145
|
-
var
|
|
7146
|
-
var
|
|
7147
|
-
"src/
|
|
7236
|
+
var import_readline, import_chalk26;
|
|
7237
|
+
var init_agent_chat = __esm({
|
|
7238
|
+
"src/lib/agent-chat.ts"() {
|
|
7148
7239
|
"use strict";
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
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/
|
|
7160
|
-
var
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
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
|
|
7591
|
-
if (status2 < 400) return
|
|
7592
|
-
if (status2 < 500) return
|
|
7593
|
-
return
|
|
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
|
|
8057
|
+
return import_chalk29.default.green;
|
|
7599
8058
|
case "POST":
|
|
7600
|
-
return
|
|
8059
|
+
return import_chalk29.default.blue;
|
|
7601
8060
|
case "PUT":
|
|
7602
8061
|
case "PATCH":
|
|
7603
|
-
return
|
|
8062
|
+
return import_chalk29.default.yellow;
|
|
7604
8063
|
case "DELETE":
|
|
7605
|
-
return
|
|
8064
|
+
return import_chalk29.default.red;
|
|
7606
8065
|
default:
|
|
7607
|
-
return
|
|
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 =
|
|
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 =
|
|
7623
|
-
const step = 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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 ?
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
8074
|
-
choices.push({ name:
|
|
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
|
|
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
|
|
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]) => `${
|
|
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
|
|
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) :
|
|
8588
|
+
const body = shown.length ? wrapCells(shown, width) : import_chalk29.default.dim("(all forwarding headers)");
|
|
8130
8589
|
const tail = noise.length ? `
|
|
8131
|
-
${PANE_INDENT}${
|
|
8132
|
-
return `${body}${tail} ${
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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(` ${
|
|
8609
|
+
console.log(` ${import_chalk29.default.dim(empty)}`);
|
|
8151
8610
|
return;
|
|
8152
8611
|
}
|
|
8153
8612
|
const v = view ?? {};
|
|
8154
|
-
if (v.url) console.log(` ${
|
|
8613
|
+
if (v.url) console.log(` ${import_chalk29.default.dim("target")} ${v.url}`);
|
|
8155
8614
|
if (v.status != null) {
|
|
8156
|
-
console.log(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
8163
|
-
console.log(` ${
|
|
8164
|
-
console.log(` ${
|
|
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(
|
|
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(
|
|
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(
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
8651
|
+
pane("Response (after) \u2014 APIblaze \u2192 client", import_chalk29.default.magenta, v.response);
|
|
8193
8652
|
console.log("");
|
|
8194
8653
|
}
|
|
8195
|
-
var
|
|
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
|
-
|
|
8200
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
8326
|
-
console.log(
|
|
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(
|
|
8348
|
-
console.log(
|
|
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
|
|
8819
|
+
var import_chalk30, activeCaller, viaCaller;
|
|
8361
8820
|
var init_logs = __esm({
|
|
8362
8821
|
"src/commands/logs.ts"() {
|
|
8363
8822
|
"use strict";
|
|
8364
|
-
|
|
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(
|
|
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
|
-
|
|
8444
|
-
|
|
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 (!
|
|
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
|
-
|
|
8530
|
-
for (const entry of
|
|
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
|
-
|
|
8534
|
-
|
|
8992
|
+
import_fs8.default.mkdirSync(import_path6.default.dirname(f), { recursive: true });
|
|
8993
|
+
import_fs8.default.writeFileSync(f, text);
|
|
8535
8994
|
}
|
|
8536
|
-
|
|
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
|
-
|
|
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 (!
|
|
8544
|
-
|
|
8545
|
-
|
|
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
|
-
|
|
8551
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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(`${
|
|
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:
|
|
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(
|
|
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(`${
|
|
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 =
|
|
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 ?
|
|
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(` ${
|
|
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 = `${
|
|
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))} ${
|
|
8763
|
-
for (const extra of right.slice(1)) console.log(` ${" ".repeat(leftW)} ${
|
|
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 =
|
|
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(` ${
|
|
8773
|
-
const ok = (text, note = "") => console.log(` ${
|
|
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
|
-
|
|
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(
|
|
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)}${
|
|
8802
|
-
console.log(
|
|
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 ${
|
|
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(`${
|
|
8815
|
-
ok(`${
|
|
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(
|
|
8818
|
-
console.log(
|
|
8819
|
-
console.log(
|
|
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(
|
|
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} ${
|
|
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(
|
|
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(
|
|
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(`${
|
|
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(
|
|
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(`${
|
|
8960
|
-
ok(`${
|
|
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(
|
|
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 ? ` ${
|
|
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 =
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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(
|
|
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(`${
|
|
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(
|
|
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 ? `${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
10631
|
+
var import_fs6 = __toESM(require("fs"));
|
|
10173
10632
|
var import_path4 = __toESM(require("path"));
|
|
10174
|
-
var
|
|
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 (
|
|
10193
|
-
|
|
10194
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
10208
|
-
|
|
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
|
|
10215
|
-
|
|
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) =>
|
|
10220
|
-
const { picked } = await inquirer3.prompt([{ type: "checkbox", name: "picked", message: "
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
10262
|
-
for (const s of fixes.skipped ?? []) say(
|
|
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
|
-
|
|
10265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10282
|
-
|
|
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(
|
|
10290
|
-
console.log(` Submission packet: ${
|
|
10291
|
-
console.log(
|
|
10292
|
-
|
|
10293
|
-
console.log(
|
|
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
|
|
10296
|
-
|
|
10297
|
-
|
|
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
|
|
10300
|
-
|
|
10301
|
-
const
|
|
10302
|
-
const
|
|
10303
|
-
|
|
10304
|
-
const
|
|
10305
|
-
const
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
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
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
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
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
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
|
|
10341
|
-
|
|
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
|
|
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
|
|
11348
|
+
var import_chalk32 = __toESM(require("chalk"));
|
|
10349
11349
|
init_brand();
|
|
10350
11350
|
|
|
10351
11351
|
// src/lib/detect-backend.ts
|
|
10352
|
-
var
|
|
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 =
|
|
11384
|
+
const st = import_fs7.default.statSync(file2);
|
|
10385
11385
|
if (!st.isFile() || st.size > max) return null;
|
|
10386
|
-
return
|
|
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 (
|
|
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 (
|
|
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}
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
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(` ${
|
|
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(
|
|
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(` ${
|
|
11133
|
-
console.log(` Any time: ${
|
|
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(
|
|
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(` ${
|
|
11146
|
-
console.log(` Any time: ${
|
|
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(
|
|
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(` ${
|
|
11156
|
-
console.log(` Your own app, when it runs: ${
|
|
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 (
|
|
11190
|
-
console.error(
|
|
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
|
-
|
|
11194
|
-
|
|
11195
|
-
console.log(`${
|
|
11196
|
-
console.log(
|
|
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
|
|
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(` ${
|
|
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}${
|
|
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(
|
|
11237
|
-
console.log(
|
|
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(` ${
|
|
11240
|
-
console.log(` ${
|
|
11241
|
-
console.log(` ${
|
|
11242
|
-
console.log(` ${
|
|
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(
|
|
11246
|
-
console.log(
|
|
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(
|
|
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(
|
|
11256
|
-
for (const [h, why] of Object.entries(t.headers_you_receive)) console.log(` ${
|
|
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(` ${
|
|
12260
|
+
console.log(` ${import_chalk33.default.dim("How to verify:")}`);
|
|
11259
12261
|
bullets(t.how_to_verify);
|
|
11260
12262
|
console.log();
|
|
11261
|
-
console.log(` ${
|
|
12263
|
+
console.log(` ${import_chalk33.default.dim("Never:")}`);
|
|
11262
12264
|
bullets(t.what_not_to_do);
|
|
11263
12265
|
console.log();
|
|
11264
|
-
console.log(
|
|
11265
|
-
for (const [k, v] of Object.entries(kit["4_env_vars"])) console.log(` ${
|
|
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(
|
|
11269
|
-
console.log(` ${
|
|
11270
|
-
console.log(` ${
|
|
11271
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
11325
|
-
var
|
|
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(
|
|
12334
|
+
if (demos) console.log(import_chalk35.default.dim(` Stopped ${demos} running demo(s).`));
|
|
11333
12335
|
const dir = getApiblazeDir();
|
|
11334
|
-
const entries2 =
|
|
12336
|
+
const entries2 = fs17.existsSync(dir) ? fs17.readdirSync(dir) : [];
|
|
11335
12337
|
if (!entries2.length) {
|
|
11336
|
-
console.log(
|
|
12338
|
+
console.log(import_chalk35.default.dim("Nothing to flush \u2014 no local apiblaze config found."));
|
|
11337
12339
|
return;
|
|
11338
12340
|
}
|
|
11339
|
-
console.log(
|
|
11340
|
-
console.log(
|
|
11341
|
-
console.log(
|
|
11342
|
-
console.log(
|
|
11343
|
-
console.log(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
12374
|
+
fs17.rmSync(path14.join(dir, e), { recursive: true, force: true });
|
|
11373
12375
|
removed++;
|
|
11374
12376
|
} catch (err) {
|
|
11375
|
-
console.log(
|
|
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) ? ` ${
|
|
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) ? ` ${
|
|
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 =
|
|
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(` ${
|
|
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(`${
|
|
11397
|
-
console.log(
|
|
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
|
|
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(
|
|
12430
|
+
console.log(import_chalk36.default.bold("API Producer"));
|
|
11429
12431
|
if (!creds) {
|
|
11430
|
-
console.log(
|
|
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(` ${
|
|
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: ${
|
|
11436
|
-
if (creds.activeTenant) console.log(` Tenant scope: ${
|
|
11437
|
-
if (Date.now() >= creds.expiresAt) console.log(
|
|
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(
|
|
11442
|
-
console.log(` Team: ${
|
|
11443
|
-
console.log(` CP key: ${
|
|
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
|
-
|
|
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(
|
|
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(
|
|
12451
|
+
console.log(import_chalk36.default.bold("\nAPI Consumer"));
|
|
11450
12452
|
if (!consumer2) {
|
|
11451
|
-
console.log(
|
|
12453
|
+
console.log(import_chalk36.default.dim(" Not logged in. Run `apiblaze consumer login`."));
|
|
11452
12454
|
} else {
|
|
11453
|
-
console.log(` ${
|
|
11454
|
-
console.log(` Tenant: ${
|
|
11455
|
-
if (Date.now() >= consumer2.expiresAt) console.log(
|
|
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
|
|
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(
|
|
11467
|
-
if (hint) console.error(
|
|
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(
|
|
11475
|
-
\u2714 Active team: ${
|
|
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(
|
|
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(`${
|
|
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} ${
|
|
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:
|
|
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
|
|
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(
|
|
11593
|
-
console.error(
|
|
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(
|
|
11599
|
-
console.log(" " +
|
|
11600
|
-
console.log(
|
|
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
|
-
${
|
|
11605
|
-
console.log(
|
|
11606
|
-
}
|
|
11607
|
-
async function runDocs(opts) {
|
|
11608
|
-
console.log("");
|
|
11609
|
-
console.log(
|
|
11610
|
-
console.log(
|
|
11611
|
-
console.log(
|
|
11612
|
-
console.log(
|
|
11613
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
11779
|
-
console.log(
|
|
11780
|
-
console.log(
|
|
11781
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
11828
|
-
for (const f of failed) log4(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
11869
|
-
ctx.log(
|
|
11870
|
-
ctx.log(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
11942
|
-
console.log(
|
|
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(
|
|
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(
|
|
11963
|
-
console.log(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
12178
|
-
|
|
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
|
-
|
|
12181
|
-
|
|
12182
|
-
|
|
12183
|
-
|
|
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
|
|
12207
|
-
|
|
12208
|
-
const
|
|
12209
|
-
|
|
12210
|
-
|
|
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
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
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 (
|
|
12218
|
-
|
|
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
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
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
|
-
|
|
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
|
|
12252
|
-
|
|
12253
|
-
const
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
}
|
|
12257
|
-
|
|
12258
|
-
|
|
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
|
-
|
|
12294
|
-
|
|
12295
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
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
|
-
|
|
12308
|
-
if (
|
|
12309
|
-
|
|
12310
|
-
|
|
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
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
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
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
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
|
});
|
|
@@ -19467,8 +20094,15 @@ program.command("login").description(`Authenticate with ${B.product}`).option("-
|
|
|
19467
20094
|
process.exit(1);
|
|
19468
20095
|
}
|
|
19469
20096
|
});
|
|
19470
|
-
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-
|
|
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) => {
|
|
19471
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
|
+
}
|
|
19472
20106
|
await runCreate({ ...opts, openapi: opts.openapi ?? opts.openapispec });
|
|
19473
20107
|
} catch (err) {
|
|
19474
20108
|
await printError(err);
|
|
@@ -19478,9 +20112,9 @@ program.command("create").description(`Put an app that is already DEPLOYED behin
|
|
|
19478
20112
|
function collect(value, previous) {
|
|
19479
20113
|
return [...previous ?? [], value];
|
|
19480
20114
|
}
|
|
19481
|
-
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("--
|
|
19482
|
-
var connector = program.command("connector").description(`Directory-ready in one command:
|
|
19483
|
-
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>`);
|
|
19484
20118
|
await runConnector(project, opts);
|
|
19485
20119
|
}));
|
|
19486
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)));
|