apiblaze 0.20.28 → 0.20.34

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 +214 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1024,7 +1024,7 @@ var import_commander = require("commander");
1024
1024
  var import_chalk54 = __toESM(require("chalk"));
1025
1025
 
1026
1026
  // package.json
1027
- var version = "0.20.28";
1027
+ var version = "0.20.33";
1028
1028
 
1029
1029
  // src/index.ts
1030
1030
  init_types();
@@ -2102,6 +2102,7 @@ function suggestTenantName(projectSlug, taken) {
2102
2102
  var fs8 = __toESM(require("fs"));
2103
2103
  var path6 = __toESM(require("path"));
2104
2104
  var crypto2 = __toESM(require("crypto"));
2105
+ var import_child_process3 = require("child_process");
2105
2106
  var import_chalk14 = __toESM(require("chalk"));
2106
2107
  var import_ora4 = __toESM(require("ora"));
2107
2108
  var import_yaml2 = require("yaml");
@@ -2414,7 +2415,7 @@ function claudeOneShot(spec2, prompt) {
2414
2415
  return { argv, status: r.status };
2415
2416
  }
2416
2417
  function claudeInteractive(spec2, prompt) {
2417
- const argv = ["claude", prompt, "--allowedTools", `mcp__${spec2.name}__*`];
2418
+ const argv = ["claude", prompt, "--allowedTools", `mcp__${spec2.name}__*`, "--append-system-prompt", claudeSystemHint(spec2)];
2418
2419
  const r = run(argv[0], argv.slice(1), { interactive: true });
2419
2420
  return { argv, status: r.status };
2420
2421
  }
@@ -2492,6 +2493,75 @@ function installIntoCodex(spec2) {
2492
2493
  return { ok: false, error: err instanceof Error ? err.message : String(err), path: file };
2493
2494
  }
2494
2495
  }
