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