@bloomai/cli 0.1.6 → 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.
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,15 +620,281 @@ 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
+ });
308
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 "";
666
+ }
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
+ };
309
898
 
310
899
  // src/commands/info.ts
311
900
  var VALID_FIELDS = [
@@ -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
  }
@@ -382,10 +968,10 @@ Examples:
382
968
  $ bloom news AAPL MSFT GOOGL News for multiple stocks
383
969
  $ bloom news TSLA --limit 5 Last 5 Tesla articles
384
970
  `).action(async (symbols, opts) => {
385
- const data = await callTool("get_news_for_symbols", {
386
- symbols: symbols.map((s) => s.toUpperCase()),
387
- limit: parseIntSafe(opts.limit, 15)
388
- });
971
+ const data = await BloomClient.instance().getNews(
972
+ symbols.map((s) => s.toUpperCase()),
973
+ parseIntArg(opts.limit, "--limit")
974
+ );
389
975
  outputResult(data, opts, (d, o) => formatNews(d, o));
390
976
  });
391
977
  }
@@ -424,11 +1010,11 @@ Examples:
424
1010
  $ bloom price MSFT --ohlc Include open/high/low/close
425
1011
  $ bloom price AAPL -f csv -o prices.csv Export to CSV file
426
1012
  `).action(async (symbol, opts) => {
427
- const data = await callTool("get_price_charts", {
428
- symbol: symbol.toUpperCase(),
429
- timeframes: opts.timeframes.split(",").map((t) => t.trim()),
430
- include_ohlc: !!opts.ohlc
431
- });
1013
+ const data = await BloomClient.instance().getPrice(
1014
+ symbol.toUpperCase(),
1015
+ opts.timeframes.split(",").map((t) => t.trim()),
1016
+ !!opts.ohlc
1017
+ );
432
1018
  outputResult(data, opts, (d, o) => formatPrices(d, o));
433
1019
  });
434
1020
  }
@@ -478,7 +1064,8 @@ function formatPrices(data, opts) {
478
1064
  }
479
1065
 
480
1066
  // src/commands/financials.ts
481
- var CHART_TYPES = [
1067
+ import { Option as Option2 } from "commander";
1068
+ var VALID_TYPES = [
482
1069
  "revenue",
483
1070
  "revenue_growth",
484
1071
  "operating_margin",
@@ -491,18 +1078,18 @@ var CHART_TYPES = [
491
1078
  ];
492
1079
  function registerFinancialsCommand(program2) {
493
1080
  addGlobalOptions(
494
- program2.command("financials").description("Quarterly/annual financial data with YoY growth").argument("<symbol>", "Ticker symbol").option("--type <type>", "Chart type: " + CHART_TYPES.join(", "), "revenue")
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
+ )
495
1084
  ).addHelpText("after", `
496
1085
  Examples:
497
- $ bloom financials AAPL Revenue (default)
498
- $ bloom financials TSLA --type operating_margin Operating margin trend
499
- $ bloom financials MSFT --type free_cash_flow Free cash flow history
500
- $ bloom financials NVDA --type net_earnings_growth Earnings growth rate
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
501
1090
  `).action(async (symbol, opts) => {
502
- const data = await callTool("get_financial_charts", {
503
- symbol: symbol.toUpperCase(),
504
- chart_type: opts.type
505
- });
1091
+ const sym = symbol.toUpperCase();
1092
+ const data = await BloomClient.instance().getFinancials(sym, opts.type);
506
1093
  outputResult(data, opts);
507
1094
  });
508
1095
  }
@@ -522,11 +1109,8 @@ Examples:
522
1109
  $ bloom sentiment All sentiment indicators
523
1110
  $ bloom sentiment --sections aaii_sentiment,cnn_fear_greed Just AAII and Fear & Greed
524
1111
  `).action(async (opts) => {
525
- const args = {};
526
- if (opts.sections) {
527
- args.sections = opts.sections.split(",").map((s) => s.trim());
528
- }
529
- const data = await callTool("get_market_sentiment", args);
1112
+ const sections = opts.sections?.split(",").map((s) => s.trim());
1113
+ const data = await BloomClient.instance().getSentiment(sections);
530
1114
  outputResult(data, opts, (d) => formatSentiment(d));
531
1115
  });
532
1116
  }
@@ -565,12 +1149,12 @@ Examples:
565
1149
  $ bloom market --type top_movers --period 1w Weekly top movers
566
1150
  $ bloom market --universe sp500 --limit 10 Top 10 S&P 500 movers
567
1151
  `).action(async (opts) => {
568
- const data = await callTool("get_market_map", {
569
- map_type: opts.type,
570
- period: opts.period,
571
- limit: parseIntSafe(opts.limit, 24),
572
- universe: opts.universe
573
- });
1152
+ const data = await BloomClient.instance().getMarket(
1153
+ opts.type,
1154
+ opts.period,
1155
+ parseIntArg(opts.limit, "--limit"),
1156
+ opts.universe
1157
+ );
574
1158
  outputResult(data, opts);
575
1159
  });
