@bankr/cli 0.3.13 → 0.3.23

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.
@@ -5,7 +5,13 @@ import { spawn } from "child_process";
5
5
  import { CLI_USER_AGENT, getApiUrl, getLlmKey, getLlmUrl, requireApiKey, } from "../lib/config.js";
6
6
  import { getBalances } from "../lib/api.js";
7
7
  import * as output from "../lib/output.js";
8
+ import { getErrorMessage } from "../lib/errors.js";
9
+ import { isHelpRequest } from "../lib/passthrough.js";
8
10
  const ERR_LLM_NOT_ENABLED = "LLM Gateway not enabled on this API key. Enable at bankr.bot/api-keys";
11
+ /** True when the model serves chat completions (image-only models don't). */
12
+ function supportsChat(m) {
13
+ return (m.output ?? ["text"]).includes("text");
14
+ }
9
15
  /**
10
16
  * Fallback model catalog — used when the gateway is unreachable or unauthenticated.
11
17
  * resolveModels() fetches live data from GET /v1/models; this list is the offline safety net.
@@ -87,6 +93,24 @@ const GATEWAY_MODELS = [
87
93
  cost: { input: 1.0, output: 5.0, cacheRead: 0.1, cacheWrite: 1.25 },
88
94
  },
89
95
  // Gemini
96
+ {
97
+ id: "gemini-3.6-flash",
98
+ name: "Gemini 3.6 Flash",
99
+ owned_by: "google",
100
+ contextWindow: 1048576,
101
+ maxTokens: 65536,
102
+ input: IMAGE_INPUT,
103
+ cost: { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0.08333 },
104
+ },
105
+ {
106
+ id: "gemini-3.5-flash-lite",
107
+ name: "Gemini 3.5 Flash Lite",
108
+ owned_by: "google",
109
+ contextWindow: 1048576,
110
+ maxTokens: 65536,
111
+ input: IMAGE_INPUT,
112
+ cost: { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0.08333 },
113
+ },
90
114
  {
91
115
  id: "gemini-3.1-pro",
92
116
  name: "Gemini 3.1 Pro",
@@ -133,6 +157,18 @@ const GATEWAY_MODELS = [
133
157
  cost: { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0.0833 },
134
158
  },
135
159
  // OpenAI
160
+ {
161
+ id: "gpt-image-2",
162
+ name: "GPT Image 2",
163
+ owned_by: "openai",
164
+ contextWindow: 32000,
165
+ maxTokens: 16384,
166
+ input: TEXT_INPUT,
167
+ output: ["image"],
168
+ // Image model — served via /v1/images/generations; output rate is per
169
+ // million image tokens.
170
+ cost: { input: 5.0, output: 30.0, cacheRead: 0, cacheWrite: 0 },
171
+ },
136
172
  {
137
173
  id: "gpt-5.5",
138
174
  name: "GPT 5.5",
@@ -278,6 +314,15 @@ const GATEWAY_MODELS = [
278
314
  input: TEXT_INPUT,
279
315
  cost: { input: 0.1, output: 0.4, cacheRead: 0, cacheWrite: 0 },
280
316
  },
317
+ {
318
+ id: "kimi-k3",
319
+ name: "Kimi K3",
320
+ owned_by: "moonshotai",
321
+ contextWindow: 1048576,
322
+ maxTokens: 131072,
323
+ input: IMAGE_INPUT,
324
+ cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 0 },
325
+ },
281
326
  {
282
327
  id: "kimi-k2.7-code",
283
328
  name: "Kimi K2.7 Code",
@@ -373,6 +418,9 @@ async function resolveModels() {
373
418
  contextWindow: m.context_window ?? fallback?.contextWindow ?? 128000,
374
419
  maxTokens: m.max_output_tokens ?? fallback?.maxTokens ?? 32768,
375
420
  input: m.input_modalities ?? fallback?.input ?? TEXT_INPUT,
421
+ ...((m.output_modalities ?? fallback?.output) && {
422
+ output: m.output_modalities ?? fallback?.output,
423
+ }),
376
424
  cost: m.pricing
377
425
  ? {
378
426
  input: m.pricing.input,
@@ -388,6 +436,7 @@ async function resolveModels() {
388
436
  }),
389
437
  ...(m.deprecation && { deprecation: m.deprecation }),
390
438
  ...(m.private && { private: true }),
439
+ ...(m.zdr && { zdr: true }),
391
440
  };
392
441
  });
393
442
  return { models, live: true };
@@ -426,46 +475,142 @@ function installConfigFile(configFile, toolName, merge) {
426
475
  output.success(`Bankr provider installed to ${configFile}`);
427
476
  }
428
477
  /* ───────────────────────────── bankr llm models ───────────────────────────── */
