@boole-digital/cli 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 (4) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +59 -0
  3. package/dist/index.js +955 -0
  4. package/package.json +31 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2026 Boole Digital. All rights reserved.
2
+
3
+ This software (the "Boole CLI") is proprietary to Boole Digital.
4
+
5
+ Permission is granted, free of charge, to any person who is an authorized user
6
+ of the Boole service to download, install, and run unmodified copies of the
7
+ Boole CLI solely to access and operate their own Boole trading computer in
8
+ accordance with the Boole Terms of Service.
9
+
10
+ You may not, except as expressly permitted above: copy, modify, merge, publish,
11
+ redistribute, sublicense, sell, reverse engineer, or create derivative works of
12
+ the Boole CLI, in whole or in part.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL BOOLE DIGITAL BE
17
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
18
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+
21
+ Trading involves substantial risk of loss. The Boole CLI operates live trading
22
+ systems; you are solely responsible for all activity conducted through it.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @boole-digital/cli
2
+
3
+ Operate your **Boole trading computer** from the terminal — and let your coding
4
+ agent (**Claude Code**, **Codex**, or **Gemini**) drive it safely.
5
+
6
+ ```bash
7
+ npx @boole-digital/cli login # sign in (opens your browser)
8
+ npx @boole-digital/cli connect # connect to your trading computer
9
+ npx @boole-digital/cli init # scaffold an agent workspace here
10
+ ```
11
+
12
+ Then open the folder in Claude Code, Codex, or Gemini and start working. The
13
+ agent reads the operating rules `init` wrote and creates strategies the right
14
+ way — every one tracked on your Boole dashboard.
15
+
16
+ ## Requirements
17
+
18
+ - Node.js >= 18 (that's it — SSH is built in, no `expect`/PuTTY needed)
19
+ - A Boole account with onboarding complete (terms + billing) at
20
+ [trade.boole.markets](https://trade.boole.markets). Your first trading
21
+ computer is created in the app; after that the CLI can connect and operate it.
22
+
23
+ ## Commands
24
+
25
+ | Command | What it does |
26
+ |---|---|
27
+ | `login [--paste]` | Sign in via the browser (`--paste` for manual code entry). |
28
+ | `connect [name]` | Connect to a trading computer; caches access for this machine. |
29
+ | `init [dir] [--force]` | Write `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` so your agent has the rules. |
30
+ | `computers` | List your trading computers. |
31
+ | `status` | Identity + computers + a live snapshot. |
32
+ | `balances [--venue <v>]` | Equity and open positions. |
33
+ | `ssh "<cmd>"` | Run a read-only command on the box. |
34
+ | `logs [name]` | Tail a strategy's logs. |
35
+ | `strategy spawn --template <t.js> --name <n> [--param K=V]...` | Create a tracked strategy through the gateway. |
36
+ | `strategy ls` | List running strategies. |
37
+ | `strategy doctor [name]` | Gateway health + tracking status. |
38
+ | `logout` | Sign out, clear the cached session. |
39
+
40
+ ### Creating a strategy
41
+
42
+ Strategies are always created through the gateway so they appear on your
43
+ dashboard with full accounting:
44
+
45
+ ```bash
46
+ boole strategy spawn --template loop_template.js --name btc-watch \
47
+ --param EXCHANGE=hyperliquid --param MARKET=BTC --param POLL_SEC=10
48
+ ```
49
+
50
+ ## Notes
51
+
52
+ - Credentials and SSH access are stored under `~/.boole/` with `0600`
53
+ permissions and are never logged.
54
+ - Set `BOOLE_API_BASE` to point at a non-production environment.
55
+
56
+ ---
57
+
58
+ © 2026 Boole Digital. Proprietary — see [LICENSE](./LICENSE). Trading involves
59
+ substantial risk of loss.
package/dist/index.js ADDED
@@ -0,0 +1,955 @@
1
+ #!/usr/bin/env node
2
+ import{createRequire as __cr}from'module';const require=__cr(import.meta.url);
3
+
4
+ // src/login.ts
5
+ import { spawn } from "node:child_process";
6
+ import { createServer } from "node:http";
7
+
8
+ // src/config.ts
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import {
12
+ mkdirSync,
13
+ existsSync,
14
+ readFileSync,
15
+ writeFileSync,
16
+ chmodSync,
17
+ rmSync
18
+ } from "node:fs";
19
+ var API_BASE = (process.env.BOOLE_API_BASE || "https://trade.boole.markets").replace(/\/+$/, "");
20
+ var CONFIG_DIR = process.env.BOOLE_HOME || join(homedir(), ".boole");
21
+ var CRED_PATH = join(CONFIG_DIR, "credentials.json");
22
+ var SESSION_PATH = join(CONFIG_DIR, "session.env");
23
+ function ensureDir() {
24
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
25
+ }
26
+ function saveCredentials(creds) {
27
+ ensureDir();
28
+ writeFileSync(CRED_PATH, JSON.stringify(creds, null, 2));
29
+ chmodSync(CRED_PATH, 384);
30
+ }
31
+ function loadCredentials() {
32
+ if (!existsSync(CRED_PATH)) return null;
33
+ try {
34
+ return JSON.parse(readFileSync(CRED_PATH, "utf8"));
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ function clearCredentials() {
40
+ if (existsSync(CRED_PATH)) rmSync(CRED_PATH);
41
+ }
42
+ function shSingleQuote(v) {
43
+ return `'${v.replace(/'/g, `'\\''`)}'`;
44
+ }
45
+ function saveSession(s) {
46
+ ensureDir();
47
+ const body = [
48
+ "# Written by `boole connect`. Do not edit by hand; re-run `boole connect` to refresh.",
49
+ `BOOLE_AGENT=${shSingleQuote(s.agent)}`,
50
+ `BOOLE_DROPLET_ID=${shSingleQuote(String(s.dropletId))}`,
51
+ `BOOLE_IP=${shSingleQuote(s.ip)}`,
52
+ `BOOLE_PASS=${shSingleQuote(s.password)}`,
53
+ `BOOLE_SSH_USER=${shSingleQuote(s.sshUser || "customer")}`,
54
+ ""
55
+ ].join("\n");
56
+ writeFileSync(SESSION_PATH, body);
57
+ chmodSync(SESSION_PATH, 384);
58
+ }
59
+ function loadSession() {
60
+ if (!existsSync(SESSION_PATH)) return null;
61
+ try {
62
+ const text = readFileSync(SESSION_PATH, "utf8");
63
+ const get = (k) => {
64
+ const m = text.match(new RegExp(`^${k}=(.*)$`, "m"));
65
+ if (!m) return "";
66
+ let v = m[1].trim();
67
+ if (v.startsWith("'") && v.endsWith("'")) v = v.slice(1, -1).replace(/'\\''/g, "'");
68
+ return v;
69
+ };
70
+ const ip = get("BOOLE_IP");
71
+ if (!ip) return null;
72
+ return {
73
+ agent: get("BOOLE_AGENT"),
74
+ dropletId: get("BOOLE_DROPLET_ID"),
75
+ ip,
76
+ password: get("BOOLE_PASS"),
77
+ sshUser: get("BOOLE_SSH_USER") || "customer"
78
+ };
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ function jwtExpiry(token) {
84
+ try {
85
+ const payload = token.split(".")[1];
86
+ const json = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
87
+ return typeof json.exp === "number" ? json.exp : 0;
88
+ } catch {
89
+ return 0;
90
+ }
91
+ }
92
+
93
+ // src/ui.ts
94
+ var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
95
+ var wrap = (code) => (s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
96
+ var c = {
97
+ bold: wrap("1"),
98
+ dim: wrap("2"),
99
+ red: wrap("31"),
100
+ green: wrap("32"),
101
+ yellow: wrap("33"),
102
+ blue: wrap("34"),
103
+ cyan: wrap("36")
104
+ };
105
+ var log = (msg = "") => console.log(msg);
106
+ var info = (msg) => console.log(`${c.cyan("\u203A")} ${msg}`);
107
+ var ok = (msg) => console.log(`${c.green("\u2713")} ${msg}`);
108
+ var warn = (msg) => console.log(`${c.yellow("!")} ${msg}`);
109
+ var err = (msg) => console.error(`${c.red("\u2717")} ${msg}`);
110
+ function die(msg) {
111
+ err(msg);
112
+ process.exit(1);
113
+ }
114
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
115
+ async function prompt(question) {
116
+ const { createInterface } = await import("node:readline");
117
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
118
+ return new Promise((resolve2) => {
119
+ rl.question(`${c.cyan("?")} ${question} `, (ans) => {
120
+ rl.close();
121
+ resolve2(ans.trim());
122
+ });
123
+ });
124
+ }
125
+
126
+ // src/login.ts
127
+ function openBrowser(url) {
128
+ const platform = process.platform;
129
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
130
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
131
+ try {
132
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
133
+ } catch {
134
+ }
135
+ }
136
+ function decodePayload(code) {
137
+ const json = Buffer.from(code.trim(), "base64").toString("utf8");
138
+ const p = JSON.parse(json);
139
+ if (!p.access_token || !p.user) throw new Error("Malformed login code.");
140
+ return {
141
+ access_token: p.access_token,
142
+ refresh_token: p.refresh_token || "",
143
+ expires_at: p.expires_at || jwtExpiry(p.access_token),
144
+ user: p.user
145
+ };
146
+ }
147
+ function successHtml(email, dashboardUrl) {
148
+ const who = email ? `<p>Signed in as <b style="color:#eaeaea">${email.replace(/[<>&]/g, "")}</b></p>` : "";
149
+ const url = dashboardUrl.replace(/"/g, "");
150
+ 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>`;
154
+ }
155
+ function emailFromCode(code) {
156
+ try {
157
+ return JSON.parse(Buffer.from(code.trim(), "base64").toString("utf8")).user?.email || "";
158
+ } catch {
159
+ return "";
160
+ }
161
+ }
162
+ async function loginPaste() {
163
+ const url = `${API_BASE}/cli-auth?paste=1`;
164
+ info("Opening the Boole login page\u2026");
165
+ log(` ${c.dim("If it does not open, visit:")} ${c.cyan(url)}`);
166
+ openBrowser(url);
167
+ const code = await prompt("Paste the login code:");
168
+ if (!code) throw new Error("No code entered.");
169
+ saveCredentials(decodePayload(code));
170
+ }
171
+ async function loginLoopback() {
172
+ let resolveCode;
173
+ let rejectCode;
174
+ const codePromise = new Promise((res, rej) => {
175
+ resolveCode = res;
176
+ rejectCode = rej;
177
+ });
178
+ const server = createServer((req, res) => {
179
+ const u = new URL(req.url || "/", "http://127.0.0.1");
180
+ if (u.pathname !== "/callback") {
181
+ res.writeHead(200);
182
+ res.end("boole");
183
+ return;
184
+ }
185
+ const code = u.searchParams.get("token");
186
+ if (!code) {
187
+ res.writeHead(400);
188
+ res.end("missing token");
189
+ rejectCode(new Error("No token in callback."));
190
+ return;
191
+ }
192
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
193
+ res.end(successHtml(emailFromCode(code), API_BASE));
194
+ queueMicrotask(() => resolveCode(code));
195
+ });
196
+ const port = await new Promise((resolve2) => {
197
+ server.listen(0, "127.0.0.1", () => {
198
+ const addr = server.address();
199
+ resolve2(typeof addr === "object" && addr ? addr.port : 0);
200
+ });
201
+ });
202
+ const url = `${API_BASE}/cli-auth?port=${port}`;
203
+ info("Opening the Boole login page in your browser\u2026");
204
+ log(` ${c.dim("If it does not open, visit:")} ${c.cyan(url)}`);
205
+ openBrowser(url);
206
+ log(c.dim(" Waiting for you to finish signing in\u2026"));
207
+ const timeout = setTimeout(() => rejectCode(new Error("Timed out waiting for login (5 min).")), 5 * 6e4);
208
+ try {
209
+ const code = await codePromise;
210
+ clearTimeout(timeout);
211
+ saveCredentials(decodePayload(code));
212
+ await sleep(1200);
213
+ } catch (e) {
214
+ clearTimeout(timeout);
215
+ server.close();
216
+ throw e;
217
+ }
218
+ server.close();
219
+ }
220
+ async function login(opts = {}) {
221
+ try {
222
+ if (opts.paste) await loginPaste();
223
+ else await loginLoopback();
224
+ } catch (e) {
225
+ if (!opts.paste) warn("Automatic handoff failed \u2014 retry with `boole login --paste`.");
226
+ throw e;
227
+ }
228
+ }
229
+
230
+ // src/agents.ts
231
+ import { randomBytes } from "node:crypto";
232
+
233
+ // src/api.ts
234
+ var AuthError = class extends Error {
235
+ };
236
+ function unwrap(body) {
237
+ if (body == null) return body;
238
+ if (Array.isArray(body)) return body;
239
+ if (Array.isArray(body.data)) return body.data;
240
+ if (body.data?.droplets) return body.data.droplets;
241
+ if (body.droplets) return body.droplets;
242
+ if (body.data !== void 0) return body.data;
243
+ return body;
244
+ }
245
+ var BooleApi = class {
246
+ creds;
247
+ constructor(creds = loadCredentials()) {
248
+ this.creds = creds;
249
+ }
250
+ get user() {
251
+ return this.creds?.user ?? null;
252
+ }
253
+ get isLoggedIn() {
254
+ return !!this.creds?.access_token;
255
+ }
256
+ setCredentials(creds) {
257
+ this.creds = creds;
258
+ saveCredentials(creds);
259
+ }
260
+ // Refresh the access token if it's missing/expired (60s skew).
261
+ async ensureFresh() {
262
+ if (!this.creds) throw new AuthError("Not logged in. Run `boole login`.");
263
+ const exp = this.creds.expires_at || jwtExpiry(this.creds.access_token);
264
+ if (exp && exp - 60 > Date.now() / 1e3) return;
265
+ if (!this.creds.refresh_token) return;
266
+ const res = await fetch(`${API_BASE}/api/v1/auth/refresh`, {
267
+ method: "POST",
268
+ headers: { "Content-Type": "application/json" },
269
+ body: JSON.stringify({ refresh_token: this.creds.refresh_token })
270
+ });
271
+ if (!res.ok) return;
272
+ const data = await res.json().catch(() => null);
273
+ if (data?.success && data.access_token) {
274
+ const next = {
275
+ access_token: data.access_token,
276
+ refresh_token: data.refresh_token || this.creds.refresh_token,
277
+ expires_at: data.expires_at || jwtExpiry(data.access_token),
278
+ user: data.user || this.creds.user
279
+ };
280
+ this.setCredentials(next);
281
+ }
282
+ }
283
+ async request(path, init2 = {}) {
284
+ await this.ensureFresh();
285
+ 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}`
292
+ }
293
+ });
294
+ let res = await doFetch();
295
+ if (res.status === 401) {
296
+ this.creds.expires_at = 0;
297
+ await this.ensureFresh();
298
+ res = await doFetch();
299
+ }
300
+ if (res.status === 401) {
301
+ throw new AuthError("Session expired or token was rotated elsewhere. Run `boole login` again.");
302
+ }
303
+ const text = await res.text();
304
+ const body = text ? (() => {
305
+ try {
306
+ return JSON.parse(text);
307
+ } catch {
308
+ return text;
309
+ }
310
+ })() : null;
311
+ if (!res.ok) {
312
+ const msg = body && (body.error || body.message) || `${res.status} ${res.statusText}`;
313
+ throw new Error(`${path}: ${msg}`);
314
+ }
315
+ return body;
316
+ }
317
+ // ---- droplets ----------------------------------------------------------
318
+ async listDroplets() {
319
+ const body = await this.request("/api/v1/droplets");
320
+ const list2 = unwrap(body);
321
+ return Array.isArray(list2) ? list2 : [];
322
+ }
323
+ async getDroplet(id) {
324
+ const body = await this.request(`/api/v1/droplets/${id}`);
325
+ return unwrap(body);
326
+ }
327
+ async createDroplet(input) {
328
+ const body = await this.request("/api/v1/droplets", {
329
+ method: "POST",
330
+ body: JSON.stringify(input)
331
+ });
332
+ return unwrap(body);
333
+ }
334
+ // Returns the plaintext SSH password for a droplet. Field name varies by
335
+ // 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" });
338
+ const pw = body?.password ?? body?.ssh_password ?? body?.data?.password ?? body?.data?.ssh_password;
339
+ if (!pw) throw new Error("SSH password not present in API response (check field name in api.ts:getSshPassword).");
340
+ return String(pw);
341
+ }
342
+ // ---- talk to the gateway on the box (via the ownership-checked proxy) ---
343
+ // GET a gateway path (e.g. "/health", "/api/venues/hyperliquid/account")
344
+ // through /api/v1/droplets/:id/proxy/* — the JWT is forwarded and ownership
345
+ // re-checked server-side.
346
+ async proxyGet(id, gatewayPath) {
347
+ const p = gatewayPath.startsWith("/") ? gatewayPath : `/${gatewayPath}`;
348
+ return this.request(`/api/v1/droplets/${id}/proxy${p}`);
349
+ }
350
+ async getHealth(id) {
351
+ return this.proxyGet(id, "/health");
352
+ }
353
+ async getAccount(id, exchange) {
354
+ return this.proxyGet(id, `/api/venues/${exchange}/account`);
355
+ }
356
+ };
357
+
358
+ // src/agents.ts
359
+ import { existsSync as existsSync2, rmSync as rmSync2 } from "node:fs";
360
+ function requireApi() {
361
+ const api = new BooleApi();
362
+ if (!api.isLoggedIn) die("Not logged in. Run `boole login` first.");
363
+ return api;
364
+ }
365
+ var READY = (d) => d.status === "active" && !!d.setup_completed_at;
366
+ function onboardingNotice() {
367
+ log("");
368
+ warn("No trading computer yet \u2014 and you can\u2019t deploy your first one from the CLI.");
369
+ log(" Finish onboarding in the Boole app first (this is required):");
370
+ log(` 1. Create your account 2. Agree to the terms 3. Set up billing`);
371
+ log(` ${c.cyan(API_BASE)}`);
372
+ log(` Deploy your first trading computer there, then come back and run ${c.cyan("boole connect")}.`);
373
+ log("");
374
+ }
375
+ function printAgents(droplets) {
376
+ if (!droplets.length) {
377
+ onboardingNotice();
378
+ return;
379
+ }
380
+ const session = loadSession();
381
+ for (const d of droplets) {
382
+ const here = session && String(session.dropletId) === String(d.id);
383
+ const state = READY(d) ? c.green(d.status) : c.yellow(d.status);
384
+ const ip = d.ip_address || c.dim("(pending)");
385
+ log(`${here ? c.cyan("\u25CF") : " "} ${c.bold(d.name)} ${state} ${ip} ${c.dim(`id:${d.id}`)}`);
386
+ }
387
+ }
388
+ async function agents() {
389
+ const api = requireApi();
390
+ printAgents(await api.listDroplets());
391
+ }
392
+ function fmtUsd(n) {
393
+ const v = typeof n === "number" ? n : Number(n);
394
+ if (!isFinite(v)) return "$0";
395
+ return `$${v.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
396
+ }
397
+ function pickAgentId(droplets) {
398
+ const session = loadSession();
399
+ if (session?.dropletId) {
400
+ const d = droplets.find((x) => String(x.id) === String(session.dropletId));
401
+ return { id: session.dropletId, name: d?.name || session.agent || String(session.dropletId) };
402
+ }
403
+ const ready = droplets.filter(READY);
404
+ if (ready.length === 1) return { id: ready[0].id, name: ready[0].name };
405
+ return null;
406
+ }
407
+ async function summary() {
408
+ const creds = loadCredentials();
409
+ if (!creds) {
410
+ warn("Not logged in. Run `boole login`.");
411
+ return;
412
+ }
413
+ log("");
414
+ ok(`Logged in as ${c.bold(creds.user.email || creds.user.id)}${creds.user.role ? c.dim(` \xB7 ${creds.user.role}`) : ""}`);
415
+ const api = new BooleApi();
416
+ let droplets = [];
417
+ try {
418
+ droplets = await api.listDroplets();
419
+ } catch (e) {
420
+ warn(`Could not list trading computers: ${e.message}`);
421
+ return;
422
+ }
423
+ if (!droplets.length) {
424
+ onboardingNotice();
425
+ return;
426
+ }
427
+ log("");
428
+ log(c.bold(`Trading computers (${droplets.length})`));
429
+ printAgents(droplets);
430
+ const pick = pickAgentId(droplets);
431
+ if (!pick) return;
432
+ log("");
433
+ log(c.bold(`Snapshot \u2014 ${pick.name}`));
434
+ try {
435
+ const health = await api.getHealth(pick.id);
436
+ ok(`online${health.defaultModel ? c.dim(` \xB7 model ${health.defaultModel}`) : ""}`);
437
+ } catch {
438
+ warn("trading computer unreachable (tunnel may be waking up \u2014 retry in a moment)");
439
+ return;
440
+ }
441
+ await printBalances(api, pick.id, "hyperliquid");
442
+ }
443
+ async function printBalances(api, id, exchange) {
444
+ try {
445
+ const acct = await api.getAccount(id, exchange);
446
+ const positions = Array.isArray(acct.positions) ? acct.positions : [];
447
+ info(`${exchange}: equity ${c.bold(fmtUsd(acct.equity))} \xB7 available ${fmtUsd(acct.available)} \xB7 ${positions.length} open position(s)`);
448
+ for (const p of positions.slice(0, 12)) {
449
+ log(` ${c.bold(String(p.market ?? "?"))} ${p.size ?? ""}`);
450
+ }
451
+ } catch (e) {
452
+ warn(`${exchange} balances unavailable: ${e.message}`);
453
+ }
454
+ }
455
+ async function status() {
456
+ await summary();
457
+ }
458
+ async function balances(opts = {}) {
459
+ const api = requireApi();
460
+ const droplets = await api.listDroplets();
461
+ const pick = pickAgentId(droplets);
462
+ if (!pick) die("No trading computer connected. Run `boole connect` first.");
463
+ log(c.bold(`${pick.name}`));
464
+ await printBalances(api, pick.id, opts.venue || "hyperliquid");
465
+ }
466
+ async function logout() {
467
+ clearCredentials();
468
+ if (existsSync2(SESSION_PATH)) rmSync2(SESSION_PATH);
469
+ ok("Logged out.");
470
+ }
471
+ async function provision(opts = {}) {
472
+ const api = requireApi();
473
+ const name = opts.name || `computer-${randomBytes(3).toString("hex")}`;
474
+ info(`Deploying ${c.bold(name)}\u2026`);
475
+ let d;
476
+ try {
477
+ d = await api.createDroplet({ name, region: opts.region, size: opts.size });
478
+ } catch (e) {
479
+ err("Could not deploy a trading computer from the CLI.");
480
+ onboardingNotice();
481
+ log(c.dim(` (reason: ${e.message})`));
482
+ process.exit(1);
483
+ }
484
+ log(c.dim(` Trading computer created (#${d.id}). Waiting for it to come online\u2026`));
485
+ const deadline = Date.now() + 8 * 6e4;
486
+ while (Date.now() < deadline) {
487
+ if (READY(d)) break;
488
+ if (d.status === "error" || d.status === "failed") die(`Deploy failed: status=${d.status}.`);
489
+ await sleep(4e3);
490
+ d = await api.getDroplet(d.id);
491
+ }
492
+ if (!READY(d)) die("Timed out waiting for the trading computer to come online. Check `boole computers` shortly.");
493
+ ok(`Trading computer ${c.bold(name)} is ready.`);
494
+ await connect({ name });
495
+ }
496
+ async function connect(opts = {}) {
497
+ const api = requireApi();
498
+ const droplets = await api.listDroplets();
499
+ if (!droplets.length) {
500
+ onboardingNotice();
501
+ return;
502
+ }
503
+ let target;
504
+ if (opts.name) {
505
+ target = droplets.find((d) => d.name === opts.name || String(d.id) === opts.name);
506
+ if (!target) die(`No trading computer named "${opts.name}". Run \`boole computers\` to list them.`);
507
+ } else {
508
+ const ready = droplets.filter(READY);
509
+ if (ready.length === 1) target = ready[0];
510
+ else {
511
+ warn("Specify which trading computer: `boole connect <name>`");
512
+ printAgents(droplets);
513
+ return;
514
+ }
515
+ }
516
+ if (!READY(target)) die(`Trading computer "${target.name}" is not ready yet (status=${target.status}).`);
517
+ if (!target.ip_address) die(`Trading computer "${target.name}" has no IP yet. Try again shortly.`);
518
+ info(`Fetching access for ${c.bold(target.name)}\u2026`);
519
+ const password = await api.getSshPassword(target.id);
520
+ saveSession({ agent: target.name, dropletId: target.id, ip: target.ip_address, password, sshUser: "customer" });
521
+ ok(`Connected to ${c.bold(target.name)} (${target.ip_address}).`);
522
+ log("");
523
+ log(` Scaffold an agent workspace here, then open it:`);
524
+ log(` ${c.cyan("boole init")}`);
525
+ log(` Open the folder in ${c.bold("Claude Code")}, ${c.bold("Codex")}, or ${c.bold("Gemini")} \u2014 each reads the`);
526
+ log(` rules it just wrote (${c.dim("CLAUDE.md / AGENTS.md / GEMINI.md")}) and operates your computer via ${c.cyan("boole")}.`);
527
+ log(` Or run a one-off: ${c.cyan('boole ssh "pm2 list"')}`);
528
+ }
529
+
530
+ // src/ssh.ts
531
+ import ssh2 from "ssh2";
532
+ var { Client } = ssh2;
533
+ function requireSession() {
534
+ const s = loadSession();
535
+ if (!s) die("No active trading computer. Run `boole connect` first.");
536
+ return s;
537
+ }
538
+ function friendly(e) {
539
+ const m = e?.message || String(e);
540
+ if (/All configured authentication methods failed|authentication/i.test(m)) {
541
+ return "SSH authentication failed \u2014 your access may have rotated. Run `boole connect` again.";
542
+ }
543
+ if (/ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENOTFOUND|timed out|Timed out/i.test(m)) {
544
+ return "Could not reach your trading computer (it may still be waking up). Try again in a moment.";
545
+ }
546
+ return `SSH error: ${m}`;
547
+ }
548
+ function open(s) {
549
+ return new Promise((resolve2, reject) => {
550
+ const conn = new Client();
551
+ conn.on("ready", () => resolve2(conn));
552
+ conn.on("error", (e) => reject(new Error(friendly(e))));
553
+ conn.connect({
554
+ host: s.ip,
555
+ port: 22,
556
+ username: s.sshUser || "customer",
557
+ password: s.password,
558
+ readyTimeout: 25e3,
559
+ // We connect to the customer's OWN freshly-provisioned box, addressed by
560
+ // the IP the authenticated Boole API returned for their account. Accept
561
+ // the host key on first use (parity with the prior transport).
562
+ hostVerifier: () => true
563
+ });
564
+ });
565
+ }
566
+ async function runRemote(command) {
567
+ const s = requireSession();
568
+ const conn = await open(s);
569
+ try {
570
+ return await new Promise((resolve2, reject) => {
571
+ conn.exec(command, (err2, stream) => {
572
+ if (err2) return reject(new Error(friendly(err2)));
573
+ let stdout = "";
574
+ let stderr = "";
575
+ stream.on("close", (code) => resolve2({ code: code ?? 0, stdout, stderr }));
576
+ stream.on("data", (d) => {
577
+ stdout += d.toString();
578
+ });
579
+ stream.stderr.on("data", (d) => {
580
+ stderr += d.toString();
581
+ });
582
+ });
583
+ });
584
+ } finally {
585
+ conn.end();
586
+ }
587
+ }
588
+ async function runRemoteInherit(command) {
589
+ const s = requireSession();
590
+ const conn = await open(s);
591
+ try {
592
+ return await new Promise((resolve2, reject) => {
593
+ conn.exec(command, (err2, stream) => {
594
+ if (err2) return reject(new Error(friendly(err2)));
595
+ stream.on("close", (code) => resolve2(code ?? 0));
596
+ stream.on("data", (d) => process.stdout.write(d));
597
+ stream.stderr.on("data", (d) => process.stderr.write(d));
598
+ });
599
+ });
600
+ } finally {
601
+ conn.end();
602
+ }
603
+ }
604
+
605
+ // src/strategy.ts
606
+ var NAME_RE = /^[a-z0-9][a-z0-9_-]{0,48}$/;
607
+ var GATE = "http://localhost:3000";
608
+ function paramLiteral(raw) {
609
+ const t = raw.trim();
610
+ if (/^-?\d+(\.\d+)?$/.test(t)) return t;
611
+ if (t === "true" || t === "false") return t;
612
+ if (t.startsWith("[") || t.startsWith("{")) return t;
613
+ return `'${raw.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
614
+ }
615
+ function parseParams(pairs) {
616
+ const out = {};
617
+ for (const p of pairs) {
618
+ const i = p.indexOf("=");
619
+ if (i < 0) die(`Bad --param "${p}" (expected KEY=VALUE).`);
620
+ const key = p.slice(0, i).trim();
621
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key)) die(`Bad param key "${key}" (UPPER_SNAKE_CASE).`);
622
+ out[key] = paramLiteral(p.slice(i + 1));
623
+ }
624
+ return out;
625
+ }
626
+ async function strategySpawn(opts) {
627
+ const template = opts.template;
628
+ const name = opts.name;
629
+ if (!template || !name) {
630
+ die("Usage: boole strategy spawn --template <template.js> --name <name> [--param KEY=VALUE]...");
631
+ }
632
+ if (!template.endsWith("_template.js")) die("--template must be a gateway template ending in _template.js (e.g. order_template.js, grid_template.js, loop_template.js).");
633
+ if (!NAME_RE.test(name)) die(`Invalid --name "${name}". Use lowercase letters, digits, "-" or "_" (max 49 chars).`);
634
+ const payload = {
635
+ templateFile: template,
636
+ name,
637
+ params: parseParams(opts.params || []),
638
+ autoStart: opts.autostart !== false
639
+ };
640
+ info(`Spawning ${c.bold(name)} from ${c.bold(template)} through the gateway\u2026`);
641
+ const b64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
642
+ const cmd = `printf %s '${b64}' | base64 -d | curl -s --max-time 60 -XPOST ${GATE}/api/strategy/spawn -H 'content-type: application/json' -d @-`;
643
+ const r = await runRemote(cmd);
644
+ let res = null;
645
+ try {
646
+ res = JSON.parse(r.stdout.trim());
647
+ } catch {
648
+ }
649
+ if (!res) {
650
+ err("The gateway did not return a valid response.");
651
+ if (r.stdout.trim()) log(c.dim(` ${r.stdout.trim().slice(0, 400)}`));
652
+ if (r.stderr.trim()) log(c.dim(` ${r.stderr.trim().slice(0, 200)}`));
653
+ process.exit(1);
654
+ }
655
+ if (res.error || res.ok === false) {
656
+ err(`Spawn rejected: ${res.error || "unknown error"}`);
657
+ if (res.suggested_name) log(c.dim(` Try --name ${res.suggested_name}`));
658
+ process.exit(1);
659
+ }
660
+ ok(`Spawned ${c.bold(name)}${res.runId ? c.dim(` \xB7 run ${res.runId}`) : ""} \u2014 it will appear on your dashboard.`);
661
+ const dropped = Array.isArray(res.skipped) ? res.skipped.filter((k) => k !== "RUN_ID") : [];
662
+ if (dropped.length) warn(`Ignored param(s) with no matching template field: ${dropped.join(", ")} \u2014 check the template's required params.`);
663
+ if (typeof res.placed === "number" || typeof res.total === "number") {
664
+ info(`Orders placed: ${res.placed ?? 0}/${res.total ?? 0}${res.failed ? c.yellow(` \xB7 ${res.failed} failed`) : ""}`);
665
+ }
666
+ if (res.firstError) warn(`First error: ${res.firstError}`);
667
+ }
668
+ async function strategyList() {
669
+ const r = await runRemote("pm2 jlist 2>/dev/null");
670
+ let procs = [];
671
+ try {
672
+ procs = JSON.parse(r.stdout.trim());
673
+ } catch {
674
+ }
675
+ if (!Array.isArray(procs)) {
676
+ warn("Could not read process list from the box.");
677
+ return;
678
+ }
679
+ if (!procs.length) {
680
+ info("No strategies running.");
681
+ return;
682
+ }
683
+ log(c.bold("Strategies"));
684
+ for (const p of procs) {
685
+ const st = p.pm2_env?.status || "?";
686
+ const dot = st === "online" ? c.green("\u25CF") : c.dim("\u25CB");
687
+ const up = p.pm2_env?.pm_uptime && st === "online" ? c.dim(` \xB7 up ${Math.max(0, Math.round((Date.now() - p.pm2_env.pm_uptime) / 6e4))}m`) : "";
688
+ const restarts = p.pm2_env?.restart_time ? c.dim(` \xB7 \u21BA${p.pm2_env.restart_time}`) : "";
689
+ log(` ${dot} ${c.bold(p.name)} ${st === "online" ? c.green(st) : c.yellow(st)}${up}${restarts}`);
690
+ }
691
+ }
692
+ async function strategyDoctor(name) {
693
+ const health = await runRemote(`curl -sf --max-time 15 ${GATE}/health`);
694
+ if (health.code !== 0) {
695
+ err("Gateway is not responding on the box.");
696
+ return;
697
+ }
698
+ ok("Gateway healthy.");
699
+ if (!name) return;
700
+ if (!NAME_RE.test(name)) die(`Invalid name "${name}".`);
701
+ const r = await runRemote("pm2 jlist 2>/dev/null");
702
+ let procs = [];
703
+ try {
704
+ procs = JSON.parse(r.stdout.trim());
705
+ } catch {
706
+ }
707
+ const p = procs.find((x) => x.name === name);
708
+ if (!p) {
709
+ warn(`No process named "${name}" \u2014 spawn it with \`boole strategy spawn\`.`);
710
+ return;
711
+ }
712
+ if (p.pm2_env?.status === "online") ok(`${c.bold(name)} is online and tracked \u2014 it will show on the dashboard.`);
713
+ else {
714
+ warn(`${c.bold(name)} is ${p.pm2_env?.status || "not online"} \u2014 check \`boole logs ${name}\`.`);
715
+ }
716
+ }
717
+
718
+ // src/rules.ts
719
+ import { writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "node:fs";
720
+ import { join as join2, resolve } from "node:path";
721
+ var OPERATING_RULES = `# Boole Trading Computer \u2014 Operating Rules (READ FIRST, IN FULL)
722
+
723
+ You operate a customer's **live** crypto trading computer through the \`boole\` CLI.
724
+ **Real money is at risk.** These are STRICT, NON-NEGOTIABLE rules.
725
+
726
+ > Why they matter: Boole tracks and controls every strategy by HOW it was created.
727
+ > If you create one any other way \u2014 writing files over SSH, starting processes
728
+ > yourself \u2014 it **silently fails**: it won't register, won't appear on the
729
+ > dashboard, can't be controlled, and may mismanage live positions. There is no
730
+ > error message. When in doubt, STOP and ask the user.
731
+
732
+ ## Boot sequence \u2014 every session, before anything else
733
+ 1. \`boole status\` \u2014 confirm you are logged in and a trading computer is connected.
734
+ If not, STOP and tell the user to run \`boole login\` then \`boole connect\`.
735
+ 2. Read this entire file.
736
+ 3. \`boole strategy ls\` \u2014 see what is already running.
737
+
738
+ ## The one rule: THINK locally, COMMIT through \`boole\`
739
+ - THINK \u2014 research, analyze data, design, draft code \u2014 freely on this machine. No limits.
740
+ - COMMIT \u2014 anything that changes the trading computer \u2014 ONLY through \`boole\` commands.
741
+
742
+ ## Creating / running a strategy \u2014 the ONLY sanctioned path
743
+ Spawn from a known-good template through the gateway gate:
744
+
745
+ boole strategy spawn --template <template.js> --name <name> \\
746
+ --param KEY=VALUE --param KEY=VALUE ...
747
+
748
+ - This registers the run and starts it tracked, so it appears on the dashboard.
749
+ - \`--name\`: lowercase letters, digits, "-" or "_" (e.g. btc-grid).
750
+ - \`--param\`: pass raw values (EXCHANGE=hyperliquid, TOTAL_USD=50). The CLI quotes them.
751
+ - Verify: \`boole strategy doctor <name>\` \u2192 should report online and tracked.
752
+
753
+ ### Templates (required params)
754
+ - order_template.js \u2014 single order: EXCHANGE, MARKET, SIDE(buy|sell), ORDER_TYPE(market|limit), TOTAL_USD, LIMIT_PRICE(0 for market)
755
+ - grid_template.js \u2014 grid: EXCHANGE, SYMBOL, LEVELS_PER_SIDE, ORDER_SIZE_USD, LEVERAGE, MAX_EXPOSURE_USD
756
+ - twap_template.js \u2014 TWAP: EXCHANGE, MARKET, SIDE, TOTAL_USD, DURATION_MIN, SLICES
757
+ - scale_template.js \u2014 ladder: EXCHANGE, MARKET, SIDE, TOTAL_USD, PRICE_LOW, PRICE_HIGH, LEVELS
758
+ - stop_template.js \u2014 conditional (no position yet): EXCHANGE, MARKET, SIDE, STOP_KIND(stop_market|stop_limit|tp_market|tp_limit), TOTAL_USD, TRIGGER_PRICE, LIMIT_PRICE(0 for *_market)
759
+ - loop_template.js \u2014 generic poll loop / price watcher: EXCHANGE, MARKET, POLL_SEC
760
+
761
+ Example:
762
+
763
+ boole strategy spawn --template loop_template.js --name btc-watch \\
764
+ --param EXCHANGE=hyperliquid --param MARKET=BTC --param POLL_SEC=10
765
+
766
+ If you omit a required param, the gate returns an error naming it \u2014 fix and retry.
767
+ If the gate says a template is not found, it is not installed on this box \u2014 pick
768
+ another from the list or ask the user.
769
+ Only use a template above. Do NOT hand-roll custom strategy files. If you need
770
+ something no template covers, STOP and ask the user.
771
+
772
+ ## Allowed \u2014 read-only inspection
773
+ - \`boole status\` / \`boole balances [--venue <v>]\` \u2014 identity, equity, positions
774
+ - \`boole strategy ls\` \u2014 running strategies
775
+ - \`boole strategy doctor <name>\` \u2014 health + tracking check
776
+ - \`boole logs <name>\` \u2014 tail a strategy's logs
777
+ - \`boole ssh "<read-only command>"\` \u2014 inspect only (cat, ls, pm2 list, curl localhost:3000/health)
778
+
779
+ ## Forbidden
780
+ - Do NOT write strategy or interface files over SSH.
781
+ - Do NOT \`pm2 start/restart/stop/delete\` by hand.
782
+ - Do NOT edit gateway files directly.
783
+ - \`boole ssh\` is for READ-ONLY inspection. To change the box, use a \`boole\` verb.
784
+ - If there is no verb for a change you need, STOP and ask. A missing verb is not permission to freelance.
785
+
786
+ ## Safety (real money)
787
+ - Trading is live. Prefer capital preservation. Confirm exchange state before acting.
788
+ - Never print or exfiltrate .env, API keys, or credentials.
789
+ - Never restart the gateway.
790
+ - Ask the user before destructive actions (closing positions, stopping strategies).
791
+
792
+ ## Notes
793
+ - Some gateway internals are intentionally not readable from your account \u2014 by design. Use the \`boole\` verbs; you don't need them.
794
+ - A commit is not done until \`boole strategy doctor\` confirms it. If it did not register, fix it before moving on.
795
+ `;
796
+ var FILES = ["CLAUDE.md", "AGENTS.md", "GEMINI.md"];
797
+ function init(opts = {}) {
798
+ const dir = resolve(opts.dir || process.cwd());
799
+ if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
800
+ let wrote = 0;
801
+ let skipped = 0;
802
+ for (const f of FILES) {
803
+ const p = join2(dir, f);
804
+ if (existsSync3(p) && !opts.force) {
805
+ warn(`${f} already exists \u2014 skipped (use --force to overwrite).`);
806
+ skipped++;
807
+ continue;
808
+ }
809
+ writeFileSync2(p, OPERATING_RULES);
810
+ wrote++;
811
+ }
812
+ log("");
813
+ if (wrote) ok(`Wrote operating rules (${FILES.join(", ")}) to ${c.bold(dir)}`);
814
+ else if (skipped) info("All rule files already present \u2014 nothing changed.");
815
+ log("");
816
+ log(` Open ${c.bold(dir)} in ${c.bold("Claude Code")}, ${c.bold("Codex")}, or ${c.bold("Gemini")}.`);
817
+ log(` Your agent reads these rules and operates your trading computer through ${c.cyan("boole")}.`);
818
+ }
819
+
820
+ // src/index.ts
821
+ var VERSION = "0.1.0";
822
+ function parse(argv) {
823
+ const _ = [];
824
+ const flags = {};
825
+ const add = (k, v) => {
826
+ if (k in flags) {
827
+ const cur = flags[k];
828
+ flags[k] = Array.isArray(cur) ? [...cur, String(v)] : [String(cur), String(v)];
829
+ } else flags[k] = v;
830
+ };
831
+ for (let i = 0; i < argv.length; i++) {
832
+ const a = argv[i];
833
+ if (a.startsWith("--")) {
834
+ const key = a.slice(2);
835
+ if (key.includes("=")) {
836
+ const [k, v] = key.split(/=(.*)/s);
837
+ add(k, v);
838
+ } else if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
839
+ add(key, argv[++i]);
840
+ } else add(key, true);
841
+ } else _.push(a);
842
+ }
843
+ return { _, flags };
844
+ }
845
+ var str = (f) => typeof f === "string" ? f : Array.isArray(f) ? f[f.length - 1] : void 0;
846
+ var list = (f) => f === void 0 ? [] : Array.isArray(f) ? f : typeof f === "string" ? [f] : [];
847
+ var HELP = `${c.bold("boole")} \u2014 operate your Boole trading computer from the terminal.
848
+
849
+ ${c.bold("Usage")}
850
+ boole <command> [options]
851
+
852
+ ${c.bold("Getting started")}
853
+ login [--paste] Sign in (opens the browser; --paste for manual code)
854
+ provision [--name <n>] Create a new trading computer (requires app onboarding)
855
+ connect [name] Connect to an existing trading computer (caches SSH access)
856
+ init [dir] [--force] Scaffold an agent workspace (CLAUDE.md / AGENTS.md / GEMINI.md)
857
+ computers List your trading computers
858
+ status Who you are + your trading computers + a live snapshot
859
+ balances [--venue <v>] Equity + open positions for the connected trading computer
860
+ logout Sign out and clear the cached session
861
+
862
+ ${c.bold("Operate")}
863
+ ssh "<command>" Run a read-only command on the connected box
864
+ logs [name] Tail a strategy's logs
865
+
866
+ ${c.bold("Strategies")} ${c.dim("(go through the gateway \u2014 always tracked on the dashboard)")}
867
+ strategy spawn --template <t.js> --name <n> [--param KEY=VALUE]... [--no-autostart]
868
+ strategy ls List running strategies
869
+ strategy doctor [name] Gateway health + a strategy's tracking status
870
+
871
+ ${c.bold("Other")}
872
+ help, --help Show this help
873
+ version, --version Show version
874
+
875
+ Env: ${c.dim("BOOLE_API_BASE")} (default https://trade.boole.markets), ${c.dim("BOOLE_HOME")}
876
+ `;
877
+ async function main() {
878
+ const { _, flags } = parse(process.argv.slice(2));
879
+ const cmd = _[0];
880
+ if (cmd === "version" || flags.version) {
881
+ log(`boole ${VERSION}`);
882
+ return;
883
+ }
884
+ if (!cmd || cmd === "help" || flags.help) {
885
+ log(HELP);
886
+ return;
887
+ }
888
+ switch (cmd) {
889
+ case "login":
890
+ await login({ paste: !!flags.paste });
891
+ await summary();
892
+ break;
893
+ case "logout":
894
+ await logout();
895
+ break;
896
+ case "status":
897
+ case "whoami":
898
+ await status();
899
+ break;
900
+ case "balances":
901
+ case "balance":
902
+ await balances({ venue: str(flags.venue) });
903
+ break;
904
+ case "computers":
905
+ case "agents":
906
+ case "ls":
907
+ await agents();
908
+ break;
909
+ case "provision":
910
+ await provision({ name: str(flags.name), region: str(flags.region), size: str(flags.size) });
911
+ break;
912
+ case "connect":
913
+ await connect({ name: _[1] });
914
+ break;
915
+ case "init":
916
+ init({ dir: _[1], force: !!flags.force });
917
+ break;
918
+ case "ssh": {
919
+ const command = _.slice(1).join(" ");
920
+ if (!command) die('Usage: boole ssh "<command>"');
921
+ process.exit(await runRemoteInherit(command));
922
+ }
923
+ case "logs": {
924
+ const proc = _[1];
925
+ const remote = proc ? `pm2 logs '${proc.replace(/'/g, "")}' --lines 50 --nostream` : "pm2 logs --lines 40 --nostream";
926
+ process.exit(await runRemoteInherit(remote));
927
+ }
928
+ case "strategy": {
929
+ const sub = _[1];
930
+ if (sub === "spawn") {
931
+ await strategySpawn({
932
+ template: str(flags.template),
933
+ name: str(flags.name),
934
+ params: list(flags.param),
935
+ autostart: !flags["no-autostart"]
936
+ });
937
+ } else if (sub === "ls" || sub === "list") {
938
+ await strategyList();
939
+ } else if (sub === "doctor") {
940
+ await strategyDoctor(_[2]);
941
+ } else {
942
+ die("Usage: boole strategy <spawn|ls|doctor> \u2026 (see `boole help`)");
943
+ }
944
+ break;
945
+ }
946
+ default:
947
+ err(`Unknown command: ${cmd}`);
948
+ log(HELP);
949
+ process.exit(1);
950
+ }
951
+ }
952
+ main().catch((e) => {
953
+ err(e?.message || String(e));
954
+ process.exit(1);
955
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@boole-digital/cli",
3
+ "version": "0.1.0",
4
+ "description": "Boole — install, sign in, and operate your trading computer from the terminal (Claude Code, Codex, Gemini).",
5
+ "type": "module",
6
+ "bin": { "boole": "dist/index.js" },
7
+ "main": "dist/index.js",
8
+ "files": ["dist/index.js", "README.md", "LICENSE"],
9
+ "license": "SEE LICENSE IN LICENSE",
10
+ "engines": { "node": ">=18" },
11
+ "repository": { "type": "git", "url": "github:Boole-Digital/portara-desktop" },
12
+ "homepage": "https://trade.boole.markets",
13
+ "publishConfig": { "access": "public" },
14
+ "keywords": ["boole", "trading", "cli", "claude-code", "codex", "gemini"],
15
+ "dependencies": {
16
+ "ssh2": "^1.16.0"
17
+ },
18
+ "scripts": {
19
+ "dev": "tsx src/index.ts",
20
+ "typecheck": "tsc --noEmit",
21
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node18 --outfile=dist/index.js --external:ssh2 --banner:js=\"import{createRequire as __cr}from'module';const require=__cr(import.meta.url);\"",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22",
26
+ "@types/ssh2": "^1.15.0",
27
+ "esbuild": "^0.23.0",
28
+ "tsx": "^4.16.0",
29
+ "typescript": "^5.5.0"
30
+ }
31
+ }