@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
@@ -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();
@@ -2,6 +2,7 @@ import chalk from "chalk";
2
2
  import { getPortfolio, } from "../lib/api.js";
3
3
  import * as output from "../lib/output.js";
4
4
  import { formatUsd, formatBalance } from "../lib/output.js";
5
+ import { getErrorMessage } from "../lib/errors.js";
5
6
  import { CHAIN_LABELS, VALID_CHAINS } from "../lib/chains.js";
6
7
  const BRAND = chalk.hex("#FF613D");
7
8
  const DIM = chalk.dim;
@@ -78,7 +79,7 @@ export async function portfolioCommand(opts) {
78
79
  spin.succeed("Portfolio loaded");
79
80
  }
80
81
  catch (err) {
81
- spin.fail(`Failed to fetch portfolio: ${err.message}`);
82
+ spin.fail(`Failed to fetch portfolio: ${getErrorMessage(err)}`);
82
83
  process.exit(1);
83
84
  }
84
85
  // JSON output mode
@@ -109,7 +110,7 @@ export async function portfolioCommand(opts) {
109
110
  printChainBalances(chain, chainData, !!includePnl);
110
111
  }
111
112
  // Print NFTs
112
- if (includeNfts && data.nfts && data.nfts.length > 0) {
113
+ if (includeNfts && data.nfts?.length) {
113
114
  console.log();
114
115
  console.log();
115
116
  console.log(` ${BRAND.bold("NFTs")} ${DIM(`${data.nfts.length} item(s)`)}`);
@@ -2,6 +2,7 @@ import chalk from "chalk";
2
2
  import { input, confirm, select } from "@inquirer/prompts";
3
3
  import { getOwnProfiles, createProfile, updateProfile, deleteProfile, addProjectUpdate, } from "../lib/api.js";
4
4
  import * as output from "../lib/output.js";
5
+ import { getErrorMessage } from "../lib/errors.js";
5
6
  const BRAND = chalk.hex("#FF613D");
6
7
  const DIM = chalk.dim;
7
8
  function printProfile(profile, json) {
@@ -74,7 +75,7 @@ export async function profileViewCommand(opts) {
74
75
  }
75
76
  }
76
77
  catch (err) {
77
- const msg = err.message;
78
+ const msg = getErrorMessage(err);
78
79
  if (msg.includes("404")) {
79
80
  output.info("No profiles found. Create one with: bankr profile create");
80
81
  }
@@ -132,7 +133,7 @@ export async function profileCreateCommand(opts) {
132
133
  printProfile(profile, !!opts.json);
133
134
  }
134
135
  catch (err) {
135
- output.error(err.message);
136
+ output.error(getErrorMessage(err));
136
137
  }
137
138
  }
138
139
  export async function profileUpdateCommand(opts) {
@@ -163,7 +164,7 @@ export async function profileUpdateCommand(opts) {
163
164
  printProfile(profile, !!opts.json);
164
165
  }
165
166
  catch (err) {
166
- output.error(err.message);
167
+ output.error(getErrorMessage(err));
167
168
  }
168
169
  }
169
170
  export async function profileDeleteCommand(opts) {
@@ -187,7 +188,7 @@ export async function profileDeleteCommand(opts) {
187
188
  output.success("Profile deleted.");
188
189
  }
189
190
  catch (err) {
190
- output.error(err.message);
191
+ output.error(getErrorMessage(err));
191
192
  }
192
193
  }
193
194
  export async function profileAddUpdateCommand(opts) {
@@ -221,7 +222,7 @@ export async function profileAddUpdateCommand(opts) {
221
222
  }
222
223
  }
223
224
  catch (err) {
224
- output.error(err.message);
225
+ output.error(getErrorMessage(err));
225
226
  }
226
227
  }
227
228
  //# sourceMappingURL=profile.js.map
@@ -2,11 +2,13 @@ import { ApiError, pollJob, submitPrompt } from "../lib/api.js";
2
2
  import { emitCESP } from "../lib/cesp/engine.js";
3
3
  import { getLastThreadId, requireApiKey, setLastThreadId, } from "../lib/config.js";
4
4
  import * as output from "../lib/output.js";
5
+ import { getErrorMessage } from "../lib/errors.js";
5
6
  /** Valid Max Mode model IDs — hardcoded because CLI is standalone (no monorepo imports).
6
7
  * Must be manually kept in sync with models in the LLM gateway DB. */
7
8
  const VALID_MAX_MODE_MODELS = new Set([
8
9
  // Claude (Vertex AI)
9
10
  "claude-fable-5",
11
+ "claude-opus-5",
10
12
  "claude-opus-4.8",
11
13
  "claude-opus-4.7",
12
14
  "claude-opus-4.6",
@@ -15,28 +17,39 @@ const VALID_MAX_MODE_MODELS = new Set([
15
17
  "claude-sonnet-4.6",
16
18
  "claude-sonnet-4.5",
17
19
  "claude-haiku-4.5",
18
- // Gemini (Vertex AI)
20
+ // Gemini (Vertex AI) — gemini-3-pro deliberately absent: removed 2026-03-09,
21
+ // both provider slugs retired. Don't re-add on a sync pass.
22
+ "gemini-3.6-flash",
23
+ "gemini-3.5-flash-lite",
19
24
  "gemini-3.1-pro",
20
- "gemini-3-pro",
21
25
  "gemini-2.5-pro",
22
26
  // OpenAI (OpenRouter)
27
+ "gpt-5.6-sol",
28
+ "gpt-5.6-terra",
29
+ "gpt-5.6-luna",
30
+ "gpt-5.5",
23
31
  "gpt-5.4",
24
32
  "gpt-5.4-mini",
25
33
  "gpt-5.2",
26
34
  "gpt-5-mini",
27
35
  // Grok (OpenRouter)
28
- "grok-4.1-fast",
36
+ "grok-4.6",
37
+ "grok-4.5",
38
+ "grok-4.3",
29
39
  // DeepSeek (OpenRouter)
30
40
  "deepseek-v3.2",
31
41
  // Qwen (OpenRouter)
32
42
  "qwen3-coder",
43
+ "qwen3.8-max",
33
44
  "qwen3.7-max",
34
45
  "qwen3.7-plus",
46
+ "qwen3.7-flash",
35
47
  "qwen3.5-plus",
36
48
  // MiniMax (direct)
37
49
  "minimax-m2.5",
38
50
  "minimax-m2.7",
39
51
  // Other (OpenRouter)
52
+ "kimi-k3",
40
53
  "kimi-k2.7-code",
41
54
  "kimi-k2.6",
42
55
  "kimi-k2.5",
@@ -95,7 +108,7 @@ export async function promptCommand(text, options = {}) {
95
108
  }
96
109
  }
97
110
  else {
98
- output.error(err.message);
111
+ output.error(getErrorMessage(err));
99
112
  }
100
113
  process.exit(1);
101
114
  }
@@ -121,7 +134,7 @@ export async function promptCommand(text, options = {}) {
121
134
  if (result.response) {
122
135
  console.log(result.response);
123
136
  }
124
- if (result.transactions && result.transactions.length > 0) {
137
+ if (result.transactions?.length) {
125
138
  console.log();
126
139
  output.dim(`${result.transactions.length} transaction(s) returned. Use the web app to review and execute.`);
127
140
  }
@@ -141,7 +154,7 @@ export async function promptCommand(text, options = {}) {
141
154
  catch (err) {
142
155
  spin.fail("Error while polling");
143
156
  emitCESP("resource.limit");
144
- output.error(err.message);
157
+ output.error(getErrorMessage(err));
145
158
  process.exit(1);
146
159
  }
147
160
  }
@@ -1,6 +1,7 @@
1
1
  import { sign } from "../lib/api.js";
2
2
  import { emitCESP } from "../lib/cesp/engine.js";
3
3
  import * as output from "../lib/output.js";
4
+ import { getErrorMessage } from "../lib/errors.js";
4
5
  export async function signCommand(opts) {
5
6
  const request = {
6
7
  signatureType: opts.type,
@@ -63,7 +64,7 @@ export async function signCommand(opts) {
63
64
  catch (err) {
64
65
  spinner.stop();
65
66
  emitCESP("task.error");
66
- output.error(err.message);
67
+ output.error(getErrorMessage(err));
67
68
  process.exit(1);
68
69
  }
69
70
  }
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
6
6
  import { readConfig, writeConfig } from "../lib/config.js";
7
7
  import { DEFAULT_SOUND_CONFIG, emitCESP, getPacksDir, listInstalledPacks, } from "../lib/cesp/engine.js";
8
8
  import * as output from "../lib/output.js";
9
+ import { getErrorMessage } from "../lib/errors.js";
9
10
  const execFileAsync = promisify(execFile);
10
11
  const REGISTRY_URL = "https://peonping.github.io/registry/index.json";
11
12
  function getSoundConfig() {
@@ -164,7 +165,7 @@ export async function soundsInstallCommand(packName) {
164
165
  }
165
166
  catch (err) {
166
167
  spin.fail("Failed to fetch registry");
167
- output.error(err.message);
168
+ output.error(getErrorMessage(err));
168
169
  process.exit(1);
169
170
  }
170
171
  // Download and extract
@@ -218,7 +219,7 @@ export async function soundsInstallCommand(packName) {
218
219
  }
219
220
  catch (err) {
220
221
  spin2.fail("Install failed");
221
- output.error(err.message);
222
+ output.error(getErrorMessage(err));
222
223
  process.exit(1);
223
224
  }
224
225
  }
@@ -232,7 +233,7 @@ export async function soundsSearchCommand(query) {
232
233
  }
233
234
  catch (err) {
234
235
  spin.fail("Failed to fetch registry");
235
- output.error(err.message);
236
+ output.error(getErrorMessage(err));
236
237
  process.exit(1);
237
238
  }
238
239
  // Filter by query if provided
@@ -1,6 +1,7 @@
1
1
  import { getJobStatus } from "../lib/api.js";
2
2
  import { requireApiKey } from "../lib/config.js";
3
3
  import * as output from "../lib/output.js";
4
+ import { getErrorMessage } from "../lib/errors.js";
4
5
  export async function statusCommand(jobId) {
5
6
  requireApiKey();
6
7
  const spin = output.spinner("Fetching job status...");
@@ -31,14 +32,14 @@ export async function statusCommand(jobId) {
31
32
  console.log();
32
33
  output.error(status.error);
33
34
  }
34
- if (status.transactions && status.transactions.length > 0) {
35
+ if (status.transactions?.length) {
35
36
  console.log();
36
37
  output.dim(`${status.transactions.length} transaction(s) attached.`);
37
38
  }
38
39
  }
39
40
  catch (err) {
40
41
  spin.fail("Failed to fetch job status");
41
- output.error(err.message);
42
+ output.error(getErrorMessage(err));
42
43
  process.exit(1);
43
44
  }
44
45
  }
@@ -1,6 +1,7 @@
1
1
  import { submit } from "../lib/api.js";
2
2
  import { emitCESP } from "../lib/cesp/engine.js";
3
3
  import * as output from "../lib/output.js";
4
+ import { getErrorMessage } from "../lib/errors.js";
4
5
  function displayResult(result) {
5
6
  if (!result.success) {
6
7
  emitCESP("task.error");
@@ -73,7 +74,7 @@ export async function submitCommand(opts) {
73
74
  }
74
75
  catch (err) {
75
76
  spinner.stop();
76
- output.error(err.message);
77
+ output.error(getErrorMessage(err));
77
78
  process.exit(1);
78
79
  }
79
80
  }
@@ -127,7 +128,7 @@ export async function submitJsonCommand(json, opts) {
127
128
  }
128
129
  catch (err) {
129
130
  spinner.stop();
130
- output.error(err.message);
131
+ output.error(getErrorMessage(err));
131
132
  process.exit(1);
132
133
  }
133
134
  }
@@ -1,7 +1,9 @@
1
1
  import { getSwapQuote, executeSwap } from "../lib/api.js";
2
2
  import * as output from "../lib/output.js";
3
+ import { getErrorMessage } from "../lib/errors.js";
3
4
  import { NATIVE_SYMBOLS, VALID_CHAINS } from "../lib/chains.js";
4
- import { ZERO_ADDRESS, resolveTokenSymbol } from "../lib/tokens.js";
5
+ import { zeroAddress } from "viem";
6
+ import { resolveTokenSymbol } from "../lib/tokens.js";
5
7
  /**
6
8
  * Resolve a token symbol or address to a contract address for swapping.
7
9
  * - The chain's native ticker (ETH/POL/BNB) or the zero address → native, sent
@@ -12,10 +14,10 @@ async function resolveSwapToken(tokenInput, chain) {
12
14
  const input = tokenInput.trim();
13
15
  const nativeSymbol = NATIVE_SYMBOLS[chain] ?? "ETH";
14
16
  const upper = input.toUpperCase();
15
- const isNative = upper === nativeSymbol.toUpperCase() ||
16
- input.toLowerCase() === ZERO_ADDRESS;
17
+ // viem's zeroAddress is already lowercase, so compare against the lowercased input.
18
+ const isNative = upper === nativeSymbol.toUpperCase() || input.toLowerCase() === zeroAddress;
17
19
  if (isNative) {
18
- return { address: ZERO_ADDRESS, symbol: nativeSymbol };
20
+ return { address: zeroAddress, symbol: nativeSymbol };
19
21
  }
20
22
  return resolveTokenSymbol(input, chain);
21
23
  }
@@ -45,7 +47,7 @@ export async function swapCommand(opts) {
45
47
  resolveSpin.stop();
46
48
  }
47
49
  catch (err) {
48
- resolveSpin.fail(err.message);
50
+ resolveSpin.fail(getErrorMessage(err));
49
51
  process.exit(1);
50
52
  }
51
53
  // Quote
@@ -62,7 +64,7 @@ export async function swapCommand(opts) {
62
64
  quoteSpin.stop();
63
65
  }
64
66
  catch (err) {
65
- quoteSpin.fail(`Quote failed: ${err.message}`);
67
+ quoteSpin.fail(`Quote failed: ${getErrorMessage(err)}`);
66
68
  process.exit(1);
67
69
  }
68
70
  output.blank();
@@ -100,7 +102,7 @@ export async function swapCommand(opts) {
100
102
  }
101
103
  }
102
104
  catch (err) {
103
- swapSpin.fail(`Swap failed: ${err.message}`);
105
+ swapSpin.fail(`Swap failed: ${getErrorMessage(err)}`);
104
106
  process.exit(1);
105
107
  }
106
108
  }
@@ -1,5 +1,6 @@
1
1
  import { searchTokens } from "../lib/api.js";
2
2
  import * as output from "../lib/output.js";
3
+ import { getErrorMessage } from "../lib/errors.js";
3
4
  export async function tokensSearchCommand(query, opts) {
4
5
  const spin = output.spinner(`Searching tokens for "${query}"...`);
5
6
  try {
@@ -22,7 +23,7 @@ export async function tokensSearchCommand(query, opts) {
22
23
  }
23
24
  }
24
25
  catch (err) {
25
- spin.fail(`Search failed: ${err.message}`);
26
+ spin.fail(`Search failed: ${getErrorMessage(err)}`);
26
27
  process.exit(1);
27
28
  }
28
29
  }
@@ -53,7 +54,7 @@ export async function tokensInfoCommand(address, opts) {
53
54
  }
54
55
  }
55
56
  catch (err) {
56
- spin.fail(`Failed to fetch token info: ${err.message}`);
57
+ spin.fail(`Failed to fetch token info: ${getErrorMessage(err)}`);
57
58
  process.exit(1);
58
59
  }
59
60
  }
@@ -1,8 +1,9 @@
1
1
  import { transfer, resolveRecipient } from "../lib/api.js";
2
2
  import * as output from "../lib/output.js";
3
+ import { getErrorMessage } from "../lib/errors.js";
3
4
  import { NATIVE_SYMBOLS } from "../lib/chains.js";
4
- import { isAddress } from "viem";
5
- import { ZERO_ADDRESS, resolveTokenSymbol } from "../lib/tokens.js";
5
+ import { isAddress, zeroAddress } from "viem";
6
+ import { resolveTokenSymbol } from "../lib/tokens.js";
6
7
  function looksLikeEnsName(value) {
7
8
  const v = value.trim().toLowerCase();
8
9
  return v.endsWith(".eth") || v.endsWith(".cb.id");
@@ -30,7 +31,7 @@ async function resolveTransferRecipient(raw) {
30
31
  };
31
32
  }
32
33
  catch (err) {
33
- spin.fail(`ENS resolution failed for "${value}": ${err.message}`);
34
+ spin.fail(`ENS resolution failed for "${value}": ${getErrorMessage(err)}`);
34
35
  process.exit(1);
35
36
  }
36
37
  }
@@ -47,7 +48,8 @@ export async function transferCommand(opts) {
47
48
  let tokenAddress;
48
49
  let tokenLabel;
49
50
  if (isNative) {
50
- tokenAddress = ZERO_ADDRESS;
51
+ // Zero address = native-token sentinel; the API detects + sanitizes it.
52
+ tokenAddress = zeroAddress;
51
53
  tokenLabel = NATIVE_SYMBOLS[chain] ?? "ETH";
52
54
  }
53
55
  else {
@@ -64,7 +66,7 @@ export async function transferCommand(opts) {
64
66
  }
65
67
  }
66
68
  catch (err) {
67
- spin.fail(err.message);
69
+ spin.fail(getErrorMessage(err));
68
70
  process.exit(1);
69
71
  }
70
72
  }
@@ -86,7 +88,7 @@ export async function transferCommand(opts) {
86
88
  output.label("Tx Hash", result.txHash ?? "unknown");
87
89
  }
88
90
  catch (err) {
89
- spin.fail(`Transfer failed: ${err.message}`);
91
+ spin.fail(`Transfer failed: ${getErrorMessage(err)}`);
90
92
  process.exit(1);
91
93
  }
92
94
  }
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { execSync } from "node:child_process";
3
3
  import * as output from "../lib/output.js";
4
+ import { getErrorMessage } from "../lib/errors.js";
4
5
  const PACKAGE_NAME = "@bankr/cli";
5
6
  /**
6
7
  * Compare two semver version strings.
@@ -79,7 +80,7 @@ export async function updateCommand(opts) {
79
80
  }
80
81
  catch (err) {
81
82
  spin.fail();
82
- output.error(err.message);
83
+ output.error(getErrorMessage(err));
83
84
  process.exit(1);
84
85
  }
85
86
  const cmp = compareSemver(current, latest);