478
+ /**
479
+ * The tier filters. Both flags read the gateway's own capability flags from
480
+ * /v1/models, so they track the live registry rather than a hardcoded list.
481
+ */
482
+ const TIER_VIEWS = {
483
+ private: {
484
+ title: "Bankr LLM Gateway — Private Models",
485
+ match: (m) => m.private === true,
486
+ empty: "No models currently offer a private (TEE) compute environment.",
487
+ countSuffix: " support :private",
488
+ },
489
+ zdr: {
490
+ title: "Bankr LLM Gateway — Zero-Retention Models",
491
+ match: (m) => m.zdr === true,
492
+ empty: "No models currently guarantee zero data retention.",
493
+ countSuffix: " support :zdr",
494
+ },
495
+ };
429
496
  export async function modelsCommand(opts = {}) {
430
497
  const spin = output.spinner("Fetching models…");
431
498
  const { models, live } = await resolveModels();
432
499
  spin.stop();
433
- // `private` is the gateway's own "supports :private" flag from
434
- // /v1/models (driven by each model's NEAR slot + NEARAI_API_KEY), so this
435
- // tracks the live registry rather than a hardcoded CLI list.
436
- const shown = opts.private ? models.filter((m) => m.private) : models;
437
- output.brandBold(opts.private
438
- ? "Bankr LLM Gateway — Private Models"
439
- : "Bankr LLM Gateway — Available Models");
500
+ // `--private` wins when both are passed: private is the narrower set (every
501
+ // private model is also ZDR), so it's the more specific of the two requests.
502
+ const view = opts.private
503
+ ? TIER_VIEWS.private
504
+ : opts.zdr
505
+ ? TIER_VIEWS.zdr
506
+ : undefined;
507
+ const shown = view ? models.filter(view.match) : models;
508
+ output.brandBold(view?.title ?? "Bankr LLM Gateway — Available Models");
440
509
  console.log();
441
- if (opts.private && shown.length === 0) {
510
+ if (view && shown.length === 0) {
442
511
  output.warn(live
443
- ? "No models currently offer a private (TEE) compute environment."
444
- : "Private status needs a live gateway — set BANKR_LLM_KEY (and BANKR_LLM_URL).");
512
+ ? view.empty
513
+ : "Tier status needs a live gateway — set BANKR_LLM_KEY (and BANKR_LLM_URL).");
445
514
  return;
446
515
  }
447
- const COL = { id: 24, name: 24, provider: 12 };
448
- const hasNotes = shown.some((m) => m.deprecation || m.private);
449
- console.log(` ${output.fmt.brandBold("Model ID".padEnd(COL.id))} ${output.fmt.brandBold("Name".padEnd(COL.name))} ${output.fmt.brandBold("Provider".padEnd(COL.provider))}${hasNotes ? ` ${output.fmt.brandBold("Notes")}` : ""}`);
450
- console.log(output.fmt.dim(` ${"─".repeat(COL.id + COL.name + COL.provider + (hasNotes ? 24 : 2))}`));
451
- for (const m of shown) {
452
- let notes = "";
516
+ // Widths are sized to the longest real value (`minimax-m2.7-highspeed` /
517
+ // `MiniMax M2.7 Highspeed` are both 22), keeping the table under ~90 columns
518
+ // once the tier list is included.
519
+ const COL = { id: 23, name: 23, provider: 11, privacy: 28 };
520
+ const isImageGen = (m) => m.output?.includes("image") === true;
521
+ /**
522
+ * Every tier this model can serve, not just the strongest.
523
+ *
524
+ * The tiers nest, so a private model is also reachable at zdr and standard —
525
+ * listing only the top one made the ladder look like three separate offers
526
+ * and hid that `glm-5.2` (no suffix) is a perfectly valid standard request.
527
+ * Read straight off the flags rather than inferring `zdr` from `private`, so
528
+ * the column reports what the gateway actually advertises.
529
+ */
530
+ const supportedTiers = (m) => {
531
+ // Each tier is painted individually. Colouring the joined string instead
532
+ // made "standard" grey on a standard-only row and green on a ZDR one — the
533
+ // same word changing colour purely because of what followed it.
534
+ const tiers = [
535
+ // The baseline, on every row, so it recedes rather than competing.
536
+ { label: "standard", paint: output.fmt.dim },
537
+ ];
538
+ if (m.zdr)
539
+ tiers.push({ label: "zdr", paint: output.fmt.success });
540
+ if (m.private) {
541
+ // Distinct from zdr: they are two rungs, not one capability.
542
+ tiers.push({ label: "private (tee)", paint: output.fmt.magic });
543
+ }
544
+ return tiers;
545
+ };
546
+ /**
547
+ * Pad from the PLAIN text — ANSI escapes have zero display width but count
548
+ * toward `.length`, so padding the painted string misaligns every coloured
549
+ * row against the header.
550
+ */
551
+ const tierCell = (m) => {
552
+ const tiers = supportedTiers(m);
553
+ const plain = tiers.map((t) => t.label).join(", ");
554
+ const painted = tiers
555
+ .map((t) => t.paint(t.label))
556
+ .join(output.fmt.dim(", "));
557
+ return painted + " ".repeat(Math.max(0, COL.privacy - plain.length));
558
+ };
559
+ /**
560
+ * Everything that is NOT a privacy tier. Kept in its own column because
561
+ * sharing one cell forced an `else if` chain where a deprecated ZDR model
562
+ * showed only "deprecated" and its tier silently disappeared.
563
+ */
564
+ const modelNotes = (m) => {
565
+ const parts = [];
453
566
  if (m.deprecation) {
454
- notes = m.deprecation.replaced_by
567
+ parts.push(m.deprecation.replaced_by
455
568
  ? `deprecated → ${m.deprecation.replaced_by}`
456
- : "deprecated";
569
+ : "deprecated");
457
570
  }
458
- else if (m.private) {
459
- notes = "Private (TEE)";
460
- }
461
- console.log(` ${output.fmt.brand(m.id.padEnd(COL.id))} ${m.name.padEnd(COL.name)} ${output.fmt.dim(m.owned_by.padEnd(COL.provider))} ${notes ? output.fmt.dim(notes) : ""}`);
571
+ if (isImageGen(m))
572
+ parts.push("Image gen (/v1/images/generations)");
573
+ return parts.join(" · ");
574
+ };
575
+ const showNotes = shown.some((m) => modelNotes(m) !== "");
576
+ const line = (cells) => console.log(` ${cells.join(" ")}`.trimEnd());
577
+ line([
578
+ output.fmt.brandBold("Model ID".padEnd(COL.id)),
579
+ output.fmt.brandBold("Name".padEnd(COL.name)),
580
+ output.fmt.brandBold("Provider".padEnd(COL.provider)),
581
+ output.fmt.brandBold("Privacy Tier".padEnd(COL.privacy)),
582
+ ...(showNotes ? [output.fmt.brandBold("Notes")] : []),
583
+ ]);
584
+ // Measured, not guessed: a fixed fudge factor for the last column left the
585
+ // rule hanging well past the header once the tier list widened it.
586
+ const notesWidth = showNotes
587
+ ? Math.max(...shown.map((m) => modelNotes(m).length))
588
+ : 0;
589
+ const rule = COL.id +
590
+ COL.name +
591
+ COL.provider +
592
+ COL.privacy +
593
+ 3 +
594
+ (showNotes ? notesWidth + 1 : 0);
595
+ console.log(output.fmt.dim(` ${"─".repeat(rule)}`));
596
+ for (const m of shown) {
597
+ line([
598
+ output.fmt.brand(m.id.padEnd(COL.id)),
599
+ m.name.padEnd(COL.name),
600
+ output.fmt.dim(m.owned_by.padEnd(COL.provider)),
601
+ tierCell(m),
602
+ ...(showNotes ? [output.fmt.dim(modelNotes(m))] : []),
603
+ ]);
462
604
  }
463
605
  console.log();
464
606
  output.dim(` Gateway: ${getLlmUrl()}`);
465
- output.dim(` ${shown.length} model${shown.length === 1 ? "" : "s"}${opts.private ? " support :private" : " available"}${live ? "" : " (cached)"}`);
466
- if (!opts.private && shown.some((m) => m.private)) {
607
+ output.dim(` ${shown.length} model${shown.length === 1 ? "" : "s"}${view?.countSuffix ?? " available"}${live ? "" : " (cached)"}`);
608
+ if (!view && shown.some((m) => m.private)) {
467
609
  output.dim(" Private (TEE) — append :private to the model id (e.g. glm-5.2:private). List: bankr llm models --private");
468
610
  }
611
+ if (!view && shown.some((m) => m.zdr)) {
612
+ output.dim(" Zero data retention — append :zdr to the model id (e.g. glm-5.2:zdr), or turn it on account-wide under Settings in the web terminal. List: bankr llm models --zdr");
613
+ }
469
614
  }
470
615
  /* ─────────────────────── bankr llm credits ─────────────────────────────────── */
471
616
  export async function creditsCommand() {
@@ -507,7 +652,7 @@ export async function creditsCommand() {
507
652
  }
508
653
  catch (err) {
509
654
  spin.stop();
510
- output.error(`Failed to fetch credits: ${err.message}`);
655
+ output.error(`Failed to fetch credits: ${getErrorMessage(err)}`);
511
656
  process.exit(1);
512
657
  }
513
658
  }
@@ -560,7 +705,7 @@ function resolveTokenAcrossChains(input, balances) {
560
705
  symbol: nativeSym,
561
706
  name: nativeSym,
562
707
  decimals: 18,
563
- balance: parseFloat(chainBal.nativeBalance || "0"),
708
+ balance: chainBal.nativeBalance || "0",
564
709
  balanceUsd: nativeUsd,
565
710
  });
566
711
  }
