@bankr/cli 0.3.12 → 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.
Files changed (41) hide show
  1. package/README.md +28 -10
  2. package/dist/cli.js +31 -8
  3. package/dist/commands/balances.js +2 -1
  4. package/dist/commands/cancel.js +2 -1
  5. package/dist/commands/club.js +4 -3
  6. package/dist/commands/fees.js +83 -45
  7. package/dist/commands/files.js +14 -13
  8. package/dist/commands/launch.d.ts +5 -0
  9. package/dist/commands/launch.js +76 -7
  10. package/dist/commands/llm.d.ts +2 -0
  11. package/dist/commands/llm.js +264 -44
  12. package/dist/commands/login.d.ts +14 -0
  13. package/dist/commands/login.js +41 -30
  14. package/dist/commands/portfolio.js +3 -2
  15. package/dist/commands/profile.js +6 -5
  16. package/dist/commands/prompt.js +19 -6
  17. package/dist/commands/sign.js +2 -1
  18. package/dist/commands/sounds.js +4 -3
  19. package/dist/commands/status.js +3 -2
  20. package/dist/commands/submit.js +3 -2
  21. package/dist/commands/swap.js +9 -7
  22. package/dist/commands/tokens.js +3 -2
  23. package/dist/commands/transfer.js +8 -6
  24. package/dist/commands/update.js +2 -1
  25. package/dist/commands/webhooks.js +15 -10
  26. package/dist/commands/whoami.js +3 -2
  27. package/dist/commands/x402.d.ts +3 -0
  28. package/dist/commands/x402.js +75 -30
  29. package/dist/lib/api.d.ts +18 -1
  30. package/dist/lib/api.js +32 -41
  31. package/dist/lib/errors.d.ts +7 -0
  32. package/dist/lib/errors.js +9 -0
  33. package/dist/lib/explorer.d.ts +7 -0
  34. package/dist/lib/explorer.js +14 -0
  35. package/dist/lib/output.d.ts +1 -0
  36. package/dist/lib/output.js +6 -0
  37. package/dist/lib/passthrough.d.ts +21 -0
  38. package/dist/lib/passthrough.js +26 -0
  39. package/dist/lib/tokens.d.ts +0 -5
  40. package/dist/lib/tokens.js +0 -5
  41. package/package.json +1 -1
@@ -1,9 +1,25 @@
1
1
  import { confirm, input, select } from "@inquirer/prompts";
2
- import { deployToken } from "../lib/api.js";
2
+ import { deployToken, } from "../lib/api.js";
3
3
  import { emitCESP } from "../lib/cesp/engine.js";
4
4
  import { requireApiKey } from "../lib/config.js";
5
5
  import * as output from "../lib/output.js";
6
+ import { getErrorMessage } from "../lib/errors.js";
6
7
  const VALID_FEE_TYPES = ["x", "farcaster", "ens", "wallet"];
