@bankr/cli 0.1.0-beta.6 → 0.1.0

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 (66) hide show
  1. package/README.md +27 -1
  2. package/dist/cli.js +222 -4
  3. package/dist/commands/balances.d.ts +5 -0
  4. package/dist/commands/balances.js +113 -0
  5. package/dist/commands/fees.d.ts +18 -0
  6. package/dist/commands/fees.js +793 -0
  7. package/dist/commands/launch.d.ts +13 -0
  8. package/dist/commands/launch.js +174 -0
  9. package/dist/commands/llm.d.ts +11 -0
  10. package/dist/commands/llm.js +319 -3
  11. package/dist/commands/login.d.ts +9 -0
  12. package/dist/commands/login.js +464 -147
  13. package/dist/commands/profile.d.ts +26 -0
  14. package/dist/commands/profile.js +183 -0
  15. package/dist/commands/prompt.js +5 -0
  16. package/dist/commands/sign.js +3 -0
  17. package/dist/commands/sounds.d.ts +12 -0
  18. package/dist/commands/sounds.js +262 -0
  19. package/dist/commands/submit.js +14 -4
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +1 -1
  22. package/dist/lib/api.d.ts +213 -0
  23. package/dist/lib/api.js +177 -3
  24. package/dist/lib/cesp/engine.d.ts +13 -0
  25. package/dist/lib/cesp/engine.js +132 -0
  26. package/dist/lib/cesp/player.d.ts +6 -0
  27. package/dist/lib/cesp/player.js +50 -0
  28. package/dist/lib/cesp/types.d.ts +38 -0
  29. package/dist/lib/cesp/types.js +2 -0
  30. package/dist/lib/config.d.ts +4 -0
  31. package/dist/lib/config.js +1 -0
  32. package/dist/lib/output.d.ts +1 -0
  33. package/dist/lib/output.js +3 -0
  34. package/package.json +4 -2
  35. package/dist/cli.d.ts.map +0 -1
  36. package/dist/cli.js.map +0 -1
  37. package/dist/commands/cancel.d.ts.map +0 -1
  38. package/dist/commands/cancel.js.map +0 -1
  39. package/dist/commands/capabilities.d.ts.map +0 -1
  40. package/dist/commands/capabilities.js.map +0 -1
  41. package/dist/commands/config.d.ts.map +0 -1
  42. package/dist/commands/config.js.map +0 -1
  43. package/dist/commands/llm.d.ts.map +0 -1
  44. package/dist/commands/llm.js.map +0 -1
  45. package/dist/commands/login.d.ts.map +0 -1
  46. package/dist/commands/login.js.map +0 -1
  47. package/dist/commands/logout.d.ts.map +0 -1
  48. package/dist/commands/logout.js.map +0 -1
  49. package/dist/commands/prompt.d.ts.map +0 -1
  50. package/dist/commands/prompt.js.map +0 -1
  51. package/dist/commands/sign.d.ts.map +0 -1
  52. package/dist/commands/sign.js.map +0 -1
  53. package/dist/commands/status.d.ts.map +0 -1
  54. package/dist/commands/status.js.map +0 -1
  55. package/dist/commands/submit.d.ts.map +0 -1
  56. package/dist/commands/submit.js.map +0 -1
  57. package/dist/commands/whoami.d.ts.map +0 -1
  58. package/dist/commands/whoami.js.map +0 -1
  59. package/dist/index.d.ts.map +0 -1
  60. package/dist/index.js.map +0 -1
  61. package/dist/lib/api.d.ts.map +0 -1
  62. package/dist/lib/api.js.map +0 -1
  63. package/dist/lib/config.d.ts.map +0 -1
  64. package/dist/lib/config.js.map +0 -1
  65. package/dist/lib/output.d.ts.map +0 -1
  66. package/dist/lib/output.js.map +0 -1
@@ -1,37 +1,422 @@
1
1
  import { confirm, input, select } from "@inquirer/prompts";
2
2
  import open from "open";
3
- import { DEFAULT_API_URL, getApiUrl, getConfigPath, readConfig, writeConfig, } from "../lib/config.js";
3
+ import { CLI_USER_AGENT, DEFAULT_API_URL, getApiUrl, getConfigPath, readConfig, writeConfig, } from "../lib/config.js";
4
4
  import * as output from "../lib/output.js";
