@bankr/cli 0.3.13 → 0.3.32

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 CHANGED
@@ -73,6 +73,12 @@ bankr agent -c "compare them"
73
73
 
74
74
  # Continue a specific thread
75
75
  bankr agent --thread thr_ABC123 "tell me more"
76
+
77
+ # Max Mode: pick the model. Only the two newest releases of each line, and
78
+ # only frontier / flagship / balanced lines, are accepted (the same set the
79
+ # web picker shows); anything else is rejected and the error lists every
80
+ # valid id.
81
+ bankr agent --model claude-opus-5 "audit this contract"
76
82
  ```
77
83
 
78
84
  ### Token Launch
@@ -108,26 +114,44 @@ bankr agent cancel job_ABC123DEF456
108
114
  Launch Claude Code / OpenCode routed through the Bankr gateway:
109
115
 
110
116
  ```bash
111
- # Claude Code (auto-translates dotted model IDs to the Anthropic wire format)
112
- bankr llm claude --model claude-opus-4-8
117
+ # Claude Code — `bankr claude` is a top-level alias for `bankr llm claude`
118
+ # (auto-translates dotted model IDs to the Anthropic wire format)
119
+ bankr claude --model claude-opus-5
120
+
121
+ # Or use dotted form — bankr claude rewrites it to claude-opus-4-8
122
+ bankr claude --model claude-opus-4.8
113
123
 
114
- # Or use dotted form — bankr llm claude rewrites it to claude-opus-4-8
115
- bankr llm claude --model claude-opus-4.8
124
+ # The [1m] suffix is optional via the gateway, but MUST be quoted if used
125
+ # (zsh reads [1m] as a glob)
126
+ bankr claude --model "claude-opus-5[1m]"
116
127
 
117
128
  # OpenCode (use the bankr/ prefix with the gateway-canonical dotted form)
118
129
  bankr llm opencode -m bankr/claude-opus-4.8
119
130
  ```
120
131
 
121
- **Claude Code model format**: Claude Code's `--model` flag expects
122
- Anthropic-style dashed IDs (`claude-opus-4-8`, `claude-sonnet-4-6`,
123
- `claude-haiku-4-5`). If you pass the gateway-canonical dotted form
124
- (`claude-opus-4.8`), Claude Code silently falls back to its default —
125
- the flag appears honored but the real request is a different model.
132
+ Because `claude` is a command, it shadows the default prompt fallthrough:
133
+ `bankr claude ...` launches Claude Code rather than prompting the agent. Use
134
+ `bankr agent "..."` to send the agent a prompt starting with that word.
126
135
 
127
- `bankr llm claude` translates dotted → dashed for you. If you use older
136
+ **Claude Code model format**: Claude Code's `--model` flag expects Anthropic
137
+ wire IDs, which use dashes rather than dots in the version (`claude-opus-5`,
138
+ `claude-sonnet-5`, `claude-opus-4-8`, `claude-haiku-4-5`). If you pass the
139
+ gateway-canonical dotted form (`claude-opus-4.8`), Claude Code silently falls
140
+ back to its default — the flag appears honored but the real request is a
141
+ different model.
142
+
143
+ `bankr claude` translates dotted → dashed for you. If you use older
128
144
  versions of the CLI (or invoke `claude` directly via `ANTHROPIC_BASE_URL`),
129
145
  pass the dashed form explicitly.
130
146
 
147
+ **1M context tier**: the gateway strips Claude Code's `[1m]` suffix and enables
148
+ the 1M beta from the model's own context window, so it's **optional here** —
149
+ `claude-opus-5` and `claude-opus-5[1m]` send an identical upstream request. If
150
+ you do use it, **quote it**: in zsh (the macOS default) `[1m]` is a glob
151
+ character class, so an unquoted id aborts with `zsh: no matches found:
152
+ claude-opus-5[1m]` before the CLI ever runs. Bash passes it through literally,
153
+ so the same command can work on one machine and fail on another.
154
+
131
155
  ### Configuration
132
156
 
133
157
  ```bash
package/dist/cli.js CHANGED
@@ -10,6 +10,8 @@ import { skillsCommand } from "./commands/capabilities.js";
10
10
  import { cancelCommand } from "./commands/cancel.js";
11
11
  import { configGetCommand, configSetCommand } from "./commands/config.js";