8
+ const VALID_CHAINS = ["base", "robinhood"];
9
+ function chainLabel(chain) {
10
+ return chain === "robinhood" ? "Robinhood Chain" : "Base";
11
+ }
12
+ function parseChain(value) {
13
+ if (!value)
14
+ return undefined;
15
+ const lower = value.toLowerCase();
16
+ if (VALID_CHAINS.includes(lower))
17
+ return lower;
18
+ // Launching is irreversible and chain-selecting — a mistyped chain must be
19
+ // a hard usage error, never a silent fallback to Base.
20
+ output.error(`Unknown chain "${value}". Valid chains: ${VALID_CHAINS.join(", ")}.`);
21
+ process.exit(1);
22
+ }
7
23
  function parseFeeType(value) {
8
24
  if (!value)
9
25
  return "x";
@@ -45,6 +61,49 @@ async function collectInteractive(options) {
45
61
  (await promptOrSkip("Token symbol (press Enter to auto-generate from name):"));
46
62
  const imageUrl = options.image ??
47
63
  (await promptOrSkip("Image URL for your token (press Enter to skip):"));
64
+ // Chain: --chain flag wins; otherwise interactive select (default Base).
65
+ // Non-interactive runs without --chain launch on Base.
66
+ let chain = parseChain(options.chain);
67
+ if (!chain && !output.isNonInteractive()) {
68
+ chain = await select({
69
+ message: "Which chain do you want to launch on?",
70
+ choices: [
71
+ { name: "Base", value: "base" },
72
+ { name: "Robinhood Chain", value: "robinhood" },
73
+ ],
74
+ default: "base",
75
+ theme: output.bankrTheme,
76
+ });
77
+ }
78
+ chain ?? (chain = "base");
79
+ // Vesting: --no-vesting wins; otherwise interactive select (default: keep
80
+ // the 15% creator vesting). Non-interactive runs keep vesting on.
81
+ let disableVesting = options.vesting === false;
82
+ if (!disableVesting && !output.isNonInteractive()) {
83
+ disableVesting = await select({
84
+ message: "Creator vesting? By default 15% of supply vests to the fee recipient over 1 year (30-day cliff).",
85
+ choices: [
86
+ { name: "15% vesting (default)", value: false },
87
+ { name: "No vesting — sell 100% into the pool", value: true },
88
+ ],
89
+ default: false,
90
+ theme: output.bankrTheme,
91
+ });
92
+ }
93
+ // Fee collection: --quote-only-fees wins; otherwise interactive select
94
+ // (default: in-kind). Non-interactive runs keep the in-kind default.
95
+ let quoteOnlyFees = options.quoteOnlyFees === true;
96
+ if (!quoteOnlyFees && !output.isNonInteractive()) {
97
+ quoteOnlyFees = await select({
98
+ message: "Fee collection? By default creator fees accrue as a mix of your token and the pool's quote token (e.g. WETH).",
99
+ choices: [
100
+ { name: "Both tokens (default)", value: false },
101
+ { name: "Quote token only", value: true },
102
+ ],
103
+ default: false,
104
+ theme: output.bankrTheme,
105
+ });
106
+ }
48
107
  const tweetUrl = options.tweet ??
49
108
  (await promptOrSkip("Associate with a tweet? Paste URL (press Enter to skip):"));
50
109
  const websiteUrl = options.website ??
@@ -64,17 +123,18 @@ async function collectInteractive(options) {
64
123
  theme: output.bankrTheme,
65
124
  });
66
125
  }
67
- if (!feeRecipient) {
68
- feeRecipient = "";
69
- }
126
+ feeRecipient || (feeRecipient = "");
70
127
  return {
71
128
  tokenName: tokenName.trim(),
72
129
  tokenSymbol: tokenSymbol.trim(),
73
130
  imageUrl: imageUrl.trim(),
131
+ chain,
74
132
  tweetUrl: tweetUrl.trim(),
75
133
  websiteUrl: websiteUrl.trim(),
76
134
  feeRecipient: feeRecipient.trim(),
77
135
  feeRecipientType,
136
+ disableVesting,
137
+ quoteOnlyFees,
78
138
  };
79
139
  }