576
1160
  }
@@ -586,10 +1170,10 @@ Examples:
586
1170
  $ bloom collections --section FAMOUS_INVESTORS Famous investor portfolios
587
1171
  $ bloom collections --section TRENDS --limit 3 Top 3 trending collections
588
1172
  `).action(async (opts) => {
589
- const data = await callTool("get_collections", {
590
- section: opts.section,
591
- limit: parseIntSafe(opts.limit, 6)
592
- });
1173
+ const data = await BloomClient.instance().getCollections(
1174
+ opts.section,
1175
+ parseIntArg(opts.limit, "--limit")
1176
+ );
593
1177
  outputResult(data, opts);
594
1178
  });
595
1179
  }
@@ -603,7 +1187,7 @@ function registerScreenCommand(program2) {
603
1187
  const args = {
604
1188
  filter_expressions: filters,
605
1189
  and_query: !opts.or,
606
- limit: parseIntSafe(opts.limit, 50)
1190
+ limit: parseIntArg(opts.limit, "--limit")
607
1191
  };
608
1192
  if (opts.symbols) {
609
1193
  args.symbols = opts.symbols.split(",").map((s) => s.trim().toUpperCase());
@@ -611,7 +1195,7 @@ function registerScreenCommand(program2) {
611
1195
  if (opts.fields) {
612
1196
  args.fields = opts.fields.split(",").map((f) => f.trim());
613
1197
  }
614
- const data = await callTool("filter_stocks", args);
1198
+ const data = await BloomClient.instance().screen(args);
615
1199
  outputResult(data, opts, (d, o) => formatScreen(d, o));
616
1200
  }
617
1201
  );
@@ -648,42 +1232,52 @@ function formatScreen(data, opts) {
648
1232
  return makeTable(headers, rows, opts);
649
1233
  }
650
1234
 
1235
+ // src/limits.ts
1236
+ var MAX_ANALYSIS_SYMBOLS = 10;
1237
+
651
1238
  // src/commands/earnings.ts
652
1239
  function registerEarningsCommand(program2) {
653
1240
  addGlobalOptions(
654
- program2.command("earnings").description("Earnings history, estimates, surprises").argument("<symbols...>", "Ticker symbols (up to 5)")
1241
+ program2.command("earnings").description("Earnings history, estimates, surprises").argument("<symbols...>", "Ticker symbols (up to 10)")
655
1242
  ).addHelpText("after", `
656
1243
  Examples:
657
1244
  $ bloom earnings AAPL Apple earnings history and surprises
658
1245
  $ bloom earnings AAPL MSFT GOOGL Compare earnings for multiple stocks
659
1246
  `).action(async (symbols, opts) => {
660
- const data = await callTool("analyze_earnings", {
661
- symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5)
662
- });
1247
+ const syms = symbols.map((s) => s.toUpperCase()).slice(0, MAX_ANALYSIS_SYMBOLS);
1248
+ const data = await BloomClient.instance().getEarnings(syms);
663
1249
  outputResult(data, opts);
664
1250
  });
665
1251
  }
666
1252
 
667
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
+ }
668
1258
  function registerEarningsCalendarCommand(program2) {
669
1259
  addGlobalOptions(
670
1260
  program2.command("earnings-calendar").description(
671
1261
  "Upcoming earnings dates (all companies, or filter by symbols)"
672
- ).argument("[symbols...]", "Ticker symbols (omit to show all)").option("--days <n>", "Look-ahead window in days (1-90)", "28")
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
+ )
673
1266
  ).addHelpText("after", `
674
1267
  Examples:
675
1268
  $ bloom earnings-calendar AAPL MSFT GOOGL Upcoming earnings for these stocks
676
1269
  $ bloom earnings-calendar TSLA --days 7 Earnings within the next week
677
1270
  $ bloom earnings-calendar NVDA --days 90 Earnings within 90 days
