apiblaze 0.2.0 → 0.3.1

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 (3) hide show
  1. package/README.md +14 -0
  2. package/dist/index.js +717 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,6 +36,9 @@ npx apiblaze dev
36
36
  # Or specify a port
37
37
  npx apiblaze dev 3000
38
38
 
39
+ # Stream full request/response traffic to a file (JSON lines)
40
+ npx apiblaze dev 3000 --capture-file traffic.jsonl
41
+
39
42
  ```
40
43
 
41
44
  ## Help
@@ -70,6 +73,17 @@ apiblaze help dev
70
73
 
71
74
  On Ctrl+C the tunnel is cleanly deregistered.
72
75
 
76
+ ### Zero-setup conveniences
77
+
78
+ - **No project yet?** If none of your projects point at this machine, `apiblaze dev`
79
+ offers to spin up a throwaway dev proxy (random name like `braveotter42`) pointed at
80
+ `http://localhost:<port>` and tunnels it immediately — pick `none` or `api_key` auth.
81
+ - **Server not running yet?** If nothing is listening on the local port, requests aren't
82
+ dropped: each one is printed in full (headers + body, with API keys/JWTs masked and JWTs
83
+ decoded) and answered with a friendly synthetic `200`. The moment your dev server comes
84
+ up, the next request forwards to it automatically. Add `--capture-file <path>` to also
85
+ stream every request/response to a JSON-lines file.
86
+
73
87
  ## License
74
88
 
75
89
  MIT