@@ -627,7 +772,7 @@ export async function creditsAddCommand(amount, opts) {
627
772
  catch (err) {
628
773
  balSpin.stop();
629
774
  // Non-fatal — proceed without balance display
630
- output.dim(` (Could not fetch balance: ${err.message})`);
775
+ output.dim(` (Could not fetch balance: ${getErrorMessage(err)})`);
631
776
  }
632
777
  }
633
778
  // When --token is passed, the chain holding the highest USD balance of
@@ -642,7 +787,7 @@ export async function creditsAddCommand(amount, opts) {
642
787
  }
643
788
  catch (err) {
644
789
  resolveSpin.stop();
645
- output.warn(`Could not fetch balances: ${err.message}`);
790
+ output.warn(`Could not fetch balances: ${getErrorMessage(err)}`);
646
791
  }
647
792
  if (!resolved) {
648
793
  output.error(`Token "${opts.token}" not found in your wallet on any supported chain.`);
@@ -720,7 +865,7 @@ export async function creditsAddCommand(amount, opts) {
720
865
  }
721
866
  catch (err) {
722
867
  spin.stop();
723
- output.error(`Failed to add credits: ${err.message}`);
868
+ output.error(`Failed to add credits: ${getErrorMessage(err)}`);
724
869
  process.exit(1);
725
870
  }
726
871
  }
