@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.
- package/README.md +28 -10
- package/dist/cli.js +28 -7
- package/dist/commands/balances.js +2 -1
- package/dist/commands/cancel.js +2 -1
- package/dist/commands/fees.js +65 -40
- package/dist/commands/launch.d.ts +4 -0
- package/dist/commands/launch.js +42 -5
- package/dist/commands/llm.d.ts +2 -0
- package/dist/commands/llm.js +245 -43
- package/dist/commands/login.d.ts +14 -0
- package/dist/commands/login.js +41 -30
- package/dist/commands/portfolio.js +3 -2
- package/dist/commands/profile.js +6 -5
- package/dist/commands/prompt.js +19 -6
- package/dist/commands/sign.js +2 -1
- package/dist/commands/sounds.js +4 -3
- package/dist/commands/status.js +3 -2
- package/dist/commands/submit.js +3 -2
- package/dist/commands/swap.js +9 -7
- package/dist/commands/tokens.js +3 -2
- package/dist/commands/transfer.js +8 -6
- package/dist/commands/update.js +2 -1
- package/dist/commands/webhooks.js +6 -2
- package/dist/commands/whoami.js +3 -2
- package/dist/commands/x402.d.ts +3 -0
- package/dist/commands/x402.js +63 -19
- package/dist/lib/api.d.ts +13 -1
- package/dist/lib/api.js +32 -41
- package/dist/lib/output.d.ts +1 -0
- package/dist/lib/output.js +6 -0
- package/dist/lib/passthrough.d.ts +21 -0
- package/dist/lib/passthrough.js +26 -0
- package/dist/lib/tokens.d.ts +0 -5
- package/dist/lib/tokens.js +0 -5
- package/package.json +1 -1
package/dist/commands/llm.js
CHANGED
|
@@ -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
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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 (
|
|
510
|
+
if (view && shown.length === 0) {
|
|
442
511
|
output.warn(live
|
|
443
|
-
?
|
|
444
|
-
: "
|
|
512
|
+
? view.empty
|
|
513
|
+
: "Tier status needs a live gateway — set BANKR_LLM_KEY (and BANKR_LLM_URL).");
|
|
445
514
|
return;
|
|
446
515
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
567
|
+
parts.push(m.deprecation.replaced_by
|
|
455
568
|
? `deprecated → ${m.deprecation.replaced_by}`
|
|
456
|
-
: "deprecated";
|
|
569
|
+
: "deprecated");
|
|
457
570
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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"}${
|
|
466
|
-
if (!
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
925
|
-
//
|
|
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
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
{ ...base, id: `${m.id}:
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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");
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -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
|
package/dist/commands/login.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
309
|
+
fatal(getErrorMessage(err));
|
|
299
310
|
}
|
|
300
311
|
// Show result
|
|
301
312
|
output.blank();
|