package/dist/index.js CHANGED
@@ -93,6 +93,7 @@ var init_auth = __esm({
93
93
  // src/lib/api.ts
94
94
  var api_exports = {};
95
95
  __export(api_exports, {
96
+ agentCall: () => agentCall,
96
97
  checkProxyName: () => checkProxyName,
97
98
  claimProxy: () => claimProxy,
98
99
  createProxy: () => createProxy,
@@ -147,6 +148,20 @@ async function apiFetch(path2, options = {}) {
147
148
  }
148
149
  return res.json();
149
150
  }
151
+ async function agentCall(path2, method, body) {
152
+ const token = getAccessToken();
153
+ const res = await fetch(`${DASHBOARD_BASE}/api/cli/agents`, {
154
+ method: "POST",
155
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
156
+ body: JSON.stringify({ path: path2, method, body })
157
+ });
158
+ let data = null;
159
+ try {
160
+ data = await res.json();
161
+ } catch {
162
+ }
163
+ return { status: res.status, data };
164
+ }
150
165
  async function getTeams() {
151
166
  const res = await apiFetch("/api/cli/teams");
152
167
  const raw = Array.isArray(res) ? res : res?.teams ?? res?.data ?? [];
@@ -203,10 +218,10 @@ var init_api = __esm({
203
218
 
204
219
  // src/index.ts
205
220
  var import_commander = require("commander");
206
- var import_chalk10 = __toESM(require("chalk"));
221
+ var import_chalk14 = __toESM(require("chalk"));
207
222
 
208
223
  // package.json
209
- var version = "0.2.0";
224
+ var version = "0.3.1";
210
225
 
211
226
  // src/index.ts
212
227
  init_types();
@@ -327,6 +342,7 @@ ${import_chalk.default.cyan("\u2192")} Team: ${import_chalk.default.bold(teamNam
327
342
  }
328
343
 
329
344
  // src/commands/dev.ts
345
+ var import_fs = __toESM(require("fs"));
330
346
  var import_chalk3 = __toESM(require("chalk"));
331
347
  var import_ora2 = __toESM(require("ora"));
332
348
  var import_inquirer = __toESM(require("inquirer"));
@@ -361,10 +377,217 @@ function colorLatency(latency) {
361
377
  if (latency < 500) return import_chalk2.default.yellow(s);
362
378
  return import_chalk2.default.red(s);
363
379
  }
380
+ function timestamp() {
381
+ return (/* @__PURE__ */ new Date()).toTimeString().slice(0, 8);
382
+ }
364
383
  function formatLogLine(entry) {
365
- const now = /* @__PURE__ */ new Date();
366
- const ts = now.toTimeString().slice(0, 8);
367
- return `${import_chalk2.default.gray(`[${ts}]`)} ${colorMethod(entry.method)} ${import_chalk2.default.white(entry.path)} ${import_chalk2.default.gray("\u2192")} ${colorStatus(entry.status)} ${import_chalk2.default.gray(`(${colorLatency(entry.latency)})`)}`;
384
+ return `${import_chalk2.default.gray(`[${timestamp()}]`)} ${colorMethod(entry.method)} ${import_chalk2.default.white(entry.path)} ${import_chalk2.default.gray("\u2192")} ${colorStatus(entry.status)} ${import_chalk2.default.gray(`(${colorLatency(entry.latency)})`)}`;
385
+ }
386
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
387
+ "authorization",
388
+ "proxy-authorization",
389
+ "cookie",
390
+ "set-cookie",
391
+ "x-api-key",
392
+ "api-key",
393
+ "apikey",
394
+ "x-auth-token",
395
+ "x-access-token",
396
+ "x-refresh-token",
397
+ "x-amz-security-token",
398
+ "x-csrf-token"
399
+ ]);
400
+ var SENSITIVE_QUERY = /* @__PURE__ */ new Set([
401
+ "api_key",
402
+ "apikey",
403
+ "key",
404
+ "token",
405
+ "access_token",
406
+ "refresh_token",
407
+ "id_token",
408
+ "secret",
409
+ "client_secret",
410
+ "password",
411
+ "sig",
412
+ "signature"
413
+ ]);
414
+ function maskSecret(value) {
415
+ const v = value.trim();
416
+ if (v.length <= 8) return "\u2022\u2022\u2022\u2022";
417
+ const head = v.slice(0, 4);
418
+ const tail = v.slice(-4);
419
+ return `${head}\u2026${tail} ${import_chalk2.default.gray(`(${v.length} chars, masked)`)}`;
420
+ }
421
+ function base64UrlDecode(seg) {
422
+ try {
423
+ const pad = seg.length % 4 === 0 ? "" : "=".repeat(4 - seg.length % 4);
424
+ const b64 = seg.replace(/-/g, "+").replace(/_/g, "/") + pad;
425
+ return Buffer.from(b64, "base64").toString("utf8");
426
+ } catch {
427
+ return null;
428
+ }
429
+ }
430
+ function decodeJwt(token) {
431
+ const parts = token.trim().split(".");
432
+ if (parts.length !== 3) return null;
433
+ const headerRaw = base64UrlDecode(parts[0]);
434
+ const payloadRaw = base64UrlDecode(parts[1]);
435
+ if (!headerRaw || !payloadRaw) return null;
436
+ try {
437
+ const header = JSON.parse(headerRaw);
438
+ const payload = JSON.parse(payloadRaw);
439
+ if (!header || typeof header !== "object" || !("alg" in header)) return null;
440
+ return { header, payload };
441
+ } catch {
442
+ return null;
443
+ }
444
+ }
445
+ function maskPath(path2) {
446
+ const q = path2.indexOf("?");
447
+ if (q < 0) return path2;
448
+ const base = path2.slice(0, q);
449
+ const query = path2.slice(q + 1);
450
+ const masked = query.split("&").map((pair) => {
451
+ const eq = pair.indexOf("=");
452
+ if (eq < 0) return pair;
453
+ const name = pair.slice(0, eq);
454
+ const val = pair.slice(eq + 1);
455
+ if (val && SENSITIVE_QUERY.has(decodeURIComponent(name).toLowerCase())) {
456
+ return `${name}=${maskSecret(decodeURIComponent(val))}`;
457
+ }
458
+ return pair;
459
+ }).join("&");
460
+ return `${base}?${masked}`;
461
+ }
462
+ function formatHeaderLines(name, value) {
463
+ const lower = name.toLowerCase();
464
+ if (lower === "authorization" || lower === "proxy-authorization") {
465
+ const m = /^Bearer\s+(.+)$/i.exec(value.trim());
466
+ if (m) {
467
+ const token = m[1];
468
+ const jwt = decodeJwt(token);
469
+ if (jwt) {
470
+ return [
471
+ ` ${import_chalk2.default.dim(name)}: Bearer ${maskSecret(token)} ${import_chalk2.default.cyan("(JWT)")}`,
472
+ ` ${import_chalk2.default.gray("\u251C header: ")} ${import_chalk2.default.gray(JSON.stringify(jwt.header))}`,
473
+ ` ${import_chalk2.default.gray("\u2514 payload:")} ${import_chalk2.default.gray(JSON.stringify(jwt.payload))}`
474
+ ];
475
+ }
476
+ return [` ${import_chalk2.default.dim(name)}: Bearer ${maskSecret(token)}`];
477
+ }
478
+ return [` ${import_chalk2.default.dim(name)}: ${maskSecret(value)}`];
479
+ }
480
+ if (SENSITIVE_HEADERS.has(lower)) {
481
+ return [` ${import_chalk2.default.dim(name)}: ${maskSecret(value)}`];
482
+ }
483
+ return [` ${import_chalk2.default.dim(name)}: ${value}`];
484
+ }
485
+ function formatBody(body, contentType) {
486
+ if (body.length === 0) return import_chalk2.default.gray(" (empty)");
487
+ if (body.includes(0)) return import_chalk2.default.gray(` <binary, ${body.length} bytes>`);
488
+ let text = body.toString("utf8");
489
+ const ct = (contentType ?? "").toLowerCase();
490
+ if (ct.includes("json") || /^[[{]/.test(text.trim())) {
491
+ try {
492
+ text = JSON.stringify(JSON.parse(text), null, 2);
493
+ } catch {
494
+ }
495
+ }
496
+ const MAX = 4e3;
497
+ const truncated = text.length > MAX;
498
+ const shown = truncated ? text.slice(0, MAX) : text;
499
+ const indented = shown.split("\n").map((l) => ` ${l}`).join("\n");
500
+ return indented + (truncated ? import_chalk2.default.gray(`
501
+ \u2026 (${text.length - MAX} more bytes \u2014 see --capture-file for full)`) : "");
502
+ }
503
+ function formatCapturedRequest(req, note) {
504
+ const headerLines = Object.keys(req.headers).flatMap((name) => formatHeaderLines(name, req.headers[name]));
505
+ const contentType = Object.keys(req.headers).find((k) => k.toLowerCase() === "content-type");
506
+ return [
507
+ `${import_chalk2.default.gray(`[${timestamp()}]`)} ${import_chalk2.default.magenta("\u26B2 CAPTURED")} ${colorMethod(req.method)} ${import_chalk2.default.white(maskPath(req.path))} ${import_chalk2.default.gray(`\u2014 ${note}`)}`,
508
+ import_chalk2.default.bold(" Headers:"),
509
+ ...headerLines,
510
+ import_chalk2.default.bold(" Body:"),
511
+ formatBody(req.body, contentType ? req.headers[contentType] : void 0),
512
+ ""
513
+ ].join("\n");
514
+ }
515
+
516
+ // src/lib/random-name.ts
517
+ var ADJECTIVES = [
518
+ "amber",
519
+ "azure",
520
+ "brave",
521
+ "bright",
522
+ "calm",
523
+ "clever",
524
+ "cosmic",
525
+ "crisp",
526
+ "eager",
527
+ "fancy",
528
+ "gentle",
529
+ "happy",
530
+ "jolly",
531
+ "keen",
532
+ "lively",
533
+ "lucky",
534
+ "mellow",
535
+ "merry",
536
+ "nimble",
537
+ "noble",
538
+ "plucky",
539
+ "quiet",
540
+ "rapid",
541
+ "shiny",
542
+ "smooth",
543
+ "snappy",
544
+ "sunny",
545
+ "swift",
546
+ "tidy",
547
+ "vivid",
548
+ "witty",
549
+ "zesty"
550
+ ];
551
+ var NOUNS = [
552
+ "badger",
553
+ "beacon",
554
+ "cedar",
555
+ "comet",
556
+ "dolphin",
557
+ "ember",
558
+ "falcon",
559
+ "finch",
560
+ "harbor",
561
+ "heron",
562
+ "koala",
563
+ "lark",
564
+ "lemur",
565
+ "maple",
566
+ "meadow",
567
+ "otter",
568
+ "panda",
569
+ "pebble",
570
+ "puffin",
571
+ "quail",
572
+ "raccoon",
573
+ "river",
574
+ "robin",
575
+ "sparrow",
576
+ "spruce",
577
+ "tiger",
578
+ "turtle",
579
+ "walrus",
580
+ "willow",
581
+ "wombat",
582
+ "yak",
583
+ "zebra"
584
+ ];
585
+ function pick(list) {
586
+ return list[Math.floor(Math.random() * list.length)];
587
+ }
588
+ function randomProxyName() {
589
+ const digits = String(Math.floor(Math.random() * 100)).padStart(2, "0");
590
+ return `${pick(ADJECTIVES)}${pick(NOUNS)}${digits}`;
368
591
  }
369
592
 
370
593
  // src/lib/tunnel-client.ts
@@ -372,6 +595,7 @@ var import_ws = __toESM(require("ws"));
372
595
  var CHUNK_BYTES = 512 * 1024;
373
596
  var PING_INTERVAL_MS = 6e4;
374
597
  var MAX_RECONNECT_DELAY_MS = 15e3;
598
+ var OFFLINE_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "EHOSTUNREACH"]);
375
599
  var STRIP_HEADERS = /* @__PURE__ */ new Set([
376
600
  "host",
377
601
  "content-length",
@@ -389,6 +613,10 @@ function stripHeaders(headers) {
389
613
  }
390
614
  return out;
391
615
  }
616
+ function encodeBody(buf) {
617
+ if (buf.includes(0)) return { value: buf.toString("base64"), encoding: "base64" };
618
+ return { value: buf.toString("utf8"), encoding: "utf8" };
619
+ }
392
620
  function startTunnelClient(opts) {
393
621
  const target = `http://127.0.0.1:${opts.localPort}`;
394
622
  const inflight = /* @__PURE__ */ new Map();
@@ -396,6 +624,7 @@ function startTunnelClient(opts) {
396
624
  let closed = false;
397
625
  let reconnects = 0;
398
626
  let pingTimer;
627
+ let capturing = false;
399
628
  function connect() {
400
629
  const url = `${opts.connectUrl}?project=${encodeURIComponent(opts.projectId)}&token=${encodeURIComponent(opts.token)}`;
401
630
  const socket = new import_ws.default(url);
@@ -455,30 +684,80 @@ function startTunnelClient(opts) {
455
684
  inflight.delete(id);
456
685
  void forward(socket, id, f);
457
686
  }
687
+ function sendResponse(socket, id, status, headers, body) {
688
+ send(socket, { id, type: "res", status, headers, bodyLen: body.length });
689
+ if (body.length === 0) {
690
+ send(socket, { id, type: "chunk", seq: 0, data: "", final: true });
691
+ return;
692
+ }
693
+ for (let off = 0, seq = 0; off < body.length; off += CHUNK_BYTES) {
694
+ const slice = body.subarray(off, Math.min(off + CHUNK_BYTES, body.length));
695
+ send(socket, { id, type: "chunk", seq: seq++, data: slice.toString("base64"), final: off + CHUNK_BYTES >= body.length });
696
+ }
697
+ }
698
+ function record(f, reqBody, status, captured, resHeaders, resBody) {
699
+ if (!opts.onRecord) return;
700
+ const req = encodeBody(reqBody);
701
+ const res = resBody ? encodeBody(resBody) : void 0;
702
+ opts.onRecord({
703
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
704
+ projectId: opts.projectId,
705
+ captured,
706
+ method: f.method,
707
+ path: f.path,
708
+ requestHeaders: f.headers,
709
+ requestBody: reqBody.length ? req.value : void 0,
710
+ requestBodyEncoding: reqBody.length ? req.encoding : void 0,
711
+ status,
712
+ responseHeaders: resHeaders,
713
+ responseBody: res?.value,
714
+ responseBodyEncoding: res?.encoding,
715
+ latencyMs: Date.now() - f.start
716
+ });
717
+ }
458
718
  async function forward(socket, id, f) {
459
719
  const body = Buffer.concat(f.chunks);
460
720
  const init = { method: f.method, headers: stripHeaders(f.headers) };
461
721
  if (body.length) init.body = body;
462
- let status = 502;
463
722
  try {
464
723
  const resp = await fetch(target + f.path, init);
465
- status = resp.status;
724
+ const status = resp.status;
466
725
  const buf = Buffer.from(await resp.arrayBuffer());
467
726
  const headers = {};
468
727
  resp.headers.forEach((value, key) => {
469
728
  if (!STRIP_HEADERS.has(key.toLowerCase())) headers[key] = value;
470
729
  });
471
- send(socket, { id, type: "res", status, headers, bodyLen: buf.length });
472
- for (let off = 0, seq = 0; off < buf.length; off += CHUNK_BYTES) {
473
- const slice = buf.subarray(off, Math.min(off + CHUNK_BYTES, buf.length));
474
- send(socket, { id, type: "chunk", seq: seq++, data: slice.toString("base64"), final: off + CHUNK_BYTES >= buf.length });
730
+ if (capturing) {
731
+ capturing = false;
732
+ opts.onResume?.();
475
733
  }
734
+ sendResponse(socket, id, status, headers, buf);
735
+ opts.onEntry({ method: f.method, path: f.path, status, latency: Date.now() - f.start });
736
+ record(f, body, status, false, headers, buf);
476
737
  } catch (err) {
477
738
  const code = err?.cause?.code;
478
- const message = code === "ECONNREFUSED" || code === "ECONNRESET" || code === "ENOTFOUND" || code === "EHOSTUNREACH" ? `No local server reachable at ${target} \u2014 is your dev server running on port ${opts.localPort}?` : err?.cause?.message || err?.message || String(err);
739
+ if (OFFLINE_CODES.has(code)) {
740
+ if (!capturing) {
741
+ capturing = true;
742
+ opts.onCaptureStart?.();
743
+ }
744
+ const note = `no local server on port ${opts.localPort}`;
745
+ const payload = Buffer.from(JSON.stringify({
746
+ apiblaze_dev: "captured",
747
+ message: `No local server on port ${opts.localPort} \u2014 request captured by \`apiblaze dev\`. Start your server and resend to forward it.`,
748
+ request: { method: f.method, path: f.path }
749
+ }, null, 2));
750
+ const headers = { "content-type": "application/json", "x-apiblaze-dev": "captured" };
751
+ sendResponse(socket, id, 200, headers, payload);
752
+ opts.onCapture?.({ method: f.method, path: f.path, headers: f.headers, body }, note);
753
+ record(f, body, 200, true, headers, payload);
754
+ return;
755
+ }
756
+ const message = err?.cause?.message || err?.message || String(err);
479
757
  send(socket, { id, type: "err", message });
758
+ opts.onEntry({ method: f.method, path: f.path, status: 502, latency: Date.now() - f.start });
759
+ record(f, body, 502, false);
480
760
  }
481
- opts.onEntry({ method: f.method, path: f.path, status, latency: Date.now() - f.start });
482
761
  }
483
762
  function send(socket, frame) {
484
763
  try {
@@ -500,6 +779,76 @@ function startTunnelClient(opts) {
500
779
  }
501
780
 
502
781
  // src/commands/dev.ts
782
+ async function offerAutoCreate(teamId, port) {
783
+ if (!process.stdin.isTTY) {
784
+ console.log(import_chalk3.default.yellow("No projects found with an internal target."));
785
+ console.log("Point a project at localhost in the dashboard, or run `apiblaze dev` in an interactive terminal to create one automatically.");
786
+ return null;
787
+ }
788
+ const { create } = await import_inquirer.default.prompt([{
789
+ type: "confirm",
790
+ name: "create",
791
+ message: `No project points at this machine. Create a quick dev proxy \u2192 ${import_chalk3.default.bold(`http://localhost:${port}`)} and tunnel it?`,
792
+ default: true
793
+ }]);
794
+ if (!create) return null;
795
+ const { auth } = await import_inquirer.default.prompt([{
796
+ type: "list",
797
+ name: "auth",
798
+ message: "How should callers authenticate to the new proxy?",
799
+ choices: [
800
+ { name: "none \u2014 open to anyone with the URL (simplest)", value: "none" },
801
+ { name: "api_key \u2014 callers must send an X-API-Key header", value: "api_key" }
802
+ ],
803
+ default: "none"
804
+ }]);
805
+ let name = randomProxyName();
806
+ for (let i = 0; i < 8; i++) {
807
+ const check = await checkProxyName(name, teamId).catch(() => null);
808
+ if (!check || check.canUseProjectName && check.canUseApiVersion) break;
809
+ name = randomProxyName();
810
+ }
811
+ const spinner = (0, import_ora2.default)(`Creating dev proxy "${name}"...`).start();
812
+ let result;
813
+ try {
814
+ result = await createProxy({ name, target_url: `http://localhost:${port}`, auth_type: auth, team_id: teamId });
815
+ spinner.succeed(import_chalk3.default.green(`Created dev proxy "${name}".`));
816
+ } catch (err) {
817
+ spinner.fail("Failed to create the dev proxy.");
818
+ throw err;
819
+ }
820
+ const version2 = result.api_version || "1.0.0";
821
+ const endpoint = `https://${name}.apiblaze.com/${version2}/dev`;
822
+ console.log(` ${import_chalk3.default.dim("Endpoint:")} ${import_chalk3.default.bold(endpoint)}`);
823
+ if (auth === "api_key") {
824
+ const key = result.api_keys?.dev ?? Object.values(result.api_keys ?? {})[0];
825
+ if (key) {
826
+ console.log(` ${import_chalk3.default.dim("API key (dev):")} ${import_chalk3.default.bold.green(key)}`);
827
+ console.log(import_chalk3.default.dim(" Send it as the X-API-Key header. It may not be shown again."));
828
+ }
829
+ }
830
+ const targets = await getLocalhostTargets(teamId).catch(() => []);
831
+ const created = targets.find((t) => t.projectId === result.project_id);
832
+ if (!created) {
833
+ console.log(import_chalk3.default.yellow(" Proxy created, but it did not appear as a localhost target \u2014 try `apiblaze dev` again."));
834
+ return null;
835
+ }
836
+ return created;
837
+ }
838
+ async function probeLocalServer(port) {
839
+ const controller = new AbortController();
840
+ const timer = setTimeout(() => controller.abort(), 1500);
841
+ try {
842
+ await fetch(`http://127.0.0.1:${port}/`, { method: "HEAD", signal: controller.signal });
843
+ return true;
844
+ } catch (err) {
845
+ if (err?.name === "AbortError") return true;
846
+ const code = err?.cause?.code;
847
+ return !(code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EHOSTUNREACH");
848
+ } finally {
849
+ clearTimeout(timer);
850
+ }
851
+ }
503
852
  async function runDev(options) {
504
853
  const creds = loadCredentials();
505
854
  if (!creds) {
@@ -540,13 +889,15 @@ async function runDev(options) {
540
889
  throw err;
541
890
  }
542
891
  }
543
- if (targets.length === 0) {
544
- console.log(import_chalk3.default.yellow("No projects found with an internal target."));
545
- console.log("Set a project's upstream target to localhost or a private IP in your APIblaze dashboard, then try again.");
546
- process.exit(0);
547
- }
548
892
  let selectedTargets;
549
- if (targets.length === 1) {
893
+ if (targets.length === 0) {
894
+ const created = await offerAutoCreate(teamId, options.port);
895
+ if (!created) {
896
+ console.log("Set a project's upstream target to localhost or a private IP, then try again.");
897
+ process.exit(0);
898
+ }
899
+ selectedTargets = [created];
900
+ } else if (targets.length === 1) {
550
901
  const { confirmed } = await import_inquirer.default.prompt([{
551
902
  type: "confirm",
552
903
  name: "confirmed",
@@ -580,6 +931,14 @@ async function runDev(options) {
580
931
  Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
581
932
  `)
582
933
  );
934
+ let recordSink;
935
+ let captureStream;
936
+ if (options.captureFile) {
937
+ captureStream = import_fs.default.createWriteStream(options.captureFile, { flags: "a" });
938
+ recordSink = (r) => captureStream.write(JSON.stringify(r) + "\n");
939
+ console.log(import_chalk3.default.gray(`Streaming full traffic to ${options.captureFile}
940
+ `));
941
+ }
583
942
  let restore = [];
584
943
  let connect;
585
944
  {
@@ -603,11 +962,27 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
603
962
  projectId,
604
963
  localPort: options.port,
605
964
  onEntry: (entry) => console.log(formatLogLine(entry)),
606
- onStatus: (status) => console.log(import_chalk3.default.gray(`[${projectId}] ${status}`))
965
+ onStatus: (status) => console.log(import_chalk3.default.gray(`[${projectId}] ${status}`)),
966
+ onCapture: (req, note) => console.log(formatCapturedRequest(req, note)),
967
+ onCaptureStart: () => console.log(
968
+ import_chalk3.default.magenta(`
969
+ \u26B2 No local server on port ${options.port} yet \u2014 capturing requests below. Start your server and they'll forward automatically.
970
+ `)
971
+ ),
972
+ onResume: () => console.log(
973
+ import_chalk3.default.green(`
974
+ \u2713 Local server detected on port ${options.port} \u2014 forwarding resumed.
975
+ `)
976
+ ),
977
+ onRecord: recordSink
607
978
  })
608
979
  );
980
+ const localUp = await probeLocalServer(options.port);
609
981
  console.log("\n" + import_chalk3.default.gray("\u2500".repeat(60)));
610
982
  console.log(import_chalk3.default.bold("Live traffic") + import_chalk3.default.gray(" (Ctrl+C to stop)"));
983
+ console.log(
984
+ localUp ? import_chalk3.default.green(`\u2713 Local server detected on port ${options.port} \u2014 forwarding live.`) : import_chalk3.default.magenta(`\u26B2 Nothing listening on port ${options.port} yet \u2014 requests will be captured until your server starts.`)
985
+ );
611
986
  console.log(import_chalk3.default.gray("\u2500".repeat(60)) + "\n");
612
987
  let isCleaningUp = false;
613
988
  async function cleanup() {
@@ -615,6 +990,7 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
615
990
  isCleaningUp = true;
616
991
  console.log(import_chalk3.default.gray("\n\nShutting down..."));
617
992
  for (const client of clients) client.close();
993
+ captureStream?.end();
618
994
  await deleteDevTunnel(restore).catch(() => {
619
995
  });
620
996
  console.log(import_chalk3.default.green("Tunnel stopped."));
@@ -687,7 +1063,7 @@ ${projects.length} project${projects.length === 1 ? "" : "s"}`));
687
1063
  }
688
1064
 
689
1065
  // src/commands/create.ts
690
- var import_fs = __toESM(require("fs"));
1066
+ var import_fs2 = __toESM(require("fs"));
691
1067
  var import_chalk5 = __toESM(require("chalk"));
692
1068
  var import_ora4 = __toESM(require("ora"));
693
1069
  init_auth();
@@ -922,7 +1298,7 @@ async function runAnonymousCreate(opts) {
922
1298
  if (opts.config) {
923
1299
  let raw = "";
924
1300
  try {
925
- raw = import_fs.default.readFileSync(opts.config, "utf8");
1301
+ raw = import_fs2.default.readFileSync(opts.config, "utf8");
926
1302
  } catch {
927
1303
  fail(`Cannot read --config file: ${opts.config}`);
928
1304
  }
@@ -1200,6 +1576,290 @@ async function runTeam(arg) {
1200
1576
  \u2714 Active team: ${import_chalk9.default.bold(chosen.name)}`));
1201
1577
  }
1202
1578
 
1579
+ // src/commands/authz.ts
1580
+ var import_chalk11 = __toESM(require("chalk"));
1581
+ init_auth();
1582
+ init_api();
1583
+
1584
+ // src/lib/agent-chat.ts
1585
+ var import_readline = __toESM(require("readline"));
1586
+ var import_chalk10 = __toESM(require("chalk"));
1587
+ init_api();
1588
+ async function runAgentChatRepl(opts) {
1589
+ const { title, subtitle, endpoint, buildBody, seedPrompt, summarizeProposal, commands } = opts;
1590
+ console.log("\n" + import_chalk10.default.cyan.bold(title));
1591
+ if (subtitle) console.log(import_chalk10.default.dim(subtitle));
1592
+ const messages = [];
1593
+ const ctx = { proposal: null, lastData: null, log: (s) => console.log(s) };
1594
+ const cmdByName = new Map(commands.map((c) => [c.name, c]));
1595
+ const footer = () => {
1596
+ const parts = [
1597
+ import_chalk10.default.dim("type to chat/refine"),
1598
+ ...commands.map((c) => import_chalk10.default.dim(`/${c.name} ${c.describe}`)),
1599
+ import_chalk10.default.dim("/show"),
1600
+ import_chalk10.default.dim("/drop discard & exit")
1601
+ ];
1602
+ return " " + import_chalk10.default.dim("[ ") + parts.join(import_chalk10.default.dim(" \xB7 ")) + import_chalk10.default.dim(" ]");
1603
+ };
1604
+ async function turn(content) {
1605
+ messages.push({ role: "user", content });
1606
+ process.stdout.write(import_chalk10.default.dim(" \u2026thinking\n"));
1607
+ const { status, data } = await agentCall(endpoint, "POST", { messages, ...buildBody(messages) });
1608
+ if (status === 402) {
1609
+ console.log(import_chalk10.default.red(" Insufficient credits \u2014 top up to keep using the agents.\n"));
1610
+ messages.pop();
1611
+ return;
1612
+ }
1613
+ if (status >= 400 || !data) {
1614
+ console.log(import_chalk10.default.red(` Error (${status}): ${(data && data.error) ?? "request failed"}
1615
+ `));
1616
+ messages.pop();
1617
+ return;
1618
+ }
1619
+ ctx.lastData = data;
1620
+ const reply = data.reply ?? data.message ?? "(no reply)";
1621
+ messages.push({ role: "assistant", content: reply });
1622
+ console.log("\n" + import_chalk10.default.cyan("agent \u203A ") + reply + "\n");
1623
+ if (data.proposal) {
1624
+ ctx.proposal = data.proposal;
1625
+ const summary = summarizeProposal?.(data);
1626
+ if (summary) console.log(import_chalk10.default.yellow(" " + summary));
1627
+ const ready = commands.filter((c) => c.needsProposal).map((c) => `/${c.name}`).join(" or ");
1628
+ if (ready) console.log(import_chalk10.default.dim(` Ready \u2014 ${ready} to make it official, or keep refining.`));
1629
+ console.log();
1630
+ }
1631
+ const llm = data.llm;
1632
+ if (llm) {
1633
+ const credits = data.credits_remaining;
1634
+ const left = typeof credits === "number" ? ` \xB7 $${(credits / 100).toFixed(2)} credit left` : "";
1635
+ console.log(import_chalk10.default.dim(` ~$${(llm.cost_estimate ?? 0).toFixed(4)} \xB7 ${llm.model ?? "?"}${left}`));
1636
+ }
1637
+ }
1638
+ const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
1639
+ const ask = (q) => new Promise((res) => rl.question(q, res));
1640
+ await turn(seedPrompt);
1641
+ for (; ; ) {
1642
+ console.log(footer());
1643
+ const line = (await ask(import_chalk10.default.green("you \u203A "))).trim();
1644
+ if (!line) continue;
1645
+ if (line === "/drop" || line === "/exit" || line === "/quit") {
1646
+ console.log(import_chalk10.default.dim(" Dropped \u2014 nothing applied.\n"));
1647
+ break;
1648
+ }
1649
+ if (line === "/show") {
1650
+ if (!ctx.proposal) console.log(import_chalk10.default.dim(" No proposal yet \u2014 keep chatting until the agent proposes one.\n"));
1651
+ else console.log("\n" + JSON.stringify(ctx.proposal, null, 2) + "\n");
1652
+ continue;
1653
+ }
1654
+ if (line.startsWith("/")) {
1655
+ const cmd = cmdByName.get(line.slice(1));
1656
+ if (!cmd) {
1657
+ console.log(import_chalk10.default.red(` Unknown command "${line}". Try one of: ${commands.map((c) => "/" + c.name).join(", ")}, /show, /drop
1658
+ `));
1659
+ continue;
1660
+ }
1661
+ if (cmd.needsProposal && !ctx.proposal) {
1662
+ console.log(import_chalk10.default.yellow(" No proposal yet \u2014 keep chatting until the agent generates one, then try again.\n"));
1663
+ continue;
1664
+ }
1665
+ await cmd.run(ctx);
1666
+ continue;
1667
+ }
1668
+ await turn(line);
1669
+ }
1670
+ rl.close();
1671
+ }
1672
+
1673
+ // src/commands/authz.ts
1674
+ async function runAuthz(projectArg, apiVersionArg) {
1675
+ const creds = loadCredentials();
1676
+ if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
1677
+ const teamId = creds.teamId ?? "";
1678
+ const projects = await getProjects(teamId);
1679
+ const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
1680
+ if (!match) {
1681
+ throw new Error(`Project "${projectArg}" not found in your active team (${creds.teamName ?? teamId}). Run \`apiblaze projects\`.`);
1682
+ }
1683
+ const projectId = match.projectId;
1684
+ const apiVersion = apiVersionArg || match.apiVersion;
1685
+ const tenant = match.tenant || projectId;
1686
+ async function apply(ctx, enable) {
1687
+ const proposal = ctx.proposal;
1688
+ if (!enable) {
1689
+ const m = await agentCall(`/projects/${projectId}/${apiVersion}/policies/model?tenantId=${encodeURIComponent(tenant)}`, "POST", proposal.model);
1690
+ if (m.status === 404) {
1691
+ ctx.log(import_chalk11.default.red(" Authorization store not provisioned yet. Open the dashboard Authorization tab for this project once (it auto-provisions the store), then re-run.\n"));
1692
+ return;
1693
+ }
1694
+ if (m.status >= 400) {
1695
+ ctx.log(import_chalk11.default.red(` Model save failed (${m.status}): ${m.data?.error ?? ""}
1696
+ `));
1697
+ return;
1698
+ }
1699
+ }
1700
+ let ok = 0;
1701
+ let fail4 = 0;
1702
+ for (const r of proposal.routes) {
1703
+ const resource = "/" + String(r.resource || "").replace(/^\/+/, "");
1704
+ const policiesBody = {
1705
+ rule_mode: r.rule_mode === "list" ? "list" : "check-write",
1706
+ on_request_read: Array.isArray(r.on_request_read) ? r.on_request_read : [],
1707
+ post_response_write: Array.isArray(r.post_response_write) ? r.post_response_write : [],
1708
+ list_objects_read: r.list_objects_read ?? null,
1709
+ authentication_config: { require_authentication: true },
1710
+ authorization_enabled: enable
1711
+ };
1712
+ const encoded = resource.replace(/\{/g, "%7B").replace(/\}/g, "%7D");
1713
+ const putPath = `/projects/${projectId}/${apiVersion}/policies/route/${r.method.toUpperCase()}${encoded}`;
1714
+ let res = await agentCall(putPath, "PUT", { ...policiesBody, resource });
1715
+ if (res.status === 404) {
1716
+ res = await agentCall(`/projects/${projectId}/${apiVersion}/policies/route`, "POST", { method: r.method.toUpperCase(), resource, ...policiesBody });
1717
+ }
1718
+ if (res.status >= 200 && res.status < 300) ok++;
1719
+ else fail4++;
1720
+ }
1721
+ if (enable) {
1722
+ const e = await agentCall(`/${projectId}/${apiVersion}/config`, "PATCH", { authorization: { enforce_authorization: true }, tenant });
1723
+ if (e.status >= 400) {
1724
+ ctx.log(import_chalk11.default.red(` Enforced ${ok} route(s) but turning on the project-level switch failed (${e.status}). Toggle "Enforce Authorization" on in the dashboard.
1725
+ `));
1726
+ return;
1727
+ }
1728
+ ctx.log(import_chalk11.default.green(` \u2713 Enforcement ON \u2014 ${ok} route(s) + project switch on${fail4 ? `, ${fail4} failed` : ""}.
1729
+ `));
1730
+ } else {
1731
+ ctx.log(import_chalk11.default.green(` \u2713 Published (shadow): model + ${ok} route(s)${fail4 ? `, ${fail4} failed` : ""}. Review, then /enable.
1732
+ `));
1733
+ }
1734
+ }
1735
+ await runAgentChatRepl({
1736
+ title: `Authorization assistant \u2014 ${projectId} ${apiVersion}`,
1737
+ subtitle: `tenant ${tenant} \xB7 discuss what authorization fits this API, then make it official.`,
1738
+ endpoint: `/projects/${projectId}/${apiVersion}/authz/chat`,
1739
+ buildBody: () => ({ included_sample_ids: [], existing_model: null, existing_routes: [] }),
1740
+ seedPrompt: "Analyze this API and tell me what authorization is feasible. If it is read-only, say so plainly. Then propose options \u2014 do not generate rules yet.",
1741
+ summarizeProposal: (data) => {
1742
+ const proposal = data.proposal;
1743
+ const sim = data.simulation;
1744
+ const cov = sim ? `, ${sim.routes_covered ?? 0} covered by rules` : "";
1745
+ const warn = sim?.blocked_without_backfill ? ` \u26A0 ${sim.blocked_without_backfill} currently-OK request(s) would be denied until tuples exist.` : "";
1746
+ return `Proposal ready: ${proposal.routes.length} route(s)${cov}.${warn}`;
1747
+ },
1748
+ commands: [
1749
+ { name: "publish", describe: "save in shadow mode", needsProposal: true, run: (ctx) => apply(ctx, false) },
1750
+ { name: "enable", describe: "turn on enforcement", needsProposal: true, run: (ctx) => apply(ctx, true) }
1751
+ ]
1752
+ });
1753
+ }
1754
+
1755
+ // src/commands/openapi.ts
1756
+ var import_chalk12 = __toESM(require("chalk"));
1757
+ init_auth();
1758
+ init_api();
1759
+ async function runOpenapi(projectArg, apiVersionArg) {
1760
+ const creds = loadCredentials();
1761
+ if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
1762
+ const projects = await getProjects(creds.teamId ?? "");
1763
+ const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
1764
+ if (!match) throw new Error(`Project "${projectArg}" not found in your active team. Run \`apiblaze projects\`.`);
1765
+ const projectId = match.projectId;
1766
+ const apiVersion = apiVersionArg || match.apiVersion;
1767
+ console.log(import_chalk12.default.dim("Gathering captured traffic samples\u2026"));
1768
+ const routesRes = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes`, "GET");
1769
+ if (routesRes.status >= 400) {
1770
+ console.log(import_chalk12.default.red(`Could not list traffic (${routesRes.status}): ${routesRes.data?.error ?? ""}`));
1771
+ return;
1772
+ }
1773
+ const routes = routesRes.data?.routes ?? (Array.isArray(routesRes.data) ? routesRes.data : []);
1774
+ const hashes = routes.map((r) => r.route_hash).filter((h) => !!h);
1775
+ const ids = /* @__PURE__ */ new Set();
1776
+ for (const h of hashes) {
1777
+ const s = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes/${encodeURIComponent(h)}/samples`, "GET");
1778
+ const arr = s.data?.samples ?? [];
1779
+ for (const x of arr) if (x.sample_id) ids.add(x.sample_id);
1780
+ }
1781
+ const sampleIds = [...ids];
1782
+ if (sampleIds.length === 0) {
1783
+ console.log(import_chalk12.default.yellow("No captured traffic samples found. Hit the dev environment of this proxy to capture some traces, then retry."));
1784
+ return;
1785
+ }
1786
+ async function publish(ctx) {
1787
+ const patch = ctx.proposal.patch;
1788
+ if (!Array.isArray(patch) || patch.length === 0) {
1789
+ ctx.log(import_chalk12.default.green(" Nothing to publish \u2014 the spec already covers the observed traffic.\n"));
1790
+ return;
1791
+ }
1792
+ const specSource = ctx.lastData?.spec_source ?? "unknown";
1793
+ const endpoint = specSource === "github" ? "open-pr" : "publish-openapi";
1794
+ const pub = await agentCall(`/projects/${projectId}/${apiVersion}/samples/${endpoint}`, "POST", { patch, sample_ids_used: sampleIds });
1795
+ if (pub.status >= 400) {
1796
+ ctx.log(import_chalk12.default.red(` Publish failed (${pub.status}): ${pub.data?.error ?? ""}
1797
+ `));
1798
+ return;
1799
+ }
1800
+ const prUrl = pub.data?.pr_url;
1801
+ ctx.log(import_chalk12.default.green(prUrl ? ` \u2713 Pull request opened: ${prUrl}
1802
+ ` : " \u2713 Published updated OpenAPI spec.\n"));
1803
+ }
1804
+ await runAgentChatRepl({
1805
+ title: `API-docs assistant \u2014 ${projectId} ${apiVersion}`,
1806
+ subtitle: `${sampleIds.length} captured sample(s) \xB7 discuss what to document, then make it official.`,
1807
+ endpoint: `/projects/${projectId}/${apiVersion}/openapi/chat`,
1808
+ buildBody: () => ({ included_sample_ids: sampleIds }),
1809
+ 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.",
1810
+ summarizeProposal: (data) => {
1811
+ const patch = data.proposal.patch;
1812
+ const n = Array.isArray(patch) ? patch.length : 0;
1813
+ return n === 0 ? "The spec already covers the observed traffic \u2014 nothing to publish." : `Patch ready: ${n} additive change(s) to the OpenAPI spec.`;
1814
+ },
1815
+ commands: [
1816
+ { name: "publish", describe: "publish / open PR", needsProposal: true, run: publish }
1817
+ ]
1818
+ });
1819
+ }
1820
+
1821
+ // src/commands/mcp.ts
1822
+ var import_chalk13 = __toESM(require("chalk"));
1823
+ init_auth();
1824
+ init_api();
1825
+ async function runMcp(projectArg, apiVersionArg, opts) {
1826
+ const creds = loadCredentials();
1827
+ if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
1828
+ const projects = await getProjects(creds.teamId ?? "");
1829
+ const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
1830
+ if (!match) throw new Error(`Project "${projectArg}" not found in your active team. Run \`apiblaze projects\`.`);
1831
+ const projectId = match.projectId;
1832
+ const apiVersion = apiVersionArg || match.apiVersion;
1833
+ const environment = opts.environment || "prod";
1834
+ async function publish(ctx) {
1835
+ const spec = ctx.proposal;
1836
+ const pub = await agentCall(`/projects/${projectId}/${apiVersion}/mcp/spec`, "PUT", { environment, spec });
1837
+ if (pub.status >= 400) {
1838
+ ctx.log(import_chalk13.default.red(` Publish failed (${pub.status}): ${pub.data?.error ?? ""}
1839
+ `));
1840
+ return;
1841
+ }
1842
+ ctx.log(import_chalk13.default.green(` \u2713 Published MCP server \u2014 ${projectId}.mcp.apiblaze.com/${apiVersion}/${environment}.
1843
+ `));
1844
+ }
1845
+ await runAgentChatRepl({
1846
+ title: `MCP builder \u2014 ${projectId} ${apiVersion} (${environment})`,
1847
+ subtitle: "discuss which routes to expose as tools, then make the catalogue official.",
1848
+ endpoint: `/projects/${projectId}/${apiVersion}/mcp/chat`,
1849
+ buildBody: () => ({ environment, included_sample_ids: [] }),
1850
+ 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.",
1851
+ summarizeProposal: (data) => {
1852
+ const spec = data.proposal;
1853
+ const tools = Array.isArray(spec.tools) ? spec.tools : [];
1854
+ const names = tools.slice(0, 12).map((t) => t.name ?? "(unnamed)").join(", ");
1855
+ return `Catalogue ready: ${tools.length} tool(s)${names ? ` \u2014 ${names}${tools.length > 12 ? ", \u2026" : ""}` : ""}.`;
1856
+ },
1857
+ commands: [
1858
+ { name: "publish", describe: `to ${projectId}.mcp.apiblaze.com`, needsProposal: true, run: publish }
1859
+ ]
1860
+ });
1861
+ }
1862
+
1203
1863
  // src/index.ts
1204
1864
  var program = new import_commander.Command();
1205
1865
  program.name("apiblaze").description("APIblaze CLI \u2014 create & manage API proxies and run dev tunnels").version(version);
@@ -1259,14 +1919,38 @@ program.command("projects").description("List the projects in your team").action
1259
1919
  process.exit(1);
1260
1920
  }
1261
1921
  });
1262
- program.command("dev").description("Start a dev tunnel for your localhost projects").argument("[port]", "Local port to tunnel (positional; overrides --port)").option("-p, --port <number>", "Local port to tunnel", "3000").action(async (port, opts) => {
1922
+ program.command("authz").description("Design API authorization interactively (chat), then publish + enable it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").action(async (project, apiVersion) => {
1923
+ try {
1924
+ await runAuthz(project, apiVersion);
1925
+ } catch (err) {
1926
+ printError(err);
1927
+ process.exit(1);
1928
+ }
1929
+ });
1930
+ program.command("openapi").description("Design your OpenAPI spec from captured traffic interactively (chat), then publish it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").action(async (project, apiVersion) => {
1931
+ try {
1932
+ await runOpenapi(project, apiVersion);
1933
+ } catch (err) {
1934
+ printError(err);
1935
+ process.exit(1);
1936
+ }
1937
+ });
1938
+ program.command("mcp").description("Design an MCP server from the spec + traffic interactively (chat), then publish it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").option("--environment <env>", "Environment to publish (default: prod)").action(async (project, apiVersion, opts) => {
1939
+ try {
1940
+ await runMcp(project, apiVersion, opts);
1941
+ } catch (err) {
1942
+ printError(err);
1943
+ process.exit(1);
1944
+ }
1945
+ });
1946
+ program.command("dev").description("Start a dev tunnel for your localhost projects").argument("[port]", "Local port to tunnel (positional; overrides --port)").option("-p, --port <number>", "Local port to tunnel", "3000").option("-o, --capture-file <path>", "Stream full request/response traffic to a file (JSON lines)").action(async (port, opts) => {
1263
1947
  try {
1264
1948
  const resolved = parseInt(port ?? opts.port, 10);
1265
1949
  if (Number.isNaN(resolved)) {
1266
- console.error(import_chalk10.default.red(`Invalid port: ${port ?? opts.port}`));
1950
+ console.error(import_chalk14.default.red(`Invalid port: ${port ?? opts.port}`));
1267
1951
  process.exit(1);
1268
1952
  }
1269
- await runDev({ port: resolved });
1953
+ await runDev({ port: resolved, captureFile: opts.captureFile });
1270
1954
  } catch (err) {
1271
1955
  printError(err);
1272
1956
  process.exit(1);
@@ -1286,23 +1970,27 @@ Examples:
1286
1970
  $ npx apiblaze login
1287
1971
  $ npx apiblaze create --name myapi --target https://api.example.com --auth api_key
1288
1972
 
1973
+ # Dev tunnel \u2014 auto-creates a proxy if none point here, and captures
1974
+ # traffic (full headers + body, secrets masked) until your server is up:
1975
+ $ npx apiblaze dev 3000
1976
+ $ npx apiblaze dev 3000 --capture-file traffic.jsonl
1977
+
1289
1978
  # Manage:
1290
1979
  $ npx apiblaze whoami
1291
1980
  $ npx apiblaze projects
1292
1981
  $ npx apiblaze team
1293
- $ npx apiblaze dev 3000
1294
1982
  $ npx apiblaze logout
1295
1983
  `
1296
1984
  );
1297
1985
  function printError(err) {
1298
1986
  if (err instanceof ApiError) {
1299
- console.error(import_chalk10.default.red(`
1987
+ console.error(import_chalk14.default.red(`
1300
1988
  API error (${err.status}): ${err.message}`));
1301
1989
  } else if (err instanceof Error) {
1302
- console.error(import_chalk10.default.red(`
1990
+ console.error(import_chalk14.default.red(`
1303
1991
  Error: ${err.message}`));
1304
1992
  } else {
1305
- console.error(import_chalk10.default.red("\nUnknown error"));
1993
+ console.error(import_chalk14.default.red("\nUnknown error"));
1306
1994
  }
1307
1995
  }
1308
1996
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Dev tunnel CLI for APIblaze — route localhost projects through your APIblaze endpoints",
5
5
  "keywords": [
6
6
  "apiblaze",