@@ -782,7 +927,7 @@ export async function creditsAutoCommand(opts) {
782
927
  }
783
928
  catch (err) {
784
929
  spin.stop();
785
- output.error(`Failed to fetch config: ${err.message}`);
930
+ output.error(`Failed to fetch config: ${getErrorMessage(err)}`);
786
931
  process.exit(1);
787
932
  }
788
933
  return;
@@ -907,7 +1052,7 @@ export async function creditsAutoCommand(opts) {
907
1052
  }
908
1053
  catch (err) {
909
1054
  spin.stop();
910
- output.error(`Failed to update config: ${err.message}`);
1055
+ output.error(`Failed to update config: ${getErrorMessage(err)}`);
911
1056
  process.exit(1);
912
1057
  }
913
1058
  }
@@ -916,13 +1061,31 @@ export async function setupOpenclawCommand(opts) {
916
1061
  const llmKey = getLlmKey();
917
1062
  const llmUrl = getLlmUrl();
918
1063
  const { models } = await resolveModels();
919
- const activeModels = models.filter((m) => !m.deprecation);
1064
+ // Coding-tool config lists chat models only — image-only models can't chat.
1065
+ const activeModels = models.filter((m) => !m.deprecation && supportsChat(m));
1066
+ // --images routes OpenClaw's built-in image_generate tool through the gateway.
1067
+ // That tool only uses OpenClaw's known image providers (not custom ones like
1068
+ // `bankr`), so enabling it takes over the `openai` provider slot. OpenClaw's
1069
+ // OpenAI provider expects a `/v1`-suffixed base; normalize so a llmUrl that
1070
+ // already ends in /v1 (or a trailing slash) doesn't produce `/v1/v1`.
1071
+ const imageBaseUrl = `${llmUrl.replace(/\/+$/, "").replace(/\/v1$/, "")}/v1`;
1072
+ const imageProvider = opts.images
1073
+ ? { baseUrl: imageBaseUrl, apiKey: llmKey ?? "${BANKR_API_KEY}" }
1074
+ : null;
1075
+ const imageGenerationModel = opts.images
1076
+ ? { primary: "openai/gpt-image-2", timeoutMs: 180000 }
1077
+ : null;
1078
+ const warnImagesSlot = () => {
1079
+ if (opts.images)
1080
+ output.warn("--images points OpenClaw's `openai` provider at the gateway; image_generate (and any OpenAI-format request) now routes through Bankr. Skip --images if you use real OpenAI in OpenClaw.");
1081
+ };
920
1082
  const providerConfig = {
921
1083
  baseUrl: llmUrl,
922
1084
  apiKey: llmKey ?? "${BANKR_API_KEY}",
923
1085
  api: "openai-completions",
924
- // Private models get both entries: the plain id and a `:private`
925
- // twin that routes through the TEE compute environment — so users can pick either.
1086
+ // Models get a twin entry per privacy tier they support, so users can pick
1087
+ // one from the model dropdown in tools that expose no other configuration.
1088
+ // Base first, then weakest tier to strongest.
926
1089
  models: activeModels.flatMap((m) => {
927
1090
  const base = {
928
1091
  id: m.id,
@@ -933,12 +1096,15 @@ export async function setupOpenclawCommand(opts) {
933
1096
  maxTokens: m.maxTokens,
934
1097
  cost: m.cost,
935
1098
  };
936
- return m.private
937
- ? [
938
- base,
939
- { ...base, id: `${m.id}:private`, name: `${m.name} (Private)` },
940
- ]
941
- : [base];
1099
+ return [
1100
+ base,
1101
+ ...(m.zdr
1102
+ ? [{ ...base, id: `${m.id}:zdr`, name: `${m.name} (ZDR)` }]
1103
+ : []),
1104
+ ...(m.private
1105
+ ? [{ ...base, id: `${m.id}:private`, name: `${m.name} (Private)` }]
1106
+ : []),
1107
+ ];
942
1108
  }),
943
1109
  };
944
1110
  if (opts.install) {
@@ -947,17 +1113,43 @@ export async function setupOpenclawCommand(opts) {
947
1113
  const models = existing.models ?? {};
948
1114
  const providers = models.providers ?? {};
949
1115
  providers.bankr = providerConfig;
1116
+ if (imageProvider) {
1117
+ providers.openai = {
1118
+ ...(providers.openai ?? {}),
1119
+ ...imageProvider,
1120
+ };
1121
+ }
950
1122
  models.providers = providers;
951
1123
  existing.models = models;
1124
+ if (imageGenerationModel) {
1125
+ const agents = existing.agents ?? {};
1126
+ const defaults = agents.defaults ?? {};
1127
+ defaults.imageGenerationModel = imageGenerationModel;
1128
+ agents.defaults = defaults;
1129
+ existing.agents = agents;
1130
+ }
952
1131
  });
953
1132
  warnIfNoKey(llmKey);
1133
+ warnImagesSlot();
954
1134
  return;
955
1135
  }
956
- console.log(JSON.stringify({ models: { providers: { bankr: providerConfig } } }, null, 2));
1136
+ const printed = {
1137
+ models: {
1138
+ providers: {
1139
+ bankr: providerConfig,
1140
+ ...(imageProvider ? { openai: imageProvider } : {}),
1141
+ },
1142
+ },
1143
+ ...(imageGenerationModel
1144
+ ? { agents: { defaults: { imageGenerationModel } } }
1145
+ : {}),
1146
+ };
1147
+ console.log(JSON.stringify(printed, null, 2));
957
1148
  console.log();
958
1149
  if (!llmKey) {
959
1150
  output.warn("No API key set. Replace ${BANKR_API_KEY} with your key or run: bankr login");
960
1151
  }
1152
+ warnImagesSlot();
961
1153
  output.dim("Add to ~/.openclaw/openclaw.json or run: bankr llm setup openclaw --install");
962
1154
  }
