apiblaze 0.20.11 → 0.20.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +258 -60
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1024,7 +1024,7 @@ var import_commander = require("commander");
1024
1024
  var import_chalk53 = __toESM(require("chalk"));
1025
1025
 
1026
1026
  // package.json
1027
- var version = "0.20.11";
1027
+ var version = "0.20.13";
1028
1028
 
1029
1029
  // src/index.ts
1030
1030
  init_types();
@@ -8360,14 +8360,17 @@ var os4 = __toESM(require("os"));
8360
8360
  var path6 = __toESM(require("path"));
8361
8361
  var import_child_process2 = require("child_process");
8362
8362
  var import_chalk46 = __toESM(require("chalk"));
8363
- var run = (cmd, args, opts = {}) => (0, import_child_process2.spawnSync)(cmd, args, {
8363
+ var useShell = process.platform === "win32";
8364
+ var winQuote = (a) => /^[A-Za-z0-9_\-.:/\\=]+$/.test(a) ? a : `"${a.replace(/"/g, '""')}"`;
8365
+ var run = (cmd, args, opts = {}) => (0, import_child_process2.spawnSync)(cmd, useShell ? args.map(winQuote) : args, {
8364
8366
  encoding: "utf-8",
8365
- stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"],
8366
- timeout: opts.inherit ? void 0 : 15e3,
8367
- shell: process.platform === "win32"
8368
- // .cmd shims on Windows
8367
+ stdio: opts.interactive ? ["inherit", "inherit", "inherit"] : opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"],
8368
+ timeout: opts.inherit || opts.interactive ? void 0 : 15e3,
8369
+ shell: useShell
8369
8370
  });
8371
+ var detected = null;
8370
8372
  function detectExternalClis() {
8373
+ if (detected) return detected;
8371
8374
  const found = [];
8372
8375
  for (const [kind, label3] of [["claude", "Claude CLI"], ["codex", "Codex CLI"]]) {
8373
8376
  try {
@@ -8376,15 +8379,50 @@ function detectExternalClis() {
8376
8379
  } catch {
8377
8380
  }
8378
8381
  }
8382
+ detected = found;
8379
8383
  return found;
8380
8384
  }
8381
- function claudeInstallArgs(spec2) {
8385
+ var OURS = /\.mcp\.(abz\.run|tryabz\.run|apiblaze\.com)\b/;
8386
+ var mask = (k) => k.length > 14 ? `${k.slice(0, 10)}\u2026${k.slice(-4)}` : "\u2022\u2022\u2022";
8387
+ async function verifyMcpEndpoint(spec2) {
8388
+ try {
8389
+ const headers = { "Content-Type": "application/json" };
8390
+ if (spec2.apiKey) headers["X-API-Key"] = spec2.apiKey;
8391
+ if (spec2.endUserId) headers["X-End-User-Id"] = spec2.endUserId;
8392
+ const res = await fetch(spec2.url, {
8393
+ method: "POST",
8394
+ headers,
8395
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
8396
+ signal: AbortSignal.timeout(1e4)
8397
+ });
8398
+ if (!res.ok) return { ok: false, status: res.status, tools: 0 };
8399
+ const body = await res.json().catch(() => null);
8400
+ if (!spec2.apiKey) return { ok: true, status: res.status, tools: body?.result?.tools?.length ?? 0 };
8401
+ if (body?.error) return { ok: false, status: res.status, tools: 0 };
8402
+ return { ok: true, status: res.status, tools: body?.result?.tools?.length ?? 0 };
8403
+ } catch {
8404
+ return { ok: false, status: 0, tools: 0 };
8405
+ }
8406
+ }
8407
+ function claudeInstallArgs(spec2, masked = false) {
8382
8408
  const args = ["mcp", "add", "--transport", "http", spec2.name, spec2.url];
8383
- if (spec2.apiKey) args.push("--header", `X-API-Key: ${spec2.apiKey}`);
8409
+ if (spec2.apiKey) args.push("--header", `X-API-Key: ${masked ? mask(spec2.apiKey) : spec2.apiKey}`);
8410
+ if (spec2.endUserId) args.push("--header", `X-End-User-Id: ${spec2.endUserId}`);
8384
8411
  return args;
8385
8412
  }
8386
8413
  function installIntoClaude(spec2) {
8387
- run("claude", ["mcp", "remove", spec2.name]);
8414
+ const existing = run("claude", ["mcp", "get", spec2.name]);
8415
+ if (existing.status === 0) {
8416
+ const desc = existing.stdout || "";
8417
+ if (!OURS.test(desc)) {
8418
+ return {
8419
+ ok: false,
8420
+ conflict: true,
8421
+ error: `Claude CLI already has an MCP server named "${spec2.name}" that is not an APIblaze proxy \u2014 not touching it. Remove or rename it (claude mcp remove ${spec2.name}) and re-run.`
8422
+ };
8423
+ }
8424
+ run("claude", ["mcp", "remove", spec2.name]);
8425
+ }
8388
8426
  const r = run("claude", claudeInstallArgs(spec2));
8389
8427
  if (r.status === 0) return { ok: true };
8390
8428
  return { ok: false, error: (r.stderr || r.stdout || `exit ${r.status}`).trim().slice(0, 400) };
@@ -8400,23 +8438,69 @@ function codexConfigPath() {
8400
8438
  var tomlStr = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
8401
8439
  function codexServerBlock(spec2) {
8402
8440
  const lines = [`[mcp_servers.${tomlStr(spec2.name)}]`, `url = ${tomlStr(spec2.url)}`];
8403
- if (spec2.apiKey) lines.push(`http_headers = { "X-API-Key" = ${tomlStr(spec2.apiKey)} }`);
8441
+ const headers = [];
8442
+ if (spec2.apiKey) headers.push(`"X-API-Key" = ${tomlStr(spec2.apiKey)}`);
8443
+ if (spec2.endUserId) headers.push(`"X-End-User-Id" = ${tomlStr(spec2.endUserId)}`);
8444
+ if (headers.length) lines.push(`http_headers = { ${headers.join(", ")} }`);
8404
8445
  return lines.join("\n") + "\n";
8405
8446
  }
8447
+ function codexSectionRanges(lines, name) {
8448
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8449
+ const header = new RegExp(`^\\[mcp_servers\\.(?:"${esc}"|${esc})\\]\\s*(#.*)?$`);
8450
+ const anyHeader = /^\s*\[[^\]]+\]\s*(#.*)?$/;
8451
+ const ranges = [];
8452
+ for (let i = 0; i < lines.length; i++) {
8453
+ if (!header.test(lines[i])) continue;
8454
+ let end = lines.length;
8455
+ for (let j = i + 1; j < lines.length; j++) {
8456
+ if (anyHeader.test(lines[j])) {
8457
+ end = j;
8458
+ break;
8459
+ }
8460
+ }
8461
+ ranges.push({ start: i, end });
8462
+ i = end - 1;
8463
+ }
8464
+ return ranges;
8465
+ }
8406
8466
  function installIntoCodex(spec2) {
8407
8467
  const file = codexConfigPath();
8408
8468
  try {
8409
8469
  fs10.mkdirSync(path6.dirname(file), { recursive: true });
8410
8470
  let text = "";
8471
+ let existed = true;
8411
8472
  try {
8412
8473
  text = fs10.readFileSync(file, "utf-8");
8413
8474
  } catch {
8475
+ existed = false;
8476
+ }
8477
+ const lines = text.split("\n");
8478
+ const ranges = codexSectionRanges(lines, spec2.name);
8479
+ for (const r of ranges) {
8480
+ const body = lines.slice(r.start, r.end).join("\n");
8481
+ const url = body.match(/^\s*url\s*=\s*"([^"]*)"/m)?.[1];
8482
+ if (url && !OURS.test(url) || !url && /^\s*command\s*=/m.test(body)) {
8483
+ return {
8484
+ ok: false,
8485
+ conflict: true,
8486
+ path: file,
8487
+ error: `~/.codex/config.toml already has an MCP server named "${spec2.name}" that is not an APIblaze proxy \u2014 not touching it. Rename or remove that block and re-run.`
8488
+ };
8489
+ }
8490
+ }
8491
+ for (const r of [...ranges].reverse()) lines.splice(r.start, r.end - r.start);
8492
+ let cleaned = lines.join("\n");
8493
+ if (cleaned.length && !cleaned.endsWith("\n")) cleaned += "\n";
8494
+ if (cleaned.length && !cleaned.endsWith("\n\n")) cleaned += "\n";
8495
+ const tmp = `${file}.tmp-${process.pid}`;
8496
+ fs10.writeFileSync(tmp, cleaned + codexServerBlock(spec2), { encoding: "utf-8", mode: 384 });
8497
+ fs10.renameSync(tmp, file);
8498
+ if (!existed) {
8499
+ try {
8500
+ fs10.chmodSync(file, 384);
8501
+ } catch {
8502
+ }
8414
8503
  }
8415
- const esc = spec2.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8416
- const section = new RegExp(`(^|\\n)\\[mcp_servers\\.(?:"${esc}"|${esc})\\][^\\[]*`, "g");
8417
- const cleaned = text.replace(section, "$1");
8418
- const sep = cleaned.length && !cleaned.endsWith("\n\n") ? cleaned.endsWith("\n") ? "\n" : "\n\n" : "";
8419
- fs10.writeFileSync(file, cleaned + sep + codexServerBlock(spec2), "utf-8");
8420
8504
  return { ok: true, path: file };
8421
8505
  } catch (err) {
8422
8506
  return { ok: false, error: err instanceof Error ? err.message : String(err), path: file };
@@ -8431,44 +8515,64 @@ var shellQuote = (s) => `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
8431
8515
  function renderCommand(argv) {
8432
8516
  return argv.map((a, i) => i === 0 || /^[A-Za-z0-9_@%+=:,./-]+$/.test(a) ? a : shellQuote(a)).join(" ");
8433
8517
  }
8434
- function installAndDemo(cli, spec2, question, log = console.log) {
8518
+ function refreshInstall(cli, spec2) {
8519
+ const r = cli === "claude" ? installIntoClaude(spec2) : installIntoCodex(spec2);
8520
+ return r.ok;
8521
+ }
8522
+ async function installAndDemo(cli, spec2, getQuestion, log = console.log) {
8435
8523
  if (cli.kind === "claude") {
8436
8524
  log(`
8437
- ${import_chalk46.default.dim("$")} ${renderCommand(["claude", ...claudeInstallArgs(spec2)])}`);
8525
+ ${import_chalk46.default.dim("$")} ${renderCommand(["claude", ...claudeInstallArgs(spec2, true)])}`);
8438
8526
  const r = installIntoClaude(spec2);
8439
8527
  if (!r.ok) {
8440
- log(import_chalk46.default.red(` Install failed: ${r.error}`));
8528
+ log(import_chalk46.default.red(` ${r.conflict ? "" : "Install failed: "}${r.error}`));
8441
8529
  return false;
8442
8530
  }
8443
8531
  log(` ${import_chalk46.default.green("\u2714")} MCP ${import_chalk46.default.bold(spec2.name)} added to Claude CLI (local scope \u2014 this directory).`);
8532
+ if (spec2.apiKey) log(import_chalk46.default.dim(" The API key is stored in Claude's local MCP config for this directory."));
8444
8533
  } else {
8445
8534
  const r = installIntoCodex(spec2);
8446
8535
  if (!r.ok) {
8447
- log(import_chalk46.default.red(` Could not write ${r.path}: ${r.error}`));
8536
+ log(import_chalk46.default.red(` ${r.conflict ? "" : `Could not write ${r.path}: `}${r.error}`));
8448
8537
  return false;
8449
8538
  }
8450
8539
  log(` ${import_chalk46.default.green("\u2714")} MCP ${import_chalk46.default.bold(spec2.name)} added to ${r.path}.`);
8451
8540
  }
8452
8541
  if (!spec2.apiKey) {
8453
- log(import_chalk46.default.dim(` This proxy authenticates by login: the first call from ${cli.label} will open its sign-in.`));
8542
+ const loginArgv = cli.kind === "claude" ? ["claude", "mcp", "login", spec2.name] : ["codex", "mcp", "login", spec2.name];
8454
8543
  log(`
8455
- ${import_chalk46.default.bold(`Your ${cli.label} is now able to talk to the ${spec2.projectLabel} API.`)}`);
8456
- return true;
8544
+ ${import_chalk46.default.dim("This proxy authenticates by login \u2014 signing you in:")}`);
8545
+ log(` ${import_chalk46.default.dim("$")} ${renderCommand(loginArgv)}`);
8546
+ const r = run(loginArgv[0], loginArgv.slice(1), { interactive: true });
8547
+ if (r.status !== 0) {
8548
+ log(import_chalk46.default.yellow(` Sign-in didn't complete (exit ${r.status ?? "?"}). Run \`${renderCommand(loginArgv)}\` yourself, then chat away.`));
8549
+ return true;
8550
+ }
8551
+ log(` ${import_chalk46.default.green("\u2714")} Signed in.`);
8457
8552
  }
8458
8553
  const oneShot = cli.kind === "claude" ? claudeOneShot : codexOneShot;
8459
8554
  const abilities = `What are the tool abilities of the MCP server "${spec2.name}"? List them briefly.`;
8460
8555
  log(`
8461
- ${import_chalk46.default.dim("Checking what the MCP exposes\u2026")}`);
8556
+ ${import_chalk46.default.dim("Checking what the API can do\u2026")}`);
8462
8557
  log(` ${import_chalk46.default.dim("$")} ${renderCommand(cli.kind === "claude" ? ["claude", "-p", abilities, "--allowedTools", `mcp__${spec2.name}__*`] : ["codex", "exec", abilities])}
8463
8558
  `);
8464
- oneShot(spec2, abilities);
8559
+ const check = oneShot(spec2, abilities);
8560
+ if (check.status !== 0) {
8561
+ log(import_chalk46.default.yellow(`
8562
+ ${cli.label} exited with ${check.status ?? "no status"} \u2014 the MCP is installed, but the demo call failed.`));
8563
+ log(import_chalk46.default.yellow(` Open ${cli.label} and try it there${cli.kind === "claude" ? " (use /mcp to inspect the connection)" : ""}.`));
8564
+ return true;
8565
+ }
8566
+ const question = await getQuestion();
8465
8567
  if (question) {
8466
8568
  log(`
8467
8569
  ${import_chalk46.default.dim("Your question, through " + cli.label + ":")}`);
8468
8570
  const shown = cli.kind === "claude" ? ["claude", "-p", question, "--allowedTools", `mcp__${spec2.name}__*`] : ["codex", "exec", question];
8469
8571
  log(` ${import_chalk46.default.dim("$")} ${renderCommand(shown)}
8470
8572
  `);
8471
- oneShot(spec2, question);
8573
+ const ans = oneShot(spec2, question);
8574
+ if (ans.status !== 0) log(import_chalk46.default.yellow(`
8575
+ ${cli.label} exited with ${ans.status ?? "no status"} on that one \u2014 the MCP stays installed.`));
8472
8576
  }
8473
8577
  log(`
8474
8578
  ${import_chalk46.default.bold(`Your ${cli.label} is now able to talk to the ${spec2.projectLabel} API.`)}`);
@@ -9694,6 +9798,14 @@ async function runRepl(p, initialMessages) {
9694
9798
  p.anon = false;
9695
9799
  claimApichat(p, loadCredentials()?.apiblazeUserId);
9696
9800
  console.log(import_chalk47.default.dim(` Workspace claimed \u2014 chat now routes on ${p.mcpHost}. History preserved.`));
9801
+ const entry = loadApichats().find((a) => apichatKey(a) === apichatKey(p));
9802
+ for (const [cli, state] of Object.entries(entry?.cliOffer ?? {})) {
9803
+ if (state === "installed" && (cli === "claude" || cli === "codex")) {
9804
+ if (refreshInstall(cli, buildInstallSpec(p))) {
9805
+ console.log(import_chalk47.default.dim(` ${cli === "claude" ? "Claude" : "Codex"} CLI's MCP entry updated to the new host.`));
9806
+ }
9807
+ }
9808
+ }
9697
9809
  }