12
12
  import { setConfigPath } from "./lib/config.js";
13
+ import { getErrorMessage } from "./lib/errors.js";
14
+ import { passthroughArgs } from "./lib/passthrough.js";
13
15
  import { claudeCommand, creditsCommand, creditsAddCommand, creditsAutoCommand, modelsCommand, opencodeCommand, setupClaudeCodeCommand, setupCursorCommand, setupOpenclawCommand, setupOpenCodeCommand, } from "./commands/llm.js";
14
16
  import { feesCommand, feesClaimCommand, feesClaimWalletCommand, } from "./commands/fees.js";
15
17
  import { launchCommand } from "./commands/launch.js";
@@ -44,14 +46,12 @@ async function readPromptInput() {
44
46
  }
45
47
  const text = Buffer.concat(chunks).toString().trim();
46
48
  if (!text) {
47
- output.error("No input received from stdin");
48
- process.exit(1);
49
+ output.fatal("No input received from stdin");
49
50
  }
50
51
  return text;
51
52
  }
52
53
  if (output.isNonInteractive()) {
53
- output.error('No prompt provided. Pass it as an argument: bankr agent "your prompt"');
54
- process.exit(1);
54
+ output.fatal('No prompt provided. Pass it as an argument: bankr agent "your prompt"');
55
55
  }
56
56
  // Interactive input — shell expansion does not apply here
