@bankr/cli 0.2.15 → 0.3.8

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.
@@ -25,7 +25,16 @@ function feeTypeLabel(type) {
25
25
  return "Wallet";
26
26
  }
27
27
  }
28
+ async function promptOrSkip(message) {
29
+ if (output.isNonInteractive())
30
+ return "";
31
+ return input({ message, theme: output.bankrTheme });
32
+ }
28
33
  async function collectInteractive(options) {
34
+ if (output.isNonInteractive() && !options.name) {
35
+ output.error("--name is required in non-interactive mode");
36
+ process.exit(1);
37
+ }
29
38
  const tokenName = options.name ??
30
39
  (await input({
31
40
  message: "What do you want to call your token?",
@@ -33,32 +42,17 @@ async function collectInteractive(options) {
33
42
  required: true,
34
43
  }));
35
44
  const tokenSymbol = options.symbol ??
36
- (await input({
37
- message: "Token symbol (press Enter to auto-generate from name):",
38
- theme: output.bankrTheme,
39
- }));
45
+ (await promptOrSkip("Token symbol (press Enter to auto-generate from name):"));
40
46
  const imageUrl = options.image ??
41
- (await input({
42
- message: "Image URL for your token (press Enter to skip):",
43
- theme: output.bankrTheme,
44
- }));
47
+ (await promptOrSkip("Image URL for your token (press Enter to skip):"));
45
48
  const tweetUrl = options.tweet ??
46
- (await input({
47
- message: "Associate with a tweet? Paste URL (press Enter to skip):",
48
- theme: output.bankrTheme,
49
- }));
49
+ (await promptOrSkip("Associate with a tweet? Paste URL (press Enter to skip):"));
50
50
  const websiteUrl = options.website ??
51
- (await input({
52
- message: "Project website URL (press Enter to skip):",
53
- theme: output.bankrTheme,
54
- }));
51
+ (await promptOrSkip("Project website URL (press Enter to skip):"));
55
52
  let feeRecipient = options.fee ??
56
- (await input({
57
- message: "Direct fees to someone? Enter username, ENS, or address (press Enter to skip):",
58
- theme: output.bankrTheme,
59
- }));
53
+ (await promptOrSkip("Direct fees to someone? Enter username, ENS, or address (press Enter to skip):"));
60
54
  let feeRecipientType = parseFeeType(options.feeType);
61
- if (feeRecipient && !options.feeType) {
55
+ if (feeRecipient && !options.feeType && !output.isNonInteractive()) {
62
56
  feeRecipientType = await select({
63
57
  message: "What type of identifier is this?",
64
58
  choices: [
@@ -108,8 +102,7 @@ export async function launchCommand(options) {
108
102
  output.keyValue("Mode", "Simulation (dry run)");
109
103
  }
110
104
  console.log();
111
- // Confirm unless --yes
112
- if (!options.yes) {
105
+ if (!options.yes && !output.isNonInteractive()) {
113
106
  const proceed = await confirm({
114
107
  message: options.simulate
115
108
  ? "Simulate this token launch?"
@@ -5,8 +5,7 @@ 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 { bankrTheme } from "../lib/output.js";
9
- const ERR_LLM_NOT_ENABLED = "LLM Gateway not enabled on this API key. Enable at bankr.bot/api";
8
+ const ERR_LLM_NOT_ENABLED = "LLM Gateway not enabled on this API key. Enable at bankr.bot/api-keys";
10
9
  /**
11
10
  * Fallback model catalog — used when the gateway is unreachable or unauthenticated.
12
11
  * resolveModels() fetches live data from GET /v1/models; this list is the offline safety net.
@@ -171,6 +170,15 @@ const GATEWAY_MODELS = [
171
170
  cost: { input: 0.05, output: 0.4, cacheRead: 0.005, cacheWrite: 0 },
172
171
  },
173
172
  // Other providers
173
+ {
174
+ id: "grok-4.3",
175
+ name: "Grok 4.3",
176
+ owned_by: "x-ai",
177
+ contextWindow: 1000000,
178
+ maxTokens: 1000000,
179
+ input: IMAGE_INPUT,
180
+ cost: { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
181
+ },
174
182
  {
175
183
  id: "grok-4.1-fast",
176
184
  name: "Grok 4.1 Fast",
@@ -198,6 +206,24 @@ const GATEWAY_MODELS = [
198
206
  input: TEXT_INPUT,
199
207
  cost: { input: 0.12, output: 0.75, cacheRead: 0.06, cacheWrite: 0 },
200
208
  },
209
+ {
210
+ id: "qwen3.7-plus",
211
+ name: "Qwen3.7 Plus",
212
+ owned_by: "qwen",
213
+ contextWindow: 1000000,
214
+ maxTokens: 65536,
215
+ input: TEXT_INPUT,
216
+ cost: { input: 0.4, output: 1.6, cacheRead: 0.04, cacheWrite: 0 },
217
+ },
218
+ {
219
+ id: "qwen3.6-flash",
220
+ name: "Qwen3.6 Flash",
221
+ owned_by: "qwen",
222
+ contextWindow: 1000000,
223
+ maxTokens: 65536,
224
+ input: TEXT_INPUT,
225
+ cost: { input: 0.25, output: 1.5, cacheRead: 0, cacheWrite: 0 },
226
+ },
201
227
  {
202
228
  id: "qwen3.5-plus",
203
229
  name: "Qwen3.5 Plus",
@@ -216,6 +242,24 @@ const GATEWAY_MODELS = [
216
242
  input: TEXT_INPUT,
217
243
  cost: { input: 0.1, output: 0.4, cacheRead: 0, cacheWrite: 0 },
218
244
  },
245
+ {
246
+ id: "kimi-k2.7-code",
247
+ name: "Kimi K2.7 Code",
248
+ owned_by: "moonshotai",
249
+ contextWindow: 262144,
250
+ maxTokens: 262144,
251
+ input: TEXT_INPUT,
252
+ cost: { input: 0.95, output: 4.0, cacheRead: 0.19, cacheWrite: 0 },
253
+ },
254
+ {
255
+ id: "kimi-k2.6",
256
+ name: "Kimi K2.6",
257
+ owned_by: "moonshotai",
258
+ contextWindow: 262144,
259
+ maxTokens: 262144,
260
+ input: TEXT_INPUT,
261
+ cost: { input: 0.95, output: 4.0, cacheRead: 0.16, cacheWrite: 0 },
262
+ },
219
263
  {
220
264
  id: "kimi-k2.5",
221
265
  name: "Kimi K2.5",
@@ -413,6 +457,89 @@ export async function creditsCommand() {
413
457
  }
414
458
  }
415
459
  /* ─────────────────────── bankr llm credits add ──────────────────────────── */
460
+ // Keep in sync with packages/shared/src/chain-config-base/chainConfigBase.ts.
461
+ // CLI is standalone-published and cannot depend on @bankr/shared.
462
+ const TOPUP_CHAINS = ["base", "polygon", "mainnet", "arbitrum", "bnb"];
463
+ const CHAIN_DISPLAY_NAME = {
464
+ base: "Base",
465
+ polygon: "Polygon",
466
+ mainnet: "Ethereum",
467
+ arbitrum: "Arbitrum",
468
+ bnb: "BNB Chain",
469
+ };
470
+ // Polygon's native POL is at a precompile, not the 0xeee placeholder.
471
+ const NATIVE_TOKEN_ADDRESS_BY_CHAIN = {
472
+ base: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
473
+ polygon: "0x0000000000000000000000000000000000001010",
474
+ mainnet: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
475
+ arbitrum: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
476
+ bnb: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
477
+ };
478
+ const BLOCK_EXPLORER_URL = {
479
+ base: "https://basescan.org",
480
+ polygon: "https://polygonscan.com",
481
+ mainnet: "https://etherscan.io",
482
+ arbitrum: "https://arbiscan.io",
483
+ bnb: "https://bscscan.com",
484
+ };
485
+ /**
486
+ * Resolve user input (symbol or 0x address) to the (chain, token) it should
487
+ * pay with. Scans every supported chain and picks the highest USD balance,
488
+ * so the user never has to think about chains. Returns null if nothing matches.
489
+ */
490
+ function resolveTokenAcrossChains(input, balances) {
491
+ const isAddress = input.startsWith("0x");
492
+ const sym = isAddress ? null : input.toUpperCase();
493
+ const candidates = [];
494
+ for (const chain of TOPUP_CHAINS) {
495
+ const chainBal = balances[chain];
496
+ if (!chainBal)
497
+ continue;
498
+ if (!isAddress) {
499
+ const nativeUsd = parseFloat(chainBal.nativeUsd || "0");
500
+ const nativeSym = nativeSymbolFor(chain);
501
+ if (sym === nativeSym && nativeUsd > 0) {
502
+ candidates.push({
503
+ chain,
504
+ address: NATIVE_TOKEN_ADDRESS_BY_CHAIN[chain],
505
+ symbol: nativeSym,
506
+ name: nativeSym,
507
+ decimals: 18,
508
+ balance: parseFloat(chainBal.nativeBalance || "0"),
509
+ balanceUsd: nativeUsd,
510
+ });
511
+ }
512
+ }
513
+ for (const tb of chainBal.tokenBalances) {
514
+ const matchesSym = sym != null && tb.token.baseToken.symbol.toUpperCase() === sym;
515
+ const matchesAddr = isAddress &&
516
+ tb.token.baseToken.address.toLowerCase() === input.toLowerCase();
517
+ if (!matchesSym && !matchesAddr)
518
+ continue;
519
+ candidates.push({
520
+ chain,
521
+ address: tb.token.baseToken.address,
522
+ symbol: tb.token.baseToken.symbol,
523
+ name: tb.token.baseToken.name,
524
+ decimals: tb.token.baseToken.decimals,
525
+ balance: tb.token.balance,
526
+ balanceUsd: tb.token.balanceUSD,
527
+ imageUrl: tb.token.baseToken.imgUrl,
528
+ });
529
+ }
530
+ }
531
+ if (candidates.length === 0)
532
+ return null;
533
+ candidates.sort((a, b) => b.balanceUsd - a.balanceUsd);
534
+ return candidates[0];
535
+ }
536
+ function nativeSymbolFor(chain) {
537
+ if (chain === "polygon")
538
+ return "POL";
539
+ if (chain === "bnb")
540
+ return "BNB";
541
+ return "ETH";
542
+ }
416
543
  export async function creditsAddCommand(amount, opts) {
417
544
  const apiKey = requireApiKey();
418
545
  const apiUrl = getApiUrl();
@@ -424,9 +551,9 @@ export async function creditsAddCommand(amount, opts) {
424
551
  output.error("Amount must be between $1 and $1,000");
425
552
  process.exit(1);
426
553
  }
427
- // Fetch current balance for display (skip when --yes since user won't see it)
554
+ // Fetch current balance for display (skip when --yes/--not-interactive since user won't see it)
428
555
  let currentBalance = "unknown";
429
- if (llmKey && !opts.yes) {
556
+ if (llmKey && !opts.yes && !output.isNonInteractive()) {
430
557
  const balSpin = output.spinner("Fetching balance…");
431
558
  try {
432
559
  const balRes = await fetch(`${llmUrl}/v1/credits`, {
@@ -448,18 +575,41 @@ export async function creditsAddCommand(amount, opts) {
448
575
  output.dim(` (Could not fetch balance: ${err.message})`);
449
576
  }
450
577
  }
578
+ // When --token is passed, the chain holding the highest USD balance of
579
+ // that symbol/address wins. Default (no --token) is Base USDC.
580
+ let resolved = null;
581
+ if (opts.token) {
582
+ const resolveSpin = output.spinner("Resolving token…");
583
+ try {
584
+ const balData = await getBalances([...TOPUP_CHAINS]);
585
+ resolved = resolveTokenAcrossChains(opts.token, balData.balances);
586
+ resolveSpin.stop();
587
+ }
588
+ catch (err) {
589
+ resolveSpin.stop();
590
+ output.warn(`Could not fetch balances: ${err.message}`);
591
+ }
592
+ if (!resolved) {
593
+ output.error(`Token "${opts.token}" not found in your wallet on any supported chain.`);
594
+ process.exit(1);
595
+ }
596
+ }
597
+ const targetChain = resolved?.chain ?? "base";
598
+ const sourceTokenLabel = resolved
599
+ ? `${resolved.symbol} on ${CHAIN_DISPLAY_NAME[targetChain]}`
600
+ : `USDC on ${CHAIN_DISPLAY_NAME.base} (default)`;
451
601
  // Show confirmation
452
602
  console.log();
453
603
  output.label("Add Credits", `$${amountUsd.toFixed(2)}`);
454
- output.keyValue("Source token", opts.token ?? "USDC (default)");
604
+ output.keyValue("Pay with", sourceTokenLabel);
455
605
  output.keyValue("Current balance", currentBalance);
456
606
  console.log();
457
- if (!opts.yes) {
607
+ if (!opts.yes && !output.isNonInteractive()) {
458
608
  const { confirm } = await import("@inquirer/prompts");
459
609
  const proceed = await confirm({
460
610
  message: "Proceed?",
461
611
  default: true,
462
- theme: bankrTheme,
612
+ theme: output.bankrTheme,
463
613
  });
464
614
  if (!proceed) {
465
615
  output.dim("Cancelled.");
@@ -468,9 +618,12 @@ export async function creditsAddCommand(amount, opts) {
468
618
  }
469
619
  const spin = output.spinner("Adding credits…");
470
620
  try {
471
- const body = { amountUsd };
472
- if (opts.token) {
473
- body.sourceToken = opts.token;
621
+ const body = {
622
+ amountUsd,
623
+ chain: targetChain,
624
+ };
625
+ if (resolved) {
626
+ body.sourceToken = resolved.address;
474
627
  }
475
628
  const res = await fetch(`${apiUrl}/llm/credits/topup`, {
476
629
  method: "POST",
@@ -507,7 +660,7 @@ export async function creditsAddCommand(amount, opts) {
507
660
  output.success(`Added $${result.creditsGranted.toFixed(2)} credits`);
508
661
  output.label("New Balance", `$${result.newBalance.toFixed(2)}`);
509
662
  if (result.txHash) {
510
- output.keyValue("Transaction", `https://basescan.org/tx/${result.txHash}`);
663
+ output.keyValue("Transaction", `${BLOCK_EXPLORER_URL[targetChain]}/tx/${result.txHash}`);
511
664
  }
512
665
  }
513
666
  catch (err) {
@@ -623,45 +776,27 @@ export async function creditsAutoCommand(opts) {
623
776
  process.exit(1);
624
777
  }
625
778
  const resolved = [];
626
- // Fetch balances to resolve both symbols and addresses
779
+ // Highest USD balance wins when the same symbol exists on multiple chains.
627
780
  const spin = output.spinner("Resolving tokens…");
628
- let baseTokens = [];
629
- let hasNativeEth = false;
781
+ let balances = {};
630
782
  try {
631
- const balData = await getBalances(["base"]);
632
- const baseChain = balData.balances.base;
633
- baseTokens = baseChain?.tokenBalances ?? [];
634
- hasNativeEth = !!baseChain && parseFloat(baseChain.nativeBalance) > 0;
783
+ const balData = await getBalances([...TOPUP_CHAINS]);
784
+ balances = balData.balances;
635
785
  }
636
786
  catch {
637
787
  output.warn("Could not fetch balances for token resolution");
638
788
  }
639
789
  spin.stop();
640
790
  for (const input of inputs) {
641
- const isAddress = input.startsWith("0x");
642
- const sym = isAddress ? null : input.toUpperCase();
643
- // Native ETH
644
- if (sym === "ETH" && hasNativeEth) {
645
- resolved.push({
646
- address: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
647
- chain: "base",
648
- symbol: "ETH",
649
- name: "Ethereum",
650
- decimals: 18,
651
- });
652
- continue;
653
- }
654
- const tb = baseTokens.find((t) => isAddress
655
- ? t.token.baseToken.address.toLowerCase() === input.toLowerCase()
656
- : t.token.baseToken.symbol.toUpperCase() === sym);
657
- if (tb) {
791
+ const hit = resolveTokenAcrossChains(input, balances);
792
+ if (hit) {
658
793
  resolved.push({
659
- address: tb.token.baseToken.address,
660
- chain: "base",
661
- symbol: tb.token.baseToken.symbol,
662
- name: tb.token.baseToken.name,
663
- decimals: tb.token.baseToken.decimals,
664
- imageUrl: tb.token.baseToken.imgUrl,
794
+ address: hit.address,
795
+ chain: hit.chain,
796
+ symbol: hit.symbol,
797
+ name: hit.name,
798
+ decimals: hit.decimals,
799
+ imageUrl: hit.imageUrl,
665
800
  });
666
801
  }
667
802
  else {
@@ -13,7 +13,6 @@ export declare function loginCommand(opts: {
13
13
  url?: boolean;
14
14
  siwe?: boolean;
15
15
  privateKey?: string;
16
- partnerKey?: string;
17
16
  allowedIps?: string;
18
17
  allowedRecipients?: string;
19
18
  }): Promise<void>;
@@ -4,7 +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
- const DEFAULT_DASHBOARD_URL = "https://bankr.bot/api";
7
+ const DEFAULT_DASHBOARD_URL = "https://bankr.bot/api-keys";
8
8
  const MAX_OTP_ATTEMPTS = 3;
9
9
  const INVALID_CODE_STATUSES = [400, 401, 422];
10
10
  function fatal(message) {
@@ -15,7 +15,8 @@ function deriveDashboardUrl(apiUrl) {
15
15
  if (apiUrl === DEFAULT_API_URL) {
16
16
  return DEFAULT_DASHBOARD_URL;
17
17
  }
18
- return (apiUrl.replace(/\/+$/, "").replace("api.", "").replace("api-", "") + "/api");
18
+ return (apiUrl.replace(/\/+$/, "").replace("api.", "").replace("api-", "") +
19
+ "/api-keys");
19
20
  }
20
21
  async function fetchPrivyConfig(apiUrl) {
21
22
  const res = await fetch(`${apiUrl}/cli/config`, {
@@ -220,8 +221,8 @@ async function emailLoginFlow(apiUrl, opts) {
220
221
  if (wallet.solAddress) {
221
222
  output.dim(` SOL: ${wallet.solAddress}`);
222
223
  }
223
- // Headless mode: when --code is provided, skip all interactive prompts
224
- const headless = !!opts.code;
224
+ // Headless mode: when --code is provided or --ni is set, skip prompts
225
+ const headless = !!opts.code || output.isNonInteractive();
225
226
  // Step 3: Accept terms if needed
226
227
  if (!wallet.hasAcceptedTerms) {
227
228
  output.blank();
@@ -263,7 +264,7 @@ async function emailLoginFlow(apiUrl, opts) {
263
264
  // Fine-grained API flags. walletApi and tokenLaunch default to enabled.
264
265
  // --read-write only controls readOnly, not which APIs are enabled.
265
266
  const enableWallet = opts.walletApi ?? true;
266
- // Agent API must be enabled from the website (bankr.bot/api), not the CLI.
267
+ // Agent API must be enabled from the website (bankr.bot/api-keys), not the CLI.
267
268
  const enableAgent = false;
268
269
  const enableTokenLaunch = opts.tokenLaunch ?? true;
269
270
  const enableLlm = opts.llm ??
@@ -317,8 +318,8 @@ async function emailLoginFlow(apiUrl, opts) {
317
318
  ];
318
319
  output.dim(` Recipients: ${parts.join(", ")}`);
319
320
  }
320
- output.dim(" Agent API: enable at bankr.bot/api");
321
- output.dim(" Manage keys at bankr.bot/api");
321
+ output.dim(" Agent API: enable at bankr.bot/api-keys");
322
+ output.dim(" Manage keys at bankr.bot/api-keys");
322
323
  return apiKeyResult.apiKey;
323
324
  }
324
325
  // ── SIWE login flow ─────────────────────────────────────────────────
@@ -365,7 +366,6 @@ async function siweLoginFlow(apiUrl, opts) {
365
366
  body: JSON.stringify({
366
367
  message,
367
368
  signature,
368
- partnerApiKey: opts.partnerKey,
369
369
  keyName: opts.keyName ?? `SIWE-${new Date().toISOString().slice(0, 10)}`,
370
370
  readOnly: opts.readOnly ?? false,
371
371
  walletApiEnabled: opts.walletApi ?? true,
@@ -395,7 +395,7 @@ async function siweLoginFlow(apiUrl, opts) {
395
395
  async function validateApiKey(apiUrl, apiKey) {
396
396
  const spin = output.spinner("Validating Bankr API key...");
397
397
  try {
398
- const res = await fetch(`${apiUrl}/agent/me`, {
398
+ const res = await fetch(`${apiUrl}/wallet/me`, {
399
399
  headers: { "X-API-Key": apiKey, "User-Agent": CLI_USER_AGENT },
400
400
  });
401
401
  if (res.ok) {
@@ -404,20 +404,14 @@ async function validateApiKey(apiUrl, apiKey) {
404
404
  else if (res.status === 401 || res.status === 403) {
405
405
  const body = (await res.json().catch(() => ({})));
406
406
  const errorType = body.error ?? "";
407
- if (res.status === 403 || errorType === "Agent API access not enabled") {
408
- spin.warn("Agent API access is not enabled");
409
- output.warn("Your API key is valid but does not have Agent API access enabled.");
410
- output.info("Go to https://bankr.bot/api and enable Agent API access for this key.");
411
- output.info("Your key has been saved — once enabled, the CLI will work without re-login.");
412
- }
413
- else if (errorType === "Authentication required") {
407
+ if (errorType === "Authentication required") {
414
408
  spin.fail("API key is not linked to a wallet");
415
- output.error("This API key is not associated with a wallet. Generate a new key at https://bankr.bot/api");
409
+ output.error("This API key is not associated with a wallet. Generate a new key at https://bankr.bot/api-keys");
416
410
  process.exit(1);
417
411
  }
418
412
  else {
419
413
  spin.fail("Invalid Bankr API key");
420
- output.error("The provided API key is invalid or inactive. Generate a new key at https://bankr.bot/api");
414
+ output.error("The provided API key is invalid or inactive. Generate a new key at https://bankr.bot/api-keys");
421
415
  process.exit(1);
422
416
  }
423
417
  }
@@ -497,13 +491,28 @@ export async function loginCommand(opts) {
497
491
  if (opts.apiKey !== undefined && !opts.apiKey.trim()) {
498
492
  fatal("--api-key requires a non-empty value");
499
493
  }
494
+ // Non-interactive mode requires a headless flag combo. Without one, the
495
+ // command would hang on a prompt — fail fast with the available paths.
496
+ // Email step 1 (`login email <addr>` without --code) is also headless:
497
+ // it sends the OTP and exits.
498
+ if (output.isNonInteractive()) {
499
+ const hasHeadlessFlag = opts.apiKey ||
500
+ opts.url ||
501
+ (opts.siwe && opts.privateKey) ||
502
+ (opts.email && opts.email.trim());
503
+ if (!hasHeadlessFlag) {
504
+ output.error("Non-interactive login requires one of: --api-key <key>, login siwe --private-key <key>, or login email <address> [--code <otp>]");
505
+ process.exit(1);
506
+ }
507
+ }
500
508
  const config = readConfig();
501
509
  const resolvedApiUrl = opts.apiUrl ?? getApiUrl();
502
510
  if (resolvedApiUrl !== DEFAULT_API_URL) {
503
511
  const nonInteractive = !!(opts.apiKey ||
504
512
  opts.url ||
505
513
  opts.email !== undefined ||
506
- opts.siwe);
514
+ opts.siwe ||
515
+ output.isNonInteractive());
507
516
  if (nonInteractive) {
508
517
  config.apiUrl = resolvedApiUrl;
509
518
  }
@@ -557,7 +566,6 @@ export async function loginCommand(opts) {
557
566
  }
558
567
  const { apiKey } = await siweLoginFlow(apiUrl, {
559
568
  privateKey: opts.privateKey,
560
- partnerKey: opts.partnerKey,
561
569
  keyName: opts.keyName,
562
570
  readOnly: !opts.readWrite,
563
571
  walletApi: opts.walletApi,
@@ -565,9 +573,6 @@ export async function loginCommand(opts) {
565
573
  ...getParsedRestrictions(),
566
574
  });
567
575
  config.apiKey = apiKey;
568
- if (opts.partnerKey) {
569
- config.partnerKey = opts.partnerKey;
570
- }
571
576
  writeConfig(config);
572
577
  output.success(`Credentials saved to ${getConfigPath()}`);
573
578
  return;
@@ -6,6 +6,9 @@ import * as output from "../lib/output.js";
6
6
  * Must be manually kept in sync with models in the LLM gateway DB. */
7
7
  const VALID_MAX_MODE_MODELS = new Set([
8
8
  // Claude (Vertex AI)
9
+ "claude-fable-5",
10
+ "claude-opus-4.8",
11
+ "claude-opus-4.7",
9
12
  "claude-opus-4.6",
10
13
  "claude-opus-4.5",
11
14
  "claude-sonnet-4.6",
@@ -26,11 +29,14 @@ const VALID_MAX_MODE_MODELS = new Set([
26
29
  "deepseek-v3.2",
27
30
  // Qwen (OpenRouter)
28
31
  "qwen3-coder",
32
+ "qwen3.7-plus",
29
33
  "qwen3.5-plus",
30
34
  // MiniMax (direct)
31
35
  "minimax-m2.5",
32
36
  "minimax-m2.7",
33
37
  // Other (OpenRouter)
38
+ "kimi-k2.7-code",
39
+ "kimi-k2.6",
34
40
  "kimi-k2.5",
35
41
  "glm-5",
36
42
  ]);
@@ -0,0 +1,8 @@
1
+ export declare function swapCommand(opts: {
2
+ from: string;
3
+ to: string;
4
+ amount: string;
5
+ chain?: string;
6
+ quoteOnly?: boolean;
7
+ }): Promise<void>;
8
+ //# sourceMappingURL=swap.d.ts.map
@@ -0,0 +1,107 @@
1
+ import { getSwapQuote, executeSwap } from "../lib/api.js";
2
+ import * as output from "../lib/output.js";
3
+ import { NATIVE_SYMBOLS, VALID_CHAINS } from "../lib/chains.js";
4
+ import { ZERO_ADDRESS, resolveTokenSymbol } from "../lib/tokens.js";
5
+ /**
6
+ * Resolve a token symbol or address to a contract address for swapping.
7
+ * - The chain's native ticker (ETH/POL/BNB) or the zero address → native, sent
8
+ * as the zero address (the API detects + sanitizes native tokens).
9
+ * - Otherwise → symbol/address resolution via the shared resolver.
10
+ */
11
+ async function resolveSwapToken(tokenInput, chain) {
12
+ const input = tokenInput.trim();
13
+ const nativeSymbol = NATIVE_SYMBOLS[chain] ?? "ETH";
14
+ const upper = input.toUpperCase();
15
+ const isNative = upper === nativeSymbol.toUpperCase() ||
16
+ input.toLowerCase() === ZERO_ADDRESS;
17
+ if (isNative) {
18
+ return { address: ZERO_ADDRESS, symbol: nativeSymbol };
19
+ }
20
+ return resolveTokenSymbol(input, chain);
21
+ }
22
+ export async function swapCommand(opts) {
23
+ const chain = opts.chain ?? "base";
24
+ if (!VALID_CHAINS.has(chain) || chain === "solana") {
25
+ output.error(`Unsupported chain "${chain}". Swaps support EVM chains: base, polygon, mainnet, unichain, worldchain, arbitrum, bnb.`);
26
+ process.exit(1);
27
+ }
28
+ // Validate amount client-side so a bad value gives a clear error instead of a
29
+ // generic 500 from the API's parseUnits(amount, decimals). Number() alone
30
+ // accepts "1e-3" / "0x10" / "Infinity" that parseUnits rejects, so require a
31
+ // plain decimal string.
32
+ if (!/^\d*\.?\d+$/.test(opts.amount) || Number(opts.amount) <= 0) {
33
+ output.error(`Invalid amount "${opts.amount}". Provide a positive decimal number, e.g. --amount 0.5.`);
34
+ process.exit(1);
35
+ }
36
+ // Resolve from/to tokens (symbol → address, native → zero address)
37
+ let fromToken;
38
+ let toToken;
39
+ const resolveSpin = output.spinner(`Resolving ${opts.from} → ${opts.to} on ${chain}...`);
40
+ try {
41
+ [fromToken, toToken] = await Promise.all([
42
+ resolveSwapToken(opts.from, chain),
43
+ resolveSwapToken(opts.to, chain),
44
+ ]);
45
+ resolveSpin.stop();
46
+ }
47
+ catch (err) {
48
+ resolveSpin.fail(err.message);
49
+ process.exit(1);
50
+ }
51
+ // Quote
52
+ const quoteSpin = output.spinner(`Fetching quote for ${opts.amount} ${fromToken.symbol} → ${toToken.symbol}...`);
53
+ let quote;
54
+ try {
55
+ quote = await getSwapQuote({
56
+ fromChain: chain,
57
+ fromToken: fromToken.address,
58
+ toChain: chain,
59
+ toToken: toToken.address,
60
+ amount: opts.amount,
61
+ });
62
+ quoteSpin.stop();
63
+ }
64
+ catch (err) {
65
+ quoteSpin.fail(`Quote failed: ${err.message}`);
66
+ process.exit(1);
67
+ }
68
+ output.blank();
69
+ output.label("You pay", `${output.formatBalance(quote.from.formattedAmount)} ${quote.from.symbol} (${output.formatUsd(quote.from.usdValue)})`);
70
+ output.label("You receive", `~${output.formatBalance(quote.to.formattedAmount)} ${quote.to.symbol} (${output.formatUsd(quote.to.usdValue)})`);
71
+ output.keyValue("Min received", `${output.formatBalance(quote.minBuyAmount)} ${quote.to.symbol}`);
72
+ output.blank();
73
+ if (opts.quoteOnly) {
74
+ return;
75
+ }
76
+ // Execute (uses the minBuyAmount from the quote as slippage protection)
77
+ const swapSpin = output.spinner(`Swapping ${opts.amount} ${fromToken.symbol} → ${toToken.symbol}...`);
78
+ try {
79
+ const result = await executeSwap({
80
+ fromChain: chain,
81
+ fromToken: fromToken.address,
82
+ toChain: chain,
83
+ toToken: toToken.address,
84
+ amount: opts.amount,
85
+ minBuyAmount: quote.minBuyAmount,
86
+ });
87
+ if (!result.success) {
88
+ swapSpin.fail(`Swap failed: ${result.error ?? "unknown error"}`);
89
+ // A reverted tx can come back as { success: false } with a hash — surface
90
+ // it so the user can inspect the failed transaction on-chain.
91
+ if (result.hash) {
92
+ output.label("Tx Hash", result.hash);
93
+ }
94
+ process.exit(1);
95
+ }
96
+ swapSpin.succeed("Swap successful");
97
+ output.label("Tx Hash", result.hash ?? "unknown");
98
+ if (result.amountReceived != null) {
99
+ output.label("Received", `${output.formatBalance(result.amountReceived)} ${toToken.symbol}`);
100
+ }
101
+ }
102
+ catch (err) {
103
+ swapSpin.fail(`Swap failed: ${err.message}`);
104
+ process.exit(1);
105
+ }
106
+ }
107
+ //# sourceMappingURL=swap.js.map