9698
9810
  } catch (err) {
9699
9811
  console.log(import_chalk47.default.red(` Claim failed: ${err instanceof Error ? err.message : String(err)}`));
@@ -9714,61 +9826,137 @@ async function runRepl(p, initialMessages) {
9714
9826
  }
9715
9827
  console.log(import_chalk47.default.dim("\nBye."));
9716
9828
  }
9717
- function rememberCliOffer(projectId, cli, state) {
9829
+ function offerKey(p) {
9830
+ return apichatKey(p);
9831
+ }
9832
+ function rememberCliOffer(p, cli, state) {
9718
9833
  const list = loadApichats();
9719
- const i = list.findIndex((a) => a.projectId === projectId);
9834
+ const i = list.findIndex((a) => apichatKey(a) === offerKey(p));
9720
9835
  if (i < 0) return;
9721
9836
  list[i].cliOffer = { ...list[i].cliOffer ?? {}, [cli]: state };
9722
9837
  list[i].updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9723
9838
  writeApichats(list);
9724
9839
  }
9725
- async function maybeInstallExternalCli(p, opts) {
9726
- const spec2 = {
9840
+ function buildInstallSpec(p) {
9841
+ return {
9727
9842
  name: p.projectId,
9728
9843
  url: `https://${p.mcpHost}/${p.version}/${p.environment}`,
9729
9844
  // consumerAuth means the door is a login — install bare, the CLI signs in.
9730
9845
  apiKey: p.consumerAuth ? void 0 : p.dpKey,
9846
+ endUserId: p.endUserId,
9731
9847
  projectLabel: p.projectId
9732
9848
  };
9733
- const clis = detectExternalClis();
9734
- if (opts.installMcp) {
9735
- const want = opts.installMcp.toLowerCase();
9736
- if (want !== "claude" && want !== "codex") fail4(`--install-mcp takes "claude" or "codex", not "${opts.installMcp}".`);
9737
- const cli = clis.find((c) => c.kind === want);
9849
+ }
9850
+ async function verifyAndHealMcpHost(p) {
9851
+ const ok = (await verifyMcpEndpoint(buildInstallSpec(p))).ok;
9852
+ if (ok) return true;
9853
+ const flipped = p.mcpHost.includes(".mcp.tryabz.run") ? p.mcpHost.replace(".mcp.tryabz.run", ".mcp.abz.run") : p.mcpHost.replace(".mcp.abz.run", ".mcp.tryabz.run");
9854
+ if (flipped === p.mcpHost) return false;
9855
+ const prev = p.mcpHost;
9856
+ p.mcpHost = flipped;
9857
+ if ((await verifyMcpEndpoint(buildInstallSpec(p))).ok) {
9858
+ const list = loadApichats();
9859
+ const i = list.findIndex((a) => apichatKey(a) === offerKey(p));
9860
+ if (i >= 0) {
9861
+ list[i].mcpHost = flipped;
9862
+ list[i].updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9863
+ writeApichats(list);
9864
+ }
9865
+ return true;
9866
+ }
9867
+ p.mcpHost = prev;
9868
+ return false;
9869
+ }
9870
+ async function maybeInstallExternalCli(p, opts) {
9871
+ const forced = (opts.installMcp ?? "").toLowerCase();
9872
+ if (forced && forced !== "claude" && forced !== "codex") {
9873
+ fail4(`--install-mcp takes "claude" or "codex", not "${opts.installMcp}".`);
9874
+ }
9875
+ if (!forced && !process.stdin.isTTY) return false;
9876
+ const offer = loadApichats().find((a) => apichatKey(a) === offerKey(p))?.cliOffer ?? {};
9877
+ if (forced) {
9878
+ const cli = detectExternalClis().find((c) => c.kind === forced);
9738
9879
  if (!cli) fail4(
9739
- `${want === "claude" ? "Claude" : "Codex"} CLI not found on this machine.`,
9740
- want === "claude" ? "Install it: npm install -g @anthropic-ai/claude-code" : "Install it: npm install -g @openai/codex"
9880
+ `${forced === "claude" ? "Claude" : "Codex"} CLI not found on this machine.`,
9881
+ forced === "claude" ? "Install it: npm install -g @anthropic-ai/claude-code" : "Install it: npm install -g @openai/codex"
9741
9882
  );
9742
- const question2 = opts.prompt ?? (process.stdin.isTTY ? await askApiQuestion() : void 0);
9743
- const ran2 = installAndDemo(cli, spec2, question2);
9744
- if (ran2) rememberCliOffer(p.projectId, cli.kind, "installed");
9883
+ if (!await ensureInstallableDoor(p, opts)) return false;
9884
+ if (!await verifyAndHealMcpHost(p)) {
9885
+ fail4(`The MCP endpoint https://${p.mcpHost}/${p.version}/${p.environment} is not answering \u2014 not installing it into ${cli.label}.`);
9886
+ }
9887
+ const ran2 = await installAndDemo(cli, buildInstallSpec(p), () => resolveQuestion(opts));
9888
+ if (ran2) rememberCliOffer(p, cli.kind, "installed");
9745
9889
  return ran2;
9746
9890
  }
9747
- if (!process.stdin.isTTY || clis.length === 0) return false;
9748
- const offer = loadApichats().find((a) => a.projectId === p.projectId)?.cliOffer ?? {};
9891
+ const clis = detectExternalClis();
9892
+ if (clis.length === 0) return false;
9893
+ for (const c of clis) {
9894
+ if (offer[c.kind] === "installed" && await verifyAndHealMcpHost(p)) {
9895
+ refreshInstall(c.kind, buildInstallSpec(p));
9896
+ }
9897
+ }
9749
9898
  const fresh = clis.filter((c) => !offer[c.kind]);
9750
9899
  if (fresh.length === 0) return false;
9751
9900
  const { default: inquirer3 } = await import("inquirer");
9752
9901
  const names = fresh.map((c) => c.label).join(" and ");
9902
+ const targetName = fresh.length > 1 ? "one of them" : fresh[0].label;
9753
9903
  const { pick: pick2 } = await inquirer3.prompt([{
9754
9904
  type: "list",
9755
9905
  name: "pick",
9756
- message: `I see ${names} ${fresh.length > 1 ? "are" : "is"} installed on this computer. Add the MCP for this proxy so you can chat with your API from there directly?`,
9906
+ message: `I see ${names} ${fresh.length > 1 ? "are" : "is"} installed on this computer. Do you want to add the MCP for this proxy to ${targetName} so you can chat with your API from there directly, or chat here directly?`,
9757
9907
  choices: [
9758
- ...fresh.map((c) => ({ name: `Yes \u2014 add it to ${c.label}`, value: c })),
9759
- { name: "No \u2014 chat here instead", value: "no" }
9908
+ ...fresh.map((c) => ({ name: c.label, value: c })),
9909
+ { name: "Chat here directly", value: "here" }
9760
9910
  ]
9761
9911
  }]);
9762
- if (pick2 === "no") {
9763
- for (const c of fresh) rememberCliOffer(p.projectId, c.kind, "declined");
9912
+ if (pick2 === "here") {
9913
+ for (const c of fresh) rememberCliOffer(p, c.kind, "declined");
9914
+ if (p.consumerAuth && p.teamId && p.tenant) {
9915
+ try {
9916
+ await ensureConsumerLogin(p.teamId, p.tenant, p.version);
9917
+ } catch (err) {
9918
+ console.log(import_chalk47.default.yellow(` Sign-in didn't complete (${err instanceof Error ? err.message : String(err)}) \u2014 the first chat turn will retry it.`));
9919
+ }
9920
+ }
9764
9921
  return false;
9765
9922
  }
9766
- const question = opts.prompt ?? await askApiQuestion();
9767
- const ran = installAndDemo(pick2, spec2, question);
9768
- if (ran) rememberCliOffer(p.projectId, pick2.kind, "installed");
9923
+ if (!await ensureInstallableDoor(p, opts)) return false;
9924
+ if (!await verifyAndHealMcpHost(p)) {
9925
+ console.log(import_chalk47.default.red(` The MCP endpoint https://${p.mcpHost}/${p.version}/${p.environment} is not answering \u2014 not installing it into ${pick2.label}. Chat here instead.`));
9926
+ return false;
9927
+ }
9928
+ const ran = await installAndDemo(pick2, buildInstallSpec(p), () => resolveQuestion(opts));
9929
+ if (ran) {
9930
+ rememberCliOffer(p, pick2.kind, "installed");
9931
+ if (p.anon) {
9932
+ console.log(import_chalk47.default.dim(` Anonymous workspace \u2014 run \`apiblaze apichat ${p.projectId}\` and /claim to keep it (and this MCP) beyond 30 days.`));
9933
+ }
9934
+ }
9769
9935
  return ran;
9770
9936
  }
9771
- async function askApiQuestion() {
9937
+ async function ensureInstallableDoor(p, opts) {
9938
+ if (p.dpKey || p.consumerAuth) return true;
9939
+ if (!process.stdin.isTTY) {
9940
+ fail4(
9941
+ `Can't tell how "${p.projectId}" authenticates (no key on file).`,
9942
+ "Pass --apikey <key> for a key-door proxy, or open it interactively once first."
9943
+ );
9944
+ }
9945
+ const { default: inquirer3 } = await import("inquirer");
9946
+ const { key } = await inquirer3.prompt([{
9947
+ type: "password",
9948
+ name: "key",
9949
+ mask: "*",
9950
+ message: `API key for ${p.projectId} (leave empty if it uses a login):`
9951
+ }]);
9952
+ if (typeof key === "string" && key.trim()) p.dpKey = key.trim();
9953
+ else p.consumerAuth = true;
9954
+ void opts;
9955
+ return true;
9956
+ }
9957
+ async function resolveQuestion(opts) {
9958
+ if (opts.prompt) return opts.prompt;
9959
+ if (!process.stdin.isTTY) return void 0;
9772
9960
  const { default: inquirer3 } = await import("inquirer");
9773
9961
  const { q } = await inquirer3.prompt([{
9774
9962
  type: "input",
@@ -9778,8 +9966,18 @@ async function askApiQuestion() {
9778
9966
  const t = (q ?? "").trim();
9779
9967
  return t || void 0;
9780
9968
  }
9969
+ async function startChat(p, messages, opts) {
9970
+ if (opts.prompt) {
9971
+ await replTurn(p, messages, opts.prompt);
9972
+ saveTranscript(p, messages);
9973
+ if (!process.stdin.isTTY) return;
9974
+ } else if (!process.stdin.isTTY) {
9975
+ fail4('Interactive chat needs a terminal. Pass -p "<question>" for a one-shot answer.');
9976
+ }
9977
+ await runRepl(p, messages);
9978
+ }
9781
9979
  async function runApichat(opts) {
9782
- setVerbose(opts.verbose !== false);
9980
+ setVerbose(opts.verbose === true);
9783
9981
  console.log(import_chalk47.default.bold("\napichat \u2014 turn any API into a chat\n"));
9784
9982
  if (opts.target && !opts.openapispec) {
9785
9983
  const { classifyTargetInput: classifyTargetInput2 } = await Promise.resolve().then(() => (init_spec_or_target(), spec_or_target_exports));
@@ -9793,7 +9991,7 @@ async function runApichat(opts) {
9793
9991
  if (opts.project) {
9794
9992
  const opened = await openDirectProject(opts.project, opts);
9795
9993
  if (await maybeInstallExternalCli(opened.p, opts)) return;
9796
- await runRepl(opened.p, opened.messages);
9994
+ await startChat(opened.p, opened.messages, opts);
9797
9995
  return;
9798
9996
  }
9799
9997
  if (!opts.openapispec && !opts.target) {
@@ -9803,7 +10001,7 @@ async function runApichat(opts) {
9803
10001
  const resumed = await noArgsMenu(opts);
9804
10002
  if (resumed) {
9805
10003
  if (await maybeInstallExternalCli(resumed.p, opts)) return;
9806
- await runRepl(resumed.p, resumed.messages);
10004
+ await startChat(resumed.p, resumed.messages, opts);
9807
10005
  return;
9808
10006
  }
9809
10007
  }
@@ -9857,8 +10055,8 @@ async function runApichat(opts) {
9857
10055
  if (p.anon) {
9858
10056
  console.log(import_chalk47.default.dim("\n Anonymous workspace \u2014 /claim inside the chat to log in and keep it beyond 30 days."));
9859
10057
  }
9860
- if (await maybeInstallExternalCli(p, opts)) return;
9861
- await runRepl(p);
10058
+ if (mcpUrl && await maybeInstallExternalCli(p, opts)) return;
10059
+ await startChat(p, [], opts);
9862
10060
  }
9863
10061
 
9864
10062
  // src/commands/consumer.ts
@@ -10243,14 +10441,14 @@ async function runAnonymousInit(root, router, opts) {
10243
10441
  }
10244
10442
  async function runSidecar(opts) {
10245
10443
  const root = path8.resolve(opts.dir ?? process.cwd());
10246
- const detected = detectNextProject(root);
10247
- if (!detected.found) {
10444
+ const detected2 = detectNextProject(root);
10445
+ if (!detected2.found) {
10248
10446
  console.log(import_chalk49.default.yellow(`No Next.js project detected in ${root}.`));
10249
10447
  console.log("Create one (e.g. `npx create-next-app`) and re-run `apiblaze init` inside it.");
10250
10448
  return;
10251
10449
  }
10252
10450
  if (!loadCredentials() && !readEnvKey(root)) {
10253
- await runAnonymousInit(root, detected.router, opts);
10451
+ await runAnonymousInit(root, detected2.router, opts);
10254
10452
  return;
10255
10453
  }
10256
10454
  if (!loadCredentials()) {
@@ -10292,7 +10490,7 @@ async function runSidecar(opts) {
10292
10490
  installSidecarPackage(root);
10293
10491
  let inspectorPath = null;
10294
10492
  if (!opts.noInspector) {
10295
- inspectorPath = generateInspector(root, detected.router);
10493
+ inspectorPath = generateInspector(root, detected2.router);
10296
10494
  if (inspectorPath) console.log(` ${import_chalk49.default.green("\u2713")} inspector at ${inspectorPath}`);
10297
10495
  }
10298
10496
  console.log("");
@@ -11079,7 +11277,7 @@ agent.command("authz").description("Chat to design and turn on access rules for
11079
11277
  program.command("rule").description("Author an object-level access rule in plain English, in one shot (billed per turn)").argument("<rule>", 'The rule in plain English, e.g. "users see only their own rows"').argument("<project>", "Project name or id").option("--enforce", "Turn enforcement on immediately (default: shadow-publish only)").option("--apiversion <version>", "API version (defaults to the project's)").action(action((rule, project, opts) => runRule(rule, project, opts)));
11080
11278
  agent.command("openapi").description("Chat to build your API spec from real traffic").argument("<project>", "Project name or id").argument("[apiVersion]", "API version (defaults to the project's)").action(action((project, apiVersion) => runOpenapi(project, apiVersion)));
11081
11279
  agent.command("mcp").description("Chat to build an MCP server for an API").argument("<project>", "Project name or id").argument("[apiVersion]", "API version (defaults to the project's)").option("--environment <env>", "Environment to publish (default: prod)").action(action((project, apiVersion, opts) => runMcp(project, apiVersion, opts)));
11082
- program.command("apichat [project]").description("Turn any API into a chat: point at an OpenAPI spec \u2014 or chat an EXISTING proxy by name (no login needed)").option("--target <url|file>", "What to chat with \u2014 pass ANY of: a target server base URL (spec auto-discovered at /openapi.json etc.), a local OpenAPI file (./openapi.yaml), or a remote OpenAPI URL (https://acme.com/openapi.yaml)").addOption(new import_commander.Option("--openapi <file|url>", "Deprecated alias \u2014 --target now detects spec files/URLs itself").hideHelp()).addOption(new import_commander.Option("--openapispec <file|url>", "Deprecated alias for --openapi").hideHelp()).option("--name <name>", "Proxy name (defaults to the target host)").option("--apiversion <version>", "API version to create (e.g. 1.0.0)").option("--environment <env>", "Environment to chat against (default: prod anonymous / dev logged-in)").option("--access <mode>", 'Who can call this API once connected (e.g. via Claude): "open" = anyone who signs in, "invite" = only you + emails you pre-approve. Default: invite when logged in, open when anonymous.').option("--target-auth-env <ENV_VAR>", "Read the upstream credential from this env var (CI-safe; required when there is no TTY and the API needs auth)").option("--force", "Proceed even if the API uses oauth2/openIdConnect target auth (you configure target auth yourself later)").option("-y, --yes", "Skip confirmation prompts").option("--tenant <slug>", "Tenant (consumer namespace: portal, login, users) for the new proxy; omit to be asked").option("--apikey <key>", "Use this API key for the proxy's door (api_key proxies). Without it, apichat detects the door and asks \u2014 or runs the consumer login for OAuth doors.").option("--xenduserid <id>", "Assert this end-user id (X-End-User-Id) \u2014 required by proxies with identified/pre-approved enforcement; you are asked for one when the proxy demands it.").option("--no-verbose", "Hide the per-turn proxy curl trace (shown by default for apichat)").option("-p, --prompt <question>", "One-shot question piped through the external agent CLI after the MCP install (used with --install-mcp or the install offer)").option("--install-mcp <cli>", "Install this proxy's MCP into an external agent CLI without asking: claude | codex. Also re-offers after an earlier decline.").action(action((project, opts) => runApichat({ ...opts, project, openapispec: opts.openapispec ?? opts.openapi })));
11280
+ program.command("apichat [project]").description("Turn any API into a chat: point at an OpenAPI spec \u2014 or chat an EXISTING proxy by name (no login needed)").option("--target <url|file>", "What to chat with \u2014 pass ANY of: a target server base URL (spec auto-discovered at /openapi.json etc.), a local OpenAPI file (./openapi.yaml), or a remote OpenAPI URL (https://acme.com/openapi.yaml)").addOption(new import_commander.Option("--openapi <file|url>", "Deprecated alias \u2014 --target now detects spec files/URLs itself").hideHelp()).addOption(new import_commander.Option("--openapispec <file|url>", "Deprecated alias for --openapi").hideHelp()).option("--name <name>", "Proxy name (defaults to the target host)").option("--apiversion <version>", "API version to create (e.g. 1.0.0)").option("--environment <env>", "Environment to chat against (default: prod anonymous / dev logged-in)").option("--access <mode>", 'Who can call this API once connected (e.g. via Claude): "open" = anyone who signs in, "invite" = only you + emails you pre-approve. Default: invite when logged in, open when anonymous.').option("--target-auth-env <ENV_VAR>", "Read the upstream credential from this env var (CI-safe; required when there is no TTY and the API needs auth)").option("--force", "Proceed even if the API uses oauth2/openIdConnect target auth (you configure target auth yourself later)").option("-y, --yes", "Skip confirmation prompts").option("--tenant <slug>", "Tenant (consumer namespace: portal, login, users) for the new proxy; omit to be asked").option("--apikey <key>", "Use this API key for the proxy's door (api_key proxies). Without it, apichat detects the door and asks \u2014 or runs the consumer login for OAuth doors.").option("--xenduserid <id>", "Assert this end-user id (X-End-User-Id) \u2014 required by proxies with identified/pre-approved enforcement; you are asked for one when the proxy demands it.").option("--verbose", "Show the per-turn proxy curl trace (hidden by default)").option("-p, --prompt <question>", "One-shot question: answered through the external agent CLI after an MCP install, or by apichat itself (exits after answering when there is no TTY)").option("--install-mcp <cli>", "Install this proxy's MCP into an external agent CLI without asking: claude | codex. Also re-offers after an earlier decline.").action(action((project, opts) => runApichat({ ...opts, project, openapispec: opts.openapispec ?? opts.openapi })));
11083
11281
  var llm = program.command("llm").description("Manage a local LLM provider key for chat (optional \u2014 lifts model quality, bills your key)");
11084
11282
  llm.command("set-key").description("Store an LLM provider key locally (OpenRouter/Anthropic/DeepSeek/OpenAI)").argument("[key]", "The API key (omit to enter it hidden at a prompt)").option("--model <id>", "Model id to use with this key (e.g. anthropic/claude-haiku-4.5)").action(action((key, opts) => runLlmSetKey(key, opts)));
11085
11283
  llm.command("show").description("Show the locally stored LLM key (masked)").action(action(() => runLlmShow()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.20.11",
3
+ "version": "0.20.13",
4
4
  "description": "APIblaze CLI — Chat with your APIs, Manage your API keys, users and groups with the APIblaze serverless proxy",
5
5
  "keywords": [
6
6
  "apiblaze",