57
57
  const text = await input({
@@ -304,8 +304,7 @@ const agentCmd = program
304
304
  const joined = text.join(" ").trim();
305
305
  if (!joined) {
306
306
  if (output.isNonInteractive()) {
307
- output.error('No prompt provided. Pass it as an argument: bankr agent "your prompt"');
308
- process.exit(1);
307
+ output.fatal('No prompt provided. Pass it as an argument: bankr agent "your prompt"');
309
308
  }
310
309
  agentCmd.help();
311
310
  return;
@@ -340,18 +339,20 @@ agentCmd
340
339
  program.commandsGroup("Tokens");
341
340
  program
342
341
  .command("launch")
343
- .description("Launch a token on Base or Robinhood Chain (interactive wizard)")
342
+ .description("Launch a token on Base, Robinhood Chain or Arbitrum (interactive wizard)")
344
343
  .optionsGroup("Token")
345
344
  .option("--name <name>", "Token name")
346
345
  .option("--symbol <symbol>", "Token symbol (default: auto-generated from name)")
347
346
  .option("--image <url>", "Token image URL")
348
- .option("--chain <chain>", "Chain to launch on: base, robinhood (default: base)")
347
+ .option("--chain <chain>", "Chain to launch on: base, robinhood, arbitrum (default: base)")
348
+ .option("--no-vesting", "Turn off the default 15% creator vesting (sell 100% of supply into the pool)")
349
349
  .optionsGroup("Links")
350
350
  .option("--tweet <url>", "Associated tweet URL")
351
351
  .option("--website <url>", "Project website URL")
352
352
  .optionsGroup("Fees")
353
353
  .option("--fee <recipient>", "Fee recipient (username, ENS, or address)")
354
354
  .option("--fee-type <type>", "Fee recipient type: x, farcaster, ens, wallet (default: x)")
355
+ .option("--quote-only-fees", "Collect all creator fees in the pool's quote token (e.g. WETH) instead of a mix of your token and the quote token")
355
356
  .optionsGroup("")
356
357
  .option("-y, --yes", "Skip confirmation prompt")
357
358
  .option("--simulate", "Simulate the deploy without broadcasting a transaction")
@@ -361,10 +362,12 @@ program
361
362
  symbol: opts.symbol,
362
363
  image: opts.image,
363
364
  chain: opts.chain,
365
+ vesting: opts.vesting,
364
366
  tweet: opts.tweet,
365
367
  website: opts.website,
366
368
  fee: opts.fee,
367
369
  feeType: opts.feeType,
370
+ quoteOnlyFees: opts.quoteOnlyFees,
368
371
  yes: opts.yes,
369
372
  simulate: opts.simulate,
370
373
  });
@@ -471,6 +474,7 @@ llmCmd
471
474
  .command("models")
472
475
  .description("List available models")
473
476
  .option("--private", "Show only models that support private (:private) inference")
477
+ .option("--zdr", "Show only models that support zero data retention (:zdr)")
474
478
  .action(modelsCommand);
475
479
  const llmCreditsCmd = llmCmd
476
480
  .command("credits")
@@ -498,6 +502,7 @@ llmSetupCmd
498
502
  .command("openclaw")
499
503
  .description("OpenClaw config")
500
504
  .option("--install", "Write config to ~/.openclaw/openclaw.json")
505
+ .option("--images", "Also route image generation (gpt-image-2) through the gateway — takes over OpenClaw's 'openai' provider slot")
501
506
  .action(setupOpenclawCommand);
502
507
  llmSetupCmd
503
508
  .command("opencode")
@@ -512,25 +517,38 @@ llmSetupCmd
512
517
  .command("claude")
513
518
  .description("Claude Code config")
514
519
  .action(setupClaudeCodeCommand);
520
+ // `helpOption(false)` forwards `-h`/`--help` to the spawned tool instead of
521
+ // letting commander print ours — on a pure passthrough launcher the user
522
+ // almost always wants the tool's help. Use `bankr llm --help` for ours.
515
523
  llmCmd
516
524
  .command("claude")
517
525
  .description("Run Claude Code via gateway")
518
526
  .allowUnknownOption()
519
527
  .allowExcessArguments()
528
+ .helpOption(false)
520
529
  .action(async () => {
521
- const idx = process.argv.indexOf("claude");
522
- const args = idx >= 0 ? process.argv.slice(idx + 1) : [];
523
- await claudeCommand(args);
530
+ await claudeCommand(passthroughArgs("claude"));
524
531
  });
525
532
  llmCmd
526
533
  .command("opencode")
527
534
  .description("Run OpenCode via gateway")
528
535
  .allowUnknownOption()
529
536
  .allowExcessArguments()
537
+ .helpOption(false)
530
538
  .action(async () => {
531
- const idx = process.argv.indexOf("opencode");
532
- const args = idx >= 0 ? process.argv.slice(idx + 1) : [];
533
- await opencodeCommand(args);
539
+ await opencodeCommand(passthroughArgs("opencode"));
540
+ });
541
+ // Top-level shorthand for the most-used launcher. This shadows the default
542
+ // prompt fallthrough, so `bankr claude ...` launches Claude Code rather than
543
+ // prompting the agent about Claude — ask the agent via `bankr agent "..."`.
544
+ program
545
+ .command("claude")
546
+ .description("Run Claude Code via gateway (alias for `bankr llm claude`)")
547
+ .allowUnknownOption()
548
+ .allowExcessArguments()
549
+ .helpOption(false)
550
+ .action(async () => {
551
+ await claudeCommand(passthroughArgs("claude"));
534
552
  });
535
553
  // Token search and info
536
554
  program.commandsGroup("Tokens");
@@ -1030,8 +1048,7 @@ program
1030
1048
  .action(async (text, opts) => {
1031
1049
  if (text.length === 0) {
1032
1050
  if (output.isNonInteractive()) {
1033
- output.error('No prompt provided. Pass it as an argument: bankr "your prompt"');
1034
- process.exit(1);
1051
+ output.fatal('No prompt provided. Pass it as an argument: bankr "your prompt"');
1035
1052
  }
1036
1053
  program.help();
1037
1054
  return;
@@ -1048,8 +1065,7 @@ async function main() {
1048
1065
  console.log();
1049
1066
  process.exit(0);
1050
1067
  }
1051
- output.error(err.message);
1052
- process.exit(1);
1068
+ output.fatal(getErrorMessage(err));
1053
1069
  }
1054
1070
  await checkForUpdate(pkg.version);
1055
1071
  }
@@ -2,6 +2,7 @@ import chalk from "chalk";
2
2
  import { getBalances, } 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;
@@ -42,8 +43,7 @@ export async function balancesCommand(opts) {
42
43
  if (chains) {
43
44
  const invalid = chains.filter((c) => !VALID_CHAINS.has(c));
44
45
  if (invalid.length > 0) {
45
- output.error(`Invalid chain${invalid.length > 1 ? "s" : ""}: ${invalid.join(", ")}. Valid chains: ${[...VALID_CHAINS].join(", ")}`);
46
- process.exit(1);
46
+ output.fatal(`Invalid chain${invalid.length > 1 ? "s" : ""}: ${invalid.join(", ")}. Valid chains: ${[...VALID_CHAINS].join(", ")}`);
47
47
  }
48
48
  }
49
49
  const spin = output.spinner("Fetching balances...");
@@ -53,7 +53,7 @@ export async function balancesCommand(opts) {
53
53
  spin.succeed("Balances loaded");
54
54
  }
55
55
  catch (err) {
56
- spin.fail(`Failed to fetch balances: ${err.message}`);
56
+ spin.fail(`Failed to fetch balances: ${getErrorMessage(err)}`);
57
57
  process.exit(1);
58
58
  }
59
59
  // JSON output mode
@@ -17,9 +17,7 @@ export async function cancelCommand(jobId) {
17
17
  }
18
18
  }
19
19
  catch (err) {
20
- spin.fail("Failed to cancel job");
21
- output.error(err.message);
22
- process.exit(1);
20
+ output.failWith(spin, "Failed to cancel job", err);
23
21
  }
24
22
  }
25
23
  //# sourceMappingURL=cancel.js.map
@@ -1,14 +1,8 @@
1
1
  import { confirm } from "@inquirer/prompts";
2
- import { CLI_USER_AGENT, getApiUrl, requireApiKey } from "../lib/config.js";
2
+ import { getApiUrl } from "../lib/config.js";
3
+ import { authHeaders } from "../lib/api.js";
3
4
  import * as output from "../lib/output.js";
4
5
  import { getErrorMessage } from "../lib/errors.js";
5
- function authHeaders() {
6
- return {
7
- "X-API-Key": requireApiKey(),
8
- "Content-Type": "application/json",
9
- "User-Agent": CLI_USER_AGENT,
10
- };
11
- }
12
6
  // Common Base tokens accepted as `--token` shortcuts. Lowercase to bypass
13
7
  // viem's strict EIP-55 checksum on the API's isAddress() check; the route
14
8
  // resolves to lowercase server-side anyway. Inlined here (not imported
@@ -79,8 +73,7 @@ export async function clubStatusCommand() {
79
73
  }
80
74
  catch (err) {
81
75
  spinner.stop();
82
- output.error(getErrorMessage(err));
83
- process.exit(1);
76
+ output.fatal(getErrorMessage(err));
84
77
  }
85
78
  }