2496
+ function codexSiblingApiblazeServers(keepName) {
2497
+ try {
2498
+ const text = fs5.readFileSync(codexConfigPath(), "utf-8");
2499
+ const lines = text.split("\n");
2500
+ const out = [];
2501
+ const header = /^\[mcp_servers\.(?:"([^"]+)"|([^\]]+))\]\s*(#.*)?$/;
2502
+ const anyHeader = /^\s*\[[^\]]+\]\s*(#.*)?$/;
2503
+ for (let i = 0; i < lines.length; i++) {
2504
+ const m = lines[i].match(header);
2505
+ if (!m) continue;
2506
+ const name = m[1] ?? m[2];
2507
+ let end = lines.length;
2508
+ for (let j = i + 1; j < lines.length; j++) {
2509
+ if (anyHeader.test(lines[j])) {
2510
+ end = j;
2511
+ break;
2512
+ }
2513
+ }
2514
+ const body = lines.slice(i, end).join("\n");
2515
+ const url = body.match(/^\s*url\s*=\s*"([^"]*)"/m)?.[1];
2516
+ if (name && name !== keepName && url && OURS.test(url)) out.push(name);
2517
+ i = end - 1;
2518
+ }
2519
+ return out;
2520
+ } catch {
2521
+ return [];
2522
+ }
2523
+ }
2524
+ async function mcpEndpointIsDead(url) {
2525
+ try {
2526
+ const res = await fetch(url, {
2527
+ method: "POST",
2528
+ headers: { "Content-Type": "application/json" },
2529
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
2530
+ signal: AbortSignal.timeout(8e3)
2531
+ });
2532
+ if (res.status === 401 || res.status === 403) return false;
2533
+ if (res.status === 404 || res.status >= 500) return true;
2534
+ return false;
2535
+ } catch {
2536
+ return false;
2537
+ }
2538
+ }
2539
+ function codexServerUrl(name) {
2540
+ try {
2541
+ const lines = fs5.readFileSync(codexConfigPath(), "utf-8").split("\n");
2542
+ const ranges = codexSectionRanges(lines, name);
2543
+ if (!ranges.length) return null;
2544
+ const body = lines.slice(ranges[0].start, ranges[0].end).join("\n");
2545
+ return body.match(/^\s*url\s*=\s*"([^"]*)"/m)?.[1] ?? null;
2546
+ } catch {
2547
+ return null;
2548
+ }
2549
+ }
2550
+ function removeCodexServer(name) {
2551
+ try {
2552
+ const file = codexConfigPath();
2553
+ const lines = fs5.readFileSync(file, "utf-8").split("\n");
2554
+ const ranges = codexSectionRanges(lines, name);
2555
+ if (!ranges.length) return false;
2556
+ for (const r of [...ranges].reverse()) lines.splice(r.start, r.end - r.start);
2557
+ const tmp = `${file}.tmp-${process.pid}`;
2558
+ fs5.writeFileSync(tmp, lines.join("\n"), { encoding: "utf-8", mode: 384 });
2559
+ fs5.renameSync(tmp, file);
2560
+ return true;
2561
+ } catch {
2562
+ return false;
2563
+ }
2564
+ }
2495
2565
  function codexOneShot(_spec, prompt) {
2496
2566
  const argv = ["codex", "exec", "--skip-git-repo-check", prompt];
2497
2567
  const r = run(argv[0], argv.slice(1), { inherit: true });
@@ -2503,6 +2573,73 @@ function codexInteractive(_spec, prompt) {
2503
2573
  return { argv, status: r.status };
2504
2574
  }
2505
2575
  var shellQuote = (s) => `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
2576
+ function directedPrompt(spec2, question) {
2577
+ return `Use the "${spec2.name}" MCP server (it exposes this API's tools) to answer: ${question}`;
2578
+ }
2579
+ function agentInstructionFile(cli, dir = process.cwd()) {
2580
+ return path4.join(dir, cli === "claude" ? "CLAUDE.md" : "AGENTS.md");
2581
+ }
2582
+ var marker = (name) => ({
2583
+ start: `<!-- apiblaze:mcp:${name} -->`,
2584
+ end: `<!-- /apiblaze:mcp:${name} -->`
2585
+ });
2586
+ function stripMarkedBlock(text, name) {
2587
+ const m = marker(name);
2588
+ for (; ; ) {
2589
+ const i = text.indexOf(m.start);
2590
+ if (i < 0) return text;
2591
+ const j = text.indexOf(m.end, i);
2592
+ if (j < 0) return text.slice(0, i);
2593
+ let k = j + m.end.length;
2594
+ if (text[k] === "\n") k++;
2595
+ text = text.slice(0, i) + text.slice(k);
2596
+ }
2597
+ }
2598
+ function writeAgentInstruction(cli, spec2, dir = process.cwd()) {
2599
+ try {
2600
+ const file = agentInstructionFile(cli, dir);
2601
+ const m = marker(spec2.name);
2602
+ const block = [
2603
+ m.start,
2604
+ `## ${spec2.projectLabel} API \u2014 use the "${spec2.name}" MCP server`,
2605
+ "",
2606
+ `This directory is connected to the \`${spec2.name}\` MCP server, which exposes the`,
2607
+ `${spec2.projectLabel} API as tools. For any question about this API \u2014 its data, its`,
2608
+ "records, what it can do \u2014 call those tools rather than answering from memory or",
2609
+ "asking for data the API can provide.",
2610
+ "",
2611
+ `_Added by \`apiblaze apichat\`. Remove this block, or run \`npx apiblaze apichat ${spec2.name} --remove-mcp ${cli}\`._`,
2612
+ m.end,
2613
+ ""
2614
+ ].join("\n");
2615
+ let text = "";
2616
+ try {
2617
+ text = fs5.readFileSync(file, "utf-8");
2618
+ } catch {
2619
+ }
2620
+ const cleaned = stripMarkedBlock(text, spec2.name);
2621
+ const sep = cleaned.length && !cleaned.endsWith("\n\n") ? cleaned.endsWith("\n") ? "\n" : "\n\n" : "";
2622
+ fs5.writeFileSync(file, cleaned + sep + block, "utf-8");
2623
+ return file;
2624
+ } catch {
2625
+ return null;
2626
+ }
2627
+ }
2628
+ function removeAgentInstruction(cli, name, dir = process.cwd()) {
2629
+ try {
2630
+ const file = agentInstructionFile(cli, dir);
2631
+ const text = fs5.readFileSync(file, "utf-8");
2632
+ const cleaned = stripMarkedBlock(text, name);
2633
+ if (cleaned === text) return false;
2634
+ fs5.writeFileSync(file, cleaned.replace(/\n{3,}/g, "\n\n"), "utf-8");
2635
+ return true;
2636
+ } catch {
2637
+ return false;
2638
+ }
2639
+ }
2640
+ function claudeSystemHint(spec2) {
2641
+ return `You are connected to the "${spec2.name}" MCP server, which exposes the ${spec2.projectLabel} API as tools (mcp__${spec2.name}__*). For any question about this API, call those tools rather than answering from memory or asking the user for data the API can provide.`;
2642
+ }
2506
2643
  function renderCommand(argv) {
2507
2644
  return argv.map((a, i) => i === 0 || /^[A-Za-z0-9_@%+=:,./-]+$/.test(a) ? a : shellQuote(a)).join(" ");
2508
2645
  }
@@ -2575,15 +2712,28 @@ async function installAndDemo(cli, spec2, getQuestion, log = console.log) {
2575
2712
  const ask = handoff ? cli.kind === "claude" ? claudeInteractive : codexInteractive : oneShot;
2576
2713
  log(`
2577
2714
  ${import_chalk12.default.dim(handoff ? `Opening ${cli.label} with your question \u2014 you'll stay in that session:` : `Your question, through ${cli.label}:`)}`);
2578
- const shown = cli.kind === "claude" ? handoff ? ["claude", question, "--allowedTools", `mcp__${spec2.name}__*`] : ["claude", "-p", question, "--allowedTools", `mcp__${spec2.name}__*`] : handoff ? ["codex", question] : ["codex", "exec", "--skip-git-repo-check", question];
2715
+ const directed = directedPrompt(spec2, question);
2716
+ const shown = cli.kind === "claude" ? handoff ? ["claude", directed, "--allowedTools", `mcp__${spec2.name}__*`, "--append-system-prompt", "\u2026"] : ["claude", "-p", directed, "--allowedTools", `mcp__${spec2.name}__*`] : handoff ? ["codex", directed] : ["codex", "exec", "--skip-git-repo-check", directed];
2579
2717
  log(` ${import_chalk12.default.dim("$")} ${renderCommand(shown)}
2580
2718
  `);
2581
- const ans = ask(spec2, question);
2719
+ const ans = ask(spec2, directedPrompt(spec2, question));
2582
2720
  if (ans.status !== 0) log(import_chalk12.default.yellow(`
2583
2721
  ${cli.label} exited with ${ans.status ?? "no status"} \u2014 the MCP stays installed.`));
2584
2722
  }
2585
- log(`
2586
- ${import_chalk12.default.bold(`Your ${cli.label} is now able to talk to the ${spec2.projectLabel} API.`)}`);
2723
+ const instructionFile = writeAgentInstruction(cli.kind, spec2);
2724
+ const off = `npx apiblaze apichat ${spec2.name} --remove-mcp ${cli.kind}`;
2725
+ const mdName = cli.kind === "claude" ? "CLAUDE.md" : "AGENTS.md";
2726
+ log("");
2727
+ log(import_chalk12.default.bgRed.white.bold(` ${cli.label} will launch with your MCP to ${spec2.projectLabel} enabled from now on. `));
2728
+ log(import_chalk12.default.red.bold(` Every ${cli.label} session in this directory can call your API's tools.`));
2729
+ if (instructionFile) {
2730
+ log(import_chalk12.default.red(` We added a "use this MCP" block to your ${import_chalk12.default.bold(mdName)} \u2014 that is what makes it stick:`));
2731
+ log(import_chalk12.default.red(` ${instructionFile}`));
2732
+ } else {
2733
+ log(import_chalk12.default.red(` (Could not write ${mdName} here, so this applies only to sessions apichat starts.)`));
2734
+ }
2735
+ log(import_chalk12.default.red(` Turn it off (removes the MCP server AND the ${mdName} block):`));
2736
+ log(import_chalk12.default.red(` ${import_chalk12.default.bold(off)}`));
2587
2737
  return true;
2588
2738
  }
2589
2739
 
@@ -4160,12 +4310,47 @@ async function maybeInstallExternalCli(p, opts) {
4160
4310
  const ran = await installAndDemo(pick2, buildInstallSpec(p), () => resolveQuestion(opts));
4161
4311
  if (ran) {
4162
4312
  rememberCliOffer(p, pick2.kind, "installed");
4313
+ if (pick2.kind === "codex") await offerToPruneCodexSiblings(p.projectId);
4163
4314
  if (p.anon) {
4164
4315
  console.log(import_chalk14.default.dim(` Anonymous workspace \u2014 run \`apiblaze apichat ${p.projectId}\` and /claim to keep it (and this MCP) beyond 30 days.`));
4165
4316
  }
4166
4317
  }
4167
4318
  return ran;
4168
4319
  }
4320
+ async function offerToPruneCodexSiblings(keepName) {
4321
+ const siblings = codexSiblingApiblazeServers(keepName);
4322
+ if (siblings.length === 0) return;
4323
+ const alive = [];
4324
+ for (const name of siblings) {
4325
+ const url = codexServerUrl(name);
4326
+ if (url && await mcpEndpointIsDead(url)) {
4327
+ if (removeCodexServer(name)) {
4328
+ console.log(import_chalk14.default.dim(` Removed ${import_chalk14.default.bold(name)} from ~/.codex/config.toml \u2014 that proxy no longer exists.`));
4329
+ continue;
4330
+ }
4331
+ }
4332
+ alive.push(name);
4333
+ }
4334
+ if (alive.length === 0) return;
4335
+ const siblingsLabel = alive;
4336
+ console.log(import_chalk14.default.yellow(`
4337
+ Codex also has ${siblingsLabel.length} other APIblaze MCP server${siblingsLabel.length > 1 ? "s" : ""} configured: ${import_chalk14.default.bold(siblingsLabel.join(", "))}.`));
4338
+ console.log(import_chalk14.default.dim(' Codex tries every one at startup and reports "MCP startup incomplete" for any that is not signed in.'));
4339
+ if (!process.stdin.isTTY) {
4340
+ console.log(import_chalk14.default.dim(` Remove the ones you don't use: apiblaze apichat <name> --install-mcp codex (or edit ~/.codex/config.toml)`));
4341
+ return;
4342
+ }
4343
+ const { default: inquirer3 } = await import("inquirer");
4344
+ const { drop } = await inquirer3.prompt([{
4345
+ type: "checkbox",
4346
+ name: "drop",
4347
+ message: "Remove any you no longer use? (space to select, enter to confirm)",
4348
+ choices: siblingsLabel.map((n) => ({ name: n, value: n }))
4349
+ }]);
4350
+ for (const n of drop ?? []) {
4351
+ console.log(removeCodexServer(n) ? ` ${import_chalk14.default.green("\u2714")} Removed ${import_chalk14.default.bold(n)} from ~/.codex/config.toml.` : import_chalk14.default.red(` Could not remove ${n}.`));
4352
+ }
4353
+ }
4169
4354
  async function authorizeClientRegistration(p, cliLabel) {
4170
4355
  if (!p.consumerAuth) return;
4171
4356
  const teamId = p.teamId ?? loadCredentials()?.teamId;
@@ -4223,6 +4408,23 @@ async function startChat(p, messages, opts) {
4223
4408
  }
4224
4409
  await runRepl(p, messages);
4225
4410
  }
4411
+ async function removeExternalMcp(projectId, cliKind) {
4412
+ const kind = cliKind.toLowerCase();
4413
+ if (kind !== "claude" && kind !== "codex") fail3(`--remove-mcp takes "claude" or "codex", not "${cliKind}".`);
4414
+ const label3 = kind === "claude" ? "Claude CLI" : "Codex CLI";
4415
+ let removed = false;
4416
+ if (kind === "claude") {
4417
+ const r = (0, import_child_process3.spawnSync)("claude", ["mcp", "remove", projectId], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 });
4418
+ removed = r.status === 0;
4419
+ } else {
4420
+ removed = removeCodexServer(projectId);
4421
+ }
4422
+ console.log(removed ? ` ${import_chalk14.default.green("\u2714")} Removed the ${import_chalk14.default.bold(projectId)} MCP server from ${label3}.` : import_chalk14.default.dim(` ${label3} had no ${projectId} MCP server configured.`));
4423
+ const hadNote = removeAgentInstruction(kind, projectId);
4424
+ console.log(hadNote ? ` ${import_chalk14.default.green("\u2714")} Removed the standing instruction from ${agentInstructionFile(kind)}.` : import_chalk14.default.dim(` No standing instruction found in ${agentInstructionFile(kind)}.`));
4425
+ console.log(import_chalk14.default.dim(`
4426
+ ${label3} will no longer launch with your API's tools in this directory.`));
4427
+ }
4226
4428
  async function runApichat(opts) {
4227
4429
  setVerbose(opts.verbose === true);
4228
4430
  console.log(import_chalk14.default.bold("\napichat \u2014 turn any API into a chat\n"));
@@ -4235,6 +4437,11 @@ async function runApichat(opts) {
4235
4437
  opts.target = void 0;
4236
4438
  }
4237
4439
  }
4440
+ if (opts.removeMcp) {
4441
+ if (!opts.project) fail3("Name the proxy: apiblaze apichat <project> --remove-mcp claude|codex");
4442
+ await removeExternalMcp(opts.project, opts.removeMcp);
4443
+ return;
4444
+ }
4238
4445
  if (opts.project) {
4239
4446
  const opened = await openDirectProject(opts.project, opts);
4240
4447
  if (await maybeInstallExternalCli(opened.p, opts)) return;
@@ -11584,7 +11791,7 @@ agent.command("authz").description("Chat to design and turn on access rules for
11584
11791
  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)));
11585
11792
  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)));
11586
11793
  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)));
11587
- 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 })));
11794
+ 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.").option("--remove-mcp <cli>", "Disconnect this proxy from an external agent CLI (claude | codex): removes the MCP server and the standing instruction").action(action((project, opts) => runApichat({ ...opts, project, openapispec: opts.openapispec ?? opts.openapi })));
11588
11795
  var llm = program.command("llm").description("Manage a local LLM provider key for chat (optional \u2014 lifts model quality, bills your key)");
11589
11796
  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)));
11590
11797
  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.28",
3
+ "version": "0.20.34",
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",