@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 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
- | `BANKR_LLM_KEY` | LLM gateway key (falls back to `BANKR_API_KEY` if not set) |
188
- | `BANKR_LLM_URL` | LLM gateway URL (default: `https://llm.bankr.bot`) |
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(pkg.version, "-v, --version", "Show CLI 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(pkg.version);
1085
+ await checkForUpdate(CLI_VERSION);
1088
1086
  }
1089
1087
  main();
1090
1088
  //# sourceMappingURL=cli.js.map
@@ -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;
@@ -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.replace(/\/+$/, "").replace("api.", "").replace("api-", "") +
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 new Error(body.message || body.error || `Wallet generation failed (${res.status})`);
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 new Error(body.message || body.error || `Accept terms failed (${res.status})`);
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 new Error(body.message || body.error || `API key generation failed (${res.status})`);
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 walletSpin = output.spinner("Setting up wallet...");
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
- const termsSpin = output.spinner("Accepting terms...");
268
- try {
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 keySpin = output.spinner("Generating API key...");
305
- let apiKeyResult;
306
- try {
307
- apiKeyResult = await callGenerateApiKey(apiUrl, tokens, {
308
- name: keyName.trim(),
309
- walletApiEnabled: enableWallet,
310
- ...overrides,
311
- tokenLaunchApiEnabled: enableTokenLaunch,
312
- llmGatewayEnabled: enableLlm,
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 = [
@@ -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
- const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
35
- return pkg.version;
34
+ return CLI_VERSION;
36
35
  }
37
36
  /**
38
37
  * Detect which package manager installed the CLI globally.
@@ -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 CLI_USER_AGENT = "bankr-cli/0.1";
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;
@@ -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 CLI_USER_AGENT = "bankr-cli/0.1";
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)) {
@@ -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. */
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bankr/cli",
3
- "version": "0.3.37",
3
+ "version": "0.3.38",
4
4
  "description": "Official CLI for the Bankr AI agent platform",
5
5
  "type": "module",
6
6
  "bin": {