86
79
  // ── Sign up ─────────────────────────────────────────────────────────────
@@ -95,8 +88,7 @@ export async function clubSignupCommand(opts) {
95
88
  if (opts.token) {
96
89
  const resolved = resolvePaymentToken(opts.token);
97
90
  if (!resolved) {
98
- output.error(`Unknown token "${opts.token}". Use one of USDC, BNKR, ETH, or pass a 0x-prefixed Base address.`);
99
- process.exit(1);
91
+ output.fatal(`Unknown token "${opts.token}". Use one of USDC, BNKR, ETH, or pass a 0x-prefixed Base address.`);
100
92
  }
101
93
  tokenAddress = resolved;
102
94
  }
@@ -143,8 +135,7 @@ export async function clubSignupCommand(opts) {
143
135
  output.dim("Run `bankr club` to see your status.");
144
136
  }
145
137
  catch (err) {
146
- output.error(getErrorMessage(err));
147
- process.exit(1);
138
+ output.fatal(getErrorMessage(err));
148
139
  }
149
140
  }
150
141
  // ── Cancel ──────────────────────────────────────────────────────────────
@@ -187,8 +178,7 @@ export async function clubCancelCommand(opts) {
187
178
  output.dim("You'll keep Bankr Club access until the end of the current billing period.");
188
179
  }
189
180
  catch (err) {
190
- output.error(getErrorMessage(err));
191
- process.exit(1);
181
+ output.fatal(getErrorMessage(err));
192
182
  }
193
183
  }
194
184
  //# sourceMappingURL=club.js.map
@@ -12,8 +12,7 @@ function isValidKey(key) {
12
12
  }
13
13
  function requireValidKey(key) {
14
14
  if (!isValidKey(key)) {
15
- output.error(`Unknown config key "${key}". Valid keys: ${ALLOWED_KEYS.join(", ")}`);
16
- process.exit(1);
15
+ output.fatal(`Unknown config key "${key}". Valid keys: ${ALLOWED_KEYS.join(", ")}`);
17
16
  }
18
17
  }