1271
+ $ bloom earnings-calendar AAPL --popular AAPL plus the popular calendar
678
1272
  `).action(
679
1273
  async (symbols, opts) => {
680
- const days = parseIntSafe(opts.days, 28);
1274
+ const days = parseIntArg(opts.days, "--days", 1, 90);
681
1275
  const params = { days: String(days) };
682
1276
  if (symbols.length > 0) {
683
1277
  params.symbols = symbols.map((s) => s.toUpperCase()).join(",");
1278
+ if (!opts.popular) params.include_popular = "false";
684
1279
  }
685
- const data = await callRest(
686
- "/markets/earnings-calendar/",
1280
+ const data = await BloomClient.instance().getEarningsCalendar(
687
1281
  params
688
1282
  );
689
1283
  const items = data.results ?? [];
@@ -696,8 +1290,8 @@ Examples:
696
1290
  r.earnings_date ?? "\u2014",
697
1291
  r.symbol,
698
1292
  r.name,
699
- r.eps_estimate != null ? `$${r.eps_estimate}` : "\u2014",
700
- r.eps_actual != null ? `$${r.eps_actual}` : "\u2014"
1293
+ formatEps(r.eps_estimate, r.currency),
1294
+ formatEps(r.eps_actual, r.currency)
701
1295
  ]),
702
1296
  o
703
1297
  );
@@ -710,7 +1304,7 @@ Examples:
710
1304
  var SECTIONS2 = ["technical_indicators", "options_analysis", "insider_activity"];
711
1305
  function registerTechnicalsCommand(program2) {
712
1306
  addGlobalOptions(
713
- 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(","))
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(","))
714
1308
  ).addHelpText("after", `
715
1309
  Examples:
716
1310
  $ bloom technicals AAPL All technical data
@@ -718,13 +1312,9 @@ Examples:
718
1312
  $ bloom technicals NVDA --sections options_analysis Options flow only
719
1313
  $ bloom technicals AAPL --sections insider_activity Insider trades only
720
1314
  `).action(async (symbols, opts) => {
721
- const args = {
722
- symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5)
723
- };
724
- if (opts.sections) {
725
- args.sections = opts.sections.split(",").map((s) => s.trim());
726
- }
727
- const data = await callTool("analyze_technicals", args);
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);
728
1318
  outputResult(data, opts);
729
1319
  });
730
1320
  }
@@ -741,12 +1331,12 @@ Examples:
741
1331
  $ bloom options-history AAPL --greeks Include advanced Greeks analysis
742
1332
  `).action(
743
1333
  async (symbol, opts) => {
744
- const data = await callTool("analyze_options_history", {
745
- symbol: symbol.toUpperCase(),
746
- lookback_days: parseIntSafe(opts.days, 30),
747
- weekly_sampling: opts.weekly ?? !opts.daily,
748
- include_greeks_analysis: !!opts.greeks
749
- });
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
+ );
750
1340
  outputResult(data, opts);
751
1341
  }
752
1342
  );
@@ -761,12 +1351,14 @@ function registerPortfolioCommand(program2) {
761
1351
  try {
762
1352
  holdings = JSON.parse(holdingsStr);
763
1353
  } catch {
764
- console.error(`Error: Invalid JSON for holdings. Use format: '{"AAPL":0.3,"MSFT":0.7}'`);
765
- process.exit(1);
766
1354
  }
767
- const args = { holdings };
768
- if (opts.strategy) args.strategy = opts.strategy;
769
- 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);
770
1362
  outputResult(data, opts);
771
1363
  });
772
1364
  }
@@ -781,7 +1373,7 @@ Examples:
781
1373
  $ bloom trades "Sell MSFT 20% to take profits" "Buy GOOGL 15% on AI momentum"
782
1374
  `).action(async (tradeStrs, opts) => {
783
1375
  const trades = tradeStrs.map(parseTrade);
784
- const data = await callTool("analyze_trades", { trades });
1376
+ const data = await BloomClient.instance().evaluateTrades(trades);
785
1377
  outputResult(data, opts);
786
1378
  });
787
1379
  }
@@ -811,15 +1403,15 @@ Format: "Buy AAPL 30% because thesis" or JSON object`
811
1403
  // src/commands/catalysts.ts
812
1404
  function registerCatalystsCommand(program2) {
813
1405
  addGlobalOptions(
814
- program2.command("catalysts").description("Forward-looking value catalysts").argument("<symbols...>", "Ticker symbols (up to 5)")
1406
+ program2.command("catalysts").description("Forward-looking value catalysts").argument("<symbols...>", "Ticker symbols (up to 10)")
815
1407
  ).addHelpText("after", `
816
1408
  Examples:
817
1409
  $ bloom catalysts AAPL Upcoming catalysts for Apple
818
1410
  $ bloom catalysts TSLA NVDA AMZN Catalysts for multiple stocks
819
1411
  `).action(async (symbols, opts) => {
820
- const data = await callTool("generate_catalysts", {
821
- symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5)
822
- });
1412
+ const data = await BloomClient.instance().getCatalysts(
1413
+ symbols.map((s) => s.toUpperCase()).slice(0, MAX_ANALYSIS_SYMBOLS)
1414
+ );
823
1415
  outputResult(data, opts);
824
1416
  });
825
1417
  }