80
140
  export async function launchCommand(options) {
@@ -97,7 +157,13 @@ export async function launchCommand(options) {
97
157
  output.keyValue("Fee recipient", state.feeRecipient
98
158
  ? `${state.feeRecipient} (${feeTypeLabel(state.feeRecipientType)})`
99
159
  : "(self)");
100
- output.keyValue("Chain", "Base");
160
+ output.keyValue("Chain", chainLabel(state.chain));
161
+ output.keyValue("Vesting", state.disableVesting
162
+ ? "Off (100% of supply sold into the pool)"
163
+ : "15% to fee recipient over 1 year (default)");
164
+ output.keyValue("Fee collection", state.quoteOnlyFees
165
+ ? "Quote token only (e.g. WETH)"
166
+ : "Both tokens (default)");
101
167
  if (options.simulate) {
102
168
  output.keyValue("Mode", "Simulation (dry run)");
103
169
  }
@@ -123,12 +189,15 @@ export async function launchCommand(options) {
123
189
  tokenName: state.tokenName,
124
190
  tokenSymbol: state.tokenSymbol || undefined,
125
191
  image: state.imageUrl || undefined,
192
+ chain: state.chain,
126
193
  tweetUrl: state.tweetUrl || undefined,
127
194
  websiteUrl: state.websiteUrl || undefined,
128
195
  feeRecipient: state.feeRecipient
129
196
  ? { type: state.feeRecipientType, value: state.feeRecipient }
130
197
  : undefined,
131
198
  simulateOnly: options.simulate,
199
+ disableVesting: state.disableVesting || undefined,
200
+ quoteOnlyFees: state.quoteOnlyFees || undefined,
132
201
  });
133
202
  const elapsed = output.formatDuration(Date.now() - startTime);
134
203
  spin.succeed(result.simulated
@@ -155,12 +224,12 @@ export async function launchCommand(options) {
155
224
  }
156
225
  }
157
226
  console.log();
158
- output.dim(`View token: https://bankr.bot/launches/${result.tokenAddress}`);
227
+ output.dim(`View token: https://bankr.bot/terminal/trade?out=${result.tokenAddress}&chain=${result.chain}`);
159
228
  }
160
229
  catch (err) {
161
230
  spin.fail("Token launch failed");
162
231
  emitCESP("task.error");
163
- output.error(err.message);
232
+ output.error(getErrorMessage(err));
164
233
  process.exit(1);
165
234
  }
166
235
  }
@@ -1,5 +1,6 @@
1
1
  export declare function modelsCommand(opts?: {
2
2
  private?: boolean;
3
+ zdr?: boolean;
3
4
  }): Promise<void>;
4
5
  export declare function creditsCommand(): Promise<void>;