5
5
  const DEFAULT_DASHBOARD_URL = "https://bankr.bot/api";
6
+ const MAX_OTP_ATTEMPTS = 3;
7
+ const INVALID_CODE_STATUSES = [400, 401, 422];
8
+ function fatal(message) {
9
+ output.error(message);
10
+ process.exit(1);
11
+ }
6
12
  function deriveDashboardUrl(apiUrl) {
7
13
  if (apiUrl === DEFAULT_API_URL) {
8
14
  return DEFAULT_DASHBOARD_URL;
9
15
  }
10
16
  return (apiUrl.replace(/\/+$/, "").replace("api.", "").replace("api-", "") + "/api");
11
17
  }
18
+ async function fetchPrivyConfig(apiUrl) {
19
+ const res = await fetch(`${apiUrl}/cli/config`, {
20
+ headers: { "User-Agent": CLI_USER_AGENT },
21
+ });
22
+ if (!res.ok) {
23
+ throw new Error(`Failed to fetch auth config (${res.status})`);
24
+ }
25
+ return (await res.json());
26
+ }
27
+ async function sendPrivyOtp(privyConfig, email) {
28
+ const res = await fetch("https://auth.privy.io/api/v1/passwordless/init", {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ "privy-app-id": privyConfig.privyAppId,
33
+ "privy-client-id": privyConfig.privyClientId,
34
+ },
35
+ body: JSON.stringify({ email, type: "email" }),
36
+ });
37
+ if (!res.ok) {
38
+ if (res.status === 429) {
39
+ throw new Error("Too many attempts. Please wait and try again.");
40
+ }
41
+ const body = await res.text().catch(() => "");
42
+ throw new Error(`Failed to send verification code: ${body || res.status}`);
43
+ }
44
+ }
45
+ async function verifyPrivyOtp(privyConfig, email, code) {
46
+ const res = await fetch("https://auth.privy.io/api/v1/passwordless/authenticate", {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ "privy-app-id": privyConfig.privyAppId,
51
+ "privy-client-id": privyConfig.privyClientId,
52
+ },
53
+ body: JSON.stringify({ email, code, mode: "login-or-sign-up" }),
54
+ });
55
+ if (!res.ok) {
56
+ if (INVALID_CODE_STATUSES.includes(res.status)) {
57
+ throw new Error("INVALID_CODE");
58
+ }
59
+ if (res.status === 429) {
60
+ throw new Error("Too many attempts. Please wait and try again.");
61
+ }
62
+ throw new Error(`Verification failed (${res.status})`);
63
+ }
64
+ return (await res.json());
65
+ }
66
+ async function callGenerateWallet(apiUrl, identityToken) {
67
+ const res = await fetch(`${apiUrl}/cli/generate-wallet`, {
68
+ method: "POST",
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ "User-Agent": CLI_USER_AGENT,
72
+ "privy-id-token": identityToken,
73
+ },
74
+ });
75
+ const body = (await res.json());
76
+ if (!res.ok) {
77
+ throw new Error(body.message || body.error || `Wallet generation failed (${res.status})`);
78
+ }
79
+ return body;
80
+ }
81
+ async function callAcceptTerms(apiUrl, identityToken) {
82
+ const res = await fetch(`${apiUrl}/user/accept-terms`, {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ "User-Agent": CLI_USER_AGENT,
87
+ "privy-id-token": identityToken,
88
+ },
89
+ });
90
+ if (!res.ok) {
91
+ const body = (await res.json().catch(() => ({})));
92
+ throw new Error(body.message || body.error || `Accept terms failed (${res.status})`);
93
+ }
94
+ }
95
+ async function callGenerateApiKey(apiUrl, identityToken, opts) {
96
+ const res = await fetch(`${apiUrl}/generate-api-key`, {
97
+ method: "POST",
98
+ headers: {
99
+ "Content-Type": "application/json",
100
+ "User-Agent": CLI_USER_AGENT,
101
+ "privy-id-token": identityToken,
102
+ },
103
+ body: JSON.stringify(opts),
104
+ });
105
+ const body = (await res.json());
106
+ if (!res.ok) {
107
+ throw new Error(body.message || body.error || `API key generation failed (${res.status})`);
108
+ }
109
+ return body;
110
+ }
111
+ // ── OTP verification helpers ─────────────────────────────────────────
112
+ async function fetchPrivyConfigOrFail(apiUrl) {
113
+ const config = await fetchPrivyConfig(apiUrl).catch(() => null);
114
+ if (!config) {
115
+ fatal("Could not connect to Bankr API. Check your connection and try again.");
116
+ }
117
+ return config;
118
+ }
119
+ async function sendOtpWithSpinner(privyConfig, email) {
120
+ const spin = output.spinner("Sending verification code...");
121
+ try {
122
+ await sendPrivyOtp(privyConfig, email);
123
+ spin.succeed(`Verification code sent to ${email}`);
124
+ }
125
+ catch (err) {
126
+ spin.fail("Failed to send verification code");
127
+ fatal(err.message);
128
+ }
129
+ }
130
+ async function verifyOtpWithSpinner(privyConfig, email, code) {
131
+ const spin = output.spinner("Verifying...");
132
+ try {
133
+ const result = await verifyPrivyOtp(privyConfig, email, code);
134
+ spin.succeed("Email verified");
135
+ return result;
136
+ }
137
+ catch (err) {
138
+ const errMsg = err.message;
139
+ spin.fail("Verification failed");
140
+ fatal(errMsg === "INVALID_CODE" ? "Invalid verification code." : errMsg);
141
+ }
142
+ }
143
+ // ── Email authentication flow ───────────────────────────────────────
144
+ async function authenticateWithEmail(apiUrl, providedEmail, providedCode) {
145
+ const email = providedEmail?.trim() ||
146
+ (await input({
147
+ message: "Email address:",
148
+ theme: output.bankrTheme,
149
+ validate: (v) => {
150
+ const trimmed = v.trim();
151
+ if (!trimmed)
152
+ return "Email is required.";
153
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed))
154
+ return "Invalid email address.";
155
+ return true;
156
+ },
157
+ })).trim();
158
+ const privyConfig = await fetchPrivyConfigOrFail(apiUrl);
159
+ // Headless: code already provided, verify directly
160
+ if (providedCode) {
161
+ const result = await verifyOtpWithSpinner(privyConfig, email, providedCode.trim());
162
+ return { identityToken: result.identity_token, email };
163
+ }
164
+ // Interactive: send OTP, then prompt for code with retries
165
+ await sendOtpWithSpinner(privyConfig, email);
166
+ for (let attempt = 0; attempt < MAX_OTP_ATTEMPTS; attempt++) {
167
+ const code = await input({
168
+ message: "Enter the 6-digit code:",
169
+ theme: output.bankrTheme,
170
+ validate: (v) => /^\d{6}$/.test(v.trim()) ? true : "Enter a 6-digit code.",
171
+ });
172
+ const verifySpin = output.spinner("Verifying...");
173
+ try {
174
+ const result = await verifyPrivyOtp(privyConfig, email, code.trim());
175
+ verifySpin.succeed("Email verified");
176
+ return { identityToken: result.identity_token, email };
177
+ }
178
+ catch (err) {
179
+ const errMsg = err.message;
180
+ if (errMsg === "INVALID_CODE") {
181
+ if (attempt < MAX_OTP_ATTEMPTS - 1) {
182
+ verifySpin.fail("Invalid code, please try again");
183
+ continue;
184
+ }
185
+ verifySpin.fail("Invalid code");
186
+ fatal("Too many invalid attempts. Please restart login.");
187
+ }
188
+ verifySpin.fail("Verification failed");
189
+ fatal(errMsg);
190
+ }
191
+ }
192
+ fatal("Verification failed. Please try again.");
193
+ }
194
+ // ── Email login flow ────────────────────────────────────────────────
195
+ async function emailLoginFlow(apiUrl, opts) {
196
+ // Step 1: Authenticate via email OTP
197
+ const { identityToken } = await authenticateWithEmail(apiUrl, opts.email, opts.code);
198
+ // Step 2: Generate/resolve wallet
199
+ const walletSpin = output.spinner("Setting up wallet...");
200
+ let wallet;
201
+ try {
202
+ wallet = await callGenerateWallet(apiUrl, identityToken);
203
+ walletSpin.stop();
204
+ }
205
+ catch (err) {
206
+ walletSpin.fail("Wallet setup failed");
207
+ fatal(err.message);
208
+ }
209
+ // Show wallet info
210
+ output.blank();
211
+ if (wallet.isNewUser) {
212
+ output.success("Welcome to Bankr!");
213
+ }
214
+ else {
215
+ output.success("Authenticated");
216
+ }
217
+ output.dim(` EVM: ${wallet.evmAddress}`);
218
+ if (wallet.solAddress) {
219
+ output.dim(` SOL: ${wallet.solAddress}`);
220
+ }
221
+ // Step 3: Accept terms if needed
222
+ if (!wallet.hasAcceptedTerms) {
223
+ output.blank();
224
+ output.dim(" Please review our Terms of Service: https://bankr.bot/terms");
225
+ output.blank();
226
+ const accepted = opts.acceptTerms ??
227
+ (await confirm({
228
+ message: "Do you accept the Terms of Service?",
229
+ default: false,
230
+ theme: output.bankrTheme,
231
+ }));
232
+ if (!accepted) {
233
+ fatal("Terms must be accepted to continue.");
234
+ }
235
+ const termsSpin = output.spinner("Accepting terms...");
236
+ try {
237
+ await callAcceptTerms(apiUrl, identityToken);
238
+ termsSpin.succeed("Terms accepted");
239
+ }
240
+ catch (err) {
241
+ termsSpin.fail("Failed to accept terms");
242
+ fatal(err.message);
243
+ }
244
+ }
245
+ // Step 4: Generate API key
246
+ output.blank();
247
+ const defaultKeyName = `CLI-${new Date().toISOString().slice(0, 10)}`;
248
+ const keyName = opts.keyName ??
249
+ (await input({
250
+ message: "Key name:",
251
+ default: defaultKeyName,
252
+ theme: output.bankrTheme,
253
+ validate: (v) => v.trim().length >= 3 ? true : "Name must be at least 3 characters.",
254
+ }));
255
+ const enableWrite = opts.readWrite ??
256
+ (await confirm({
257
+ message: "Enable write operations?",
258
+ default: false,
259
+ theme: output.bankrTheme,
260
+ }));
261
+ if (enableWrite) {
262
+ output.dim(" Includes: transactions, transfers, automations, order management");
263
+ }
264
+ else {
265
+ output.dim(" Read-only: portfolio, balances, prices, research");
266
+ }
267
+ const enableLlm = opts.llm ??
268
+ (await confirm({
269
+ message: "Enable LLM gateway?",
270
+ default: false,
271
+ theme: output.bankrTheme,
272
+ }));
273
+ const keySpin = output.spinner("Generating API key...");
274
+ let apiKeyResult;
275
+ try {
276
+ apiKeyResult = await callGenerateApiKey(apiUrl, identityToken, {
277
+ name: keyName.trim(),
278
+ agentApiEnabled: { readOnly: !enableWrite },
279
+ llmGatewayEnabled: enableLlm,
280
+ });
281
+ keySpin.stop();
282
+ }
283
+ catch (err) {
284
+ keySpin.fail("Failed to generate API key");
285
+ fatal(err.message);
286
+ }
287
+ // Show result
288
+ output.blank();
289
+ const mode = enableWrite ? "read-write" : "read-only";
290
+ output.success(`API key "${apiKeyResult.name}" saved`);
291
+ output.dim(` Mode: ${mode}`);
292
+ output.dim(` LLM: ${apiKeyResult.llmGatewayEnabled ? "enabled" : "disabled"}`);
293
+ output.dim(" Manage keys at bankr.bot/api");
294
+ return apiKeyResult.apiKey;
295
+ }
296
+ // ── SIWE login flow ─────────────────────────────────────────────────
297
+ async function siweLoginFlow(apiUrl, opts) {
298
+ // Dynamic import to avoid loading viem for non-SIWE flows
299
+ const { privateKeyToAccount } = await import("viem/accounts");
300
+ const account = privateKeyToAccount(opts.privateKey);
301
+ // 1. Fetch nonce
302
+ const nonceSpinner = output.spinner("Fetching SIWE nonce...");
303
+ const nonceRes = await fetch(`${apiUrl}/cli/siwe/nonce`, {
304
+ headers: { "User-Agent": CLI_USER_AGENT },
305
+ });
306
+ if (!nonceRes.ok) {
307
+ nonceSpinner.fail("Failed to fetch nonce");
308
+ fatal(`Nonce request failed (${nonceRes.status})`);
309
+ }
310
+ const { nonce } = (await nonceRes.json());
311
+ nonceSpinner.succeed("Nonce received");
312
+ // 2. Create and sign SIWE message
313
+ const domain = new URL(apiUrl).host;
314
+ const message = [
315
+ `${domain} wants you to sign in with your Ethereum account:`,
316
+ account.address,
317
+ "",
318
+ "Sign in to Bankr",
319
+ "",
320
+ `URI: ${apiUrl}/cli/siwe/verify`,
321
+ `Version: 1`,
322
+ `Chain ID: 1`,
323
+ `Nonce: ${nonce}`,
324
+ `Issued At: ${new Date().toISOString()}`,
325
+ ].join("\n");
326
+ const signSpinner = output.spinner("Signing SIWE message...");
327
+ const signature = await account.signMessage({ message });
328
+ signSpinner.succeed("Message signed");
329
+ // 3. Verify with API
330
+ const verifySpinner = output.spinner("Verifying and creating wallet...");
331
+ const verifyRes = await fetch(`${apiUrl}/cli/siwe/verify`, {
332
+ method: "POST",
333
+ headers: {
334
+ "Content-Type": "application/json",
335
+ "User-Agent": CLI_USER_AGENT,
336
+ },
337
+ body: JSON.stringify({
338
+ message,
339
+ signature,
340
+ partnerApiKey: opts.partnerKey,
341
+ keyName: opts.keyName ?? `SIWE-${new Date().toISOString().slice(0, 10)}`,
342
+ readOnly: opts.readOnly ?? false,
343
+ }),
344
+ });
345
+ if (!verifyRes.ok) {
346
+ verifySpinner.fail("SIWE verification failed");
347
+ const body = (await verifyRes.json().catch(() => ({})));
348
+ fatal(body.message || `Verification failed (${verifyRes.status})`);
349
+ }
350
+ const result = (await verifyRes.json());
351
+ verifySpinner.succeed("Wallet created");
352
+ output.blank();
353
+ output.success("SIWE authentication successful");
354
+ output.dim(` EVM: ${result.walletAddress}`);
355
+ if (result.solAddress) {
356
+ output.dim(` SOL: ${result.solAddress}`);
357
+ }
358
+ output.dim(` Mode: ${result.readOnly ? "read-only" : "read-write"}`);
359
+ return { apiKey: result.apiKey, walletAddress: result.walletAddress };
360
+ }
361
+ // ── API key validation helper ────────────────────────────────────────
362
+ async function validateApiKey(apiUrl, apiKey) {
363
+ const spin = output.spinner("Validating Bankr API key...");
364
+ try {
365
+ const res = await fetch(`${apiUrl}/agent/me`, {
366
+ headers: { "X-API-Key": apiKey, "User-Agent": CLI_USER_AGENT },
367
+ });
368
+ if (res.ok) {
369
+ spin.succeed("Bankr API key validated successfully");
370
+ }
371
+ else if (res.status === 401 || res.status === 403) {
372
+ const body = (await res.json().catch(() => ({})));
373
+ const errorType = body.error ?? "";
374
+ if (res.status === 403 || errorType === "Agent API access not enabled") {
375
+ spin.warn("Agent API access is not enabled");
376
+ output.warn("Your API key is valid but does not have Agent API access enabled.");
377
+ output.info("Go to https://bankr.bot/api and enable Agent API access for this key.");
378
+ output.info("Your key has been saved — once enabled, the CLI will work without re-login.");
379
+ }
380
+ else if (errorType === "Authentication required") {
381
+ spin.fail("API key is not linked to a wallet");
382
+ output.error("This API key is not associated with a wallet. Generate a new key at https://bankr.bot/api");
383
+ process.exit(1);
384
+ }
385
+ else {
386
+ spin.fail("Invalid Bankr API key");
387
+ output.error("The provided API key is invalid or inactive. Generate a new key at https://bankr.bot/api");
388
+ process.exit(1);
389
+ }
390
+ }
391
+ else {
392
+ spin.warn("Could not validate Bankr API key (API unreachable), saving anyway");
393
+ }
394
+ }
395
+ catch {
396
+ spin.warn("Could not validate Bankr API key (network error), saving anyway");
397
+ }
398
+ }
399
+ // ── Main login command ──────────────────────────────────────────────
12
400
  export async function loginCommand(opts) {
13
- // Fail fast if --api-key was passed with an empty value (e.g., from unset env var)
401
+ // Fail fast if flags were passed with empty values
14
402
  if (opts.apiKey !== undefined && !opts.apiKey.trim()) {
15
- output.error("--api-key requires a non-empty value");
16
- process.exit(1);
403
+ fatal("--api-key requires a non-empty value");
17
404
  }
18
- if (opts.llmKey !== undefined && !opts.llmKey.trim()) {
19
- output.error("--llm-key requires a non-empty value");
20
- process.exit(1);
21
- }
22
- const nonInteractive = !!(opts.apiKey || opts.url);
23
405
  const config = readConfig();
24
406
  const resolvedApiUrl = opts.apiUrl ?? getApiUrl();
25
407
  if (resolvedApiUrl !== DEFAULT_API_URL) {
408
+ const nonInteractive = !!(opts.apiKey ||
409
+ opts.url ||
410
+ opts.email !== undefined ||
411
+ opts.siwe);
26
412
  if (nonInteractive) {
27
- // Non-interactive: accept the custom URL silently
28
413
  config.apiUrl = resolvedApiUrl;
29
414
  }
30
415
  else {
31
- console.log();
416
+ output.blank();
32
417
  output.warn(`Bankr API URL is set to a non-official URL: ${resolvedApiUrl}`);
33
418
  output.dim(` Official URL: ${DEFAULT_API_URL}`);
34
- console.log();
419
+ output.blank();
35
420
  const proceed = await confirm({
36
421
  message: "Continue with this URL?",
37
422
  default: false,
@@ -50,15 +435,43 @@ export async function loginCommand(opts) {
50
435
  config.apiUrl = opts.apiUrl;
51
436
  }
52
437
  const apiUrl = config.apiUrl || DEFAULT_API_URL;
438
+ function saveCredentials(apiKey, llmKey) {
439
+ config.apiKey = apiKey;
440
+ if (llmKey) {
441
+ config.llmKey = llmKey.trim();
442
+ }
443
+ writeConfig(config);
444
+ output.success(`Credentials saved to ${getConfigPath()}`);
445
+ output.dim(`Bankr API URL: ${apiUrl}`);
446
+ }
447
+ // --siwe: SIWE login flow (headless agent onboarding)
448
+ if (opts.siwe) {
449
+ if (!opts.privateKey) {
450
+ fatal("--private-key is required with --siwe");
451
+ }
452
+ const { apiKey } = await siweLoginFlow(apiUrl, {
453
+ privateKey: opts.privateKey,
454
+ partnerKey: opts.partnerKey,
455
+ keyName: opts.keyName,
456
+ readOnly: !opts.readWrite,
457
+ });
458
+ config.apiKey = apiKey;
459
+ if (opts.partnerKey) {
460
+ config.partnerKey = opts.partnerKey;
461
+ }
462
+ writeConfig(config);
463
+ output.success(`Credentials saved to ${getConfigPath()}`);
464
+ return;
465
+ }
53
466
  // --url: print the dashboard URL and exit
54
467
  if (opts.url) {
55
468
  const dashboardUrl = deriveDashboardUrl(apiUrl);
56
- console.log();
469
+ output.blank();
57
470
  output.brandBold(" Bankr CLI Login");
58
- console.log();
471
+ output.blank();
59
472
  output.info("Generate your Bankr API key at:");
60
473
  output.brandBold(` ${dashboardUrl}`);
61
- console.log();
474
+ output.blank();
62
475
  output.dim("Once you have the key, run: bankr login --api-key bk_YOUR_KEY");
63
476
  return;
64
477
  }
@@ -66,50 +479,40 @@ export async function loginCommand(opts) {
66
479
  if (opts.apiKey) {
67
480
  const apiKey = opts.apiKey.trim();
68
481
  if (!apiKey.startsWith("bk_")) {
69
- output.error('Invalid format. Bankr API keys start with "bk_".');
70
- process.exit(1);
71
- }
72
- const spin = output.spinner("Validating Bankr API key...");
73
- try {
74
- const res = await fetch(`${apiUrl}/agent/me`, {
75
- headers: { "X-API-Key": apiKey },
76
- });
77
- if (res.ok) {
78
- spin.succeed("Bankr API key validated successfully");
79
- }
80
- else if (res.status === 401 || res.status === 403) {
81
- spin.fail("Invalid Bankr API key");
82
- output.error("API rejected the provided key.");
83
- process.exit(1);
84
- }
85
- else {
86
- spin.warn("Could not validate Bankr API key (API unreachable), saving anyway");
87
- }
88
- }
89
- catch {
90
- spin.warn("Could not validate Bankr API key (network error), saving anyway");
482
+ fatal('Invalid format. Bankr API keys start with "bk_".');
91
483
  }
92
- config.apiKey = apiKey;
93
- if (opts.llmKey) {
94
- config.llmKey = opts.llmKey.trim();
95
- }
96
- writeConfig(config);
97
- output.success(`Credentials saved to ${getConfigPath()}`);
98
- output.dim(`Bankr API URL: ${apiUrl}`);
99
- if (opts.llmKey) {
100
- output.dim(`Bankr LLM Key: (saved separately)`);
484
+ await validateApiKey(apiUrl, apiKey);
485
+ saveCredentials(apiKey, opts.llmKey);
486
+ return;
487
+ }
488
+ // `login email [address]` subcommand
489
+ if (opts.email !== undefined) {
490
+ // Headless step 1: email provided but no code -- send OTP and exit
491
+ if (opts.email && !opts.code) {
492
+ const privyConfig = await fetchPrivyConfigOrFail(apiUrl);
493
+ await sendOtpWithSpinner(privyConfig, opts.email);
494
+ output.blank();
495
+ output.info(`Complete login with: bankr login email ${opts.email} --code <code>`);
496
+ return;
101
497
  }
498
+ const apiKey = await emailLoginFlow(apiUrl, opts);
499
+ output.blank();
500
+ saveCredentials(apiKey, opts.llmKey);
102
501
  return;
103
502
  }
104
- // Interactive flow (original behavior)
105
- console.log();
503
+ // Interactive flow
504
+ output.blank();
106
505
  output.brandBold(" Bankr CLI Login");
107
- console.log();
506
+ output.blank();
108
507
  const choice = await select({
109
- message: "How would you like to authenticate?",
508
+ message: "How would you like to log in?",
110
509
  choices: [
111
510
  {
112
- name: "Open bankr.bot/api to generate a new Bankr API key",
511
+ name: "Sign in with email",
512
+ value: "email",
513
+ },
514
+ {
515
+ name: "Open bankr.bot to generate an API key",
113
516
  value: "browser",
114
517
  },
115
518
  {
@@ -119,6 +522,12 @@ export async function loginCommand(opts) {
119
522
  ],
120
523
  theme: output.bankrTheme,
121
524
  });
525
+ if (choice === "email") {
526
+ const apiKey = await emailLoginFlow(apiUrl, {});
527
+ output.blank();
528
+ saveCredentials(apiKey, opts.llmKey);
529
+ return;
530
+ }
122
531
  if (choice === "browser") {
123
532
  const dashboardUrl = deriveDashboardUrl(apiUrl);
124
533
  output.info("Opening Bankr dashboard...");
@@ -130,9 +539,9 @@ export async function loginCommand(opts) {
130
539
  output.dim("Could not open browser automatically.");
131
540
  output.dim(`Visit ${dashboardUrl} to get your Bankr API key.`);
132
541
  }
133
- console.log();
542
+ output.blank();
134
543
  output.dim("Generate a Bankr API key, then paste it below.");
135
- console.log();
544
+ output.blank();
136
545
  }
137
546
  const apiKey = await input({
138
547
  message: "Paste your Bankr API key:",
@@ -147,99 +556,7 @@ export async function loginCommand(opts) {
147
556
  return true;
148
557
  },
149
558
  });
150
- const spin = output.spinner("Validating Bankr API key...");
151
- try {
152
- const res = await fetch(`${apiUrl}/agent/me`, {
153
- headers: { "X-API-Key": apiKey.trim() },
154
- });
155
- if (res.ok) {
156
- spin.succeed("Bankr API key validated successfully");
157
- }
158
- else if (res.status === 401 || res.status === 403) {
159
- const body = await res.json().catch(() => ({}));
160
- const errorType = body.error ?? "";
161
- if (res.status === 403 || errorType === "Agent API access not enabled") {
162
- spin.fail("Agent API access is not enabled");
163
- output.error("Your API key is valid but does not have Agent API access enabled.");
164
- output.info("Go to https://bankr.bot/api and enable Agent API access for this key.");
165
- }
166
- else if (errorType === "Authentication required") {
167
- spin.fail("API key is not linked to a wallet");
168
- output.error("This API key is not associated with a wallet. Generate a new key at https://bankr.bot/api");
169
- }
170
- else {
171
- spin.fail("Invalid Bankr API key");
172
- output.error("The provided API key is invalid or inactive. Generate a new key at https://bankr.bot/api");
173
- }
174
- process.exit(1);
175
- }
176
- else {
177
- spin.warn("Could not validate Bankr API key (API unreachable), saving anyway");
178
- }
179
- }
180
- catch {
181
- spin.warn("Could not validate Bankr API key (network error), saving anyway");
182
- }
183
- config.apiKey = apiKey.trim();
184
- // Ask if user wants a separate LLM key (interactive only)
185
- if (!opts.llmKey) {
186
- console.log();
187
- const useSeparateLlmKey = await confirm({
188
- message: "Use a different key for the LLM gateway?",
189
- default: false,
190
- theme: output.bankrTheme,
191
- });
192
- if (useSeparateLlmKey) {
193
- const llmChoice = await select({
194
- message: "How would you like to provide the LLM gateway key?",
195
- choices: [
196
- {
197
- name: "Open bankr.bot/api to generate a new key",
198
- value: "browser",
199
- },
200
- {
201
- name: "Paste an existing LLM gateway key",
202
- value: "paste",
203
- },
204
- ],
205
- theme: output.bankrTheme,
206
- });
207
- if (llmChoice === "browser") {
208
- const dashboardUrl = deriveDashboardUrl(apiUrl);
209
- output.info("Opening Bankr dashboard...");
210
- output.dim(dashboardUrl);
211
- try {
212
- await open(dashboardUrl);
213
- }
214
- catch {
215
- output.dim("Could not open browser automatically.");
216
- output.dim(`Visit ${dashboardUrl} to get your LLM gateway key.`);
217
- }
218
- console.log();
219
- output.dim("Generate an LLM gateway key, then paste it below.");
220
- console.log();
221
- }
222
- const llmKey = await input({
223
- message: "Paste your LLM gateway key:",
224
- theme: output.bankrTheme,
225
- transformer: (value) => "*".repeat(value.length),
226
- validate: (value) => {
227
- if (!value.trim())
228
- return "LLM key is required.";
229
- return true;
230
- },
231
- });
232
- config.llmKey = llmKey.trim();
233
- }
234
- }
235
- else {
236
- config.llmKey = opts.llmKey.trim();
237
- }
238
- writeConfig(config);
239
- output.success(`Credentials saved to ${getConfigPath()}`);
240
- output.dim(`Bankr API URL: ${apiUrl}`);
241
- if (config.llmKey) {
242
- output.dim(`Bankr LLM Key: (saved separately)`);
243
- }
559
+ await validateApiKey(apiUrl, apiKey.trim());
560
+ saveCredentials(apiKey.trim(), opts.llmKey);
244
561
  }
245
562
  //# sourceMappingURL=login.js.map