963
1155
  /* ──────────────────────── bankr llm setup opencode ─────────────────────────── */
@@ -965,11 +1157,15 @@ export async function setupOpenCodeCommand(opts) {
965
1157
  const llmKey = getLlmKey();
966
1158
  const llmUrl = getLlmUrl();
967
1159
  const { models } = await resolveModels();
968
- const activeModels = models.filter((m) => !m.deprecation);
1160
+ // Coding-tool config lists chat models only — image-only models can't chat.
1161
+ const activeModels = models.filter((m) => !m.deprecation && supportsChat(m));
969
1162
  const modelsObj = {};
970
1163
  for (const m of activeModels) {
971
1164
  modelsObj[m.id] = { name: m.name };
972
- // Private models also expose a `:private` variant (TEE compute environment).
1165
+ // One variant per privacy tier the model supports, weakest to strongest.
1166
+ if (m.zdr) {
1167
+ modelsObj[`${m.id}:zdr`] = { name: `${m.name} (ZDR)` };
1168
+ }
973
1169
  if (m.private) {
974
1170
  modelsObj[`${m.id}:private`] = { name: `${m.name} (Private)` };
975
1171
  }
@@ -1122,6 +1318,9 @@ function translateClaudeModelArgs(args) {
1122
1318
  return out;
1123
1319
  }
1124
1320
  export async function claudeCommand(args) {
1321
+ if (isHelpRequest(args)) {
1322
+ return launchTool("claude", args, {}, "https://docs.anthropic.com/en/docs/claude-code");
1323
+ }
1125
1324
  const llmKey = requireAuth();
1126
1325
  const llmUrl = getLlmUrl();
1127
1326
  const translated = translateClaudeModelArgs(args);
@@ -1130,6 +1329,9 @@ export async function claudeCommand(args) {
1130
1329
  }
1131
1330
  /* ────────────────────────── bankr llm opencode ───────────────────────────── */
1132
1331
  export async function opencodeCommand(args) {
1332
+ if (isHelpRequest(args)) {
1333
+ return launchTool("opencode", args, {}, "https://opencode.ai");
1334
+ }
1133
1335
  const llmKey = requireAuth();
1134
1336
  const llmUrl = getLlmUrl();
1135
1337
  const configFile = join(homedir(), ".config", "opencode", "opencode.json");
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Both Privy credentials minted by the OTP exchange. The identity token alone
3
+ * authenticates, but only the ACCESS token carries the session `sid` claim —
4
+ * so sending it too is what makes a CLI login show up in Active Sessions and
5
+ * be killable by "log out this device". Keep them paired; never send just one.
6
+ */
7
+ interface CliAuthTokens {
8
+ identityToken: string;
9
+ accessToken: string;
10
+ }
11
+ /** The auth headers every authenticated CLI call sends — one place, so a new
12
+ * endpoint can't silently omit the access token and go unrevocable again. */
13
+ export declare function authHeaders(tokens: CliAuthTokens): Record<string, string>;
1
14
  export declare function loginCommand(opts: {
2
15
  apiUrl?: string;
3
16
  apiKey?: string;
@@ -16,4 +29,5 @@ export declare function loginCommand(opts: {
16
29
  allowedIps?: string;
17
30
  allowedRecipients?: string;
18
31
  }): Promise<void>;
32
+ export {};
19
33
  //# sourceMappingURL=login.d.ts.map
@@ -4,6 +4,7 @@ import open from "open";
4
4
  import { isAddress } from "viem";
5
5
  import { CLI_USER_AGENT, DEFAULT_API_URL, getApiUrl, getConfigPath, readConfig, writeConfig, } from "../lib/config.js";
6
6
  import * as output from "../lib/output.js";
7
+ import { getErrorMessage } from "../lib/errors.js";
7
8
  const DEFAULT_DASHBOARD_URL = "https://bankr.bot/api-keys";
8
9
  const MAX_OTP_ATTEMPTS = 3;
9
10
  const INVALID_CODE_STATUSES = [400, 401, 422];
@@ -45,6 +46,16 @@ async function sendPrivyOtp(privyConfig, email) {
45
46
  throw new Error(`Failed to send verification code: ${body || res.status}`);
46
47
  }
47
48
  }
49
+ /** The auth headers every authenticated CLI call sends — one place, so a new
50
+ * endpoint can't silently omit the access token and go unrevocable again. */
51
+ export function authHeaders(tokens) {
52
+ return {
53
+ "Content-Type": "application/json",
54
+ "User-Agent": CLI_USER_AGENT,
55
+ "privy-id-token": tokens.identityToken,
56
+ "x-access-token": tokens.accessToken,
57
+ };
58
+ }
48
59
  async function verifyPrivyOtp(privyConfig, email, code) {
49
60
  const res = await fetch("https://auth.privy.io/api/v1/passwordless/authenticate", {
50
61
  method: "POST",
@@ -66,14 +77,10 @@ async function verifyPrivyOtp(privyConfig, email, code) {
66
77
  }
67
78
  return (await res.json());
68
79
  }
69
- async function callGenerateWallet(apiUrl, identityToken) {
80
+ async function callGenerateWallet(apiUrl, tokens) {
70
81
  const res = await fetch(`${apiUrl}/cli/generate-wallet`, {
71
82
  method: "POST",
72
- headers: {
73
- "Content-Type": "application/json",
74
- "User-Agent": CLI_USER_AGENT,
75
- "privy-id-token": identityToken,
76
- },
83
+ headers: authHeaders(tokens),
77
84
  });
78
85
  const body = (await res.json());
79
86
  if (!res.ok) {
@@ -81,28 +88,20 @@ async function callGenerateWallet(apiUrl, identityToken) {
81
88
  }
82
89
  return body;
83
90
  }
84
- async function callAcceptTerms(apiUrl, identityToken) {
91
+ async function callAcceptTerms(apiUrl, tokens) {
85
92
  const res = await fetch(`${apiUrl}/user/accept-terms`, {
86
93
  method: "POST",
87
- headers: {
88
- "Content-Type": "application/json",
89
- "User-Agent": CLI_USER_AGENT,
90
- "privy-id-token": identityToken,
91
- },
94
+ headers: authHeaders(tokens),
92
95
  });
93
96
  if (!res.ok) {
94
97
  const body = (await res.json().catch(() => ({})));
95
98
  throw new Error(body.message || body.error || `Accept terms failed (${res.status})`);
96
99
  }
97
100
  }
98
- async function callGenerateApiKey(apiUrl, identityToken, opts) {
101
+ async function callGenerateApiKey(apiUrl, tokens, opts) {
99
102
  const res = await fetch(`${apiUrl}/api-keys`, {
100
103
  method: "POST",
101
- headers: {
102
- "Content-Type": "application/json",
103
- "User-Agent": CLI_USER_AGENT,
104
- "privy-id-token": identityToken,
105
- },
104
+ headers: authHeaders(tokens),
106
105
  body: JSON.stringify(opts),
107
106
  });
108
107
  const body = (await res.json());
@@ -127,7 +126,7 @@ async function sendOtpWithSpinner(privyConfig, email) {
127
126
  }
128
127
  catch (err) {
129
128
  spin.fail("Failed to send verification code");
130
- fatal(err.message);
129
+ fatal(getErrorMessage(err));
131
130
  }
132
131
  }
133
132
  async function verifyOtpWithSpinner(privyConfig, email, code) {
@@ -138,7 +137,7 @@ async function verifyOtpWithSpinner(privyConfig, email, code) {
138
137
  return result;
139
138
  }
140
139
  catch (err) {
141
- const errMsg = err.message;
140
+ const errMsg = getErrorMessage(err);
142
141
  spin.fail("Verification failed");
143
142
  fatal(errMsg === "INVALID_CODE" ? "Invalid verification code." : errMsg);
144
143
  }
@@ -162,7 +161,13 @@ async function authenticateWithEmail(apiUrl, providedEmail, providedCode) {
162
161
  // Headless: code already provided, verify directly
163
162
  if (providedCode) {
164
163
  const result = await verifyOtpWithSpinner(privyConfig, email, providedCode.trim());
165
- return { identityToken: result.identity_token, email };
164
+ return {
165
+ tokens: {
166
+ identityToken: result.identity_token,
167
+ accessToken: result.token,
168
+ },
169
+ email,
170
+ };
166
171
  }
167
172
  // Interactive: send OTP, then prompt for code with retries
168
173
  await sendOtpWithSpinner(privyConfig, email);
@@ -176,10 +181,16 @@ async function authenticateWithEmail(apiUrl, providedEmail, providedCode) {
176
181
  try {
177
182
  const result = await verifyPrivyOtp(privyConfig, email, code.trim());
178
183
  verifySpin.succeed("Email verified");
179
- return { identityToken: result.identity_token, email };
184
+ return {
185
+ tokens: {
186
+ identityToken: result.identity_token,
187
+ accessToken: result.token,
188
+ },
189
+ email,
190
+ };
180
191
  }
181
192
  catch (err) {
182
- const errMsg = err.message;
193
+ const errMsg = getErrorMessage(err);
183
194
  if (errMsg === "INVALID_CODE") {
184
195
  if (attempt < MAX_OTP_ATTEMPTS - 1) {
185
196
  verifySpin.fail("Invalid code, please try again");
@@ -197,17 +208,17 @@ async function authenticateWithEmail(apiUrl, providedEmail, providedCode) {
197
208
  // ── Email login flow ────────────────────────────────────────────────
198
209
  async function emailLoginFlow(apiUrl, opts) {
199
210
  // Step 1: Authenticate via email OTP
200
- const { identityToken } = await authenticateWithEmail(apiUrl, opts.email, opts.code);
211
+ const { tokens } = await authenticateWithEmail(apiUrl, opts.email, opts.code);
201
212
  // Step 2: Generate/resolve wallet
202
213
  const walletSpin = output.spinner("Setting up wallet...");
203
214
  let wallet;
204
215
  try {
205
- wallet = await callGenerateWallet(apiUrl, identityToken);
216
+ wallet = await callGenerateWallet(apiUrl, tokens);
206
217
  walletSpin.stop();
207
218
  }
208
219
  catch (err) {
209
220
  walletSpin.fail("Wallet setup failed");
210
- fatal(err.message);
221
+ fatal(getErrorMessage(err));
211
222
  }
212
223
  // Show wallet info
213
224
  output.blank();
@@ -241,12 +252,12 @@ async function emailLoginFlow(apiUrl, opts) {
241
252
  }
242
253
  const termsSpin = output.spinner("Accepting terms...");
243
254
  try {
244
- await callAcceptTerms(apiUrl, identityToken);
255
+ await callAcceptTerms(apiUrl, tokens);
245
256
  termsSpin.succeed("Terms accepted");
246
257
  }
247
258
  catch (err) {
248
259
  termsSpin.fail("Failed to accept terms");
249
- fatal(err.message);
260
+ fatal(getErrorMessage(err));
250
261
  }
251
262
  }
252
263
  // Step 4: Generate API key
@@ -281,7 +292,7 @@ async function emailLoginFlow(apiUrl, opts) {
281
292
  const keySpin = output.spinner("Generating API key...");
282
293
  let apiKeyResult;
283
294
  try {
284
- apiKeyResult = await callGenerateApiKey(apiUrl, identityToken, {
295
+ apiKeyResult = await callGenerateApiKey(apiUrl, tokens, {
285
296
  name: keyName.trim(),
286
297
  walletApiEnabled: enableWallet,
287
298
  agentApiEnabled: enableAgent,
@@ -295,7 +306,7 @@ async function emailLoginFlow(apiUrl, opts) {
295
306
  }
296
307
  catch (err) {
297
308
  keySpin.fail("Failed to generate API key");
298
- fatal(err.message);
309
+ fatal(getErrorMessage(err));
299
310
  }
300
311
  // Show result
301
312
  output.blank();