5
6
  export declare function creditsAddCommand(amount: string, opts: {
@@ -15,6 +16,7 @@ export declare function creditsAutoCommand(opts: {
15
16
  }): Promise<void>;
16
17
  export declare function setupOpenclawCommand(opts: {
17
18
  install?: boolean;
19
+ images?: boolean;
18
20
  }): Promise<void>;
19
21
  export declare function setupOpenCodeCommand(opts: {
20
22
  install?: boolean;
@@ -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.
@@ -14,6 +20,15 @@ const IMAGE_INPUT = ["text", "image"];
14
20
  const TEXT_INPUT = ["text"];
15
21
  const GATEWAY_MODELS = [
16
22
  // Claude
23
+ {
24
+ id: "claude-fable-5",
25
+ name: "Claude Fable 5",
26
+ owned_by: "anthropic",
27
+ contextWindow: 1000000,
28
+ maxTokens: 128000,
29
+ input: IMAGE_INPUT,
30
+ cost: { input: 10.0, output: 50.0, cacheRead: 1.0, cacheWrite: 12.5 },
31
+ },
17
32
  {
18
33
  id: "claude-opus-4.8",
19
34
  name: "Claude Opus 4.8",
@@ -78,6 +93,24 @@ const GATEWAY_MODELS = [
78
93
  cost: { input: 1.0, output: 5.0, cacheRead: 0.1, cacheWrite: 1.25 },
79
94
  },
80
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
+ },
81
114
  {
82
115
  id: "gemini-3.1-pro",
83
116
  name: "Gemini 3.1 Pro",
@@ -124,6 +157,27 @@ const GATEWAY_MODELS = [
124
157
  cost: { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0.0833 },
125
158
  },
126
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
+ },
172
+ {
173
+ id: "gpt-5.5",
174
+ name: "GPT 5.5",
175
+ owned_by: "openai",
176
+ contextWindow: 1050000,
177
+ maxTokens: 128000,
178
+ input: IMAGE_INPUT,
179
+ cost: { input: 5.0, output: 30.0, cacheRead: 0.5, cacheWrite: 0 },
180
+ },
127
181
  {
128
182
  id: "gpt-5.4",
129
183
  name: "GPT 5.4",
@@ -260,6 +314,15 @@ const GATEWAY_MODELS = [
260
314
  input: TEXT_INPUT,
261
315
  cost: { input: 0.1, output: 0.4, cacheRead: 0, cacheWrite: 0 },
262
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
+ },
263
326
  {
264
327
  id: "kimi-k2.7-code",
265
328
  name: "Kimi K2.7 Code",
@@ -355,6 +418,9 @@ async function resolveModels() {
355
418
  contextWindow: m.context_window ?? fallback?.contextWindow ?? 128000,
356
419
  maxTokens: m.max_output_tokens ?? fallback?.maxTokens ?? 32768,
357
420
  input: m.input_modalities ?? fallback?.input ?? TEXT_INPUT,
421
+ ...((m.output_modalities ?? fallback?.output) && {
422
+ output: m.output_modalities ?? fallback?.output,
423
+ }),
358
424
  cost: m.pricing
359
425
  ? {
360
426
  input: m.pricing.input,
@@ -370,6 +436,7 @@ async function resolveModels() {
370
436
  }),
371
437
  ...(m.deprecation && { deprecation: m.deprecation }),
372
438
  ...(m.private && { private: true }),
439
+ ...(m.zdr && { zdr: true }),
373
440
  };
374
441
  });
375
442
  return { models, live: true };
@@ -408,46 +475,142 @@ function installConfigFile(configFile, toolName, merge) {
408
475
  output.success(`Bankr provider installed to ${configFile}`);
409
476
  }
410
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
+ };
411
496
  export async function modelsCommand(opts = {}) {
412
497
  const spin = output.spinner("Fetching models…");
413
498
  const { models, live } = await resolveModels();
414
499
  spin.stop();
415
- // `private` is the gateway's own "supports :private" flag from
416
- // /v1/models (driven by each model's NEAR slot + NEARAI_API_KEY), so this
417
- // tracks the live registry rather than a hardcoded CLI list.
418
- const shown = opts.private ? models.filter((m) => m.private) : models;
419
- output.brandBold(opts.private
420
- ? "Bankr LLM Gateway — Private Models"
421
- : "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");
422
509
  console.log();
423
- if (opts.private && shown.length === 0) {
510
+ if (view && shown.length === 0) {
424
511
  output.warn(live
425
- ? "No models currently offer a private (TEE) compute environment."
426
- : "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).");
427
514
  return;
428
515
  }
429
- const COL = { id: 24, name: 24, provider: 12 };
430
- const hasNotes = shown.some((m) => m.deprecation || m.private);
431
- 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")}` : ""}`);
432
- console.log(output.fmt.dim(` ${"─".repeat(COL.id + COL.name + COL.provider + (hasNotes ? 24 : 2))}`));
433
- for (const m of shown) {
434
- 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 = [];
435
566
  if (m.deprecation) {
436
- notes = m.deprecation.replaced_by
567
+ parts.push(m.deprecation.replaced_by
437
568
  ? `deprecated → ${m.deprecation.replaced_by}`
438
- : "deprecated";
569
+ : "deprecated");
439
570
  }
440
- else if (m.private) {
441
- notes = "Private (TEE)";
442
- }
443
- 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
+ ]);
444
604
  }
445
605
  console.log();
446
606
  output.dim(` Gateway: ${getLlmUrl()}`);
447
- output.dim(` ${shown.length} model${shown.length === 1 ? "" : "s"}${opts.private ? " support :private" : " available"}${live ? "" : " (cached)"}`);
448
- 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)) {
449
609
  output.dim(" Private (TEE) — append :private to the model id (e.g. glm-5.2:private). List: bankr llm models --private");
450
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
+ }
451
614
  }
452
615
  /* ─────────────────────── bankr llm credits ─────────────────────────────────── */
453
616
  export async function creditsCommand() {
@@ -489,7 +652,7 @@ export async function creditsCommand() {
489
652
  }
490
653
  catch (err) {
491
654
  spin.stop();
492
- output.error(`Failed to fetch credits: ${err.message}`);
655
+ output.error(`Failed to fetch credits: ${getErrorMessage(err)}`);
493
656
  process.exit(1);
494
657
  }
495
658
  }
@@ -542,7 +705,7 @@ function resolveTokenAcrossChains(input, balances) {
542
705
  symbol: nativeSym,
543
706
  name: nativeSym,
544
707
  decimals: 18,
545
- balance: parseFloat(chainBal.nativeBalance || "0"),
708
+ balance: chainBal.nativeBalance || "0",
546
709
  balanceUsd: nativeUsd,
547
710
  });
