@bloomai/cli 0.1.5 → 0.1.7

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/README.md +10 -4
  2. package/SKILL.md +8 -3
  3. package/dist/index.js +1520 -332
  4. package/package.json +9 -5
package/dist/index.js CHANGED
@@ -1,29 +1,263 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync2 } from "fs";
4
+ import { readFileSync as readFileSync5 } from "fs";
5
5
  import path from "path";
6
6
  import { fileURLToPath } from "url";
7
- import { Command } from "commander";
7
+ import { Command as Command3 } from "commander";
8
8
 
9
- // src/auth.ts
10
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
9
+ // src/errors.ts
10
+ var BloomError = class extends Error {
11
+ constructor(msg) {
12
+ super(msg);
13
+ this.name = "BloomError";
14
+ }
15
+ };
16
+ var AuthError = class extends BloomError {
17
+ constructor(msg) {
18
+ super(msg);
19
+ this.name = "AuthError";
20
+ }
21
+ };
22
+ var RateLimitError = class extends BloomError {
23
+ constructor(msg) {
24
+ super(msg);
25
+ this.name = "RateLimitError";
26
+ }
27
+ };
28
+ var TickerNotFoundError = class extends BloomError {
29
+ ticker;
30
+ constructor(ticker, msg) {
31
+ super(msg ?? `Ticker not found: ${ticker}`);
32
+ this.name = "TickerNotFoundError";
33
+ this.ticker = ticker;
34
+ }
35
+ };
36
+ var ApiError = class extends BloomError {
37
+ status;
38
+ constructor(status2, msg) {
39
+ super(msg);
40
+ this.name = "ApiError";
41
+ this.status = status2;
42
+ }
43
+ };
44
+ function errorMessage(body) {
45
+ const text = body.trim();
46
+ try {
47
+ const parsed = JSON.parse(text);
48
+ const msg = typeof parsed === "string" ? parsed : parsed?.error ?? parsed?.detail ?? parsed?.message;
49
+ if (typeof msg === "string") return msg;
50
+ } catch {
51
+ }
52
+ return text.startsWith("<") ? "" : text;
53
+ }
54
+ var TICKER_NOT_FOUND = /invalid symbol|symbol doesn't exist|investment not found/i;
55
+ function httpError(status2, body, ticker) {
56
+ const msg = errorMessage(body);
57
+ if (status2 === 401) {
58
+ return new AuthError("API key was rejected (HTTP 401). It may be invalid or revoked.");
59
+ }
60
+ if (status2 === 403) {
61
+ return new AuthError(msg || "Authentication failed");
62
+ }
63
+ if (status2 === 429) {
64
+ return new RateLimitError(msg || "Rate limited");
65
+ }
66
+ if (ticker && TICKER_NOT_FOUND.test(msg)) {
67
+ return new TickerNotFoundError(ticker, msg);
68
+ }
69
+ return new ApiError(status2, msg ? `${msg} (HTTP ${status2})` : `API error ${status2}`);
70
+ }
71
+ function mcpError(code, message) {
72
+ const msg = message ? String(message) : "";
73
+ const lower = msg.toLowerCase();
74
+ if (code === -32e3 || lower.includes("unauthorized") || lower.includes("auth")) {
75
+ return new AuthError(msg || "Authentication failed");
76
+ }
77
+ if (code === -32001 || lower.includes("rate limit")) {
78
+ return new RateLimitError(msg || "Rate limited");
79
+ }
80
+ if (lower.includes("not found") && lower.includes("ticker")) {
81
+ const match = msg.match(/ticker[:\s]+([A-Z]+)/i);
82
+ return new TickerNotFoundError(match?.[1] ?? "UNKNOWN", msg);
83
+ }
84
+ return new ApiError(code, msg || `MCP error ${code}`);
85
+ }
86
+
87
+ // src/debug.ts
88
+ var _debug = false;
89
+ function setDebug(v) {
90
+ _debug = v;
91
+ }
92
+ function debugLog(...args) {
93
+ if (_debug) {
94
+ console.error("[debug]", ...args);
95
+ }
96
+ }
97
+
98
+ // src/cache.ts
99
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync, statSync } from "fs";
100
+ import { createHash } from "crypto";
11
101
  import { join } from "path";
12
102
  import { homedir } from "os";
13
- var CONFIG_DIR = join(homedir(), ".config", "bloom-cli");
14
- var CONFIG_FILE = join(CONFIG_DIR, "config.json");
103
+ var TTL = {
104
+ quote: 300,
105
+ // 5 min
106
+ news: 14400,
107
+ // 4 hours
108
+ info: 86400,
109
+ // 24 hours
110
+ financials: 604800,
111
+ // 7 days
112
+ earnings: 86400,
113
+ // 24 hours
114
+ technicals: 3600,
115
+ // 1 hour
116
+ sentiment: 1800,
117
+ // 30 min
118
+ default: 3600
119
+ // 1 hour fallback
120
+ };
121
+ var _noCache = false;
122
+ var _cacheDir;
123
+ function setNoCache(v) {
124
+ _noCache = v;
125
+ }
126
+ function setCacheDir(dir) {
127
+ _cacheDir = dir;
128
+ }
129
+ function getCacheDir() {
130
+ return _cacheDir ?? process.env.BLOOM_CACHE_DIR ?? join(homedir(), ".cache", "bloom-cli");
131
+ }
132
+ function ensureCacheDir() {
133
+ const dir = getCacheDir();
134
+ mkdirSync(dir, { recursive: true });
135
+ return dir;
136
+ }
137
+ function cacheKey(toolName, args) {
138
+ const sortedArgs = JSON.stringify(args, Object.keys(args).sort());
139
+ const hash = createHash("md5").update(`${toolName}:${sortedArgs}`).digest("hex");
140
+ return hash;
141
+ }
142
+ function getCached(toolName, args) {
143
+ if (_noCache) return void 0;
144
+ const key = cacheKey(toolName, args);
145
+ const filePath = join(getCacheDir(), `${key}.json`);
146
+ let raw;
147
+ try {
148
+ raw = readFileSync(filePath, "utf-8");
149
+ } catch {
150
+ return void 0;
151
+ }
152
+ try {
153
+ const entry = JSON.parse(raw);
154
+ if (Date.now() > entry.expiresAt) {
155
+ try {
156
+ unlinkSync(filePath);
157
+ } catch {
158
+ }
159
+ debugLog(`cache MISS (expired) ${toolName}`);
160
+ return void 0;
161
+ }
162
+ debugLog(`cache HIT ${toolName} (expires in ${Math.round((entry.expiresAt - Date.now()) / 1e3)}s)`);
163
+ return entry.data;
164
+ } catch {
165
+ try {
166
+ unlinkSync(filePath);
167
+ } catch {
168
+ }
169
+ return void 0;
170
+ }
171
+ }
172
+ function setCached(toolName, args, data, category) {
173
+ if (_noCache) return;
174
+ const ttlSeconds = TTL[category] ?? TTL.default;
175
+ const key = cacheKey(toolName, args);
176
+ const entry = {
177
+ data,
178
+ expiresAt: Date.now() + ttlSeconds * 1e3,
179
+ toolName,
180
+ category,
181
+ createdAt: Date.now()
182
+ };
183
+ try {
184
+ const dir = ensureCacheDir();
185
+ const filePath = join(dir, `${key}.json`);
186
+ writeFileSync(filePath, JSON.stringify(entry), { mode: 384 });
187
+ debugLog(`cache SET ${toolName} (ttl=${ttlSeconds}s, category=${category})`);
188
+ } catch (err) {
189
+ debugLog(`cache WRITE ERROR: ${err}`);
190
+ }
191
+ }
192
+ function clearCache() {
193
+ const dir = getCacheDir();
194
+ let cleared = 0;
195
+ try {
196
+ const files = readdirSync(dir);
197
+ for (const f of files) {
198
+ if (f.endsWith(".json")) {
199
+ try {
200
+ unlinkSync(join(dir, f));
201
+ cleared++;
202
+ } catch {
203
+ }
204
+ }
205
+ }
206
+ } catch {
207
+ }
208
+ return { cleared };
209
+ }
210
+ function cacheStats() {
211
+ const dir = getCacheDir();
212
+ const result = {
213
+ items: 0,
214
+ totalBytes: 0,
215
+ expired: 0,
216
+ byCategory: {},
217
+ cacheDir: dir
218
+ };
219
+ try {
220
+ const files = readdirSync(dir);
221
+ for (const f of files) {
222
+ if (!f.endsWith(".json")) continue;
223
+ const filePath = join(dir, f);
224
+ try {
225
+ const st = statSync(filePath);
226
+ result.totalBytes += st.size;
227
+ const raw = readFileSync(filePath, "utf-8");
228
+ const entry = JSON.parse(raw);
229
+ result.items++;
230
+ if (Date.now() > entry.expiresAt) {
231
+ result.expired++;
232
+ }
233
+ const cat = entry.category || "unknown";
234
+ result.byCategory[cat] = (result.byCategory[cat] ?? 0) + 1;
235
+ } catch {
236
+ }
237
+ }
238
+ } catch {
239
+ }
240
+ return result;
241
+ }
242
+
243
+ // src/auth.ts
244
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
245
+ import { join as join2 } from "path";
246
+ import { homedir as homedir2 } from "os";
247
+ var CONFIG_DIR = join2(homedir2(), ".config", "bloom-cli");
248
+ var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
15
249
  function readConfig() {
16
- if (!existsSync(CONFIG_FILE)) return {};
250
+ if (!existsSync2(CONFIG_FILE)) return {};
17
251
  try {
18
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
252
+ return JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
19
253
  } catch {
20
254
  return {};
21
255
  }
22
256
  }