19
18
  function displayValue(key, value) {
@@ -6,6 +6,7 @@ import { emitCESP } from "../lib/cesp/engine.js";
6
6
  import { requireApiKey } from "../lib/config.js";
7
7
  import { txExplorer } from "../lib/explorer.js";
8
8
  import * as output from "../lib/output.js";
9
+ import { getErrorMessage } from "../lib/errors.js";
9
10
  // ---------------------------------------------------------------------------
10
11
  // Constants
11
12
  // ---------------------------------------------------------------------------
@@ -214,8 +215,13 @@ function renderDashboard(data) {
214
215
  }
215
216
  console.log(`${DIM(` Wallet: ${data.address}`)}`);
216
217
  // Tokens can span launch chains ("chain" absent on pre-multichain rows).
218
+ const launchChainLabel = {
219
+ base: "Base",
220
+ robinhood: "Robinhood",
221
+ arbitrum: "Arbitrum",
222
+ };
217
223
  const chainNames = [
218
- ...new Set(data.tokens.map((t) => (t.chain ?? "base") === "robinhood" ? "Robinhood" : "Base")),
224
+ ...new Set(data.tokens.map((t) => launchChainLabel[t.chain ?? "base"] ?? "Base")),
219
225
  ].join(" + ") || "Base";
220
226
  console.log(`${DIM(` Chain: ${chainNames} \u00b7 Tokens: ${data.tokens.length} \u00b7 Period: ${data.days} days`)}`);
221
227
  if (data.tokens.length === 0) {
@@ -283,28 +289,33 @@ function renderDashboard(data) {
283
289
  console.log(` ${BRAND_BOLD("\u2550".repeat(62))}`);
284
290
  console.log("");
285
291
  for (const token of data.tokens) {
286
- const wethIsToken0 = token.token0Label === "WETH";
287
- const claimableWeth = wethIsToken0
288
- ? token.claimable.token0
289
- : token.claimable.token1;
290
- const claimableToken = wethIsToken0
292
+ // Numeraire leg = the side whose label differs from the launched token's
293
+ // symbol \u2014 stock-paired launches label it with the stock ticker, not WETH.
294
+ const tokenIsToken0 = token.token0Label === token.symbol;
295
+ const numeraireLabel = tokenIsToken0
296
+ ? token.token1Label
297
+ : token.token0Label;
298
+ const claimableNumeraire = tokenIsToken0
291
299
  ? token.claimable.token1
292
300
  : token.claimable.token0;
293
- const claimedWeth = wethIsToken0
294
- ? token.claimed.token0
295
- : token.claimed.token1;
296
- const claimedToken = wethIsToken0
301
+ const claimableToken = tokenIsToken0
302
+ ? token.claimable.token0
303
+ : token.claimable.token1;
304
+ const claimedNumeraire = tokenIsToken0
297
305
  ? token.claimed.token1
298
306
  : token.claimed.token0;
299
- const tokenSymbol = wethIsToken0 ? token.token1Label : token.token0Label;
307
+ const claimedToken = tokenIsToken0
308
+ ? token.claimed.token0
309
+ : token.claimed.token1;
310
+ const tokenSymbol = tokenIsToken0 ? token.token0Label : token.token1Label;
300
311
  const sourceLabel = token.source === "clanker" ? DIM(" [Clanker]") : DIM(" [Doppler]");
301
312
  console.log(` ${BOLD(WHITE(token.name))} ${DIM("($" + token.symbol + ")")}${sourceLabel}`);
302
313
  const shareInfo = token.source === "clanker"
303
314
  ? `${DIM("Creator")} ${DIM("\u00b7 Token:")} ${DIM(token.tokenAddress)}`
304
315
  : `${DIM("Share:")} ${token.share} ${DIM("\u00b7 Token:")} ${DIM(token.tokenAddress)}`;
305
316
  console.log(` ${shareInfo}`);
306
- console.log(` ${DIM("Claimable:")} ${BRIGHT_GREEN(claimableWeth.padStart(12))} WETH ${DIM("|")} ${BRIGHT_GREEN(claimableToken.padStart(18))} ${tokenSymbol}`);
307
- console.log(` ${DIM("Claimed:")} ${WHITE(claimedWeth.padStart(12))} WETH ${DIM("|")} ${WHITE(claimedToken.padStart(18))} ${tokenSymbol} ${DIM(`(${token.claimed.count} claims)`)}`);
317
+ console.log(` ${DIM("Claimable:")} ${BRIGHT_GREEN(claimableNumeraire.padStart(12))} ${numeraireLabel} ${DIM("|")} ${BRIGHT_GREEN(claimableToken.padStart(18))} ${tokenSymbol}`);
318
+ console.log(` ${DIM("Claimed:")} ${WHITE(claimedNumeraire.padStart(12))} ${numeraireLabel} ${DIM("|")} ${WHITE(claimedToken.padStart(18))} ${tokenSymbol} ${DIM(`(${token.claimed.count} claims)`)}`);
308
319
  console.log("");
309
320
  }
310
321
  console.log(` ${BRAND_BOLD("\u2550".repeat(62))}`);
@@ -323,8 +334,7 @@ export async function feesCommand(address, opts) {
323
334
  if (opts.days) {
324
335
  days = parseInt(opts.days, 10);
325
336
  if (isNaN(days) || days < MIN_DAYS || days > MAX_DAYS) {
326
- output.error(`Days must be between ${MIN_DAYS} and ${MAX_DAYS}`);
327
- process.exit(1);
337
+ output.fatal(`Days must be between ${MIN_DAYS} and ${MAX_DAYS}`);
328
338
  }
329
339
  }
330
340
  // --token flag: look up fees by token address (no wallet needed)
@@ -343,8 +353,7 @@ export async function feesCommand(address, opts) {
343
353
  catch (err) {
344
354
  spin.fail("Failed to fetch token fee data");
345
355
  emitCESP("task.error");
346
- output.error(err.message);
347
- process.exit(1);
356
+ output.fatal(getErrorMessage(err));
348
357
  }
349
358
  return;
350
359
  }
@@ -364,9 +373,7 @@ export async function feesCommand(address, opts) {
364
373
  spin.succeed(`Wallet: ${walletAddress}`);
365
374
  }
366
375
  catch (err) {
367
- spin.fail("Failed to fetch wallet info");
368
- output.error(err.message);
369
- process.exit(1);
376
+ output.failWith(spin, "Failed to fetch wallet info", err);
370
377
  }
371
378
  }
372
379
  // Fetch data
@@ -384,8 +391,7 @@ export async function feesCommand(address, opts) {
384
391
  catch (err) {
385
392
  spin.fail("Failed to fetch fee data");
386
393
  emitCESP("task.error");
387
- output.error(err.message);
388
- process.exit(1);
394
+ output.fatal(getErrorMessage(err));
389
395
  }
390
396
  }
391
397
  export async function feesClaimCommand(tokenAddress, opts) {
@@ -445,8 +451,7 @@ export async function feesClaimCommand(tokenAddress, opts) {
445
451
  catch (err) {
446
452
  spin.fail("Claim submission failed");
447
453
  emitCESP("task.error");
448
- output.error(err.message);
449
- process.exit(1);
454
+ output.fatal(getErrorMessage(err));
450
455
  }
451
456
  }
452
457
  // ---------------------------------------------------------------------------
@@ -459,8 +464,7 @@ export async function feesClaimWalletCommand(address, opts) {
459
464
  address = undefined;
460
465
  }
461
466
  if (output.isNonInteractive() && !opts.all) {
462
- output.error("Use --all to select all tokens in non-interactive mode, or run interactively to choose");
463
- process.exit(1);
467
+ output.fatal("Use --all to select all tokens in non-interactive mode, or run interactively to choose");
464
468
  }
465
469
  // Load .env from cwd if present (no dependency needed)
466
470
  try {
@@ -498,13 +502,11 @@ export async function feesClaimWalletCommand(address, opts) {
498
502
  privateKey = process.env.BANKR_PRIVATE_KEY.trim();
499
503
  }
500
504
  else if (!address) {
501
- output.error("Address required when no private key is available. Provide an address argument or set BANKR_PRIVATE_KEY in .env");
502
- process.exit(1);
505
+ output.fatal("Address required when no private key is available. Provide an address argument or set BANKR_PRIVATE_KEY in .env");
503
506
  }
504
507
  else {
505
508
  if (output.isNonInteractive()) {
506
- output.error("Private key required in non-interactive mode. Use --private-key or set BANKR_PRIVATE_KEY");
507
- process.exit(1);
509
+ output.fatal("Private key required in non-interactive mode. Use --private-key or set BANKR_PRIVATE_KEY");
508
510
  }
509
511
  privateKey = await password({
510
512
  message: "Enter private key for this wallet (0x...):",
@@ -523,8 +525,7 @@ export async function feesClaimWalletCommand(address, opts) {
523
525
  account = privateKeyToAccount(privateKey);
524
526
  }
525
527
  catch {
526
- output.error("Invalid private key format");
527
- process.exit(1);
528
+ output.fatal("Invalid private key format");
528
529
  }
529
530
  // Step 3: Resolve address — derive from key if not provided, verify match if provided
530
531
  if (!address) {
@@ -533,12 +534,10 @@ export async function feesClaimWalletCommand(address, opts) {
533
534
  }
534
535
  else {
535
536
  if (!isAddress(address)) {
536
- output.error("Invalid EVM address");
537
- process.exit(1);
537
+ output.fatal("Invalid EVM address");
538
538
  }
539
539
  if (account.address.toLowerCase() !== address.toLowerCase()) {
540
- output.error(`Private key does not match address ${address}. Derived: ${account.address}`);
541
- process.exit(1);
540
+ output.fatal(`Private key does not match address ${address}. Derived: ${account.address}`);
542
541
  }
543
542
  }
544
543
  console.log();
@@ -550,42 +549,62 @@ export async function feesClaimWalletCommand(address, opts) {
550
549
  const result = await getBeneficiaryFeesStreaming(address, (progress) => {
551
550
  fetchSpin.text = `Scanning pools... ${progress.scanned}/${progress.total} (${progress.found ?? 0} with claimable fees)`;
552
551
  });
553
- fetchSpin.succeed(`Scanned ${result.totalLaunches} launches — ${result.poolsWithShares} pools with shares, ${result.tokens.length} with claimable fees`);
552
+ // A time-budget stop leaves `scanned` below `totalLaunches`; report the
553
+ // number actually scanned so this line can't contradict the warning below.
554
+ const scanned = result.scanned ?? result.totalLaunches;
555
+ fetchSpin.succeed(`Scanned ${scanned} launches — ${result.poolsWithShares} pools with shares, ${result.tokens.length} with claimable fees`);
556
+ if (result.truncated) {
557
+ output.warn(`Scan stopped early at ${scanned} launches — this wallet has more than the server scans in one request. Claimable fees beyond that are not listed.`);
558
+ }
554
559
  allTokens = result.tokens;
555
560
  }
556
561
  catch (err) {
557
- fetchSpin.fail("Failed to fetch fee data");
558
- output.error(err.message);
559
- process.exit(1);
562
+ output.failWith(fetchSpin, "Failed to fetch fee data", err);
560
563
  }
561
564
  // Step 5: Filter to tokens with claimable fees (API already filters, but
562
565
  // double-check for any rounding edge cases)
563
566
  const parseAmount = (s) => parseFloat(s.replace(/^[<>]/, "")) || 0;
567
+ // Numeraire leg = the side whose label differs from the launched token's
568
+ // symbol — stock-paired launches label it with the stock ticker, not WETH.
569
+ const numeraireLegOf = (token) => {
570
+ const tokenIsToken0 = token.token0Label === token.symbol;
571
+ return {
572
+ label: tokenIsToken0 ? token.token1Label : token.token0Label,
573
+ claimable: tokenIsToken0
574
+ ? token.claimable.token1
575
+ : token.claimable.token0,
576
+ tokenClaimable: tokenIsToken0
577
+ ? token.claimable.token0
578
+ : token.claimable.token1,
579
+ };
580
+ };
564
581
  const claimableTokens = allTokens.filter((token) => {
565
- const wethIsToken0 = token.token0Label === "WETH";
566
- const claimableWeth = wethIsToken0
567
- ? token.claimable.token0
568
- : token.claimable.token1;
569
- const claimableToken = wethIsToken0
570
- ? token.claimable.token1
571
- : token.claimable.token0;
572
- return parseAmount(claimableWeth) > 0 || parseAmount(claimableToken) > 0;
582
+ const leg = numeraireLegOf(token);
583
+ return (parseAmount(leg.claimable) > 0 || parseAmount(leg.tokenClaimable) > 0);
573
584
  });
574
585
  if (claimableTokens.length === 0) {
575
586
  console.log();
576
587
  output.dim("No claimable fees found for this wallet.");
577
588
  return;
578
589
  }
579
- // Show total claimable WETH
590
+ // Show total claimable WETH — WETH legs only; stock-paired numeraires are a
591
+ // different unit and get their own per-ticker lines.
580
592
  const totalWeth = claimableTokens.reduce((sum, token) => {
581
- const wethIsToken0 = token.token0Label === "WETH";
582
- const wethStr = wethIsToken0
583
- ? token.claimable.token0
584
- : token.claimable.token1;
585
- return sum + parseAmount(wethStr);
593
+ const leg = numeraireLegOf(token);
594
+ return leg.label === "WETH" ? sum + parseAmount(leg.claimable) : sum;
586
595
  }, 0);
596
+ const nonWethTotals = new Map();
597
+ for (const token of claimableTokens) {
598
+ const leg = numeraireLegOf(token);
599
+ if (leg.label !== "WETH" && parseAmount(leg.claimable) > 0) {
600
+ nonWethTotals.set(leg.label, (nonWethTotals.get(leg.label) ?? 0) + parseAmount(leg.claimable));
601
+ }
602
+ }
587
603
  console.log();
588
604
  output.keyValue("Total claimable", `${totalWeth.toFixed(6)} WETH across ${claimableTokens.length} token${claimableTokens.length !== 1 ? "s" : ""}`);
605
+ for (const [label, amount] of nonWethTotals) {
606
+ output.keyValue("", `${amount.toFixed(6)} ${label} (stock-paired)`);
607
+ }
589
608
  // Step 6: Select tokens
590
609
  let selectedTokens;
591
610
  if (opts.all) {
@@ -593,12 +612,9 @@ export async function feesClaimWalletCommand(address, opts) {
593
612
  }
594
613
  else {
595
614
  const choices = claimableTokens.map((token) => {
596
- const wethIsToken0 = token.token0Label === "WETH";
597
- const claimableWeth = wethIsToken0
598
- ? token.claimable.token0
599
- : token.claimable.token1;
615
+ const leg = numeraireLegOf(token);
600
616
  return {
601
- name: `${token.name} ($${token.symbol}) - ${claimableWeth} WETH`,
617
+ name: `${token.name} ($${token.symbol}) - ${leg.claimable} ${leg.label}`,
602
618
  value: token.tokenAddress,
603
619
  checked: true,
604
620
  };
@@ -650,8 +666,7 @@ export async function feesClaimWalletCommand(address, opts) {
650
666
  output.warn(`${baseChainTokens.length - buildableTokens.length} token(s) missing initializer data — skipped`);
651
667
  }
652
668
  if (buildableTokens.length === 0) {
653
- output.error("No transactions could be built");
654
- process.exit(1);
669
+ output.fatal("No transactions could be built");
655
670
  }
656
671
  // Encode calldata locally (instant, no RPC)
657
672
  const txData = buildableTokens.map((t) => ({
@@ -676,7 +691,7 @@ export async function feesClaimWalletCommand(address, opts) {
676
691
  return (gas * GAS_BUFFER) / 100n;
677
692
  }
678
693
  catch (err) {
679
- output.warn(`${token.symbol}: gas estimate failed — ${err.message}`);
694
+ output.warn(`${token.symbol}: gas estimate failed — ${getErrorMessage(err)}`);
680
695
  return null;
681
696
  }
682
697
  }));
@@ -737,7 +752,7 @@ export async function feesClaimWalletCommand(address, opts) {
737
752
  pendingTxs.push({ token: tx.token, hash });
738
753
  }
739
754
  catch (err) {
740
- sendSpin.fail(`${tx.token.symbol}: Send failed — ${err.message}`);
755
+ sendSpin.fail(`${tx.token.symbol}: Send failed — ${getErrorMessage(err)}`);
741
756
  emitCESP("task.error");
742
757
  failCount++;
743
758
  if (readyTxs.indexOf(tx) < readyTxs.length - 1) {
@@ -767,7 +782,7 @@ export async function feesClaimWalletCommand(address, opts) {
767
782
  token,
768
783
  hash,
769
784
  status: "failed",
770
- error: err.message,
785
+ error: getErrorMessage(err),
771
786
  };
772
787
  }
773
788
  }));