@@ -848,11 +1440,11 @@ function registerPositionCommand(program2) {
848
1440
  const args = {
849
1441
  bull_return: bullReturn,
850
1442
  bear_return: bearReturn,
851
- conviction: parseIntSafe(opts.conviction, 5),
1443
+ conviction: parseIntArg(opts.conviction, "--conviction", 1, 10),
852
1444
  portfolio_value: portfolioValue
853
1445
  };
854
1446
  if (opts.symbol) args.symbol = opts.symbol.toUpperCase();
855
- const data = await callTool("calculate_position_size", args);
1447
+ const data = await BloomClient.instance().positionSize(args);
856
1448
  outputResult(data, opts);
857
1449
  }
858
1450
  );
@@ -861,16 +1453,16 @@ function registerPositionCommand(program2) {
861
1453
  // src/commands/transcript.ts
862
1454
  function registerTranscriptCommand(program2) {
863
1455
  addGlobalOptions(
864
- 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")
865
1457
  ).action(
866
1458
  async (symbol, keywords, opts) => {
867
1459
  const args = {
868
1460
  symbol: symbol.toUpperCase(),
869
1461
  keywords,
870
- max_results: parseIntSafe(opts.max, 10)
1462
+ max_results: parseIntArg(opts.max, "--max")
871
1463
  };
872
1464
  if (opts.quarter) args.fiscal_quarter = opts.quarter;
873
- const data = await callTool("search_transcript", args);
1465
+ const data = await BloomClient.instance().searchTranscript(args);
874
1466
  outputResult(data, opts, (d, o) => formatTranscript(d, o));
875
1467
  }
876
1468
  );
@@ -913,11 +1505,11 @@ function registerAiTradesCommand(program2) {
913
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")
914
1506
  ).action(async (opts) => {
915
1507
  const args = {
916
- limit: parseIntSafe(opts.limit, 10)
1508
+ limit: parseIntArg(opts.limit, "--limit", 1, 50)
917
1509
  };
918
1510
  if (opts.model) args.ai_model = opts.model;
919
1511
  if (opts.symbol) args.symbol = opts.symbol.toUpperCase();
920
- const data = await callTool("get_ai_trade_ideas", args);
1512
+ const data = await BloomClient.instance().getAiTrades(args);
921
1513
  if (opts.raw) {
922
1514
  outputResult(data, opts);
923
1515
  return;
@@ -945,7 +1537,7 @@ function formatTrades(data, opts) {
945
1537
  String(t.shares ?? "-"),
946
1538
  t.fill_price != null ? `$${Number(t.fill_price).toFixed(2)}` : "-",
947
1539
  gain,
948
- truncate2(String(t.rationale ?? "-"), 60)
1540
+ truncate(String(t.rationale ?? "-"), 60)
949
1541
  ];
950
1542
  });
951
1543
  return makeTable(
@@ -954,9 +1546,6 @@ function formatTrades(data, opts) {
954
1546
  opts
955
1547
  );
956
1548
  }
957
- function truncate2(s, max) {
958
- return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
959
- }
960
1549
 
961
1550
  // src/commands/ai-portfolio.ts
962
1551
  function registerAiPortfolioCommand(program2) {
@@ -970,7 +1559,7 @@ Examples:
970
1559
  `).action(async (opts) => {
971
1560
  const args = {};
972
1561
  if (opts.model) args.ai_model = opts.model;
973
- const data = await callTool("get_ai_trade_ideas", args);
1562
+ const data = await BloomClient.instance().getAiPortfolio(args);
974
1563
  if (opts.raw) {
975
1564
  outputResult(data, opts);
976
1565
  return;
@@ -1077,6 +1666,7 @@ function registerMaCommand(program2) {
1077
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")
1078
1667
  ).action(
1079
1668
  async (symbol, opts) => {
1669
+ const limit = parseIntArg(opts.limit, "--limit");
1080
1670
  const indicators = [];
1081
1671
  if (opts.sma) {
1082
1672
  for (const p of opts.sma.split(",")) {
@@ -1093,12 +1683,10 @@ function registerMaCommand(program2) {
1093
1683
  if (indicators.length === 0) {
1094
1684
  indicators.push("sma_100", "ema_100");
1095
1685
  }
1096
- const args = {
1097
- symbol: symbol.toUpperCase(),
1686
+ const data = await BloomClient.instance().getMovingAverages(
1687
+ symbol.toUpperCase(),
1098
1688
  indicators
1099
- };
1100
- const data = await callTool("get_moving_averages", args);
1101
- const limit = parseInt(opts.limit, 10) || 20;
1689
+ );
1102
1690
  let parsed = null;
1103
1691
  try {
1104
1692
  parsed = typeof data === "string" ? JSON.parse(data) : data;
@@ -1142,7 +1730,7 @@ function registerBacktestCommand(program2) {
1142
1730
  addGlobalOptions(
1143
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")
1144
1732
  ).action(async (opts) => {
1145
- const data = await callRest("backtest/", {
1733
+ const data = await BloomClient.instance().backtest({
1146
1734
  symbols: opts.symbols,
1147
1735
  allocations: opts.allocations,
1148
1736
  benchmark: opts.benchmark,
@@ -1169,7 +1757,9 @@ function formatBacktest(data) {
1169
1757
  ]);
1170
1758
  }
1171
1759
  if (rows.length === 0) return JSON.stringify(data, null, 2);
1172
- 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");
1173
1763
  }
1174
1764
  function fmt(v) {
1175
1765
  if (v == null || v === "") return "-";
@@ -1199,7 +1789,7 @@ Examples:
1199
1789
  };
1200
1790
  if (opts.start) params.start_date = opts.start;
1201
1791
  if (opts.end) params.end_date = opts.end;
1202
- const data = await callRest("correlation/", params);
1792
+ const data = await BloomClient.instance().correlation(params);
1203
1793
  outputResult(data, opts, formatCorrelation);
1204
1794
  });
1205
1795
  }
@@ -1216,29 +1806,56 @@ function formatCorrelation(data) {
1216
1806
  }
1217
1807
 
1218
1808
  // src/commands/risk.ts
1809
+ var STALE_DAYS = 456;
1219
1810
  function registerRiskCommand(program2) {
1220
1811
  addGlobalOptions(
1221
- program2.command("risk").description("Risk factors for a stock").argument("<symbol>", "Ticker symbol")
1812
+ program2.command("risk").description("Risk factors from the company's latest ingested SEC 10-K (check filingDate)").argument("<symbol>", "Ticker symbol")
1222
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
+
1223
1817
  Examples:
1224
1818
  $ bloom risk AAPL Risk factors for Apple
1225
1819
  $ bloom risk TSLA Risk factors for Tesla
1226
1820
  `).action(async (symbol, opts) => {
1227
- const data = await callRest(`investments/${symbol.toUpperCase()}/risk_factors/`);
1821
+ const data = await BloomClient.instance().getRisk(symbol.toUpperCase());
1822
+ const warning = staleWarning(riskRows(data));
1823
+ if (warning && !opts.quiet) console.error(warning);
1228
1824
  outputResult(data, opts, formatRisk);
1229
1825
  });
1230
1826
  }
1231
- function formatRisk(data) {
1232
- if (!Array.isArray(data)) return JSON.stringify(data, null, 2);
1233
- if (data.length === 0) return "(no risk factors found)";
1234
- const rows = data.map((r) => [
1235
- String(r.category ?? r.risk_type ?? "-"),
1236
- truncate3(String(r.description ?? r.text ?? "-"), 80)
1237
- ]);
1238
- return makeTable(["Category", "Description"], rows);
1827
+ function riskRows(data) {
1828
+ return data?.results ?? [];
1239
1829
  }
1240
- function truncate3(s, max) {
1241
- 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}`;
1242
1859
  }
1243
1860
 
1244
1861
  // src/commands/drawdown.ts
@@ -1246,52 +1863,24 @@ function registerDrawdownCommand(program2) {
1246
1863
  addGlobalOptions(
1247
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)")
1248
1865
  ).action(async (opts) => {
1249
- const data = await callRest("portfolios/drawdown/", {
1866
+ const data = await BloomClient.instance().getDrawdown({
1250
1867
  symbols: opts.symbols,
1251
1868
  allocations: opts.allocations
1252
1869
  });
1253
1870
  outputResult(data, opts, formatDrawdown);
1254
1871
  });
1255
1872
  }
1256
- function formatDrawdown(data) {
1257
- const obj = data;
1258
- if (!obj || typeof obj !== "object") return JSON.stringify(data, null, 2);
1259
- const intervals = obj.interval_results ?? obj;
1260
- const rows = [];
1261
- for (const [interval, dd] of Object.entries(intervals)) {
1262
- if (typeof dd !== "object" || !dd) continue;
1263
- const d = dd;
1264
- let troughDate = "-";
1265
- let recoveryDate = "not recovered";
1266
- const plotData = d.plot_data;
1267
- if (plotData && plotData.length > 0) {
1268
- let minDD = 0;
1269
- let minIdx = -1;
1270
- for (let i = 0; i < plotData.length; i++) {
1271
- if (plotData[i].drawdown < minDD) {
1272
- minDD = plotData[i].drawdown;
1273
- minIdx = i;
1274
- }
1275
- }
1276
- if (minIdx >= 0) {
1277
- troughDate = plotData[minIdx].date;
1278
- for (let i = minIdx + 1; i < plotData.length; i++) {
1279
- if (plotData[i].drawdown >= 0) {
1280
- recoveryDate = plotData[i].date;
1281
- break;
1282
- }
1283
- }
1284
- }
1285
- }
1286
- rows.push([
1287
- interval,
1288
- d.max_drawdown != null ? `${(Number(d.max_drawdown) * 100).toFixed(2)}%` : "-",
1289
- troughDate,
1290
- recoveryDate
1291
- ]);
1292
- }
1293
- if (rows.length === 0) return JSON.stringify(data, null, 2);
1294
- 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)}%`;
1295
1884
  }
1296
1885
 
1297
1886
  // src/commands/macro.ts
@@ -1308,7 +1897,7 @@ Examples:
1308
1897
  if (opts.indicators) {
1309
1898
  args.indicators = opts.indicators;
1310
1899
  }
1311
- const data = await callTool("get_macro_indicators", args);
1900
+ const data = await BloomClient.instance().getMacro(args);
1312
1901
  outputResult(data, opts, formatMacro);
1313
1902
  });