548
711
  }
@@ -609,7 +772,7 @@ export async function creditsAddCommand(amount, opts) {
609
772
  catch (err) {
610
773
  balSpin.stop();
611
774
  // Non-fatal — proceed without balance display
612
- output.dim(` (Could not fetch balance: ${err.message})`);
775
+ output.dim(` (Could not fetch balance: ${getErrorMessage(err)})`);
613
776
  }
614
777
  }
615
778
  // When --token is passed, the chain holding the highest USD balance of
@@ -624,7 +787,7 @@ export async function creditsAddCommand(amount, opts) {
624
787
  }
625
788
  catch (err) {
626
789
  resolveSpin.stop();
627
- output.warn(`Could not fetch balances: ${err.message}`);
790
+ output.warn(`Could not fetch balances: ${getErrorMessage(err)}`);
628
791
  }
629
792
  if (!resolved) {
630
793
  output.error(`Token "${opts.token}" not found in your wallet on any supported chain.`);
@@ -702,7 +865,7 @@ export async function creditsAddCommand(amount, opts) {
702
865
  }
703
866
  catch (err) {
704
867
  spin.stop();
705
- output.error(`Failed to add credits: ${err.message}`);
868
+ output.error(`Failed to add credits: ${getErrorMessage(err)}`);
706
869
  process.exit(1);
707
870
  }
708
871
  }
@@ -764,7 +927,7 @@ export async function creditsAutoCommand(opts) {
764
927
  }
765
928
  catch (err) {
766
929
  spin.stop();
767
- output.error(`Failed to fetch config: ${err.message}`);
930
+ output.error(`Failed to fetch config: ${getErrorMessage(err)}`);
768
931
  process.exit(1);
769
932
  }
770
933
  return;
@@ -889,7 +1052,7 @@ export async function creditsAutoCommand(opts) {
889
1052
  }
890
1053
  catch (err) {
891
1054
  spin.stop();
892
- output.error(`Failed to update config: ${err.message}`);
1055
+ output.error(`Failed to update config: ${getErrorMessage(err)}`);
893
1056
  process.exit(1);
894
1057
  }
895
1058
  }
