@bankr/cli 0.3.37 → 0.3.38
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 +9 -6
- package/dist/cli.js +3 -5
- package/dist/commands/login.d.ts +37 -0
- package/dist/commands/login.js +209 -39
- package/dist/commands/update.js +2 -3
- package/dist/lib/config.d.ts +2 -1
- package/dist/lib/config.js +18 -2
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,8 @@ bankr whoami
|
|
|
58
58
|
bankr logout
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
Accounts with two-factor authentication enabled get a browser step during `bankr login email`: the CLI prints (and, when interactive, opens) an approval link on bankr.bot, you verify with your passkey there, and the CLI continues once it sees the approval. The link is valid for five minutes; in a headless run, pass it along to whoever holds the passkey. The approval also covers the rest of that login for five minutes, so if you linger on the key-name prompts past that you'll get a second link. If it expires, or you'd rather skip the hop, generate a key at [bankr.bot/api-keys](https://bankr.bot/api-keys) and use `bankr login --api-key`.
|
|
62
|
+
|
|
61
63
|
### Prompts
|
|
62
64
|
|
|
63
65
|
```bash
|
|
@@ -180,12 +182,13 @@ Credentials are stored in `~/.bankr/config.json`.
|
|
|
180
182
|
|
|
181
183
|
Environment variables override stored values:
|
|
182
184
|
|
|
183
|
-
| Variable | Description
|
|
184
|
-
| --------------- |
|
|
185
|
-
| `BANKR_API_KEY` | API key (overrides stored key)
|
|
186
|
-
| `BANKR_API_URL` | API URL (default: `https://api.bankr.bot`)
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
185
|
+
| Variable | Description |
|
|
186
|
+
| --------------- | -------------------------------------------------------------------------- |
|
|
187
|
+
| `BANKR_API_KEY` | API key (overrides stored key) |
|
|
188
|
+
| `BANKR_API_URL` | API URL (default: `https://api.bankr.bot`) |
|
|
189
|
+
| `BANKR_WEB_URL` | Web app URL for browser approval links (default: derived from the API URL) |
|
|
190
|
+
| `BANKR_LLM_KEY` | LLM gateway key (falls back to `BANKR_API_KEY` if not set) |
|
|
191
|
+
| `BANKR_LLM_URL` | LLM gateway URL (default: `https://llm.bankr.bot`) |
|
|
189
192
|
|
|
190
193
|
## API Key
|
|
191
194
|
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
2
|
import { Command } from "commander";
|
|
4
3
|
import { input } from "@inquirer/prompts";
|
|
5
4
|
import { balancesCommand } from "./commands/balances.js";
|
|
@@ -9,7 +8,7 @@ import { swapCommand } from "./commands/swap.js";
|
|
|
9
8
|
import { skillsCommand } from "./commands/capabilities.js";
|
|
10
9
|
import { cancelCommand } from "./commands/cancel.js";
|
|
11
10
|
import { configGetCommand, configSetCommand } from "./commands/config.js";
|
|
12
|
-
import { setConfigPath } from "./lib/config.js";
|
|
11
|
+
import { CLI_VERSION, setConfigPath } from "./lib/config.js";
|
|
13
12
|
import { getErrorMessage } from "./lib/errors.js";
|
|
14
13
|
import { passthroughArgs } from "./lib/passthrough.js";
|
|
15
14
|
import { claudeCommand, creditsCommand, creditsAddCommand, creditsAutoCommand, modelsCommand, opencodeCommand, setupClaudeCodeCommand, setupCursorCommand, setupOpenclawCommand, setupOpenCodeCommand, } from "./commands/llm.js";
|
|
@@ -33,7 +32,6 @@ import { filesListCommand, filesUploadCommand, filesDownloadCommand, filesMkdirC
|
|
|
33
32
|
import { clubStatusCommand, clubSignupCommand, clubCancelCommand, } from "./commands/club.js";
|
|
34
33
|
import * as output from "./lib/output.js";
|
|
35
34
|
import { checkForUpdate } from "./lib/updateCheck.js";
|
|
36
|
-
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
37
35
|
/**
|
|
38
36
|
* Read prompt text from stdin (piped) or interactively.
|
|
39
37
|
* Bypasses shell expansion so dollar signs and special characters are preserved.
|
|
@@ -81,7 +79,7 @@ function colorizeHelp(text) {
|
|
|
81
79
|
program
|
|
82
80
|
.name("bankr")
|
|
83
81
|
.description("Bankr AI agent CLI\n\n Docs: https://docs.bankr.bot\n Skills: https://docs.bankr.bot/skills/overview")
|
|
84
|
-
.version(
|
|
82
|
+
.version(CLI_VERSION, "-v, --version", "Show CLI version")
|
|
85
83
|
.helpOption("-h, --help", "Display help")
|
|
86
84
|
.helpCommand(false)
|
|
87
85
|
.option("--config <path>", "Path to config file (default: ~/.bankr/config.json)")
|
|
@@ -1084,7 +1082,7 @@ async function main() {
|
|
|
1084
1082
|
}
|
|
1085
1083
|
output.fatal(getErrorMessage(err));
|
|
1086
1084
|
}
|
|
1087
|
-
await checkForUpdate(
|
|
1085
|
+
await checkForUpdate(CLI_VERSION);
|
|
1088
1086
|
}
|
|
1089
1087
|
main();
|
|
1090
1088
|
//# sourceMappingURL=cli.js.map
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a login-gated call answers with the MFA step-up envelope. A
|
|
3
|
+
* passkey ceremony can't run in a terminal, so the email flow can't recover;
|
|
4
|
+
* the caller turns this into the browser-key guidance instead of the server's
|
|
5
|
+
* fallback text.
|
|
6
|
+
*/
|
|
7
|
+
export declare class MfaStepUpError extends Error {
|
|
8
|
+
readonly context: string;
|
|
9
|
+
constructor(context: string);
|
|
10
|
+
}
|
|
11
|
+
/** Keyed on `code`, never on message text, so a server wording change can't
|
|
12
|
+
* silently turn this back into the opaque failure. */
|
|
13
|
+
export declare function mfaStepUpErrorFrom(body: unknown): MfaStepUpError | null;
|
|
14
|
+
/** The web app that fronts an API URL: `BANKR_WEB_URL` when set (local dev,
|
|
15
|
+
* staging), bankr.bot for the official API, else the API host with its
|
|
16
|
+
* `api.`/`api-` prefix dropped. */
|
|
17
|
+
export declare function deriveWebOrigin(apiUrl: string): string;
|
|
1
18
|
/**
|
|
2
19
|
* Both Privy credentials minted by the OTP exchange. The identity token alone
|
|
3
20
|
* authenticates, but only the ACCESS token carries the session `sid` claim —
|
|
@@ -24,6 +41,26 @@ export declare function resolveKeyPermissionOverrides(opts: {
|
|
|
24
41
|
readOnly?: boolean;
|
|
25
42
|
agentApiEnabled?: boolean;
|
|
26
43
|
};
|
|
44
|
+
declare const CHALLENGE_STATUSES: readonly ["pending", "verified", "expired", "abandoned", "failed"];
|
|
45
|
+
export type ChallengeStatus = (typeof CHALLENGE_STATUSES)[number];
|
|
46
|
+
/**
|
|
47
|
+
* One poll's outcome from its HTTP status. Only a definitive refusal ends the
|
|
48
|
+
* wait: the gate (401, an API without this route yet), another wallet's token
|
|
49
|
+
* (403) or a gone challenge (404). Anything else — 429, 5xx, a dropped
|
|
50
|
+
* connection — is a blip, so the wait keeps polling as if still pending.
|
|
51
|
+
*/
|
|
52
|
+
export declare function pollOutcome(httpStatus: number, body: unknown): ChallengeStatus;
|
|
53
|
+
/**
|
|
54
|
+
* Poll until the challenge leaves `pending` or the deadline passes: once at
|
|
55
|
+
* once, then every 2s for the first 30s, then every 5s. Pure over its
|
|
56
|
+
* collaborators so the loop is unit-testable without a network.
|
|
57
|
+
*/
|
|
58
|
+
export declare function waitForChallengeApproval(deps: {
|
|
59
|
+
poll: () => Promise<ChallengeStatus>;
|
|
60
|
+
sleep: (ms: number) => Promise<void>;
|
|
61
|
+
deadlineMs: number;
|
|
62
|
+
now?: () => number;
|
|
63
|
+
}): Promise<ChallengeStatus>;
|
|
27
64
|
export declare function loginCommand(opts: {
|
|
28
65
|
apiUrl?: string;
|
|
29
66
|
apiKey?: string;
|
package/dist/commands/login.js
CHANGED
|
@@ -8,12 +8,50 @@ import { getErrorMessage } from "../lib/errors.js";
|
|
|
8
8
|
const DEFAULT_DASHBOARD_URL = "https://bankr.bot/api-keys";
|
|
9
9
|
const MAX_OTP_ATTEMPTS = 3;
|
|
10
10
|
const INVALID_CODE_STATUSES = [400, 401, 422];
|
|
11
|
+
/** The `code` the API's MFA gates send on every step-up 401 envelope
|
|
12
|
+
* (`sendStepUpRequired` in packages/api/src/middleware/mfaGate.ts). Duplicated
|
|
13
|
+
* here because the CLI can't import `@bankr/*` packages. */
|
|
14
|
+
const MFA_STEP_UP_CODE = "MFA_STEP_UP_REQUIRED";
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when a login-gated call answers with the MFA step-up envelope. A
|
|
17
|
+
* passkey ceremony can't run in a terminal, so the email flow can't recover;
|
|
18
|
+
* the caller turns this into the browser-key guidance instead of the server's
|
|
19
|
+
* fallback text.
|
|
20
|
+
*/
|
|
21
|
+
export class MfaStepUpError extends Error {
|
|
22
|
+
constructor(context) {
|
|
23
|
+
super("MFA step-up required");
|
|
24
|
+
this.context = context;
|
|
25
|
+
this.name = "MfaStepUpError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Keyed on `code`, never on message text, so a server wording change can't
|
|
29
|
+
* silently turn this back into the opaque failure. */
|
|
30
|
+
export function mfaStepUpErrorFrom(body) {
|
|
31
|
+
if (!body || typeof body !== "object")
|
|
32
|
+
return null;
|
|
33
|
+
const { code, context } = body;
|
|
34
|
+
if (code !== MFA_STEP_UP_CODE)
|
|
35
|
+
return null;
|
|
36
|
+
return new MfaStepUpError(typeof context === "string" ? context : "unknown");
|
|
37
|
+
}
|
|
38
|
+
/** The web app that fronts an API URL: `BANKR_WEB_URL` when set (local dev,
|
|
39
|
+
* staging), bankr.bot for the official API, else the API host with its
|
|
40
|
+
* `api.`/`api-` prefix dropped. */
|
|
41
|
+
export function deriveWebOrigin(apiUrl) {
|
|
42
|
+
const override = process.env.BANKR_WEB_URL?.trim();
|
|
43
|
+
if (override)
|
|
44
|
+
return override.replace(/\/+$/, "");
|
|
45
|
+
if (apiUrl === DEFAULT_API_URL)
|
|
46
|
+
return "https://bankr.bot";
|
|
47
|
+
// Anchored to the host's start so a host merely containing "api." is intact.
|
|
48
|
+
return apiUrl.replace(/\/+$/, "").replace(/^(https?:\/\/)api[.-]/, "$1");
|
|
49
|
+
}
|
|
11
50
|
function deriveDashboardUrl(apiUrl) {
|
|
12
51
|
if (apiUrl === DEFAULT_API_URL) {
|
|
13
52
|
return DEFAULT_DASHBOARD_URL;
|
|
14
53
|
}
|
|
15
|
-
return (apiUrl
|
|
16
|
-
"/api-keys");
|
|
54
|
+
return `${deriveWebOrigin(apiUrl)}/api-keys`;
|
|
17
55
|
}
|
|
18
56
|
async function fetchPrivyConfig(apiUrl) {
|
|
19
57
|
const res = await fetch(`${apiUrl}/cli/config`, {
|
|
@@ -100,7 +138,10 @@ async function callGenerateWallet(apiUrl, tokens) {
|
|
|
100
138
|
});
|
|
101
139
|
const body = (await res.json());
|
|
102
140
|
if (!res.ok) {
|
|
103
|
-
throw
|
|
141
|
+
throw (mfaStepUpErrorFrom(body) ??
|
|
142
|
+
new Error(body.message ||
|
|
143
|
+
body.error ||
|
|
144
|
+
`Wallet generation failed (${res.status})`));
|
|
104
145
|
}
|
|
105
146
|
return body;
|
|
106
147
|
}
|
|
@@ -111,7 +152,8 @@ async function callAcceptTerms(apiUrl, tokens) {
|
|
|
111
152
|
});
|
|
112
153
|
if (!res.ok) {
|
|
113
154
|
const body = (await res.json().catch(() => ({})));
|
|
114
|
-
throw
|
|
155
|
+
throw (mfaStepUpErrorFrom(body) ??
|
|
156
|
+
new Error(body.message || body.error || `Accept terms failed (${res.status})`));
|
|
115
157
|
}
|
|
116
158
|
}
|
|
117
159
|
async function callGenerateApiKey(apiUrl, tokens, opts) {
|
|
@@ -122,10 +164,115 @@ async function callGenerateApiKey(apiUrl, tokens, opts) {
|
|
|
122
164
|
});
|
|
123
165
|
const body = (await res.json());
|
|
124
166
|
if (!res.ok) {
|
|
125
|
-
throw
|
|
167
|
+
throw (mfaStepUpErrorFrom(body) ??
|
|
168
|
+
new Error(body.message ||
|
|
169
|
+
body.error ||
|
|
170
|
+
`API key generation failed (${res.status})`));
|
|
126
171
|
}
|
|
127
172
|
return body;
|
|
128
173
|
}
|
|
174
|
+
// ── Out-of-band MFA approval ─────────────────────────────────────────
|
|
175
|
+
// Mirrors `MFA_CHALLENGE_STATUSES` in @bankr/shared (the CLI can't import it).
|
|
176
|
+
const CHALLENGE_STATUSES = [
|
|
177
|
+
"pending",
|
|
178
|
+
"verified",
|
|
179
|
+
"expired",
|
|
180
|
+
"abandoned",
|
|
181
|
+
"failed",
|
|
182
|
+
];
|
|
183
|
+
/** Narrow the status poll body; anything unrecognised reads as `failed`. */
|
|
184
|
+
function parseChallengeStatus(body) {
|
|
185
|
+
const status = body?.status;
|
|
186
|
+
return CHALLENGE_STATUSES.includes(status)
|
|
187
|
+
? status
|
|
188
|
+
: "failed";
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* One poll's outcome from its HTTP status. Only a definitive refusal ends the
|
|
192
|
+
* wait: the gate (401, an API without this route yet), another wallet's token
|
|
193
|
+
* (403) or a gone challenge (404). Anything else — 429, 5xx, a dropped
|
|
194
|
+
* connection — is a blip, so the wait keeps polling as if still pending.
|
|
195
|
+
*/
|
|
196
|
+
export function pollOutcome(httpStatus, body) {
|
|
197
|
+
if (httpStatus >= 200 && httpStatus < 300)
|
|
198
|
+
return parseChallengeStatus(body);
|
|
199
|
+
return [401, 403, 404].includes(httpStatus) ? "failed" : "pending";
|
|
200
|
+
}
|
|
201
|
+
/** Poll cadence: 2s while an approval is likely imminent, then 5s. */
|
|
202
|
+
const POLL_FAST_MS = 2000;
|
|
203
|
+
const POLL_SLOW_MS = 5000;
|
|
204
|
+
const POLL_FAST_WINDOW_MS = 30000;
|
|
205
|
+
/**
|
|
206
|
+
* Poll until the challenge leaves `pending` or the deadline passes: once at
|
|
207
|
+
* once, then every 2s for the first 30s, then every 5s. Pure over its
|
|
208
|
+
* collaborators so the loop is unit-testable without a network.
|
|
209
|
+
*/
|
|
210
|
+
export async function waitForChallengeApproval(deps) {
|
|
211
|
+
const now = deps.now ?? Date.now;
|
|
212
|
+
const startedAt = now();
|
|
213
|
+
for (;;) {
|
|
214
|
+
const status = await deps.poll();
|
|
215
|
+
if (status !== "pending")
|
|
216
|
+
return status;
|
|
217
|
+
if (now() >= deps.deadlineMs)
|
|
218
|
+
return "expired";
|
|
219
|
+
await deps.sleep(now() - startedAt < POLL_FAST_WINDOW_MS ? POLL_FAST_MS : POLL_SLOW_MS);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function createMfaChallenge(apiUrl, tokens, context) {
|
|
223
|
+
const res = await fetch(`${apiUrl}/user/mfa/challenge`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: authHeaders(tokens),
|
|
226
|
+
body: JSON.stringify({ context }),
|
|
227
|
+
});
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
throw new Error(`Couldn't start passkey approval (${res.status})`);
|
|
230
|
+
}
|
|
231
|
+
return (await res.json());
|
|
232
|
+
}
|
|
233
|
+
async function readMfaChallengeStatus(apiUrl, tokens, token) {
|
|
234
|
+
try {
|
|
235
|
+
const res = await fetch(`${apiUrl}/user/mfa/challenge/${token}`, {
|
|
236
|
+
headers: authHeaders(tokens),
|
|
237
|
+
});
|
|
238
|
+
return pollOutcome(res.status, await res.json().catch(() => null));
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return "pending";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Mirrors the server's challenge TTL; the deadline is kept on the local clock
|
|
245
|
+
* so a skewed machine can't expire the wait early (the poll reports a real
|
|
246
|
+
* server-side expiry as `expired` regardless). */
|
|
247
|
+
const CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
|
248
|
+
/**
|
|
249
|
+
* The browser hop for an MFA-enabled account: create a challenge for THIS
|
|
250
|
+
* session, hand the user the confirm link, and wait for their passkey on the
|
|
251
|
+
* web to mint the grants. Resolves true once approved.
|
|
252
|
+
*/
|
|
253
|
+
async function approveInBrowser(apiUrl, tokens) {
|
|
254
|
+
const { token } = await createMfaChallenge(apiUrl, tokens, "api-key:generate");
|
|
255
|
+
const url = `${deriveWebOrigin(apiUrl)}/mfa/confirm/${token}`;
|
|
256
|
+
output.blank();
|
|
257
|
+
output.warn("Two-factor authentication is enabled on this account.");
|
|
258
|
+
output.info("Approve this login with your passkey:");
|
|
259
|
+
output.brandBold(` ${url}`);
|
|
260
|
+
if (!output.isNonInteractive()) {
|
|
261
|
+
await open(url).catch(() => undefined);
|
|
262
|
+
}
|
|
263
|
+
const spin = output.spinner("Waiting for approval in your browser...");
|
|
264
|
+
const status = await waitForChallengeApproval({
|
|
265
|
+
poll: () => readMfaChallengeStatus(apiUrl, tokens, token),
|
|
266
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
267
|
+
deadlineMs: Date.now() + CHALLENGE_TTL_MS,
|
|
268
|
+
});
|
|
269
|
+
if (status === "verified") {
|
|
270
|
+
spin.succeed("Approved");
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
spin.fail(`Approval ${status}`);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
129
276
|
// ── OTP verification helpers ─────────────────────────────────────────
|
|
130
277
|
async function fetchPrivyConfigOrFail(apiUrl) {
|
|
131
278
|
const config = await fetchPrivyConfig(apiUrl).catch(() => null);
|
|
@@ -220,20 +367,57 @@ async function authenticateWithEmail(apiUrl, providedEmail, providedCode) {
|
|
|
220
367
|
}
|
|
221
368
|
output.fatal("Verification failed. Please try again.");
|
|
222
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Run a login-gated call; on the MFA step-up envelope, take the browser
|
|
372
|
+
* approval hop once and retry. A second refusal, or an approval that expires
|
|
373
|
+
* or fails, exits with the browser-key guidance.
|
|
374
|
+
*/
|
|
375
|
+
async function runGatedStep(apiUrl, tokens, label, run) {
|
|
376
|
+
for (const retry of [false, true]) {
|
|
377
|
+
const spin = output.spinner(label);
|
|
378
|
+
try {
|
|
379
|
+
const result = await run();
|
|
380
|
+
spin.stop();
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
spin.stop();
|
|
385
|
+
if (!(err instanceof MfaStepUpError) || retry) {
|
|
386
|
+
return failLoginStep(`${label} failed`, err, apiUrl);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const approved = await approveInBrowser(apiUrl, tokens).catch((err) => {
|
|
390
|
+
output.error(getErrorMessage(err));
|
|
391
|
+
return false;
|
|
392
|
+
});
|
|
393
|
+
if (!approved)
|
|
394
|
+
return exitWithApiKeyGuidance(apiUrl);
|
|
395
|
+
}
|
|
396
|
+
throw new Error("unreachable");
|
|
397
|
+
}
|
|
398
|
+
/** Exit with the generic epilogue, or the browser-key path on a step-up
|
|
399
|
+
* refusal the approval hop didn't clear. */
|
|
400
|
+
function failLoginStep(label, err, apiUrl) {
|
|
401
|
+
if (!(err instanceof MfaStepUpError)) {
|
|
402
|
+
return output.fatal(`${label}: ${getErrorMessage(err)}`);
|
|
403
|
+
}
|
|
404
|
+
return exitWithApiKeyGuidance(apiUrl);
|
|
405
|
+
}
|
|
406
|
+
/** The remediation is the point of this message, so every line stays on
|
|
407
|
+
* stderr with the headline: a scripted caller capturing stderr sees all of it. */
|
|
408
|
+
function exitWithApiKeyGuidance(apiUrl) {
|
|
409
|
+
output.error("Two-factor authentication is enabled on this account.");
|
|
410
|
+
output.errorDetail(" The passkey approval didn't complete.");
|
|
411
|
+
output.errorDetail(` Generate an API key at ${deriveDashboardUrl(apiUrl)} instead (verify with your passkey there), then run:`);
|
|
412
|
+
output.errorDetail(" bankr login --api-key bk_YOUR_KEY");
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
223
415
|
// ── Email login flow ────────────────────────────────────────────────
|
|
224
416
|
async function emailLoginFlow(apiUrl, opts) {
|
|
225
417
|
// Step 1: Authenticate via email OTP
|
|
226
418
|
const { tokens } = await authenticateWithEmail(apiUrl, opts.email, opts.code);
|
|
227
419
|
// Step 2: Generate/resolve wallet
|
|
228
|
-
const
|
|
229
|
-
let wallet;
|
|
230
|
-
try {
|
|
231
|
-
wallet = await callGenerateWallet(apiUrl, tokens);
|
|
232
|
-
walletSpin.stop();
|
|
233
|
-
}
|
|
234
|
-
catch (err) {
|
|
235
|
-
output.failWith(walletSpin, "Wallet setup failed", err);
|
|
236
|
-
}
|
|
420
|
+
const wallet = await runGatedStep(apiUrl, tokens, "Setting up wallet...", () => callGenerateWallet(apiUrl, tokens));
|
|
237
421
|
// Show wallet info
|
|
238
422
|
output.blank();
|
|
239
423
|
if (wallet.isNewUser) {
|
|
@@ -264,14 +448,8 @@ async function emailLoginFlow(apiUrl, opts) {
|
|
|
264
448
|
if (!accepted) {
|
|
265
449
|
output.fatal("Terms must be accepted to continue.");
|
|
266
450
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
await callAcceptTerms(apiUrl, tokens);
|
|
270
|
-
termsSpin.succeed("Terms accepted");
|
|
271
|
-
}
|
|
272
|
-
catch (err) {
|
|
273
|
-
output.failWith(termsSpin, "Failed to accept terms", err);
|
|
274
|
-
}
|
|
451
|
+
await runGatedStep(apiUrl, tokens, "Accepting terms...", () => callAcceptTerms(apiUrl, tokens));
|
|
452
|
+
output.success("Terms accepted");
|
|
275
453
|
}
|
|
276
454
|
// Step 4: Generate API key
|
|
277
455
|
output.blank();
|
|
@@ -301,23 +479,15 @@ async function emailLoginFlow(apiUrl, opts) {
|
|
|
301
479
|
default: false,
|
|
302
480
|
theme: output.bankrTheme,
|
|
303
481
|
}));
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
allowedIps: opts.allowedIps,
|
|
314
|
-
allowedRecipients: opts.allowedRecipients,
|
|
315
|
-
});
|
|
316
|
-
keySpin.stop();
|
|
317
|
-
}
|
|
318
|
-
catch (err) {
|
|
319
|
-
output.failWith(keySpin, "Failed to generate API key", err);
|
|
320
|
-
}
|
|
482
|
+
const apiKeyResult = await runGatedStep(apiUrl, tokens, "Generating API key...", () => callGenerateApiKey(apiUrl, tokens, {
|
|
483
|
+
name: keyName.trim(),
|
|
484
|
+
walletApiEnabled: enableWallet,
|
|
485
|
+
...overrides,
|
|
486
|
+
tokenLaunchApiEnabled: enableTokenLaunch,
|
|
487
|
+
llmGatewayEnabled: enableLlm,
|
|
488
|
+
allowedIps: opts.allowedIps,
|
|
489
|
+
allowedRecipients: opts.allowedRecipients,
|
|
490
|
+
}));
|
|
321
491
|
// Show result
|
|
322
492
|
output.blank();
|
|
323
493
|
const features = [
|
package/dist/commands/update.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { execSync } from "node:child_process";
|
|
3
2
|
import * as output from "../lib/output.js";
|
|
4
3
|
import { getErrorMessage } from "../lib/errors.js";
|
|
4
|
+
import { CLI_VERSION } from "../lib/config.js";
|
|
5
5
|
const PACKAGE_NAME = "@bankr/cli";
|
|
6
6
|
/**
|
|
7
7
|
* Compare two semver version strings.
|
|
@@ -31,8 +31,7 @@ async function fetchLatestVersion() {
|
|
|
31
31
|
return data.version;
|
|
32
32
|
}
|
|
33
33
|
function getCurrentVersion() {
|
|
34
|
-
|
|
35
|
-
return pkg.version;
|
|
34
|
+
return CLI_VERSION;
|
|
36
35
|
}
|
|
37
36
|
/**
|
|
38
37
|
* Detect which package manager installed the CLI globally.
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export declare function setConfigPath(path: string): void;
|
|
|
11
11
|
export declare function getConfigPath(): string;
|
|
12
12
|
export declare const DEFAULT_API_URL = "https://api.bankr.bot";
|
|
13
13
|
export declare const DEFAULT_LLM_URL = "https://llm.bankr.bot";
|
|
14
|
-
export declare const
|
|
14
|
+
export declare const CLI_VERSION: string;
|
|
15
|
+
export declare const CLI_USER_AGENT: string;
|
|
15
16
|
export declare function readConfig(): BankrConfig;
|
|
16
17
|
export declare function writeConfig(config: BankrConfig): void;
|
|
17
18
|
export declare function clearConfig(): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
2
|
+
import { homedir, platform, release } from "os";
|
|
3
3
|
import { dirname, join } from "path";
|
|
4
4
|
let configPathOverride;
|
|
5
5
|
const DEFAULT_CONFIG_FILE = join(homedir(), ".bankr", "config.json");
|
|
@@ -11,7 +11,23 @@ export function getConfigPath() {
|
|
|
11
11
|
}
|
|
12
12
|
export const DEFAULT_API_URL = "https://api.bankr.bot";
|
|
13
13
|
export const DEFAULT_LLM_URL = "https://llm.bankr.bot";
|
|
14
|
-
export const
|
|
14
|
+
export const CLI_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8")).version;
|
|
15
|
+
/** Browser-style platform token so the API's shared user-agent parser (which
|
|
16
|
+
* clamps to known families) names the OS on an out-of-band approval screen
|
|
17
|
+
* as "Bankr CLI · macOS". Never the hostname or username. */
|
|
18
|
+
function platformToken() {
|
|
19
|
+
switch (platform()) {
|
|
20
|
+
case "darwin":
|
|
21
|
+
return "Macintosh";
|
|
22
|
+
case "win32":
|
|
23
|
+
return "Windows NT";
|
|
24
|
+
case "linux":
|
|
25
|
+
return "Linux";
|
|
26
|
+
default:
|
|
27
|
+
return platform();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const CLI_USER_AGENT = `bankr-cli/${CLI_VERSION} (${platformToken()}; ${platform()} ${release()})`;
|
|
15
31
|
function ensureConfigDir() {
|
|
16
32
|
const configDir = dirname(getConfigPath());
|
|
17
33
|
if (!existsSync(configDir)) {
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export declare function brand(msg: string): void;
|
|
|
22
22
|
export declare function brandBold(msg: string): void;
|
|
23
23
|
export declare function success(msg: string): void;
|
|
24
24
|
export declare function error(msg: string): void;
|
|
25
|
+
/** Dim follow-up line for an error, on stderr so it stays with the headline. */
|
|
26
|
+
export declare function errorDetail(msg: string): void;
|
|
25
27
|
/** Collapses the error-then-exit epilogue every command repeats. */
|
|
26
28
|
export declare function fatal(message: string): never;
|
|
27
29
|
/** Same epilogue, for the spinner-driven variant that reports a caught error. */
|
package/dist/lib/output.js
CHANGED
|
@@ -40,6 +40,10 @@ export function success(msg) {
|
|
|
40
40
|
export function error(msg) {
|
|
41
41
|
console.error(chalk.red(`✗ ${msg}`));
|
|
42
42
|
}
|
|
43
|
+
/** Dim follow-up line for an error, on stderr so it stays with the headline. */
|
|
44
|
+
export function errorDetail(msg) {
|
|
45
|
+
console.error(chalk.dim(msg));
|
|
46
|
+
}
|
|
43
47
|
/** Collapses the error-then-exit epilogue every command repeats. */
|
|
44
48
|
export function fatal(message) {
|
|
45
49
|
error(message);
|