1314
1903
  }
@@ -1345,7 +1934,7 @@ Examples:
1345
1934
  $ bloom sectors Sector performance across all time periods
1346
1935
  $ bloom sectors -f json Output as JSON
1347
1936
  `).action(async (opts) => {
1348
- const data = await callTool("get_sector_performance", {});
1937
+ const data = await BloomClient.instance().getSectors();
1349
1938
  outputResult(data, opts, formatSectors);
1350
1939
  });
1351
1940
  }
@@ -1404,7 +1993,7 @@ Examples:
1404
1993
  $ bloom dcf TSLA DCF valuation for Tesla
1405
1994
  `).action(async (symbol, opts) => {
1406
1995
  const sym = symbol.toUpperCase();
1407
- const result = await callTool("get_dcf_valuation", { symbol: sym });
1996
+ const result = await BloomClient.instance().getDcf(sym);
1408
1997
  outputResult(result, opts, () => formatDcf(result, sym));
1409
1998
  });
1410
1999
  }
@@ -1428,39 +2017,27 @@ function formatDcf(data, symbol) {
1428
2017
  // src/commands/holders.ts
1429
2018
  function registerHoldersCommand(program2) {
1430
2019
  addGlobalOptions(
1431
- 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")
2020
+ program2.command("holders").description("Institutional holders of a stock").argument("<symbol>", "Ticker symbol")
1432
2021
  ).addHelpText("after", `
1433
2022
  Examples:
1434
- $ bloom holders AAPL All holders (institutional + mutual fund)
1435
- $ bloom holders TSLA --type institutional Institutional holders only
1436
- $ bloom holders NVDA --limit 5 Top 5 holders
2023
+ $ bloom holders AAPL Top institutional holders of Apple
2024
+ $ bloom holders TSLA Tesla's institutional ownership
1437
2025
  `).action(async (symbol, opts) => {
1438
2026
  const sym = symbol.toUpperCase();
1439
- const limit = parseIntSafe(opts.limit, 15);
1440
- const args = { symbol: sym };
1441
- if (opts.type) args.holder_type = opts.type;
1442
- const data = await callTool("get_holders", args);
1443
- outputResult(data, opts, () => formatHolders(data, limit));
2027
+ const data = await BloomClient.instance().getHolders(sym);
2028
+ outputResult(data, opts, formatHolders);
1444
2029
  });
1445
2030
  }
1446
- function formatHolders(data, limit) {
1447
- const parts = [];
1448
- const addSection = (title, list) => {
1449
- const items = list;
1450
- if (items && items.length > 0) {
1451
- const rows = items.slice(0, limit).map((h) => [
1452
- String(h.holder ?? "-"),
1453
- fmtNum2(h.shares),
1454
- h.dateReported ? String(h.dateReported).slice(0, 10) : "-",
1455
- fmtNum2(h.change)
1456
- ]);
1457
- parts.push(`${title}
1458
- ${makeTable(["Holder", "Shares", "Date", "Change"], rows)}`);
1459
- }
1460
- };
1461
- addSection("INSTITUTIONAL HOLDERS", data.institutional);
1462
- addSection("MUTUAL FUND HOLDERS", data.mutual_fund);
1463
- 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);
1464
2041
  }
