@boole-digital/cli 0.2.1 → 0.2.4

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 (2) hide show
  1. package/dist/index.js +66 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16,7 +16,8 @@ import {
16
16
  chmodSync,
17
17
  rmSync
18
18
  } from "node:fs";
19
- var API_BASE = (process.env.BOOLE_API_BASE || "https://trade.boole.markets").replace(/\/+$/, "");
19
+ var API_BASE = (process.env.BOOLE_API_BASE || "https://api.cest-finyx.com").replace(/\/+$/, "");
20
+ var WEB_BASE = (process.env.BOOLE_WEB_BASE || process.env.BOOLE_API_BASE || "https://trade.boole.markets").replace(/\/+$/, "");
20
21
  var CONFIG_DIR = process.env.BOOLE_HOME || join(homedir(), ".boole");
21
22
  var CRED_PATH = join(CONFIG_DIR, "credentials.json");
22
23
  var SESSION_PATH = join(CONFIG_DIR, "session.env");
@@ -145,12 +146,12 @@ function decodePayload(code) {
145
146
  };
146
147
  }
147
148
  function successHtml(email, dashboardUrl) {
148
- const who = email ? `<p>Signed in as <b style="color:#eaeaea">${email.replace(/[<>&]/g, "")}</b></p>` : "";
149
+ const who = email ? `<p class="who">Signed in as: <b>${email.replace(/[<>&]/g, "")}</b></p>` : "";
149
150
  const url = dashboardUrl.replace(/"/g, "");
150
151
  return `<!doctype html><meta charset=utf-8><title>Boole</title>
151
- <style>body{font:16px -apple-system,system-ui,sans-serif;background:#0b0b0b;color:#eaeaea;display:grid;place-items:center;height:100vh;margin:0}div{text-align:center}h1{font-size:22px}p{color:#9a9a9a;margin:6px 0}.btn{display:inline-block;margin-top:18px;padding:10px 20px;border-radius:8px;background:#6d5efc;color:#fff;text-decoration:none;font-weight:600}.btn:hover{background:#5a4ef0}</style>
152
- <div><h1>\u2713 Signed in to Boole</h1>${who}<p style="color:#34d399;font-weight:600">Your CLI is authenticated.</p><p>You can close this tab now and return to your terminal.</p>
153
- <a class="btn" href="${url}">See dashboard \u2192</a></div>`;
152
+ <style>body{font:16px -apple-system,system-ui,sans-serif;background:#0b0b0b;color:#eaeaea;display:grid;place-items:center;height:100vh;margin:0}div{text-align:center}h1{font-size:22px;font-weight:600;margin:0 0 14px}p{margin:6px 0;color:#9a9a9a}.who b{color:#eaeaea;font-weight:600}.tag{color:#7a7a7a;margin-top:16px}.btn{display:inline-block;margin-top:22px;padding:10px 22px;border-radius:8px;background:#1c1c22;color:#c8c8d0;text-decoration:none;font-weight:500;border:1px solid #2e2e38}.btn:hover{background:#26262e;color:#eaeaea}</style>
153
+ <div><h1>Authenticated to Boole</h1>${who}<p class="tag">Go build something great</p>
154
+ <a class="btn" href="${url}">Continue to Boole</a></div>`;
154
155
  }
155
156
  function emailFromCode(code) {
156
157
  try {
@@ -160,7 +161,7 @@ function emailFromCode(code) {
160
161
  }
161
162
  }
162
163
  async function loginPaste() {
163
- const url = `${API_BASE}/cli-auth?paste=1`;
164
+ const url = `${WEB_BASE}/cli-auth?paste=1`;
164
165
  info("Opening the Boole login page\u2026");
165
166
  log(` ${c.dim("If it does not open, visit:")} ${c.cyan(url)}`);
166
167
  openBrowser(url);
@@ -190,7 +191,7 @@ async function loginLoopback() {
190
191
  return;
191
192
  }
192
193
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
193
- res.end(successHtml(emailFromCode(code), API_BASE));
194
+ res.end(successHtml(emailFromCode(code), WEB_BASE));
194
195
  queueMicrotask(() => resolveCode(code));
195
196
  });
196
197
  const port = await new Promise((resolve2) => {
@@ -199,7 +200,7 @@ async function loginLoopback() {
199
200
  resolve2(typeof addr === "object" && addr ? addr.port : 0);
200
201
  });
201
202
  });
202
- const url = `${API_BASE}/cli-auth?port=${port}`;
203
+ const url = `${WEB_BASE}/cli-auth?port=${port}`;
203
204
  info("Opening the Boole login page in your browser\u2026");
204
205
  log(` ${c.dim("If it does not open, visit:")} ${c.cyan(url)}`);
205
206
  openBrowser(url);
@@ -283,14 +284,27 @@ var BooleApi = class {
283
284
  async request(path, init2 = {}) {
284
285
  await this.ensureFresh();
285
286
  if (!this.creds) throw new AuthError("Not logged in. Run `boole login`.");
286
- const doFetch = () => fetch(`${API_BASE}${path}`, {
287
- ...init2,
288
- headers: {
289
- "Content-Type": "application/json",
290
- ...init2.headers || {},
291
- Authorization: `Bearer ${this.creds.access_token}`
287
+ const TIMEOUT_MS = Number(process.env.BOOLE_TIMEOUT_MS) || 12e3;
288
+ const doFetch = async () => {
289
+ const ac = new AbortController();
290
+ const t = setTimeout(() => ac.abort(), TIMEOUT_MS);
291
+ try {
292
+ return await fetch(`${API_BASE}${path}`, {
293
+ ...init2,
294
+ signal: ac.signal,
295
+ headers: {
296
+ "Content-Type": "application/json",
297
+ ...init2.headers || {},
298
+ Authorization: `Bearer ${this.creds.access_token}`
299
+ }
300
+ });
301
+ } catch (e) {
302
+ if (e?.name === "AbortError") throw new Error(`${path}: timed out after ${TIMEOUT_MS / 1e3}s (network or box waking up)`);
303
+ throw e;
304
+ } finally {
305
+ clearTimeout(t);
292
306
  }
293
- });
307
+ };
294
308
  let res = await doFetch();
295
309
  if (res.status === 401) {
296
310
  this.creds.expires_at = 0;
@@ -333,8 +347,15 @@ var BooleApi = class {
333
347
  }
334
348
  // Returns the plaintext SSH password for a droplet. Field name varies by
335
349
  // server version, so probe the likely shapes.
336
- async getSshPassword(id) {
337
- const body = await this.request(`/api/v1/droplets/${id}/ssh-password`, { method: "POST" });
350
+ // The ssh-password endpoint is gated by a server-side connect access code
351
+ // (env.CLAUDE_CONNECT_ACCESS_CODE) with brute-force lockout, so we MUST send it
352
+ // in the POST body (the web app does the same). Without it the backend replies
353
+ // "Incorrect access code".
354
+ async getSshPassword(id, accessCode) {
355
+ const body = await this.request(`/api/v1/droplets/${id}/ssh-password`, {
356
+ method: "POST",
357
+ body: JSON.stringify({ accessCode: (accessCode || "").trim() })
358
+ });
338
359
  const pw = body?.password ?? body?.ssh_password ?? body?.data?.password ?? body?.data?.ssh_password;
339
360
  if (!pw) throw new Error("SSH password not present in API response (check field name in api.ts:getSshPassword).");
340
361
  return String(pw);
@@ -368,7 +389,7 @@ function onboardingNotice() {
368
389
  warn("No trading computer yet \u2014 and you can\u2019t deploy your first one from the CLI.");
369
390
  log(" Finish onboarding in the Boole app first (this is required):");
370
391
  log(` 1. Create your account 2. Agree to the terms 3. Set up billing`);
371
- log(` ${c.cyan(API_BASE)}`);
392
+ log(` ${c.cyan(WEB_BASE)}`);
372
393
  log(` Deploy your first trading computer there, then come back and run ${c.cyan("boole connect")}.`);
373
394
  log("");
374
395
  }
@@ -389,7 +410,7 @@ function orient() {
389
410
  log(`${c.bold("Do this next:")} ${c.cyan("boole login")}`);
390
411
  } else if (!sess) {
391
412
  log(`${c.bold("You are:")} logged in as ${c.bold(creds.user.email || creds.user.id)}, ${c.yellow("no box connected")}.`);
392
- log(`${c.bold("Do this next:")} ${c.cyan("boole connect")} ${c.dim(`# no trading computer yet? finish onboarding at ${API_BASE}`)}`);
413
+ log(`${c.bold("Do this next:")} ${c.cyan("boole connect")} ${c.dim(`# no trading computer yet? finish onboarding at ${WEB_BASE}`)}`);
393
414
  } else {
394
415
  log(`${c.bold("You are:")} connected to ${c.bold(sess.agent || sess.ip)} ${c.green("\u2014 ready")}.`);
395
416
  log(c.bold("Do this next:"));
@@ -445,7 +466,8 @@ function pickAgentId(droplets) {
445
466
  if (ready.length === 1) return { id: ready[0].id, name: ready[0].name };
446
467
  return null;
447
468
  }
448
- async function summary() {
469
+ async function summary(opts = {}) {
470
+ const snapshot = opts.snapshot !== false;
449
471
  const creds = loadCredentials();
450
472
  if (!creds) {
451
473
  warn("Not logged in. Run `boole login`.");
@@ -483,7 +505,7 @@ async function summary() {
483
505
  log(c.bold(`Trading computers (${droplets.length})`));
484
506
  printAgents(droplets);
485
507
  const pick = pickAgentId(droplets);
486
- if (pick) {
508
+ if (pick && snapshot) {
487
509
  log("");
488
510
  log(c.bold(`Snapshot \u2014 ${pick.name}`));
489
511
  try {
@@ -493,6 +515,8 @@ async function summary() {
493
515
  } catch {
494
516
  warn("trading computer unreachable (tunnel may be waking up \u2014 retry in a moment)");
495
517
  }
518
+ } else if (pick) {
519
+ log(c.dim(` Run ${c.cyan("boole status")} for balances + live snapshot.`));
496
520
  }
497
521
  operatingBrief();
498
522
  }
@@ -583,7 +607,23 @@ async function connect(opts = {}) {
583
607
  if (!READY(target)) die(`Trading computer "${target.name}" is not ready yet (status=${target.status}).`);
584
608
  if (!target.ip_address) die(`Trading computer "${target.name}" has no IP yet. Try again shortly.`);
585
609
  info(`Fetching access for ${c.bold(target.name)}\u2026`);
586
- const password = await api.getSshPassword(target.id);
610
+ let password;
611
+ try {
612
+ password = await api.getSshPassword(target.id, opts.code || "");
613
+ } catch (e) {
614
+ if (!/access code/i.test(e?.message || "")) throw e;
615
+ let code = (opts.code || "").trim();
616
+ if (!code && process.stdin.isTTY) {
617
+ code = (await prompt("This computer needs a connect access code (from the Boole app):")).trim();
618
+ }
619
+ if (!code) die("This computer needs a connect access code: run `boole connect --code <code>` (from the Boole app).");
620
+ try {
621
+ password = await api.getSshPassword(target.id, code);
622
+ } catch (e2) {
623
+ if (/access code/i.test(e2?.message || "")) die("Connect access code was rejected \u2014 re-run `boole connect --code <code>` with the correct code.");
624
+ throw e2;
625
+ }
626
+ }
587
627
  saveSession({ agent: target.name, dropletId: target.id, ip: target.ip_address, password, sshUser: "customer" });
588
628
  ok(`Connected to ${c.bold(target.name)} (${target.ip_address}).`);
589
629
  log("");
@@ -711,7 +751,7 @@ function init(opts = {}) {
711
751
  }
712
752
 
713
753
  // src/index.ts
714
- var VERSION = "0.2.1";
754
+ var VERSION = "0.2.4";
715
755
  function parse(argv) {
716
756
  const _ = [];
717
757
  const flags = {};
@@ -756,7 +796,7 @@ ${c.bold("Operate the box")}
756
796
  ${c.bold("Other")}
757
797
  help, --help \xB7 version, --version
758
798
 
759
- Env: ${c.dim("BOOLE_API_BASE")} (default https://trade.boole.markets), ${c.dim("BOOLE_HOME")}
799
+ Env: ${c.dim("BOOLE_API_BASE")} (REST API) \xB7 ${c.dim("BOOLE_WEB_BASE")} (login page) \xB7 ${c.dim("BOOLE_HOME")} \xB7 ${c.dim("BOOLE_TIMEOUT_MS")}
760
800
  `;
761
801
  async function main() {
762
802
  const { _, flags } = parse(process.argv.slice(2));
@@ -776,7 +816,7 @@ async function main() {
776
816
  switch (cmd) {
777
817
  case "login":
778
818
  await login({ paste: !!flags.paste });
779
- await summary();
819
+ await summary({ snapshot: false });
780
820
  break;
781
821
  case "logout":
782
822
  await logout();
@@ -798,7 +838,7 @@ async function main() {
798
838
  await provision({ name: str(flags.name), region: str(flags.region), size: str(flags.size) });
799
839
  break;
800
840
  case "connect":
801
- await connect({ name: _[1] });
841
+ await connect({ name: _[1], code: str(flags.code) });
802
842
  break;
803
843
  case "init":
804
844
  init({ dir: _[1], force: !!flags.force });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boole-digital/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.4",
4
4
  "description": "Boole — install, sign in, and operate your trading computer from the terminal (Claude Code, Codex, Gemini).",
5
5
  "type": "module",
6
6
  "bin": { "boole": "dist/index.js" },