@@ -898,13 +1061,31 @@ export async function setupOpenclawCommand(opts) {
898
1061
  const llmKey = getLlmKey();
899
1062
  const llmUrl = getLlmUrl();
900
1063
  const { models } = await resolveModels();
901
- 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
+ };
902
1082
  const providerConfig = {
903
1083
  baseUrl: llmUrl,
904
1084
  apiKey: llmKey ?? "${BANKR_API_KEY}",
905
1085
  api: "openai-completions",
906
- // Private models get both entries: the plain id and a `:private`
907
- // 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.
908
1089
  models: activeModels.flatMap((m) => {
909
1090
  const base = {
910
1091
  id: m.id,
@@ -915,12 +1096,15 @@ export async function setupOpenclawCommand(opts) {
915
1096
  maxTokens: m.maxTokens,
916
1097
  cost: m.cost,
917
1098
  };
918
- return m.private
919
- ? [
920
- base,
921
- { ...base, id: `${m.id}:private`, name: `${m.name} (Private)` },
922
- ]
923
- : [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
+ ];
924
1108
  }),
925
1109
  };
926
1110
  if (opts.install) {
@@ -929,17 +1113,43 @@ export async function setupOpenclawCommand(opts) {
929
1113
  const models = existing.models ?? {};
930
1114
  const providers = models.providers ?? {};
931
1115
  providers.bankr = providerConfig;
1116
+ if (imageProvider) {
1117
+ providers.openai = {
1118
+ ...(providers.openai ?? {}),
1119
+ ...imageProvider,
1120
+ };
1121
+ }
932
1122
  models.providers = providers;
933
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
+ }
934
1131
  });
935
1132
  warnIfNoKey(llmKey);
1133
+ warnImagesSlot();
936
1134
  return;
937
1135
  }
938
- 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));
939
1148
  console.log();
940
1149
  if (!llmKey) {
941
1150
  output.warn("No API key set. Replace ${BANKR_API_KEY} with your key or run: bankr login");
942
1151
  }
1152
+ warnImagesSlot();
943
1153
  output.dim("Add to ~/.openclaw/openclaw.json or run: bankr llm setup openclaw --install");
944
1154
  }
945
1155
  /* ──────────────────────── bankr llm setup opencode ─────────────────────────── */
@@ -947,11 +1157,15 @@ export async function setupOpenCodeCommand(opts) {
947
1157
  const llmKey = getLlmKey();
948
1158
  const llmUrl = getLlmUrl();
949
1159
  const { models } = await resolveModels();
950
- 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));
951
1162
  const modelsObj = {};
952
1163
  for (const m of activeModels) {
953
1164
  modelsObj[m.id] = { name: m.name };
954
- // 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
+ }
955
1169
  if (m.private) {
956
1170
  modelsObj[`${m.id}:private`] = { name: `${m.name} (Private)` };
957
1171
  }
@@ -997,7 +1211,7 @@ export async function setupCursorCommand() {
997
1211
  const { models } = await resolveModels();
998
1212
  const activeModels = models.filter((m) => !m.deprecation);
999
1213
  // Pick one model per provider as recommended examples
1000
- const recommendedIds = ["claude-opus-4.8", "gemini-3.1-pro", "gpt-5.2"];
1214
+ const recommendedIds = ["claude-fable-5", "gemini-3.1-pro", "gpt-5.5"];
1001
1215
  const recommended = recommendedIds.filter((id) => activeModels.some((m) => m.id === id));
1002
1216
  output.brandBold("Cursor — Bankr LLM Gateway");
1003
1217
  console.log();
@@ -1104,6 +1318,9 @@ function translateClaudeModelArgs(args) {
1104
1318
  return out;
1105
1319
  }
1106
1320
  export async function claudeCommand(args) {
1321
+ if (isHelpRequest(args)) {
1322
+ return launchTool("claude", args, {}, "https://docs.anthropic.com/en/docs/claude-code");
1323
+ }
1107
1324
  const llmKey = requireAuth();
1108
1325
  const llmUrl = getLlmUrl();
1109
1326
  const translated = translateClaudeModelArgs(args);
@@ -1112,6 +1329,9 @@ export async function claudeCommand(args) {
1112
1329
  }
1113
1330
  /* ────────────────────────── bankr llm opencode ───────────────────────────── */
1114
1331
  export async function opencodeCommand(args) {
1332
+ if (isHelpRequest(args)) {
1333
+ return launchTool("opencode", args, {}, "https://opencode.ai");
1334
+ }
1115
1335
  const llmKey = requireAuth();
1116
1336
  const llmUrl = getLlmUrl();
1117
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