1465
2042
  function fmtNum2(v) {
1466
2043
  if (v == null) return "-";
@@ -1482,8 +2059,8 @@ Examples:
1482
2059
  $ bloom short-interest AMC --limit 20 Last 20 short interest records
1483
2060
  `).action(async (symbol, opts) => {
1484
2061
  const sym = symbol.toUpperCase();
1485
- const limit = parseIntSafe(opts.limit, 10);
1486
- 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);
1487
2064
  outputResult(data, opts, formatShort);
1488
2065
  });
1489
2066
  }
@@ -1512,33 +2089,28 @@ function fmtNum3(v) {
1512
2089
  // src/commands/insider.ts
1513
2090
  function registerInsiderCommand(program2) {
1514
2091
  addGlobalOptions(
1515
- program2.command("insider").description("Insider trading activity (SEC Form 4)").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Max transactions to show", "20")
2092
+ program2.command("insider").description("Insider trading activity").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Max records", "20")
1516
2093
  ).addHelpText("after", `
1517
2094
  Examples:
1518
2095
  $ bloom insider AAPL Recent insider trades for Apple
1519
- $ bloom insider TSLA --limit 5 Last 5 insider transactions
2096
+ $ bloom insider TSLA --limit 10 Last 10 insider transactions
1520
2097
  `).action(async (symbol, opts) => {
1521
2098
  const sym = symbol.toUpperCase();
1522
- const limit = parseIntSafe(opts.limit, 20);
1523
- 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);
1524
2101
  outputResult(data, opts, formatInsider);
1525
2102
  });
1526
2103
  }
1527
2104
  function formatInsider(data) {
1528
- if (!Array.isArray(data) || data.length === 0) return "(no insider trading data)";
1529
- const rows = data.map((t) => {
1530
- const val = t.securitiesTransacted && t.price ? `$${(Number(t.securitiesTransacted) * Number(t.price)).toLocaleString()}` : "-";
1531
- return [
1532
- String(t.filingDate ?? t.transactionDate ?? "-").slice(0, 10),
1533
- String(t.reportingName ?? "-"),
1534
- String(t.typeOfOwner ?? "-"),
1535
- String(t.transactionType ?? t.acquistionOrDisposition ?? "-"),
1536
- fmtNum4(t.securitiesTransacted),
1537
- t.price != null ? `$${Number(t.price).toFixed(2)}` : "-",
1538
- val
1539
- ];
1540
- });
1541
- 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);
1542
2114
  }
1543
2115
  function fmtNum4(v) {
1544
2116
  if (v == null) return "-";
@@ -1558,10 +2130,10 @@ Examples:
1558
2130
  $ bloom dividends JNJ --from 2024-01-01 Dividends since Jan 2024
1559
2131
  `).action(async (symbol, opts) => {
1560
2132
  const sym = symbol.toUpperCase();
1561
- const limit = parseIntSafe(opts.limit, 20);
2133
+ const limit = parseIntArg(opts.limit, "--limit");
1562
2134
  const args = { symbol: sym, limit };
1563
2135
  if (opts.from) args.from_date = opts.from;
1564
- const data = await callTool("get_dividends", args);
2136
+ const data = await BloomClient.instance().getDividends(args);
1565
2137
  outputResult(data, opts, formatDividends);
1566
2138
  });