23
- function writeConfig(config) {
257
+ function writeConfig(config2) {
24
258
  try {
25
- mkdirSync(CONFIG_DIR, { recursive: true });
26
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
259
+ mkdirSync2(CONFIG_DIR, { recursive: true });
260
+ writeFileSync2(CONFIG_FILE, JSON.stringify(config2, null, 2) + "\n", {
27
261
  mode: 384
28
262
  });
29
263
  } catch (err) {
@@ -41,36 +275,49 @@ function getApiKey() {
41
275
  function requireApiKey() {
42
276
  const key = getApiKey();
43
277
  if (!key) {
44
- console.error(
45
- "Error: No API key found. Set BLOOM_API_KEY or run: bloom auth login"
46
- );
47
- process.exit(1);
278
+ throw new AuthError("No API key found. Set BLOOM_API_KEY or run: bloom auth login");
48
279
  }
49
280
  return key;
50
281
  }
51
- function login(apiKey) {
52
- const config = readConfig();
53
- config.api_key = apiKey;
54
- writeConfig(config);
282
+ async function login(apiKey) {
283
+ const check = await verifyApiKey(apiKey);
284
+ if (check === "invalid") {
285
+ console.error(`Error: API key ${maskKey(apiKey)} was rejected by the server. Nothing was saved.`);
286
+ process.exit(1);
287
+ }
288
+ const config2 = readConfig();
289
+ config2.api_key = apiKey;
290
+ writeConfig(config2);
55
291
  console.log("API key saved to " + CONFIG_FILE);
292
+ if (check === "unverified") {
293
+ console.error("Warning: could not reach the server to verify the key.");
294
+ }
56
295
  }
57
296
  function logout() {
58
- if (existsSync(CONFIG_FILE)) {
59
- const config = readConfig();
60
- delete config.api_key;
61
- writeConfig(config);
297
+ if (existsSync2(CONFIG_FILE)) {
298
+ const config2 = readConfig();
299
+ delete config2.api_key;
300
+ writeConfig(config2);
62
301
  }
63
302
  console.log("Logged out. API key cleared.");
64
303
  }
65
- function status() {
304
+ async function status() {
66
305
  const envKey = process.env.BLOOM_API_KEY;
67
306
  const storedKey = readConfig().api_key;
68
- if (envKey) {
69
- console.log(`Authenticated via BLOOM_API_KEY (${maskKey(envKey)})`);
70
- } else if (storedKey) {
71
- console.log(`Authenticated via ${CONFIG_FILE} (${maskKey(storedKey)})`);
72
- } else {
307
+ const key = envKey || storedKey;
308
+ if (!key) {
73
309
  console.log("Not authenticated. Set BLOOM_API_KEY or run: bloom auth login");
310
+ process.exit(1);
311
+ }
312
+ const source = envKey ? "BLOOM_API_KEY" : CONFIG_FILE;
313
+ const check = await verifyApiKey(key);
314
+ if (check === "valid") {
315
+ console.log(`Authenticated via ${source} (${maskKey(key)})`);
316
+ } else if (check === "invalid") {
317
+ console.log(`Invalid key via ${source} (${maskKey(key)}): rejected by the server`);
318
+ process.exit(1);
319
+ } else {
320
+ console.log(`Key set via ${source} (${maskKey(key)}), not verified: server unreachable`);
74
321
  }
75
322
  }
76
323
  function maskKey(key) {
@@ -78,44 +325,32 @@ function maskKey(key) {
78
325
  return key.slice(0, 4) + "..." + key.slice(-4);
79
326
  }
80
327
 
81
- // src/commands/auth.ts
82
- import { createInterface } from "readline";
83
- function registerAuthCommand(program2) {
84
- const auth = program2.command("auth").description("Manage API authentication");
85
- auth.command("login").description("Store API key").argument("[key]", "API key (or pass via stdin)").action(async (key) => {
86
- if (!key) {
87
- const rl = createInterface({
88
- input: process.stdin,
89
- output: process.stderr
90
- });
91
- key = await new Promise((resolve) => {
92
- rl.question("API key: ", (answer) => {
93
- rl.close();
94
- resolve(answer.trim());
95
- });
96
- });
97
- }
98
- if (!key) {
99
- console.error("Error: No API key provided");
100
- process.exit(1);
101
- }
102
- login(key);
103
- });
104
- auth.command("status").description("Show current auth state").action(() => {
105
- status();
106
- });
107
- auth.command("logout").description("Clear stored credentials").action(() => {
108
- logout();
109
- });
110
- }
111
-
112
328
  // src/api.ts
113
- var API_BASE = "https://api.getbloom.app";
114
- var MCP_URL = `${API_BASE}/mcp/`;
329
+ var _apiBase = "https://api.getbloom.app";
330
+ function setApiBase(base) {
331
+ _apiBase = base;
332
+ }
115
333
  var requestId = 0;
334
+ async function verifyApiKey(apiKey) {
335
+ const url = `${_apiBase}/mcp/test-auth/`;
336
+ debugLog(`\u2192 GET ${url}`);
337
+ try {
338
+ const res = await fetch(url, {
339
+ headers: { Authorization: `Bearer ${apiKey}` },
340
+ signal: AbortSignal.timeout(1e4)
341
+ });
342
+ debugLog(`\u2190 ${res.status}`);
343
+ if (res.ok) return "valid";
344
+ if (res.status === 401 || res.status === 403) return "invalid";
345
+ } catch (err) {
346
+ debugLog(` key check failed: ${err instanceof Error ? err.message : String(err)}`);
347
+ }
348
+ return "unverified";
349
+ }
116
350
  async function callTool(toolName, args) {
117
351
  const apiKey = requireApiKey();
118
352
  requestId++;
353
+ const mcpUrl = `${_apiBase}/mcp/`;
119
354
  const body = {
120
355
  jsonrpc: "2.0",
121
356
  method: "tools/call",
@@ -125,7 +360,10 @@ async function callTool(toolName, args) {
125
360
  },
126
361
  id: requestId
127
362
  };
128
- const res = await fetch(MCP_URL, {
363
+ debugLog(`\u2192 POST ${mcpUrl}`);
364
+ debugLog(` tool: ${toolName}`, JSON.stringify(args));
365
+ const start = performance.now();
366
+ const res = await fetch(mcpUrl, {
129
367
  method: "POST",
130
368
  headers: {
131
369
  Authorization: `Bearer ${apiKey}`,
@@ -133,13 +371,15 @@ async function callTool(toolName, args) {
133
371
  },
134
372
  body: JSON.stringify(body)
135
373
  });
374
+ const elapsed = Math.round(performance.now() - start);
375
+ debugLog(`\u2190 ${res.status} (${elapsed}ms)`);
136
376
  if (!res.ok) {
137
377
  const text2 = await res.text().catch(() => "");
138
- throw new Error(`API error ${res.status}: ${text2 || res.statusText}`);
378
+ throw httpError(res.status, text2 || res.statusText);
139
379
  }
140
380
  const data = await res.json();
141
381
  if (data.error) {
142
- throw new Error(`MCP error ${data.error.code}: ${data.error.message}`);
382
+ throw mcpError(data.error.code, data.error.message);
143
383
  }
144
384
  if (!data.result?.content?.length) {
145
385
  throw new Error("Empty response from API");
@@ -153,12 +393,14 @@ async function callTool(toolName, args) {
153
393
  }
154
394
  async function callRest(path2, params) {
155
395
  const apiKey = requireApiKey();
156
- const url = new URL(path2, API_BASE);
396
+ const url = new URL(path2, _apiBase);
157
397
  if (params) {
158
398
  for (const [k, v] of Object.entries(params)) {
159
399
  url.searchParams.set(k, v);
160
400
  }
161
401
  }
402
+ debugLog(`\u2192 GET ${url.toString()}`);
403
+ const start = performance.now();
162
404
  const res = await fetch(url.toString(), {
163
405
  method: "GET",
164
406
  headers: {
@@ -166,17 +408,74 @@ async function callRest(path2, params) {
166
408
  "Content-Type": "application/json"
167
409
  }
168
410
  });
411
+ const elapsed = Math.round(performance.now() - start);
412
+ debugLog(`\u2190 ${res.status} (${elapsed}ms)`);
169
413
  if (!res.ok) {
170
414
  const text = await res.text().catch(() => "");
171
- throw new Error(`API error ${res.status}: ${text || res.statusText}`);
415
+ const ticker = path2.match(/^\/?investments\/([^/]+)\//)?.[1];
416
+ throw httpError(res.status, text || res.statusText, ticker && decodeURIComponent(ticker));
172
417
  }
173
418
  return res.json();
174
419
  }
420
+ async function callToolCached(toolName, args, category = "default") {
421
+ const cacheToolName = `${_apiBase}:${category}:${toolName}`;
422
+ const cached = getCached(cacheToolName, args);
423
+ if (cached !== void 0) {
424
+ return cached;
425
+ }
426
+ debugLog(`cache MISS ${toolName}`);
427
+ const result = await callTool(toolName, args);
428
+ setCached(cacheToolName, args, result, category);
429
+ return result;
430
+ }
431
+ async function callRestCached(path2, params, category = "default") {
432
+ const cacheToolName = `${_apiBase}:${category}:REST:${path2}`;
433
+ const cacheArgs = params ?? {};
434
+ const cached = getCached(cacheToolName, cacheArgs);
435
+ if (cached !== void 0) {
436
+ return cached;
437
+ }
438
+ debugLog(`cache MISS ${cacheToolName}`);
439
+ const result = await callRest(path2, params);
440
+ if (result?.results?.length !== 0) {
441
+ setCached(cacheToolName, cacheArgs, result, category);
442
+ }
443
+ return result;
444
+ }
445
+
446
+ // src/config.ts
447
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
448
+ import { join as join3 } from "path";
449
+ import { homedir as homedir3 } from "os";
450
+ var CONFIG_DIR2 = join3(homedir3(), ".config", "bloom-cli");
451
+ var CONFIG_FILE2 = join3(CONFIG_DIR2, "config.json");
452
+ function readConfigFile() {
453
+ if (!existsSync3(CONFIG_FILE2)) return {};
454
+ try {
455
+ return JSON.parse(readFileSync3(CONFIG_FILE2, "utf-8"));
456
+ } catch {
457
+ return {};
458
+ }
459
+ }
460
+ function loadConfig(overrides) {
461
+ const file = readConfigFile();
462
+ return {
463
+ apiKey: process.env.BLOOM_API_KEY ?? file.api_key ?? overrides?.apiKey,
464
+ apiBase: process.env.BLOOM_API_BASE ?? file.api_base ?? overrides?.apiBase ?? "https://api.getbloom.app",
465
+ cacheDir: process.env.BLOOM_CACHE_DIR ?? file.cache_dir ?? overrides?.cacheDir ?? join3(homedir3(), ".cache", "bloom-cli"),
466
+ noCache: overrides?.noCache ?? false,
467
+ debug: overrides?.debug ?? false
468
+ };
469
+ }
470
+
471
+ // src/global-opts.ts
472
+ import { Option } from "commander";
175
473
 
176
474
  // src/format.ts
177
475
  import Table from "cli-table3";
178
- import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
476
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
179
477
  import { dirname } from "path";
478
+ var FORMATS = ["json", "csv", "table", "markdown"];
180
479
  function resolveFormat(opts) {
181
480
  if (opts.format) return opts.format;
182
481
  if (opts.output || !process.stdout.isTTY) return "json";
@@ -187,15 +486,19 @@ function outputResult(data, opts, tableFormatter) {
187
486
  emit(JSON.stringify(data, null, 2), opts);
188
487
  return;
189
488
  }
190
- const fmt2 = resolveFormat(opts);
191
- if (fmt2 === "json") {
489
+ const fmt3 = resolveFormat(opts);
490
+ if (fmt3 === "json") {
192
491
  emit(JSON.stringify(data, null, 2), opts);
193
492
  return;
194
493
  }
195
- if (fmt2 === "csv") {
494
+ if (fmt3 === "csv") {
196
495
  emit(toCSV(data), opts);
197
496
  return;
198
497
  }
498
+ if (fmt3 === "markdown") {
499
+ emit(toMarkdown(data), opts);
500
+ return;
501
+ }
199
502
  if (tableFormatter) {
200
503
  emit(tableFormatter(data, opts), opts);
201
504
  } else {
@@ -204,8 +507,8 @@ function outputResult(data, opts, tableFormatter) {
204
507
  }
205
508
  function emit(content, opts) {
206
509
  if (opts.output) {
207
- mkdirSync2(dirname(opts.output), { recursive: true });
208
- writeFileSync2(opts.output, content + "\n");
510
+ mkdirSync4(dirname(opts.output), { recursive: true });
511
+ writeFileSync4(opts.output, content + "\n");
209
512
  if (!opts.quiet) {
210
513
  console.error(`Written to ${opts.output}`);
211
514
  }
@@ -228,6 +531,23 @@ function toCSV(data) {
228
531
  }
229
532
  return lines.join("\n");
230
533
  }
534
+ function toMarkdown(data, _title) {
535
+ const rows = toRows(data);
536
+ if (rows.length === 0) return "(no data)";
537
+ const headers = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
538
+ const lines = [];
539
+ lines.push("| " + headers.join(" | ") + " |");
540
+ lines.push("| " + headers.map(() => "---").join(" | ") + " |");
541
+ for (const row of rows) {
542
+ const vals = headers.map((h) => {
543
+ const v = row[h];
544
+ const s = String(v ?? "");
545
+ return s.replace(/\|/g, "\\|").replace(/\n/g, " ");
546
+ });
547
+ lines.push("| " + vals.join(" | ") + " |");
548
+ }
549
+ return lines.join("\n");
550
+ }
231
551
  function toRows(data) {
232
552
  const flatten = (v) => flattenObj(v);
233
553
  if (Array.isArray(data)) return data.map(flatten);
@@ -270,8 +590,11 @@ function flattenObj(obj, prefix = "") {
270
590
  return result;
271
591
  }
272
592
  function autoTable(data, opts) {
273
- const rows = toRows(data);
593
+ let rows = toRows(data);
274
594
  if (rows.length === 0) return "(no data)";
595
+ if (rows.length === 1 && Object.keys(rows[0]).length > 6) {
596
+ rows = Object.entries(rows[0]).map(([key, value]) => ({ key, value }));
597
+ }
275
598
  const headers = Object.keys(rows[0]);
276
599
  const table = new Table({
277
600
  head: opts?.quiet ? [] : headers,
@@ -297,16 +620,282 @@ function makeTable(headers, rows, opts) {
297
620
  function truncate(s, max) {
298
621
  return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
299
622
  }
300
- function parseIntSafe(value, fallback) {
301
- const n = parseInt(value, 10);
302
- return Number.isNaN(n) ? fallback : n;
623
+ function parseIntArg(value, name, min = 1, max = Number.MAX_SAFE_INTEGER) {
624
+ const n = /^-?\d+$/.test(value.trim()) ? Number(value) : NaN;
625
+ if (Number.isNaN(n) || n < min || n > max) {
626
+ const range = max === Number.MAX_SAFE_INTEGER ? `>= ${min}` : `${min}-${max}`;
627
+ throw new BloomError(`${name} must be a whole number ${range}, got "${value}"`);
628
+ }
629
+ return n;
303
630
  }
304
631
 
305
632
  // src/global-opts.ts
306
633
  function addGlobalOptions(cmd) {
307
- return cmd.option("-o, --output <file>", "Write output to file").option("-f, --format <fmt>", "Output format: json, csv, table").option("-q, --quiet", "Suppress headers, data only").option("--raw", "Raw JSON from API");
634
+ return cmd.option("-o, --output <file>", "Write output to file").addOption(
635
+ new Option("-f, --format <fmt>", "Output format").choices(FORMATS)
636
+ ).option("-q, --quiet", "Suppress headers, data only").option("--raw", "Raw JSON from API").option("--no-cache", "Bypass disk cache").option("--debug", "Show debug info on stderr");
637
+ }
638
+
639
+ // src/commands/auth.ts
640
+ import { createInterface } from "readline";
641
+ function registerAuthCommand(program2) {
642
+ const auth = program2.command("auth").description("Manage API authentication");
643
+ auth.command("login").description("Store API key").argument("[key]", "API key (or pass via stdin)").action(async (key) => {
644
+ if (!key) {
645
+ key = (await readKey()).trim();
646
+ }
647
+ if (!key) {
648
+ console.error("Error: No API key provided");
649
+ process.exit(1);
650
+ }
651
+ await login(key);
652
+ });
653
+ auth.command("status").description("Show current auth state").action(status);
654
+ auth.command("logout").description("Clear stored credentials").action(() => {
655
+ logout();
656
+ });
657
+ }
658
+ async function readKey() {
659
+ const rl = createInterface({ input: process.stdin, output: process.stderr, prompt: "API key: " });
660
+ if (process.stdin.isTTY) rl.prompt();
661
+ for await (const line of rl) {
662
+ rl.close();
663
+ return line;
664
+ }
665
+ return "";
308
666
  }
309
667
 
668
+ // src/client.ts
669
+ var _instance;
670
+ var BloomClient = class _BloomClient {
671
+ constructor() {
672
+ }
673
+ // --- Singleton ---
674
+ static instance() {
675
+ if (!_instance) {
676
+ _instance = new _BloomClient();
677
+ }
678
+ return _instance;
679
+ }
680
+ static resetInstance() {
681
+ _instance = void 0;
682
+ }
683
+ // --- Info ---
684
+ // cacheCategory lets the `quote` alias use the 5-minute quote TTL instead of
685
+ // the 24-hour info TTL, so an advertised "current" price is never a day old.
686
+ async getInfo(symbols, fields, cacheCategory = "info") {
687
+ const args = { symbols };
688
+ if (fields) args.fields = fields;
689
+ return callToolCached("get_symbols_metadata", args, cacheCategory);
690
+ }
691
+ // --- News ---
692
+ async getNews(symbols, limit = 15) {
693
+ return callToolCached("get_news_for_symbols", { symbols, limit }, "news");
694
+ }
695
+ // --- Price ---
696
+ async getPrice(symbol, timeframes, includeOhlc = false) {
697
+ return callToolCached("get_price_charts", {
698
+ symbol,
699
+ timeframes,
700
+ include_ohlc: includeOhlc
701
+ }, "quote");
702
+ }
703
+ // --- Financials ---
704
+ async getFinancials(symbol, chartType = "revenue") {
705
+ return callToolCached("get_financial_charts", { symbol, chart_type: chartType }, "financials");
706
+ }
707
+ // --- Earnings ---
708
+ async getEarnings(symbols) {
709
+ return callToolCached("analyze_earnings", { symbols }, "earnings");
710
+ }
711
+ // --- Earnings Calendar ---
712
+ async getEarningsCalendar(params) {
713
+ return callRestCached("/markets/earnings-calendar/", params, "earnings");
714
+ }
715
+ // --- Sentiment ---
716
+ async getSentiment(sections) {
717
+ const args = {};
718
+ if (sections) args.sections = sections;
719
+ return callToolCached("get_market_sentiment", args, "sentiment");
720
+ }
721
+ // --- Market ---
722
+ async getMarket(mapType, period, limit, universe) {
723
+ return callToolCached("get_market_map", {
724
+ map_type: mapType,
725
+ period,
726
+ limit,
727
+ universe
728
+ }, "default");
729
+ }
730
+ // --- Collections ---
731
+ async getCollections(section, limit) {
732
+ return callToolCached("get_collections", { section, limit }, "default");
733
+ }
734
+ // --- Screen ---
735
+ async screen(args) {
736
+ return callToolCached("filter_stocks", args, "default");
737
+ }
738
+ // --- Technicals ---
739
+ async getTechnicals(symbols, sections) {
740
+ const args = { symbols };
741
+ if (sections) args.sections = sections;
742
+ return callToolCached("analyze_technicals", args, "technicals");
743
+ }
744
+ // --- Options ---
745
+ async getOptions(symbol, lookbackDays, weeklySampling, includeGreeks) {
746
+ return callToolCached("analyze_options_history", {
747
+ symbol,
748
+ lookback_days: lookbackDays,
749
+ weekly_sampling: weeklySampling,
750
+ include_greeks_analysis: includeGreeks
751
+ }, "technicals");
752
+ }
753
+ // --- Portfolio ---
754
+ async analyzePortfolio(holdings, strategy) {
755
+ const args = { holdings };
756
+ if (strategy) args.strategy = strategy;
757
+ return callToolCached("analyze_portfolio", args, "default");
758
+ }
759
+ // --- Trades ---
760
+ async evaluateTrades(trades) {
761
+ return callToolCached("analyze_trades", { trades }, "default");
762
+ }
763
+ // --- Catalysts ---
764
+ async getCatalysts(symbols) {
765
+ return callToolCached("generate_catalysts", { symbols }, "default");
766
+ }
767
+ // --- Position Size ---
768
+ async positionSize(args) {
769
+ return callToolCached("calculate_position_size", args, "default");
770
+ }
771
+ // --- Transcript ---
772
+ async searchTranscript(args) {
773
+ return callToolCached("search_transcript", args, "default");
774
+ }
775
+ // --- AI Trades ---
776
+ async getAiTrades(args) {
777
+ return callToolCached("get_ai_trade_ideas", args, "default");
778
+ }
779
+ // --- AI Portfolio ---
780
+ async getAiPortfolio(args) {
781
+ return callToolCached("get_ai_trade_ideas", args, "default");
782
+ }
783
+ // --- Moving Averages ---
784
+ // Folded into get_technicals as the "moving_averages" part. Unwrap the slice
785
+ // back into the {symbol, current_price, indicators} shape callers expect.
786
+ async getMovingAverages(symbol, indicators) {
787
+ const raw = await callToolCached(
788
+ "get_technicals",
789
+ { symbol, parts: ["moving_averages"], ma_indicators: indicators },
790
+ "technicals"
791
+ );
792
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
793
+ const ma = parsed?.moving_averages;
794
+ if (ma && typeof ma === "object") {
795
+ return { symbol: parsed.symbol, ...ma };
796
+ }
797
+ return parsed;
798
+ }
799
+ // --- Backtest ---
800
+ async backtest(params) {
801
+ return callRestCached("backtest/", params, "default");
802
+ }
803
+ // --- Correlation ---
804
+ async correlation(params) {
805
+ return callRestCached("correlation/", params, "default");
806
+ }
807
+ // --- Risk ---
808
+ async getRisk(symbol) {
809
+ return callRestCached(`investments/${encodeURIComponent(symbol)}/risk_factors/`, void 0, "default");
810
+ }
811
+ // --- Drawdown ---
812
+ async getDrawdown(params) {
813
+ return callRestCached("portfolios/drawdown/", params, "default");
814
+ }
815
+ // --- Macro ---
816
+ async getMacro(args) {
817
+ return callToolCached("get_macro_indicators", args, "default");
818
+ }
819
+ // --- Sectors ---
820
+ async getSectors() {
821
+ return callToolCached("get_sector_performance", {}, "default");
822
+ }
823
+ // --- DCF ---
824
+ async getDcf(symbol) {
825
+ return callToolCached("get_dcf_valuation", { symbol }, "default");
826
+ }
827
+ // --- Holders ---
828
+ async getHolders(symbol) {
829
+ return callToolCached("get_holders", { symbol }, "default");
830
+ }
831
+ // --- Short Interest ---
832
+ async getShortInterest(symbol, limit) {
833
+ return callToolCached("get_short_interest", { symbol, limit }, "default");
834
+ }
835
+ // --- Insider ---
836
+ async getInsider(symbol, limit) {
837
+ return callToolCached("get_insider_trading", { symbol, limit }, "default");
838
+ }
839
+ // --- Dividends ---
840
+ async getDividends(args) {
841
+ return callToolCached("get_dividends", args, "default");
842
+ }
843
+ // --- Splits ---
844
+ async getSplits(args) {
845
+ return callToolCached("get_splits", args, "default");
846
+ }
847
+ // --- IPO ---
848
+ async getIpo(args) {
849
+ return callToolCached("get_ipo_calendar", args, "default");
850
+ }
851
+ // --- Compare ---
852
+ async compare(symbols) {
853
+ return callToolCached("get_symbols_metadata", { symbols }, "info");
854
+ }
855
+ // --- Ratings ---
856
+ async getRatings(symbol, limit) {
857
+ return callToolCached("get_analyst_ratings", { symbol, limit }, "default");
858
+ }
859
+ // --- Stats ---
860
+ async getStats(symbol) {
861
+ return callRestCached(`investments/${encodeURIComponent(symbol)}/stats/`, void 0, "quote");
862
+ }
863
+ // --- Peers ---
864
+ async getPeers(symbol) {
865
+ return callRestCached(`investments/${encodeURIComponent(symbol)}/peers/`, void 0, "quote");
866
+ }
867
+ // --- Bottom Line ---
868
+ async getBottomLine(symbol) {
869
+ return callRestCached(`investments/${encodeURIComponent(symbol)}/bottom_line_v2/`, void 0, "info");
870
+ }
871
+ // --- Insights ---
872
+ async getInsights(symbol, symbols) {
873
+ if (symbol) {
874
+ return callRestCached(`investments/${encodeURIComponent(symbol)}/insights/`, void 0, "default");
875
+ }
876
+ const params = {};
877
+ if (symbols) params.symbols = symbols.join(",");
878
+ return callRestCached("investments/all_insights/", params, "default");
879
+ }
880
+ // --- Arena Activity ---
881
+ async getArenaActivity(model, limit) {
882
+ const params = {};
883
+ if (model) params.model = model;
884
+ if (limit) params.limit = String(limit);
885
+ return callRestCached("api/arena/activity/", params, "default");
886
+ }
887
+ // --- Portfolio News ---
888
+ async getPortfolioNews(symbols) {
889
+ const params = {};
890
+ if (symbols) params.symbols = symbols.join(",");
891
+ return callRestCached("smart-summaries/portfolio-news/", params, "news");
892
+ }
893
+ // --- Collection Returns ---
894
+ async getCollectionReturns(collectionIds, timerange) {
895
+ return callRestCached("collections/returns/", { collection_ids: collectionIds, timerange }, "default");
896
+ }
897
+ };
898
+
310
899
  // src/commands/info.ts
311
900
  var VALID_FIELDS = [
312
901
  "basic_metadata",
@@ -318,15 +907,12 @@ var VALID_FIELDS = [
318
907
  ];
319
908
  function registerInfoCommand(program2) {
320
909
  addGlobalOptions(
321
- program2.command("info").description("Company fundamentals, ratings, peers, holdings").argument("<symbols...>", "Ticker symbols (e.g. AAPL MSFT)").option("--fields <fields>", "Comma-separated: " + VALID_FIELDS.join(","))
910
+ program2.command("info").alias("quote").description("Company fundamentals, ratings, peers, holdings").argument("<symbols...>", "Ticker symbols (e.g. AAPL MSFT)").option("--fields <fields>", "Comma-separated: " + VALID_FIELDS.join(","))
322
911
  ).action(async (symbols, opts) => {
323
- const args = {
324
- symbols: symbols.map((s) => s.toUpperCase())
325
- };
326
- if (opts.fields) {
327
- args.fields = opts.fields.split(",").map((f) => f.trim());
328
- }
329
- const data = await callTool("get_symbols_metadata", args);
912
+ const syms = symbols.map((s) => s.toUpperCase());
913
+ const fields = opts.fields?.split(",").map((f) => f.trim());
914
+ const cacheCategory = program2.args[0] === "quote" ? "quote" : "info";
915
+ const data = await BloomClient.instance().getInfo(syms, fields, cacheCategory);
330
916
  outputResult(data, opts, (d) => formatInfo(d));
331
917
  });
332
918
  }
@@ -376,11 +962,16 @@ function formatSection(obj, indent = "") {
376
962
  function registerNewsCommand(program2) {
377
963
  addGlobalOptions(
378
964
  program2.command("news").description("Recent news from Benzinga channels").argument("<symbols...>", "Ticker symbols").option("--limit <n>", "Max articles (default: 15)", "15")
379
- ).action(async (symbols, opts) => {
380
- const data = await callTool("get_news_for_symbols", {
381
- symbols: symbols.map((s) => s.toUpperCase()),
382
- limit: parseIntSafe(opts.limit, 15)
383
- });
965
+ ).addHelpText("after", `
966
+ Examples:
967
+ $ bloom news AAPL Latest Apple news
968
+ $ bloom news AAPL MSFT GOOGL News for multiple stocks
969
+ $ bloom news TSLA --limit 5 Last 5 Tesla articles
970
+ `).action(async (symbols, opts) => {
971
+ const data = await BloomClient.instance().getNews(
972
+ symbols.map((s) => s.toUpperCase()),
973
+ parseIntArg(opts.limit, "--limit")
974
+ );
384
975
  outputResult(data, opts, (d, o) => formatNews(d, o));
385
976
  });
386
977
  }
@@ -412,12 +1003,18 @@ function formatNews(data, opts) {
412
1003
  function registerPriceCommand(program2) {
413
1004
  addGlobalOptions(
414
1005
  program2.command("price").description("Historical price data").argument("<symbol>", "Ticker symbol").option("--timeframes <tf>", "Comma-separated: 1d,1w,1m,3m,6m,ytd,1y,2y,5y", "1d,1m,1y").option("--ohlc", "Include full OHLC data")
415
- ).action(async (symbol, opts) => {
416
- const data = await callTool("get_price_charts", {
417
- symbol: symbol.toUpperCase(),
418
- timeframes: opts.timeframes.split(",").map((t) => t.trim()),
419
- include_ohlc: !!opts.ohlc
420
- });
1006
+ ).addHelpText("after", `
1007
+ Examples:
1008
+ $ bloom price AAPL Default timeframes (1d, 1m, 1y)
1009
+ $ bloom price TSLA --timeframes 1d,5y Daily and 5-year price history
1010
+ $ bloom price MSFT --ohlc Include open/high/low/close
1011
+ $ bloom price AAPL -f csv -o prices.csv Export to CSV file
1012
+ `).action(async (symbol, opts) => {
1013
+ const data = await BloomClient.instance().getPrice(
1014
+ symbol.toUpperCase(),
1015
+ opts.timeframes.split(",").map((t) => t.trim()),
1016
+ !!opts.ohlc
1017
+ );
421
1018
  outputResult(data, opts, (d, o) => formatPrices(d, o));
422
1019
  });
423
1020
  }
@@ -467,7 +1064,8 @@ function formatPrices(data, opts) {
467
1064
  }
468
1065
 
469
1066
  // src/commands/financials.ts
470
- var CHART_TYPES = [
1067
+ import { Option as Option2 } from "commander";
1068
+ var VALID_TYPES = [
471
1069
  "revenue",
472
1070
  "revenue_growth",
473
1071
  "operating_margin",
@@ -480,12 +1078,18 @@ var CHART_TYPES = [
480
1078
  ];
481
1079
  function registerFinancialsCommand(program2) {
482
1080
  addGlobalOptions(
483
- program2.command("financials").description("Quarterly/annual financial data with YoY growth").argument("<symbol>", "Ticker symbol").option("--type <type>", "Chart type: " + CHART_TYPES.join(", "), "revenue")
484
- ).action(async (symbol, opts) => {
485
- const data = await callTool("get_financial_charts", {
486
- symbol: symbol.toUpperCase(),
487
- chart_type: opts.type
488
- });
1081
+ program2.command("financials").description("Financial charts: revenue, margins, earnings, FCF").argument("<symbol>", "Ticker symbol").addOption(
1082
+ new Option2("--type <type>", "Chart type").choices(VALID_TYPES).default("revenue")
1083
+ )
1084
+ ).addHelpText("after", `
1085
+ Examples:
1086
+ $ bloom financials AAPL Revenue (default)
1087
+ $ bloom financials TSLA --type operating_margin Operating margins
1088
+ $ bloom financials MSFT --type free_cash_flow Free cash flow
1089
+ $ bloom financials NVDA --type revenue_growth Revenue growth rate
1090
+ `).action(async (symbol, opts) => {
1091
+ const sym = symbol.toUpperCase();
1092
+ const data = await BloomClient.instance().getFinancials(sym, opts.type);
489
1093
  outputResult(data, opts);
490
1094
  });
491
1095
  }
@@ -500,12 +1104,13 @@ var SECTIONS = [
500
1104
  function registerSentimentCommand(program2) {
501
1105
  addGlobalOptions(
502
1106
  program2.command("sentiment").description("Market mood: AAII, Fear&Greed, VIX, momentum").option("--sections <s>", "Comma-separated: " + SECTIONS.join(","))
503
- ).action(async (opts) => {
504
- const args = {};
505
- if (opts.sections) {
506
- args.sections = opts.sections.split(",").map((s) => s.trim());
507
- }
508
- const data = await callTool("get_market_sentiment", args);
1107
+ ).addHelpText("after", `
1108
+ Examples:
1109
+ $ bloom sentiment All sentiment indicators
1110
+ $ bloom sentiment --sections aaii_sentiment,cnn_fear_greed Just AAII and Fear & Greed
1111
+ `).action(async (opts) => {
1112
+ const sections = opts.sections?.split(",").map((s) => s.trim());
1113
+ const data = await BloomClient.instance().getSentiment(sections);
509
1114
  outputResult(data, opts, (d) => formatSentiment(d));
510
1115
  });
511
1116
  }
@@ -536,13 +1141,20 @@ function formatSentiment(data) {
536
1141
  function registerMarketCommand(program2) {
537
1142
  addGlobalOptions(
538
1143
  program2.command("market").description("Market performance, sectors, global").option("--type <type>", "top_movers|sector_breakdown|global_markets|major_indexes", "top_movers").option("--period <p>", "1d|1w|1m|3m|6m|ytd|1y|5y", "1d").option("--limit <n>", "Max stocks for top_movers", "24").option("--universe <u>", "all|sp500|international|crypto|community", "all")
539
- ).action(async (opts) => {
540
- const data = await callTool("get_market_map", {
541
- map_type: opts.type,
542
- period: opts.period,
543
- limit: parseIntSafe(opts.limit, 24),
544
- universe: opts.universe
545
- });
1144
+ ).addHelpText("after", `
1145
+ Examples:
1146
+ $ bloom market Today's top movers
1147
+ $ bloom market --type sector_breakdown Sector performance
1148
+ $ bloom market --type global_markets Global market overview
1149
+ $ bloom market --type top_movers --period 1w Weekly top movers
1150
+ $ bloom market --universe sp500 --limit 10 Top 10 S&P 500 movers
1151
+ `).action(async (opts) => {
1152
+ const data = await BloomClient.instance().getMarket(
1153
+ opts.type,
1154
+ opts.period,
1155
+ parseIntArg(opts.limit, "--limit"),
1156
+ opts.universe
1157
+ );
546
1158
  outputResult(data, opts);
547
1159
  });
548
1160
  }
@@ -551,11 +1163,17 @@ function registerMarketCommand(program2) {
551
1163
  function registerCollectionsCommand(program2) {
552
1164
  addGlobalOptions(
553
1165
  program2.command("collections").description("Curated stock collections by theme").option("--section <s>", "GET_STARTED|VALUE|TRENDS|BEAT_UP|FAMOUS_INVESTORS|HIGH_FLYERS|SECTORS|GEO|LEADERBOARDS", "GET_STARTED").option("--limit <n>", "Max collections", "6")
554
- ).action(async (opts) => {
555
- const data = await callTool("get_collections", {
556
- section: opts.section,
557
- limit: parseIntSafe(opts.limit, 6)
558
- });
1166
+ ).addHelpText("after", `
1167
+ Examples:
1168
+ $ bloom collections Default collections
1169
+ $ bloom collections --section VALUE Value investing picks
1170
+ $ bloom collections --section FAMOUS_INVESTORS Famous investor portfolios
1171
+ $ bloom collections --section TRENDS --limit 3 Top 3 trending collections
1172
+ `).action(async (opts) => {
1173
+ const data = await BloomClient.instance().getCollections(
1174
+ opts.section,
1175
+ parseIntArg(opts.limit, "--limit")
1176
+ );
559
1177
  outputResult(data, opts);
560
1178
  });
561
1179
  }
@@ -569,7 +1187,7 @@ function registerScreenCommand(program2) {
569
1187
  const args = {
570
1188
  filter_expressions: filters,
571
1189
  and_query: !opts.or,
572
- limit: parseIntSafe(opts.limit, 50)
1190
+ limit: parseIntArg(opts.limit, "--limit")
573
1191
  };
574
1192
  if (opts.symbols) {
575
1193
  args.symbols = opts.symbols.split(",").map((s) => s.trim().toUpperCase());
@@ -577,7 +1195,7 @@ function registerScreenCommand(program2) {
577
1195
  if (opts.fields) {
578
1196
  args.fields = opts.fields.split(",").map((f) => f.trim());
579
1197
  }
580
- const data = await callTool("filter_stocks", args);
1198
+ const data = await BloomClient.instance().screen(args);
581
1199
  outputResult(data, opts, (d, o) => formatScreen(d, o));
582
1200
  }
583
1201
  );
@@ -614,32 +1232,54 @@ function formatScreen(data, opts) {
614
1232
  return makeTable(headers, rows, opts);
615
1233
  }
616
1234
 
1235
+ // src/limits.ts
1236
+ var MAX_ANALYSIS_SYMBOLS = 10;
1237
+
617
1238
  // src/commands/earnings.ts
618
1239
  function registerEarningsCommand(program2) {
619
1240
  addGlobalOptions(
620
- program2.command("earnings").description("Earnings history, estimates, surprises").argument("<symbols...>", "Ticker symbols (up to 5)")
621
- ).action(async (symbols, opts) => {
622
- const data = await callTool("analyze_earnings", {
623
- symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5)
624
- });
1241
+ program2.command("earnings").description("Earnings history, estimates, surprises").argument("<symbols...>", "Ticker symbols (up to 10)")
1242
+ ).addHelpText("after", `
1243
+ Examples:
1244
+ $ bloom earnings AAPL Apple earnings history and surprises
1245
+ $ bloom earnings AAPL MSFT GOOGL Compare earnings for multiple stocks
1246
+ `).action(async (symbols, opts) => {
1247
+ const syms = symbols.map((s) => s.toUpperCase()).slice(0, MAX_ANALYSIS_SYMBOLS);
1248
+ const data = await BloomClient.instance().getEarnings(syms);
625
1249
  outputResult(data, opts);
626
1250
  });
627
1251
  }
628
1252
 
629
1253
  // src/commands/earnings-calendar.ts
1254
+ function formatEps(value, currency) {
1255
+ if (value == null) return "\u2014";
1256
+ return !currency || currency === "USD" ? `$${value}` : `${value} ${currency}`;
1257
+ }
630
1258
  function registerEarningsCalendarCommand(program2) {
631
1259
  addGlobalOptions(
632
1260
  program2.command("earnings-calendar").description(
633
- "Upcoming earnings dates for symbols (default 28 days)"
634
- ).argument("<symbols...>", "Ticker symbols").option("--days <n>", "Look-ahead window in days (1-90)", "28")
635
- ).action(
1261
+ "Upcoming earnings dates (all companies, or filter by symbols)"
1262
+ ).argument("[symbols...]", "Ticker symbols (omit to show all)").option("--days <n>", "Look-ahead window in days (1-90)", "28").option(
1263
+ "--popular",
1264
+ "Also include top companies by market cap, not just the given symbols"
1265
+ )
1266
+ ).addHelpText("after", `
1267
+ Examples:
1268
+ $ bloom earnings-calendar AAPL MSFT GOOGL Upcoming earnings for these stocks
1269
+ $ bloom earnings-calendar TSLA --days 7 Earnings within the next week
1270
+ $ bloom earnings-calendar NVDA --days 90 Earnings within 90 days
1271
+ $ bloom earnings-calendar AAPL --popular AAPL plus the popular calendar
1272
+ `).action(
636
1273
  async (symbols, opts) => {
637
- const days = parseIntSafe(opts.days, 28);
638
- const syms = symbols.map((s) => s.toUpperCase()).join(",");
639
- const data = await callRest("/markets/earnings-calendar/", {
640
- symbols: syms,
641
- days: String(days)
642
- });
1274
+ const days = parseIntArg(opts.days, "--days", 1, 90);
1275
+ const params = { days: String(days) };
1276
+ if (symbols.length > 0) {
1277
+ params.symbols = symbols.map((s) => s.toUpperCase()).join(",");
1278
+ if (!opts.popular) params.include_popular = "false";
1279
+ }
1280
+ const data = await BloomClient.instance().getEarningsCalendar(
1281
+ params
1282
+ );
643
1283
  const items = data.results ?? [];
644
1284
  outputResult(items, opts, (d, o) => {
645
1285
  const rows = d;
@@ -650,8 +1290,8 @@ function registerEarningsCalendarCommand(program2) {
650
1290
  r.earnings_date ?? "\u2014",
651
1291
  r.symbol,
652
1292
  r.name,
653
- r.eps_estimate != null ? `$${r.eps_estimate}` : "\u2014",
654
- r.eps_actual != null ? `$${r.eps_actual}` : "\u2014"
1293
+ formatEps(r.eps_estimate, r.currency),
1294
+ formatEps(r.eps_actual, r.currency)
655
1295
  ]),
656
1296
  o
657
1297
  );
@@ -664,15 +1304,17 @@ function registerEarningsCalendarCommand(program2) {
664
1304
  var SECTIONS2 = ["technical_indicators", "options_analysis", "insider_activity"];
665
1305
  function registerTechnicalsCommand(program2) {
666
1306
  addGlobalOptions(
667
- program2.command("technicals").description("RSI, MAs, patterns, options flow, insider trades").argument("<symbols...>", "Ticker symbols (up to 5)").option("--sections <s>", "Comma-separated: " + SECTIONS2.join(","))
668
- ).action(async (symbols, opts) => {
669
- const args = {
670
- symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5)
671
- };
672
- if (opts.sections) {
673
- args.sections = opts.sections.split(",").map((s) => s.trim());
674
- }
675
- const data = await callTool("analyze_technicals", args);
1307
+ program2.command("technicals").description("RSI, MAs, patterns, options flow, insider trades").argument("<symbols...>", "Ticker symbols (up to 10)").option("--sections <s>", "Comma-separated: " + SECTIONS2.join(","))
1308
+ ).addHelpText("after", `
1309
+ Examples:
1310
+ $ bloom technicals AAPL All technical data
1311
+ $ bloom technicals TSLA --sections technical_indicators RSI, MAs, patterns only
1312
+ $ bloom technicals NVDA --sections options_analysis Options flow only
1313
+ $ bloom technicals AAPL --sections insider_activity Insider trades only
1314
+ `).action(async (symbols, opts) => {
1315
+ const syms = symbols.map((s) => s.toUpperCase()).slice(0, MAX_ANALYSIS_SYMBOLS);
1316
+ const sections = opts.sections?.split(",").map((s) => s.trim());
1317
+ const data = await BloomClient.instance().getTechnicals(syms, sections);
676
1318
  outputResult(data, opts);
677
1319
  });
678
1320
  }
@@ -681,14 +1323,20 @@ function registerTechnicalsCommand(program2) {
681
1323
  function registerOptionsCommand(program2) {
682
1324
  addGlobalOptions(
683
1325
  program2.command("options-history").description("Options sentiment evolution over time").argument("<symbol>", "Ticker symbol").option("--days <n>", "Lookback days (7-90)", "30").option("--weekly", "Use weekly sampling (default)").option("--daily", "Use daily data").option("--greeks", "Include advanced Greeks analysis")
684
- ).action(
1326
+ ).addHelpText("after", `
1327
+ Examples:
1328
+ $ bloom options-history AAPL 30-day options sentiment (default)
1329
+ $ bloom options-history TSLA --days 90 90-day lookback
1330
+ $ bloom options-history NVDA --daily Daily granularity
1331
+ $ bloom options-history AAPL --greeks Include advanced Greeks analysis
1332
+ `).action(
685
1333
  async (symbol, opts) => {
686
- const data = await callTool("analyze_options_history", {
687
- symbol: symbol.toUpperCase(),
688
- lookback_days: parseIntSafe(opts.days, 30),
689
- weekly_sampling: opts.weekly ?? !opts.daily,
690
- include_greeks_analysis: !!opts.greeks
691
- });
1334
+ const data = await BloomClient.instance().getOptions(
1335
+ symbol.toUpperCase(),
1336
+ parseIntArg(opts.days, "--days", 7, 90),
1337
+ opts.weekly ?? !opts.daily,
1338
+ !!opts.greeks
1339
+ );
692
1340
  outputResult(data, opts);
693
1341
  }
694
1342
  );
@@ -703,12 +1351,14 @@ function registerPortfolioCommand(program2) {
703
1351
  try {
704
1352
  holdings = JSON.parse(holdingsStr);
705
1353
  } catch {
706
- console.error(`Error: Invalid JSON for holdings. Use format: '{"AAPL":0.3,"MSFT":0.7}'`);
707
- process.exit(1);
708
1354
  }
709
- const args = { holdings };
710
- if (opts.strategy) args.strategy = opts.strategy;
711
- const data = await callTool("analyze_portfolio", args);
1355
+ const valid = holdings !== null && typeof holdings === "object" && !Array.isArray(holdings) && Object.keys(holdings).length > 0 && Object.values(holdings).every((w) => typeof w === "number" && Number.isFinite(w));
1356
+ if (!valid) {
1357
+ throw new BloomError(
1358
+ `holdings must be a JSON object of symbol to numeric weight, e.g. '{"AAPL":0.3,"MSFT":0.7}'`
1359
+ );
1360
+ }
1361
+ const data = await BloomClient.instance().analyzePortfolio(holdings, opts.strategy);
712
1362
  outputResult(data, opts);
713
1363
  });
714
1364
  }
@@ -717,9 +1367,13 @@ function registerPortfolioCommand(program2) {
717
1367
  function registerTradesCommand(program2) {
718
1368
  addGlobalOptions(
719
1369
  program2.command("trades").description("AI trade evaluation").argument("<trades...>", 'Trade descriptions: "Buy AAPL 30% because..." "Sell MSFT 20% because..."')
720
- ).action(async (tradeStrs, opts) => {
1370
+ ).addHelpText("after", `
1371
+ Examples:
1372
+ $ bloom trades "Buy AAPL 30% because of strong iPhone sales"
1373
+ $ bloom trades "Sell MSFT 20% to take profits" "Buy GOOGL 15% on AI momentum"
1374
+ `).action(async (tradeStrs, opts) => {
721
1375
  const trades = tradeStrs.map(parseTrade);
722
- const data = await callTool("analyze_trades", { trades });
1376
+ const data = await BloomClient.instance().evaluateTrades(trades);
723
1377
  outputResult(data, opts);
724
1378
  });
725
1379
  }
@@ -749,11 +1403,15 @@ Format: "Buy AAPL 30% because thesis" or JSON object`
749
1403
  // src/commands/catalysts.ts
750
1404
  function registerCatalystsCommand(program2) {
751
1405
  addGlobalOptions(
752
- program2.command("catalysts").description("Forward-looking value catalysts").argument("<symbols...>", "Ticker symbols (up to 5)")
753
- ).action(async (symbols, opts) => {
754
- const data = await callTool("generate_catalysts", {
755
- symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5)
756
- });
1406
+ program2.command("catalysts").description("Forward-looking value catalysts").argument("<symbols...>", "Ticker symbols (up to 10)")
1407
+ ).addHelpText("after", `
1408
+ Examples:
1409
+ $ bloom catalysts AAPL Upcoming catalysts for Apple
1410
+ $ bloom catalysts TSLA NVDA AMZN Catalysts for multiple stocks
1411
+ `).action(async (symbols, opts) => {
1412
+ const data = await BloomClient.instance().getCatalysts(
1413
+ symbols.map((s) => s.toUpperCase()).slice(0, MAX_ANALYSIS_SYMBOLS)
1414
+ );
757
1415
  outputResult(data, opts);
758
1416
  });
759
1417
  }
@@ -782,11 +1440,11 @@ function registerPositionCommand(program2) {
782
1440
  const args = {
783
1441
  bull_return: bullReturn,
784
1442
  bear_return: bearReturn,
785
- conviction: parseIntSafe(opts.conviction, 5),
1443
+ conviction: parseIntArg(opts.conviction, "--conviction", 1, 10),
786
1444
  portfolio_value: portfolioValue
787
1445
  };
788
1446
  if (opts.symbol) args.symbol = opts.symbol.toUpperCase();
789
- const data = await callTool("calculate_position_size", args);
1447
+ const data = await BloomClient.instance().positionSize(args);
790
1448
  outputResult(data, opts);
791
1449
  }
792
1450
  );
@@ -795,16 +1453,16 @@ function registerPositionCommand(program2) {
795
1453
  // src/commands/transcript.ts
796
1454
  function registerTranscriptCommand(program2) {
797
1455
  addGlobalOptions(
798
- program2.command("transcript").description("Search earnings call transcripts").argument("<symbol>", "Ticker symbol").argument("<keywords...>", "Keywords to search (OR logic)").option("--quarter <q>", 'Fiscal quarter, e.g. "Q1 2024"').option("--max <n>", "Max results", "10")
1456
+ program2.command("transcript").alias("transcripts").description("Search earnings call transcripts").argument("<symbol>", "Ticker symbol").argument("<keywords...>", "Keywords to search (OR logic)").option("--quarter <q>", 'Fiscal quarter, e.g. "Q1 2024"').option("--max <n>", "Max results", "10")
799
1457
  ).action(
800
1458
  async (symbol, keywords, opts) => {
801
1459
  const args = {
802
1460
  symbol: symbol.toUpperCase(),
803
1461
  keywords,
804
- max_results: parseIntSafe(opts.max, 10)
1462
+ max_results: parseIntArg(opts.max, "--max")
805
1463
  };
806
1464
  if (opts.quarter) args.fiscal_quarter = opts.quarter;
807
- const data = await callTool("search_transcript", args);
1465
+ const data = await BloomClient.instance().searchTranscript(args);
808
1466
  outputResult(data, opts, (d, o) => formatTranscript(d, o));
809
1467
  }
810
1468
  );
@@ -847,11 +1505,11 @@ function registerAiTradesCommand(program2) {
847
1505
  program2.command("ai-trades").description("Recent trade decisions from AI models competing with real money (InvestingArena)").option("--model <m>", "Filter by AI model: claude|openai|gemini").option("--symbol <s>", "Filter by stock ticker symbol (e.g., AAPL)").option("--limit <n>", "Max recent trades per model (default: 10, max: 50)", "10")
848
1506
  ).action(async (opts) => {
849
1507
  const args = {
850
- limit: parseIntSafe(opts.limit, 10)
1508
+ limit: parseIntArg(opts.limit, "--limit", 1, 50)
851
1509
  };
852
1510
  if (opts.model) args.ai_model = opts.model;
853
1511
  if (opts.symbol) args.symbol = opts.symbol.toUpperCase();
854
- const data = await callTool("get_ai_trade_ideas", args);
1512
+ const data = await BloomClient.instance().getAiTrades(args);
855
1513
  if (opts.raw) {
856
1514
  outputResult(data, opts);
857
1515
  return;
@@ -879,7 +1537,7 @@ function formatTrades(data, opts) {
879
1537
  String(t.shares ?? "-"),
880
1538
  t.fill_price != null ? `$${Number(t.fill_price).toFixed(2)}` : "-",
881
1539
  gain,
882
- truncate2(String(t.rationale ?? "-"), 60)
1540
+ truncate(String(t.rationale ?? "-"), 60)
883
1541
  ];
884
1542
  });
885
1543
  return makeTable(
@@ -888,18 +1546,20 @@ function formatTrades(data, opts) {
888
1546
  opts
889
1547
  );
890
1548
  }
891
- function truncate2(s, max) {
892
- return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
893
- }
894
1549
 
895
1550
  // src/commands/ai-portfolio.ts
896
1551
  function registerAiPortfolioCommand(program2) {
897
1552
  addGlobalOptions(
898
1553
  program2.command("ai-portfolio").description("Current portfolio positions held by AI models (InvestingArena live accounts)").option("--model <m>", "Filter by AI model: claude|openai|gemini")
899
- ).action(async (opts) => {
1554
+ ).addHelpText("after", `
1555
+ Examples:
1556
+ $ bloom ai-portfolio All AI model portfolios
1557
+ $ bloom ai-portfolio --model claude Claude's current positions
1558
+ $ bloom ai-portfolio --model openai OpenAI's current positions
1559
+ `).action(async (opts) => {
900
1560
  const args = {};
901
1561
  if (opts.model) args.ai_model = opts.model;
902
- const data = await callTool("get_ai_trade_ideas", args);
1562
+ const data = await BloomClient.instance().getAiPortfolio(args);
903
1563
  if (opts.raw) {
904
1564
  outputResult(data, opts);
905
1565
  return;
@@ -1006,6 +1666,7 @@ function registerMaCommand(program2) {
1006
1666
  program2.command("ma").description("Moving averages (SMA/EMA) for a symbol").argument("<symbol>", "Ticker symbol (e.g., AAPL)").option("--sma <periods>", "Comma-separated SMA periods (e.g., 50,100,200)").option("--ema <periods>", "Comma-separated EMA periods (e.g., 20,50)").option("--limit <n>", "Number of data points to display (default: 20)", "20")
1007
1667
  ).action(
1008
1668
  async (symbol, opts) => {
1669
+ const limit = parseIntArg(opts.limit, "--limit");
1009
1670
  const indicators = [];
1010
1671
  if (opts.sma) {
1011
1672
  for (const p of opts.sma.split(",")) {
@@ -1022,12 +1683,10 @@ function registerMaCommand(program2) {
1022
1683
  if (indicators.length === 0) {
1023
1684
  indicators.push("sma_100", "ema_100");
1024
1685
  }
1025
- const args = {
1026
- symbol: symbol.toUpperCase(),
1686
+ const data = await BloomClient.instance().getMovingAverages(
1687
+ symbol.toUpperCase(),
1027
1688
  indicators
1028
- };
1029
- const data = await callTool("get_moving_averages", args);
1030
- const limit = parseInt(opts.limit, 10) || 20;
1689
+ );
1031
1690
  let parsed = null;
1032
1691
  try {
1033
1692
  parsed = typeof data === "string" ? JSON.parse(data) : data;
@@ -1071,7 +1730,7 @@ function registerBacktestCommand(program2) {
1071
1730
  addGlobalOptions(
1072
1731
  program2.command("backtest").description("Backtest a portfolio against a benchmark").requiredOption("--symbols <s>", "Comma-separated symbols (e.g., AAPL,MSFT)").requiredOption("--allocations <a>", "Comma-separated weights (e.g., 0.5,0.5)").option("--benchmark <b>", "Benchmark symbol (default: SPY)", "SPY").option("--strategy <s>", "Strategy: buy_and_hold", "buy_and_hold")
1073
1732
  ).action(async (opts) => {
1074
- const data = await callRest("backtest/", {
1733
+ const data = await BloomClient.instance().backtest({
1075
1734
  symbols: opts.symbols,
1076
1735
  allocations: opts.allocations,
1077
1736
  benchmark: opts.benchmark,
@@ -1098,7 +1757,9 @@ function formatBacktest(data) {
1098
1757
  ]);
1099
1758
  }
1100
1759
  if (rows.length === 0) return JSON.stringify(data, null, 2);
1101
- return makeTable(["Interval", "Return", "Benchmark", "Alpha", "Sharpe", "Max DD"], rows);
1760
+ const table = makeTable(["Interval", "Return", "Benchmark", "Alpha", "Sharpe", "Max DD"], rows);
1761
+ const warnings = Array.isArray(obj.warnings) ? obj.warnings : [];
1762
+ return [table, ...warnings.map((w) => `Warning: ${w}`)].join("\n");
1102
1763
  }
1103
1764
  function fmt(v) {
1104
1765
  if (v == null || v === "") return "-";
@@ -1117,13 +1778,18 @@ function fmtNum(v) {
1117
1778
  function registerCorrelationCommand(program2) {
1118
1779
  addGlobalOptions(
1119
1780
  program2.command("correlation").description("Correlation matrix for a set of symbols").argument("<symbols...>", "Ticker symbols (2+)").option("--start <date>", "Start date (YYYY-MM-DD)").option("--end <date>", "End date (YYYY-MM-DD)")
1120
- ).action(async (symbols, opts) => {
1781
+ ).addHelpText("after", `
1782
+ Examples:
1783
+ $ bloom correlation AAPL MSFT GOOGL Correlation matrix
1784
+ $ bloom correlation AAPL TSLA --start 2025-01-01 Since Jan 2025
1785
+ $ bloom correlation SPY QQQ IWM --start 2024-01-01 --end 2024-12-31 Full year
1786
+ `).action(async (symbols, opts) => {
1121
1787
  const params = {
1122
1788
  symbols: symbols.map((s) => s.toUpperCase()).join(",")
1123
1789
  };
1124
1790
  if (opts.start) params.start_date = opts.start;
1125
1791
  if (opts.end) params.end_date = opts.end;
1126
- const data = await callRest("correlation/", params);
1792
+ const data = await BloomClient.instance().correlation(params);
1127
1793
  outputResult(data, opts, formatCorrelation);
1128
1794
  });
1129
1795
  }
@@ -1140,25 +1806,56 @@ function formatCorrelation(data) {
1140
1806
  }
1141
1807
 
1142
1808
  // src/commands/risk.ts
1809
+ var STALE_DAYS = 456;
1143
1810
  function registerRiskCommand(program2) {
1144
1811
  addGlobalOptions(
1145
- program2.command("risk").description("Risk factors for a stock").argument("<symbol>", "Ticker symbol")
1146
- ).action(async (symbol, opts) => {
1147
- const data = await callRest(`investments/${symbol.toUpperCase()}/risk_factors/`);
1812
+ program2.command("risk").description("Risk factors from the company's latest ingested SEC 10-K (check filingDate)").argument("<symbol>", "Ticker symbol")
1813
+ ).addHelpText("after", `
1814
+ Each row carries filingDate/periodEnd for its 10-K (null on older rows; use
1815
+ created). A warning goes to stderr when the source is over ~15 months old.
1816
+
1817
+ Examples:
1818
+ $ bloom risk AAPL Risk factors for Apple
1819
+ $ bloom risk TSLA Risk factors for Tesla
1820
+ `).action(async (symbol, opts) => {
1821
+ const data = await BloomClient.instance().getRisk(symbol.toUpperCase());
1822
+ const warning = staleWarning(riskRows(data));
1823
+ if (warning && !opts.quiet) console.error(warning);
1148
1824
  outputResult(data, opts, formatRisk);
1149
1825
  });
1150
1826
  }
1151
- function formatRisk(data) {
1152
- if (!Array.isArray(data)) return JSON.stringify(data, null, 2);
1153
- if (data.length === 0) return "(no risk factors found)";
1154
- const rows = data.map((r) => [
1155
- String(r.category ?? r.risk_type ?? "-"),
1156
- truncate3(String(r.description ?? r.text ?? "-"), 80)
1157
- ]);
1158
- return makeTable(["Category", "Description"], rows);
1827
+ function riskRows(data) {
1828
+ return data?.results ?? [];
1159
1829
  }
1160
- function truncate3(s, max) {
1161
- return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1830
+ function sourceInfo(rows) {
1831
+ let best = null;
1832
+ for (const r of rows) {
1833
+ const raw = r.filingDate ?? r.created;
1834
+ if (!raw) continue;
1835
+ const date = String(raw).slice(0, 10);
1836
+ if (!best || date > best.date) best = { date, how: r.filingDate ? "filed" : "ingested" };
1837
+ }
1838
+ return best;
1839
+ }
1840
+ function staleWarning(rows) {
1841
+ const src = sourceInfo(rows);
1842
+ if (!src) return null;
1843
+ const ageDays = (Date.now() - Date.parse(src.date)) / 864e5;
1844
+ if (ageDays <= STALE_DAYS) return null;
1845
+ return `Warning: these risk factors come from a 10-K ${src.how} ${src.date} (${Math.floor(ageDays / 30)} months ago) and may be out of date.`;
1846
+ }
1847
+ function formatRisk(data) {
1848
+ const rows = riskRows(data);
1849
+ if (rows.length === 0) return "(no risk factors found)";
1850
+ const src = sourceInfo(rows);
1851
+ const period = rows.find((r) => r.periodEnd)?.periodEnd;
1852
+ const header = `Source: SEC 10-K${src ? ` ${src.how} ${src.date}` : ""}${period ? ` (fiscal year ended ${period})` : ""}`;
1853
+ const table = makeTable(
1854
+ ["Risk"],
1855
+ rows.map((r) => [truncate(String(r.keyPoint ?? r.details ?? "-"), 100)])
1856
+ );
1857
+ return `${header}
1858
+ ${table}`;
1162
1859
  }
1163
1860
 
1164
1861
  // src/commands/drawdown.ts
@@ -1166,64 +1863,41 @@ function registerDrawdownCommand(program2) {
1166
1863
  addGlobalOptions(
1167
1864
  program2.command("drawdown").description("Portfolio drawdown analysis").requiredOption("--symbols <s>", "Comma-separated symbols (e.g., AAPL,MSFT)").requiredOption("--allocations <a>", "Comma-separated weights (e.g., 0.5,0.5)")
1168
1865
  ).action(async (opts) => {
1169
- const data = await callRest("portfolios/drawdown/", {
1866
+ const data = await BloomClient.instance().getDrawdown({
1170
1867
  symbols: opts.symbols,
1171
1868
  allocations: opts.allocations
1172
1869
  });
1173
1870
  outputResult(data, opts, formatDrawdown);
1174
1871
  });
1175
1872
  }
1176
- function formatDrawdown(data) {
1177
- const obj = data;
1178
- if (!obj || typeof obj !== "object") return JSON.stringify(data, null, 2);
1179
- const intervals = obj.interval_results ?? obj;
1180
- const rows = [];
1181
- for (const [interval, dd] of Object.entries(intervals)) {
1182
- if (typeof dd !== "object" || !dd) continue;
1183
- const d = dd;
1184
- let troughDate = "-";
1185
- let recoveryDate = "not recovered";
1186
- const plotData = d.plot_data;
1187
- if (plotData && plotData.length > 0) {
1188
- let minDD = 0;
1189
- let minIdx = -1;
1190
- for (let i = 0; i < plotData.length; i++) {
1191
- if (plotData[i].drawdown < minDD) {
1192
- minDD = plotData[i].drawdown;
1193
- minIdx = i;
1194
- }
1195
- }
1196
- if (minIdx >= 0) {
1197
- troughDate = plotData[minIdx].date;
1198
- for (let i = minIdx + 1; i < plotData.length; i++) {
1199
- if (plotData[i].drawdown >= 0) {
1200
- recoveryDate = plotData[i].date;
1201
- break;
1202
- }
1203
- }
1204
- }
1205
- }
1206
- rows.push([
1207
- interval,
1208
- d.max_drawdown != null ? `${(Number(d.max_drawdown) * 100).toFixed(2)}%` : "-",
1209
- troughDate,
1210
- recoveryDate
1211
- ]);
1212
- }
1213
- if (rows.length === 0) return JSON.stringify(data, null, 2);
1214
- return makeTable(["Interval", "Max Drawdown", "Trough Date", "Recovery"], rows);
1873
+ function formatDrawdown(data, opts) {
1874
+ const intervals = data?.interval_results;
1875
+ if (!intervals) return JSON.stringify(data, null, 2);
1876
+ const rows = Object.entries(intervals).map(([iv, r]) => [iv, fmt2(r.max_drawdown ?? r.error)]);
1877
+ return makeTable(["Interval", "Max Drawdown"], rows, opts);
1878
+ }
1879
+ function fmt2(v) {
1880
+ if (v == null) return "-";
1881
+ const n = Number(v);
1882
+ if (isNaN(n)) return String(v);
1883
+ return `${(n * 100).toFixed(2)}%`;
1215
1884
  }
1216
1885
 
1217
1886
  // src/commands/macro.ts
1218
1887
  function registerMacroCommand(program2) {
1219
1888
  addGlobalOptions(
1220
1889
  program2.command("macro").description("Macroeconomic indicators (GDP, CPI, unemployment, fed funds, treasury, inflation)").option("--indicators <list>", "Comma-separated: GDP,CPI,UNEMPLOYMENT,FED_FUNDS,TREASURY_10Y,INFLATION")
1221
- ).action(async (opts) => {
1890
+ ).addHelpText("after", `
1891
+ Examples:
1892
+ $ bloom macro All macro indicators
1893
+ $ bloom macro --indicators GDP,CPI,UNEMPLOYMENT Specific indicators only
1894
+ $ bloom macro --indicators FED_FUNDS,TREASURY_10Y Interest rates
1895
+ `).action(async (opts) => {
1222
1896
  const args = {};
1223
1897
  if (opts.indicators) {
1224
1898
  args.indicators = opts.indicators;
1225
1899
  }
1226
- const data = await callTool("get_macro_indicators", args);
1900
+ const data = await BloomClient.instance().getMacro(args);
1227
1901
  outputResult(data, opts, formatMacro);
1228
1902
  });
1229
1903
  }
@@ -1255,8 +1929,12 @@ function formatMacro(data) {
1255
1929
  function registerSectorsCommand(program2) {
1256
1930
  addGlobalOptions(
1257
1931
  program2.command("sectors").description("Sector performance across time periods")
1258
- ).action(async (opts) => {
1259
- const data = await callTool("get_sector_performance", {});
1932
+ ).addHelpText("after", `
1933
+ Examples:
1934
+ $ bloom sectors Sector performance across all time periods
1935
+ $ bloom sectors -f json Output as JSON
1936
+ `).action(async (opts) => {
1937
+ const data = await BloomClient.instance().getSectors();
1260
1938
  outputResult(data, opts, formatSectors);
1261
1939
  });
1262
1940
  }
@@ -1309,9 +1987,13 @@ function formatSectors(data) {
1309
1987
  function registerDcfCommand(program2) {
1310
1988
  addGlobalOptions(
1311
1989
  program2.command("dcf").description("Discounted cash flow valuation").argument("<symbol>", "Ticker symbol")
1312
- ).action(async (symbol, opts) => {
1990
+ ).addHelpText("after", `
1991
+ Examples:
1992
+ $ bloom dcf AAPL Discounted cash flow valuation for Apple
1993
+ $ bloom dcf TSLA DCF valuation for Tesla
1994
+ `).action(async (symbol, opts) => {
1313
1995
  const sym = symbol.toUpperCase();
1314
- const result = await callTool("get_dcf_valuation", { symbol: sym });
1996
+ const result = await BloomClient.instance().getDcf(sym);
1315
1997
  outputResult(result, opts, () => formatDcf(result, sym));
1316
1998
  });
1317
1999
  }
@@ -1335,34 +2017,27 @@ function formatDcf(data, symbol) {
1335
2017
  // src/commands/holders.ts
1336
2018
  function registerHoldersCommand(program2) {
1337
2019
  addGlobalOptions(
1338
- program2.command("holders").description("Institutional and mutual fund holders").argument("<symbol>", "Ticker symbol").option("--type <t>", "institutional or mutual-fund (default: both)").option("--limit <n>", "Max holders to show", "15")
1339
- ).action(async (symbol, opts) => {
2020
+ program2.command("holders").description("Institutional holders of a stock").argument("<symbol>", "Ticker symbol")
2021
+ ).addHelpText("after", `
2022
+ Examples:
2023
+ $ bloom holders AAPL Top institutional holders of Apple
2024
+ $ bloom holders TSLA Tesla's institutional ownership
2025
+ `).action(async (symbol, opts) => {
1340
2026
  const sym = symbol.toUpperCase();
1341
- const limit = parseIntSafe(opts.limit, 15);
1342
- const args = { symbol: sym };
1343
- if (opts.type) args.holder_type = opts.type;
1344
- const data = await callTool("get_holders", args);
1345
- outputResult(data, opts, () => formatHolders(data, limit));
2027
+ const data = await BloomClient.instance().getHolders(sym);
2028
+ outputResult(data, opts, formatHolders);
1346
2029
  });
1347
2030
  }
1348
- function formatHolders(data, limit) {
1349
- const parts = [];
1350
- const addSection = (title, list) => {
1351
- const items = list;
1352
- if (items && items.length > 0) {
1353
- const rows = items.slice(0, limit).map((h) => [
1354
- String(h.holder ?? "-"),
1355
- fmtNum2(h.shares),
1356
- h.dateReported ? String(h.dateReported).slice(0, 10) : "-",
1357
- fmtNum2(h.change)
1358
- ]);
1359
- parts.push(`${title}
1360
- ${makeTable(["Holder", "Shares", "Date", "Change"], rows)}`);
1361
- }
1362
- };
1363
- addSection("INSTITUTIONAL HOLDERS", data.institutional);
1364
- addSection("MUTUAL FUND HOLDERS", data.mutual_fund);
1365
- return parts.length > 0 ? parts.join("\n\n") : "(no holder data found)";
2031
+ function formatHolders(data) {
2032
+ if (!Array.isArray(data) || data.length === 0) return "(no holders data)";
2033
+ const rows = data.map((h) => [
2034
+ String(h.holder ?? "-"),
2035
+ fmtNum2(h.shares),
2036
+ h.dateReported ? String(h.dateReported).slice(0, 10) : "-",
2037
+ h.change != null ? String(h.change) : "-",
2038
+ h.weightPercent != null ? `${Number(h.weightPercent).toFixed(2)}%` : "-"
2039
+ ]);
2040
+ return makeTable(["Holder", "Shares", "Date", "Change", "Weight %"], rows);
1366
2041
  }
1367
2042
  function fmtNum2(v) {
1368
2043
  if (v == null) return "-";
@@ -1378,10 +2053,14 @@ function fmtNum2(v) {
1378
2053
  function registerShortInterestCommand(program2) {
1379
2054
  addGlobalOptions(
1380
2055
  program2.command("short-interest").description("Short interest history").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Number of records", "10")
1381
- ).action(async (symbol, opts) => {
2056
+ ).addHelpText("after", `
2057
+ Examples:
2058
+ $ bloom short-interest GME Short interest history for GameStop
2059
+ $ bloom short-interest AMC --limit 20 Last 20 short interest records
2060
+ `).action(async (symbol, opts) => {
1382
2061
  const sym = symbol.toUpperCase();
1383
- const limit = parseIntSafe(opts.limit, 10);
1384
- const data = await callTool("get_short_interest", { symbol: sym, limit });
2062
+ const limit = parseIntArg(opts.limit, "--limit");
2063
+ const data = await BloomClient.instance().getShortInterest(sym, limit);
1385
2064
  outputResult(data, opts, formatShort);
1386
2065
  });
1387
2066
  }
@@ -1410,29 +2089,28 @@ function fmtNum3(v) {
1410
2089
  // src/commands/insider.ts
1411
2090
  function registerInsiderCommand(program2) {
1412
2091
  addGlobalOptions(
1413
- program2.command("insider").description("Insider trading activity (SEC Form 4)").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Max transactions to show", "20")
1414
- ).action(async (symbol, opts) => {
2092
+ program2.command("insider").description("Insider trading activity").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Max records", "20")
2093
+ ).addHelpText("after", `
2094
+ Examples:
2095
+ $ bloom insider AAPL Recent insider trades for Apple
2096
+ $ bloom insider TSLA --limit 10 Last 10 insider transactions
2097
+ `).action(async (symbol, opts) => {
1415
2098
  const sym = symbol.toUpperCase();
1416
- const limit = parseIntSafe(opts.limit, 20);
1417
- const data = await callTool("get_insider_trading", { symbol: sym, limit });
2099
+ const limit = parseIntArg(opts.limit, "--limit");
2100
+ const data = await BloomClient.instance().getInsider(sym, limit);
1418
2101
  outputResult(data, opts, formatInsider);
1419
2102
  });
1420
2103
  }
1421
2104
  function formatInsider(data) {
1422
- if (!Array.isArray(data) || data.length === 0) return "(no insider trading data)";
1423
- const rows = data.map((t) => {
1424
- const val = t.securitiesTransacted && t.price ? `$${(Number(t.securitiesTransacted) * Number(t.price)).toLocaleString()}` : "-";
1425
- return [
1426
- String(t.filingDate ?? t.transactionDate ?? "-").slice(0, 10),
1427
- String(t.reportingName ?? "-"),
1428
- String(t.typeOfOwner ?? "-"),
1429
- String(t.transactionType ?? t.acquistionOrDisposition ?? "-"),
1430
- fmtNum4(t.securitiesTransacted),
1431
- t.price != null ? `$${Number(t.price).toFixed(2)}` : "-",
1432
- val
1433
- ];
1434
- });
1435
- return makeTable(["Date", "Name", "Title", "Type", "Shares", "Price", "Value"], rows);
2105
+ if (!Array.isArray(data) || data.length === 0) return "(no insider data)";
2106
+ const rows = data.map((r) => [
2107
+ String(r.filingDate ?? r.date ?? "-").slice(0, 10),
2108
+ String(r.reportingName ?? r.name ?? "-"),
2109
+ String(r.transactionType ?? r.type ?? "-"),
2110
+ r.securitiesTransacted != null ? fmtNum4(r.securitiesTransacted) : "-",
2111
+ r.price != null ? `$${Number(r.price).toFixed(2)}` : "-"
2112
+ ]);
2113
+ return makeTable(["Date", "Name", "Type", "Shares", "Price"], rows);
1436
2114
  }
1437
2115
  function fmtNum4(v) {
1438
2116
  if (v == null) return "-";
@@ -1445,12 +2123,17 @@ function fmtNum4(v) {
1445
2123
  function registerDividendsCommand(program2) {
1446
2124
  addGlobalOptions(
1447
2125
  program2.command("dividends").description("Dividend history for a stock").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Number of records", "20").option("--from <date>", "Start date (YYYY-MM-DD)")
1448
- ).action(async (symbol, opts) => {
2126
+ ).addHelpText("after", `
2127
+ Examples:
2128
+ $ bloom dividends KO Dividend history for Coca-Cola
2129
+ $ bloom dividends AAPL --limit 8 Last 8 dividends
2130
+ $ bloom dividends JNJ --from 2024-01-01 Dividends since Jan 2024
2131
+ `).action(async (symbol, opts) => {
1449
2132
  const sym = symbol.toUpperCase();
1450
- const limit = parseIntSafe(opts.limit, 20);
2133
+ const limit = parseIntArg(opts.limit, "--limit");
1451
2134
  const args = { symbol: sym, limit };
1452
2135
  if (opts.from) args.from_date = opts.from;
1453
- const data = await callTool("get_dividends", args);
2136
+ const data = await BloomClient.instance().getDividends(args);
1454
2137
  outputResult(data, opts, formatDividends);
1455
2138
  });
1456
2139
  }
@@ -1471,11 +2154,15 @@ function formatDividends(data) {
1471
2154
  function registerSplitsCommand(program2) {
1472
2155
  addGlobalOptions(
1473
2156
  program2.command("splits").description("Stock split history").argument("<symbol>", "Ticker symbol").option("--from <date>", "Start date (YYYY-MM-DD)")
1474
- ).action(async (symbol, opts) => {
2157
+ ).addHelpText("after", `
2158
+ Examples:
2159
+ $ bloom splits AAPL Stock split history for Apple
2160
+ $ bloom splits TSLA --from 2020-01-01 Splits since 2020
2161
+ `).action(async (symbol, opts) => {
1475
2162
  const sym = symbol.toUpperCase();
1476
2163
  const args = { symbol: sym };
1477
2164
  if (opts.from) args.from_date = opts.from;
1478
- const data = await callTool("get_splits", args);
2165
+ const data = await BloomClient.instance().getSplits(args);
1479
2166
  outputResult(data, opts, formatSplits);
1480
2167
  });
1481
2168
  }
@@ -1491,27 +2178,30 @@ function formatSplits(data) {
1491
2178
  // src/commands/ipo.ts
1492
2179
  function registerIpoCommand(program2) {
1493
2180
  addGlobalOptions(
1494
- program2.command("ipo").description("Upcoming and recent IPOs").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)")
1495
- ).action(async (opts) => {
2181
+ program2.command("ipo").description("IPO calendar and recent filings").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)")
2182
+ ).addHelpText("after", `
2183
+ Examples:
2184
+ $ bloom ipo Upcoming and recent IPOs
2185
+ $ bloom ipo --from 2025-01-01 IPOs since Jan 2025
2186
+ `).action(async (opts) => {
1496
2187
  const args = {};
1497
2188
  if (opts.from) args.from_date = opts.from;
1498
2189
  if (opts.to) args.to_date = opts.to;
1499
- const data = await callTool("get_ipo_calendar", args);
1500
- outputResult(data, opts, formatIpos);
2190
+ const data = await BloomClient.instance().getIpo(args);
2191
+ outputResult(data, opts, formatIpo);
1501
2192
  });
1502
2193
  }
1503
- function formatIpos(data) {
1504
- const ipos = Array.isArray(data) ? data : data?.ipos;
1505
- if (!Array.isArray(ipos) || ipos.length === 0) return "(no IPO data found)";
1506
- const rows = ipos.map((ipo) => [
1507
- String(ipo.date ?? ipo.start_date ?? "-"),
1508
- String(ipo.name ?? ipo.company ?? "-"),
1509
- String(ipo.code ?? ipo.ticker ?? ipo.symbol ?? "-"),
1510
- String(ipo.exchange ?? "-"),
1511
- ipo.price_from && ipo.price_to ? `$${ipo.price_from}-$${ipo.price_to}` : ipo.price ? `$${ipo.price}` : "-",
1512
- ipo.shares ? fmtNum5(ipo.shares) : "-"
2194
+ function formatIpo(data) {
2195
+ if (!Array.isArray(data) || data.length === 0) return "(no IPO data)";
2196
+ const rows = data.map((r) => [
2197
+ String(r.date ?? "-"),
2198
+ String(r.symbol ?? "-"),
2199
+ String(r.company ?? r.name ?? "-"),
2200
+ String(r.exchange ?? "-"),
2201
+ r.priceRange ? String(r.priceRange) : "-",
2202
+ r.shares ? fmtNum5(r.shares) : "-"
1513
2203
  ]);
1514
- return makeTable(["Date", "Company", "Symbol", "Exchange", "Price Range", "Shares"], rows);
2204
+ return makeTable(["Date", "Symbol", "Company", "Exchange", "Price Range", "Shares"], rows);
1515
2205
  }
1516
2206
  function fmtNum5(v) {
1517
2207
  if (v == null) return "-";
@@ -1525,9 +2215,14 @@ function fmtNum5(v) {
1525
2215
  function registerCompareCommand(program2) {
1526
2216
  addGlobalOptions(
1527
2217
  program2.command("compare").description("Side-by-side comparison of stocks").argument("<symbols...>", "Ticker symbols (2-10)")
1528
- ).action(async (symbols, opts) => {
2218
+ ).addHelpText("after", `
2219
+ Examples:
2220
+ $ bloom compare AAPL MSFT Side-by-side comparison
2221
+ $ bloom compare AAPL MSFT GOOGL AMZN Compare big tech stocks
2222
+ $ bloom compare TSLA RIVN LCID Compare EV manufacturers
2223
+ `).action(async (symbols, opts) => {
1529
2224
  const syms = symbols.map((s) => s.toUpperCase()).slice(0, 10);
1530
- const data = await callTool("get_symbols_metadata", { symbols: syms });
2225
+ const data = await BloomClient.instance().compare(syms);
1531
2226
  outputResult(data, opts, (d) => formatComparison(d, syms));
1532
2227
  });
1533
2228
  }
@@ -1538,12 +2233,10 @@ function formatComparison(data, symbols) {
1538
2233
  const metrics = [
1539
2234
  ["Price", "latest_price", "$"],
1540
2235
  ["Market Cap", "market_cap", "cap"],
1541
- ["P/E", "pe_ratio", ""],
1542
- ["EPS", "eps", "$"],
2236
+ ["P/E", "price_to_earnings", ""],
1543
2237
  ["Dividend Yield", "dividend_yield", "%"],
1544
2238
  ["52W High", "week_52_high", "$"],
1545
2239
  ["52W Low", "week_52_low", "$"],
1546
- ["Beta", "beta", ""],
1547
2240
  ["Sector", "sector", ""]
1548
2241
  ];
1549
2242
  const bySymbol = {};
@@ -1554,15 +2247,15 @@ function formatComparison(data, symbols) {
1554
2247
  bySymbol[sym] = meta;
1555
2248
  }
1556
2249
  const headers = ["Metric", ...symbols];
1557
- const rows = metrics.map(([label, key, fmt2]) => {
2250
+ const rows = metrics.map(([label, key, fmt3]) => {
1558
2251
  const vals = symbols.map((s) => {
1559
2252
  const meta = bySymbol[s];
1560
2253
  if (!meta) return "-";
1561
2254
  const v = meta[key];
1562
2255
  if (v == null) return "-";
1563
- if (fmt2 === "$") return `$${Number(v).toFixed(2)}`;
1564
- if (fmt2 === "%") return `${(Number(v) * 100).toFixed(2)}%`;
1565
- if (fmt2 === "cap") return fmtCap(v);
2256
+ if (fmt3 === "$") return `$${Number(v).toFixed(2)}`;
2257
+ if (fmt3 === "%") return `${(Number(v) * 100).toFixed(2)}%`;
2258
+ if (fmt3 === "cap") return fmtCap(v);
1566
2259
  return String(v);
1567
2260
  });
1568
2261
  return [label, ...vals];
@@ -1582,10 +2275,14 @@ function fmtCap(v) {
1582
2275
  function registerRatingsCommand(program2) {
1583
2276
  addGlobalOptions(
1584
2277
  program2.command("ratings").description("Analyst ratings, grades, and price targets").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Max records to show", "20")
1585
- ).action(async (symbol, opts) => {
2278
+ ).addHelpText("after", `
2279
+ Examples:
2280
+ $ bloom ratings AAPL Analyst ratings and price targets
2281
+ $ bloom ratings NVDA --limit 5 Last 5 analyst actions
2282
+ `).action(async (symbol, opts) => {
1586
2283
  const sym = symbol.toUpperCase();
1587
- const limit = parseIntSafe(opts.limit, 20);
1588
- const result = await callTool("get_analyst_ratings", { symbol: sym, limit });
2284
+ const limit = parseIntArg(opts.limit, "--limit");
2285
+ const result = await BloomClient.instance().getRatings(sym, limit);
1589
2286
  outputResult(result, opts, () => {
1590
2287
  const obj = result;
1591
2288
  return formatRatings(obj.grades, obj.estimates);
@@ -1625,16 +2322,447 @@ function fmtNum6(v) {
1625
2322
  return `$${n.toLocaleString()}`;
1626
2323
  }
1627
2324
 
2325
+ // src/commands/analytics.ts
2326
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4 } from "fs";
2327
+ import { dirname as dirname2 } from "path";
2328
+ function parseHoldings(input) {
2329
+ let raw = input;
2330
+ try {
2331
+ raw = readFileSync4(input, "utf8");
2332
+ } catch {
2333
+ }
2334
+ try {
2335
+ const parsed = JSON.parse(raw);
2336
+ if (Array.isArray(parsed)) {
2337
+ return parsed.map((h) => ({
2338
+ symbol: String(h.symbol).toUpperCase(),
2339
+ value: Number(h.value),
2340
+ cost_basis: h.cost_basis !== void 0 ? Number(h.cost_basis) : void 0,
2341
+ shares: h.shares !== void 0 ? Number(h.shares) : void 0
2342
+ }));
2343
+ }
2344
+ return Object.entries(parsed).map(([symbol, value]) => ({
2345
+ symbol: symbol.toUpperCase(),
2346
+ value: Number(value)
2347
+ }));
2348
+ } catch {
2349
+ const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
2350
+ return parts.map((p) => {
2351
+ const [sym, val] = p.split("=");
2352
+ if (!sym || !val) {
2353
+ throw new Error(`Invalid holding: ${p}. Use 'AAPL=145000' format.`);
2354
+ }
2355
+ return { symbol: sym.toUpperCase().trim(), value: Number(val) };
2356
+ });
2357
+ }
2358
+ }
2359
+ var DIVIDER = "\u2500".repeat(60);
2360
+ function fmtMoney(n) {
2361
+ if (n >= 1e6) return `$${(n / 1e6).toFixed(2)}M`;
2362
+ if (n >= 1e3) return `$${(n / 1e3).toFixed(1)}K`;
2363
+ return `$${n.toFixed(0)}`;
2364
+ }
2365
+ function fmtPct(n, signed = false) {
2366
+ const v = (n * 100).toFixed(1);
2367
+ return signed && n >= 0 ? `+${v}%` : `${v}%`;
2368
+ }
2369
+ function bar(pct, width = 16) {
2370
+ const filled = Math.round(pct * width);
2371
+ return "\u2588".repeat(Math.max(0, filled)) + " ".repeat(Math.max(0, width - filled));
2372
+ }
2373
+ function renderText(data) {
2374
+ const lines = [];
2375
+ const s = data.summary;
2376
+ const headerParts = [fmtMoney(s.total_value)];
2377
+ if (s.ytd_return !== null && s.ytd_return !== void 0) {
2378
+ headerParts.push(`${fmtPct(s.ytd_return, true)} YTD`);
2379
+ }
2380
+ if (s.vs_spy !== null && s.vs_spy !== void 0) {
2381
+ headerParts.push(`${fmtPct(s.vs_spy, true)} SPY`);
2382
+ }
2383
+ lines.push("");
2384
+ lines.push(" " + headerParts.join(" \u2022 "));
2385
+ lines.push("");
2386
+ lines.push(" " + s.headline_insight);
2387
+ lines.push("");
2388
+ lines.push(" " + DIVIDER);
2389
+ lines.push("");
2390
+ lines.push(" CONCENTRATION");
2391
+ lines.push("");
2392
+ for (const sec of data.concentration.by_sector) {
2393
+ const name = sec.name.padEnd(14);
2394
+ lines.push(` ${name} ${bar(sec.pct)} ${fmtPct(sec.pct)}`);
2395
+ }
2396
+ lines.push("");
2397
+ lines.push(" " + data.concentration.insight);
2398
+ lines.push("");
2399
+ lines.push(" " + DIVIDER);
2400
+ lines.push("");
2401
+ lines.push(" HOW YOU TRADE");
2402
+ lines.push("");
2403
+ if (!data.behavior.available) {
2404
+ lines.push(" " + (data.behavior.reason || "Not enough trade history yet."));
2405
+ } else {
2406
+ if (data.behavior.insight) lines.push(" " + data.behavior.insight);
2407
+ lines.push("");
2408
+ for (const s2 of data.behavior.strengths) lines.push(` \u2713 ${s2.text}`);
2409
+ for (const w of data.behavior.weaknesses) lines.push(` \u26A0 ${w.text}`);
2410
+ }
2411
+ lines.push("");
2412
+ lines.push(" " + DIVIDER);
2413
+ lines.push("");
2414
+ lines.push(" RISK");
2415
+ lines.push("");
2416
+ const r = data.risk;
2417
+ if (r.volatility && r.volatility.value != null) {
2418
+ lines.push(
2419
+ ` Volatility ${r.volatility.label.padEnd(8)} ${fmtPct(r.volatility.value)}`
2420
+ );
2421
+ }
2422
+ if (r.correlation_spy != null) {
2423
+ const label = r.correlation_spy >= 0.8 ? "High" : r.correlation_spy >= 0.5 ? "Med" : "Low";
2424
+ lines.push(
2425
+ ` Correlation ${label.padEnd(8)} ${r.correlation_spy.toFixed(2)} to SPY`
2426
+ );
2427
+ }
2428
+ if (r.max_drawdown != null) {
2429
+ lines.push(` Max drawdown ${fmtPct(r.max_drawdown)}`);
2430
+ }
2431
+ if (r.sharpe != null) {
2432
+ lines.push(` Sharpe ${r.sharpe.toFixed(2)}`);
2433
+ }
2434
+ lines.push("");
2435
+ lines.push(" " + r.insight);
2436
+ lines.push("");
2437
+ lines.push(" " + DIVIDER);
2438
+ lines.push("");
2439
+ lines.push(" WHAT TO WATCH");
2440
+ lines.push("");
2441
+ if (data.watch.alerts.length === 0) {
2442
+ lines.push(" Nothing flagged.");
2443
+ } else {
2444
+ for (const w of data.watch.alerts) {
2445
+ lines.push(` ${w.ticker.padEnd(6)} ${w.reason}`);
2446
+ }
2447
+ }
2448
+ lines.push("");
2449
+ lines.push(" " + DIVIDER);
2450
+ lines.push("");
2451
+ lines.push(" PROJECTION");
2452
+ lines.push("");
2453
+ for (const p of data.projection.points) {
2454
+ if (p.year === 0) continue;
2455
+ lines.push(
2456
+ ` ${`${p.year}Y`.padEnd(4)} p10 ${fmtMoney(p.p10)} p50 ${fmtMoney(p.p50)} p90 ${fmtMoney(p.p90)}`
2457
+ );
2458
+ }
2459
+ lines.push("");
2460
+ lines.push(" " + data.projection.disclaimer);
2461
+ lines.push("");
2462
+ return lines.join("\n");
2463
+ }
2464
+ function renderMarkdown(data) {
2465
+ const lines = [];
2466
+ const s = data.summary;
2467
+ lines.push("# Portfolio Analytics");
2468
+ lines.push("");
2469
+ const hp = [`**${fmtMoney(s.total_value)}**`];
2470
+ if (s.ytd_return != null) hp.push(`${fmtPct(s.ytd_return, true)} YTD`);
2471
+ if (s.vs_spy != null) hp.push(`${fmtPct(s.vs_spy, true)} SPY`);
2472
+ lines.push(hp.join(" \u2022 "));
2473
+ lines.push("");
2474
+ lines.push(`> ${s.headline_insight}`);
2475
+ lines.push("");
2476
+ lines.push("## Concentration");
2477
+ lines.push("");
2478
+ lines.push("| Sector | Allocation | Value |");
2479
+ lines.push("|---|---|---|");
2480
+ for (const sec of data.concentration.by_sector) {
2481
+ lines.push(`| ${sec.name} | ${fmtPct(sec.pct)} | ${fmtMoney(sec.value)} |`);
2482
+ }
2483
+ lines.push("");
2484
+ lines.push(data.concentration.insight);
2485
+ lines.push("");
2486
+ lines.push("## How You Trade");
2487
+ lines.push("");
2488
+ if (!data.behavior.available) {
2489
+ lines.push(data.behavior.reason || "Not enough trade history yet.");
2490
+ } else {
2491
+ if (data.behavior.insight) {
2492
+ lines.push(data.behavior.insight);
2493
+ lines.push("");
2494
+ }
2495
+ for (const x of data.behavior.strengths) lines.push(`- \u2713 ${x.text}`);
2496
+ for (const x of data.behavior.weaknesses) lines.push(`- \u26A0 ${x.text}`);
2497
+ }
2498
+ lines.push("");
2499
+ lines.push("## Risk");
2500
+ lines.push("");
2501
+ const r = data.risk;
2502
+ if (r.volatility && r.volatility.value != null) lines.push(`- Volatility: ${r.volatility.label} (${fmtPct(r.volatility.value)})`);
2503
+ if (r.correlation_spy != null) lines.push(`- Correlation to SPY: ${r.correlation_spy.toFixed(2)}`);
2504
+ if (r.max_drawdown != null) lines.push(`- Max drawdown: ${fmtPct(r.max_drawdown)}`);
2505
+ if (r.sharpe != null) lines.push(`- Sharpe: ${r.sharpe.toFixed(2)}`);
2506
+ lines.push("");
2507
+ lines.push(r.insight);
2508
+ lines.push("");
2509
+ lines.push("## What to Watch");
2510
+ lines.push("");
2511
+ if (data.watch.alerts.length === 0) {
2512
+ lines.push("Nothing flagged.");
2513
+ } else {
2514
+ for (const w of data.watch.alerts) {
2515
+ lines.push(`- **${w.ticker}** \u2014 ${w.reason}`);
2516
+ }
2517
+ }
2518
+ lines.push("");
2519
+ lines.push("## Projection");
2520
+ lines.push("");
2521
+ lines.push("| Year | p10 | p50 | p90 |");
2522
+ lines.push("|---|---|---|---|");
2523
+ for (const p of data.projection.points) {
2524
+ lines.push(`| ${p.year}Y | ${fmtMoney(p.p10)} | ${fmtMoney(p.p50)} | ${fmtMoney(p.p90)} |`);
2525
+ }
2526
+ lines.push("");
2527
+ lines.push(`*${data.projection.disclaimer}*`);
2528
+ return lines.join("\n");
2529
+ }
2530
+ function emit2(content, opts) {
2531
+ if (opts.output) {
2532
+ mkdirSync5(dirname2(opts.output), { recursive: true });
2533
+ writeFileSync5(opts.output, content, "utf8");
2534
+ if (!opts.quiet) console.error(`Wrote ${opts.output}`);
2535
+ } else {
2536
+ console.log(content);
2537
+ }
2538
+ }
2539
+ function registerAnalyticsCommand(program2) {
2540
+ addGlobalOptions(
2541
+ program2.command("analytics").description(
2542
+ "One-page portfolio analytics: concentration, risk, watch, projection"
2543
+ ).argument(
2544
+ "<holdings>",
2545
+ "Holdings as JSON, file path, or shorthand (e.g. 'AAPL=145000,MSFT=91000')"
2546
+ ).option(
2547
+ "--section <s>",
2548
+ "Show only one section: concentration, behavior, risk, watch, projection"
2549
+ ).option("--md", "Output as markdown (alias for --format markdown)")
2550
+ ).action(
2551
+ async (holdingsInput, opts) => {
2552
+ let holdings;
2553
+ try {
2554
+ holdings = parseHoldings(holdingsInput);
2555
+ } catch (e) {
2556
+ console.error(`Error parsing holdings: ${e.message}`);
2557
+ process.exit(1);
2558
+ }
2559
+ const result = await callTool("analyze_portfolio", {
2560
+ holdings,
2561
+ parts: [
2562
+ "summary",
2563
+ "concentration",
2564
+ "asset_class",
2565
+ "geography",
2566
+ "behavior",
2567
+ "risk",
2568
+ "watch",
2569
+ "projection"
2570
+ ]
2571
+ });
2572
+ if (result && typeof result === "object" && "error" in result && result.error) {
2573
+ console.error(`Error: ${result.error}`);
2574
+ process.exit(1);
2575
+ }
2576
+ const data = result;
2577
+ if (opts.raw) {
2578
+ emit2(JSON.stringify(data, null, 2), opts);
2579
+ return;
2580
+ }
2581
+ let payload = data;
2582
+ if (opts.section) {
2583
+ const sec = opts.section.toLowerCase();
2584
+ const map = {
2585
+ summary: data.summary,
2586
+ concentration: data.concentration,
2587
+ behavior: data.behavior,
2588
+ risk: data.risk,
2589
+ watch: data.watch,
2590
+ projection: data.projection
2591
+ };
2592
+ if (!(sec in map)) {
2593
+ console.error(
2594
+ `Unknown section: ${sec}. Choose: summary, concentration, behavior, risk, watch, projection`
2595
+ );
2596
+ process.exit(1);
2597
+ }
2598
+ payload = map[sec];
2599
+ }
2600
+ const fmt3 = opts.md ? "markdown" : opts.format ?? resolveFormat(opts);
2601
+ if (fmt3 === "json") {
2602
+ emit2(JSON.stringify(payload, null, 2), opts);
2603
+ return;
2604
+ }
2605
+ if (fmt3 === "markdown") {
2606
+ if (opts.section) {
2607
+ emit2(JSON.stringify(payload, null, 2), opts);
2608
+ } else {
2609
+ emit2(renderMarkdown(data), opts);
2610
+ }
2611
+ return;
2612
+ }
2613
+ if (opts.section) {
2614
+ emit2(JSON.stringify(payload, null, 2), opts);
2615
+ } else {
2616
+ emit2(renderText(data), opts);
2617
+ }
2618
+ }
2619
+ );
2620
+ }
2621
+
2622
+ // src/commands/stats.ts
2623
+ function registerStatsCommand(program2) {
2624
+ addGlobalOptions(
2625
+ program2.command("stats").description("Key stats: beta, 52-week range, volume, P/E, dividend yield").argument("<symbol>", "Ticker symbol")
2626
+ ).addHelpText("after", `
2627
+ Examples:
2628
+ $ bloom stats AAPL Key statistics for Apple
2629
+ $ bloom stats TSLA -f json Stats as JSON
2630
+ `).action(async (symbol, opts) => {
2631
+ const data = await BloomClient.instance().getStats(symbol.toUpperCase());
2632
+ outputResult(data, opts);
2633
+ });
2634
+ }
2635
+
2636
+ // src/commands/peers.ts
2637
+ function registerPeersCommand(program2) {
2638
+ addGlobalOptions(
2639
+ program2.command("peers").description("Peer companies and competitors").argument("<symbol>", "Ticker symbol")
2640
+ ).addHelpText("after", `
2641
+ Examples:
2642
+ $ bloom peers AAPL Companies similar to Apple
2643
+ $ bloom peers TSLA Tesla's peer group
2644
+ `).action(async (symbol, opts) => {
2645
+ const data = await BloomClient.instance().getPeers(symbol.toUpperCase());
2646
+ outputResult(data, opts, formatPeers);
2647
+ });
2648
+ }
2649
+ function formatPeers(data, opts) {
2650
+ const peers = data?.results;
2651
+ if (!Array.isArray(peers)) return JSON.stringify(data, null, 2);
2652
+ const num = (v, f) => v == null || v === "" || isNaN(Number(v)) ? "-" : f(Number(v));
2653
+ const pct = (n) => `${(n * 100).toFixed(2)}%`;
2654
+ const cap = (n) => n >= 1e12 ? `$${(n / 1e12).toFixed(2)}T` : n >= 1e9 ? `$${(n / 1e9).toFixed(2)}B` : `$${(n / 1e6).toFixed(2)}M`;
2655
+ const rows = peers.map((p) => [
2656
+ String(p.symbol ?? ""),
2657
+ truncate(String(p.name ?? ""), 32),
2658
+ String(p.sector || p.category || ""),
2659
+ num(p.latestPrice, (n) => `$${n.toFixed(2)}`),
2660
+ num(p.changePercent1d, pct),
2661
+ num(p.changePercent1y, pct),
2662
+ num(p.priceToEarnings, (n) => n.toFixed(2)),
2663
+ num(p.marketCap, cap)
2664
+ ]);
2665
+ return makeTable(["Symbol", "Name", "Sector", "Price", "1D", "1Y", "P/E", "Mkt Cap"], rows, opts);
2666
+ }
2667
+
2668
+ // src/commands/bottom-line.ts
2669
+ function registerBottomLineCommand(program2) {
2670
+ addGlobalOptions(
2671
+ program2.command("bottom-line").description("Buy/sell/hold verdict with reasoning").argument("<symbol>", "Ticker symbol")
2672
+ ).addHelpText("after", `
2673
+ Examples:
2674
+ $ bloom bottom-line AAPL Bloom's bottom line on Apple
2675
+ $ bloom bottom-line NVDA Buy/sell/hold verdict for NVIDIA
2676
+ `).action(async (symbol, opts) => {
2677
+ const data = await BloomClient.instance().getBottomLine(symbol.toUpperCase());
2678
+ outputResult(data, opts, formatBottomLine);
2679
+ });
2680
+ }
2681
+ function formatBottomLine(data, opts) {
2682
+ const bl = data?.results;
2683
+ if (!bl?.verdict) return JSON.stringify(data, null, 2);
2684
+ const rows = bl.details.map((d) => [d.rating, d.title, d.descriptions.map((x) => `- ${x}`).join("\n")]);
2685
+ const table = makeTable(["Rating", "Area", "Reasoning"], rows, opts);
2686
+ return [`${bl.symbol}: ${bl.verdict}`, ...bl.lead ? [bl.lead] : [], "", table].join("\n");
2687
+ }
2688
+
2689
+ // src/commands/insights.ts
2690
+ function registerInsightsCommand(program2) {
2691
+ addGlobalOptions(
2692
+ program2.command("insights").description("AI-generated insights for stocks").argument("[symbols...]", "Ticker symbols (omit for all recent)")
2693
+ ).addHelpText("after", `
2694
+ Examples:
2695
+ $ bloom insights AAPL Insights for Apple
2696
+ $ bloom insights AAPL MSFT Insights for multiple stocks
2697
+ $ bloom insights All recent insights
2698
+ `).action(async (symbols, opts) => {
2699
+ if (symbols.length === 1) {
2700
+ const data = await BloomClient.instance().getInsights(symbols[0].toUpperCase());
2701
+ outputResult(data, opts);
2702
+ } else {
2703
+ const syms = symbols.length > 0 ? symbols.map((s) => s.toUpperCase()) : void 0;
2704
+ const data = await BloomClient.instance().getInsights(void 0, syms);
2705
+ outputResult(data, opts);
2706
+ }
2707
+ });
2708
+ }
2709
+
2710
+ // src/commands/arena-activity.ts
2711
+ function registerArenaActivityCommand(program2) {
2712
+ addGlobalOptions(
2713
+ program2.command("arena-activity").description("Live AI arena trade activity feed").option("--model <m>", "Filter by model: claude, openai, gemini").option("--limit <n>", "Max results", "20")
2714
+ ).addHelpText("after", `
2715
+ Examples:
2716
+ $ bloom arena-activity All recent arena trades
2717
+ $ bloom arena-activity --model claude Claude's trades only
2718
+ $ bloom arena-activity --limit 5 Last 5 trades
2719
+ `).action(async (opts) => {
2720
+ const limit = parseIntArg(opts.limit, "--limit");
2721
+ const data = await BloomClient.instance().getArenaActivity(opts.model, limit);
2722
+ outputResult(data, opts);
2723
+ });
2724
+ }
2725
+
2726
+ // src/commands/portfolio-news.ts
2727
+ function registerPortfolioNewsCommand(program2) {
2728
+ addGlobalOptions(
2729
+ program2.command("portfolio-news").description("Smart news summary for portfolio symbols").argument("[symbols...]", "Ticker symbols")
2730
+ ).addHelpText("after", `
2731
+ Examples:
2732
+ $ bloom portfolio-news AAPL MSFT GOOG News digest for holdings
2733
+ $ bloom portfolio-news News for all watched symbols
2734
+ `).action(async (symbols, opts) => {
2735
+ const syms = symbols.length > 0 ? symbols.map((s) => s.toUpperCase()) : void 0;
2736
+ const data = await BloomClient.instance().getPortfolioNews(syms);
2737
+ outputResult(data, opts);
2738
+ });
2739
+ }
2740
+
2741
+ // src/commands/collection-returns.ts
2742
+ function registerCollectionReturnsCommand(program2) {
2743
+ addGlobalOptions(
2744
+ program2.command("collection-returns").description("Performance returns for the given collections").requiredOption("--ids <ids>", "Comma-separated collection IDs (from `bloom collections`)").option("--timerange <range>", "1d|1w|1m|3m|ytd|1y|5y", "ytd")
2745
+ ).addHelpText("after", `
2746
+ Examples:
2747
+ $ bloom collection-returns --ids 113 YTD return for collection 113
2748
+ $ bloom collection-returns --ids 113,114 --timerange 1y
2749
+ $ bloom collection-returns --ids 113 -f table As table
2750
+ `).action(async (opts) => {
2751
+ const data = await BloomClient.instance().getCollectionReturns(opts.ids, opts.timerange);
2752
+ outputResult(data, opts);
2753
+ });
2754
+ }
2755
+
1628
2756
  // src/index.ts
1629
- var program = new Command();
2757
+ var program = new Command3();
1630
2758
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
1631
2759
  var pkgPath = path.join(__dirname, "..", "package.json");
1632
- var compiledVersion = "0.1.5".length > 0 ? "0.1.5" : void 0;
2760
+ var compiledVersion = "0.1.7".length > 0 ? "0.1.7" : void 0;
1633
2761
  var envVersion = process.env.BLOOM_CLI_VERSION ?? process.env.npm_package_version;
1634
2762
  var fallbackVersion = compiledVersion ?? "unknown";
1635
2763
  var cliVersion = envVersion ?? fallbackVersion;
1636
2764
  try {
1637
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
2765
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
1638
2766
  if (typeof pkg.version === "string") {
1639
2767
  cliVersion = pkg.version;
1640
2768
  }
@@ -1643,7 +2771,24 @@ try {
1643
2771
  if (cliVersion === "0.0.0") {
1644
2772
  cliVersion = fallbackVersion;
1645
2773
  }
1646
- program.name("bloom").description("Bloom CLI \u2014 financial data for coding agents and humans").version(cliVersion);
2774
+ var config = loadConfig();
2775
+ if (config.apiBase !== "https://api.getbloom.app") {
2776
+ setApiBase(config.apiBase);
2777
+ }
2778
+ setCacheDir(config.cacheDir);
2779
+ program.hook("preAction", (_thisCommand, actionCommand) => {
2780
+ for (const [key, value] of Object.entries(program.opts())) {
2781
+ if (program.getOptionValueSource(key) === "cli" && actionCommand.getOptionValueSource(key) !== "cli") {
2782
+ actionCommand.setOptionValueWithSource(key, value, "cli");
2783
+ }
2784
+ }
2785
+ const opts = actionCommand.opts();
2786
+ if (opts.debug) setDebug(true);
2787
+ if (opts.cache === false) setNoCache(true);
2788
+ });
2789
+ addGlobalOptions(
2790
+ program.name("bloom").description("Bloom CLI \u2014 financial data for coding agents and humans").version(cliVersion).enablePositionalOptions()
2791
+ );
1647
2792
  registerAuthCommand(program);
1648
2793
  registerInfoCommand(program);
1649
2794
  registerNewsCommand(program);
@@ -1680,7 +2825,50 @@ registerSplitsCommand(program);
1680
2825
  registerIpoCommand(program);
1681
2826
  registerCompareCommand(program);
1682
2827
  registerRatingsCommand(program);
2828
+ registerAnalyticsCommand(program);
2829
+ registerStatsCommand(program);
2830
+ registerPeersCommand(program);
2831
+ registerBottomLineCommand(program);
2832
+ registerInsightsCommand(program);
2833
+ registerArenaActivityCommand(program);
2834
+ registerPortfolioNewsCommand(program);
2835
+ registerCollectionReturnsCommand(program);
2836
+ var cacheCmd = program.command("cache").description("Manage disk cache");
2837
+ cacheCmd.command("clear").description("Clear all cached data").action(() => {
2838
+ const { cleared } = clearCache();
2839
+ console.log(`Cleared ${cleared} cached items.`);
2840
+ });
2841
+ cacheCmd.command("stats").description("Show cache statistics").action(() => {
2842
+ const stats = cacheStats();
2843
+ console.log(`Cache directory: ${stats.cacheDir}`);
2844
+ console.log(`Items: ${stats.items} (${stats.expired} expired)`);
2845
+ console.log(`Total size: ${(stats.totalBytes / 1024).toFixed(1)} KB`);
2846
+ if (Object.keys(stats.byCategory).length > 0) {
2847
+ console.log("By category:");
2848
+ for (const [cat, count] of Object.entries(stats.byCategory)) {
2849
+ console.log(` ${cat}: ${count}`);
2850
+ }
2851
+ }
2852
+ });
1683
2853
  program.parseAsync().catch((err) => {
1684
- console.error("Error:", err.message || err);
2854
+ if (err instanceof AuthError) {
2855
+ console.error(`Error: ${err.message}`);
2856
+ console.error("Run `bloom auth login` to authenticate.");
2857
+ process.exit(1);
2858
+ }
2859
+ if (err instanceof RateLimitError) {
2860
+ console.error("Rate limited. Try again in a moment.");
2861
+ process.exit(2);
2862
+ }
2863
+ if (err instanceof TickerNotFoundError) {
2864
+ console.error(`Ticker not found: ${err.ticker}`);
2865
+ process.exit(1);
2866
+ }
2867
+ if (err instanceof BloomError) {
2868
+ console.error(`Error: ${err.message}`);
2869
+ process.exit(1);
2870
+ }
2871
+ const msg = err instanceof Error ? err.message : String(err);
2872
+ console.error("Error:", msg);
1685
2873
  process.exit(1);
1686
2874
  });