1567
2139
  }
@@ -1590,7 +2162,7 @@ Examples:
1590
2162
  const sym = symbol.toUpperCase();
1591
2163
  const args = { symbol: sym };
1592
2164
  if (opts.from) args.from_date = opts.from;
1593
- const data = await callTool("get_splits", args);
2165
+ const data = await BloomClient.instance().getSplits(args);
1594
2166
  outputResult(data, opts, formatSplits);
1595
2167
  });
1596
2168
  }
@@ -1606,31 +2178,30 @@ function formatSplits(data) {
1606
2178
  // src/commands/ipo.ts
1607
2179
  function registerIpoCommand(program2) {
1608
2180
  addGlobalOptions(
1609
- program2.command("ipo").description("Upcoming and recent IPOs").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)")
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)")
1610
2182
  ).addHelpText("after", `
1611
2183
  Examples:
1612
- $ bloom ipo Recent and upcoming IPOs
1613
- $ bloom ipo --from 2025-01-01 --to 2025-06-30 IPOs in H1 2025
2184
+ $ bloom ipo Upcoming and recent IPOs
2185
+ $ bloom ipo --from 2025-01-01 IPOs since Jan 2025
1614
2186
  `).action(async (opts) => {
1615
2187
  const args = {};
1616
2188
  if (opts.from) args.from_date = opts.from;
1617
2189
  if (opts.to) args.to_date = opts.to;
1618
- const data = await callTool("get_ipo_calendar", args);
1619
- outputResult(data, opts, formatIpos);
2190
+ const data = await BloomClient.instance().getIpo(args);
2191
+ outputResult(data, opts, formatIpo);
1620
2192
  });
1621
2193
  }
1622
- function formatIpos(data) {
1623
- const ipos = Array.isArray(data) ? data : data?.ipos;
1624
- if (!Array.isArray(ipos) || ipos.length === 0) return "(no IPO data found)";
1625
- const rows = ipos.map((ipo) => [
1626
- String(ipo.date ?? ipo.start_date ?? "-"),
1627
- String(ipo.name ?? ipo.company ?? "-"),
1628
- String(ipo.code ?? ipo.ticker ?? ipo.symbol ?? "-"),
1629
- String(ipo.exchange ?? "-"),
1630
- ipo.price_from && ipo.price_to ? `$${ipo.price_from}-$${ipo.price_to}` : ipo.price ? `$${ipo.price}` : "-",
1631
- 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) : "-"
1632
2203
  ]);
1633
- return makeTable(["Date", "Company", "Symbol", "Exchange", "Price Range", "Shares"], rows);
2204
+ return makeTable(["Date", "Symbol", "Company", "Exchange", "Price Range", "Shares"], rows);
1634
2205
  }
1635
2206
  function fmtNum5(v) {
1636
2207
  if (v == null) return "-";
@@ -1651,7 +2222,7 @@ Examples:
1651
2222
  $ bloom compare TSLA RIVN LCID Compare EV manufacturers
1652
2223
  `).action(async (symbols, opts) => {
1653
2224
  const syms = symbols.map((s) => s.toUpperCase()).slice(0, 10);
1654
- const data = await callTool("get_symbols_metadata", { symbols: syms });
2225
+ const data = await BloomClient.instance().compare(syms);
1655
2226
  outputResult(data, opts, (d) => formatComparison(d, syms));
1656
2227
  });
1657
2228
  }
@@ -1662,12 +2233,10 @@ function formatComparison(data, symbols) {
1662
2233
  const metrics = [
1663
2234
  ["Price", "latest_price", "$"],
1664
2235
  ["Market Cap", "market_cap", "cap"],
1665
- ["P/E", "pe_ratio", ""],
1666
- ["EPS", "eps", "$"],
2236
+ ["P/E", "price_to_earnings", ""],
1667
2237
  ["Dividend Yield", "dividend_yield", "%"],
1668
2238
  ["52W High", "week_52_high", "$"],
1669
2239
  ["52W Low", "week_52_low", "$"],
1670
- ["Beta", "beta", ""],
1671
2240
  ["Sector", "sector", ""]
1672
2241
  ];
1673
2242
  const bySymbol = {};
@@ -1678,15 +2247,15 @@ function formatComparison(data, symbols) {
1678
2247
  bySymbol[sym] = meta;
1679
2248
  }
1680
2249
  const headers = ["Metric", ...symbols];
1681
- const rows = metrics.map(([label, key, fmt2]) => {
2250
+ const rows = metrics.map(([label, key, fmt3]) => {
1682
2251
  const vals = symbols.map((s) => {
1683
2252
  const meta = bySymbol[s];
1684
2253
  if (!meta) return "-";
1685
2254
  const v = meta[key];
1686
2255
  if (v == null) return "-";
1687
- if (fmt2 === "$") return `$${Number(v).toFixed(2)}`;
1688
- if (fmt2 === "%") return `${(Number(v) * 100).toFixed(2)}%`;
1689
- 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);
1690
2259
  return String(v);
1691
2260
  });
1692
2261
  return [label, ...vals];
@@ -1712,8 +2281,8 @@ Examples:
1712
2281
  $ bloom ratings NVDA --limit 5 Last 5 analyst actions
1713
2282
  `).action(async (symbol, opts) => {
1714
2283
  const sym = symbol.toUpperCase();
1715
- const limit = parseIntSafe(opts.limit, 20);
1716
- 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);
1717
2286
  outputResult(result, opts, () => {
1718
2287
  const obj = result;
1719
2288
  return formatRatings(obj.grades, obj.estimates);
@@ -1753,16 +2322,447 @@ function fmtNum6(v) {
1753
2322
  return `$${n.toLocaleString()}`;
1754
2323
  }
1755
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
+
1756
2756
  // src/index.ts
1757
- var program = new Command();
2757
+ var program = new Command3();
1758
2758
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
1759
2759
  var pkgPath = path.join(__dirname, "..", "package.json");
1760
- var compiledVersion = "0.1.6".length > 0 ? "0.1.6" : void 0;
2760
+ var compiledVersion = "0.1.7".length > 0 ? "0.1.7" : void 0;
1761
2761
  var envVersion = process.env.BLOOM_CLI_VERSION ?? process.env.npm_package_version;
1762
2762
  var fallbackVersion = compiledVersion ?? "unknown";
1763
2763
  var cliVersion = envVersion ?? fallbackVersion;
1764
2764
  try {
1765
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
2765
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
1766
2766
  if (typeof pkg.version === "string") {
1767
2767
  cliVersion = pkg.version;
1768
2768
  }
@@ -1771,7 +2771,24 @@ try {
1771
2771
  if (cliVersion === "0.0.0") {
1772
2772
  cliVersion = fallbackVersion;
1773
2773
  }
1774
- 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
+ );
1775
2792
  registerAuthCommand(program);
1776
2793
  registerInfoCommand(program);
1777
2794
  registerNewsCommand(program);
@@ -1808,7 +2825,50 @@ registerSplitsCommand(program);
1808
2825
  registerIpoCommand(program);
1809
2826
  registerCompareCommand(program);
1810
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
+ });
1811
2853
  program.parseAsync().catch((err) => {
1812
- 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);
1813
2873
  process.exit(1);
1814
2874
  });