@bloomai/cli 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/SKILL.md +8 -3
- package/dist/index.js +1372 -312
- 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,15 +620,281 @@ function makeTable(headers, rows, opts) {
|
|
|
297
620
|
function truncate(s, max) {
|
|
298
621
|
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
299
622
|
}
|
|
300
|
-
function
|
|
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
|
+
});
|
|
308
657
|
}
|
|
658
|
+
async function readKey() {
|
|
659
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, prompt: "API key: " });
|
|
660
|
+
if (process.stdin.isTTY) rl.prompt();
|
|
661
|
+
for await (const line of rl) {
|
|
662
|
+
rl.close();
|
|
663
|
+
return line;
|
|
664
|
+
}
|
|
665
|
+
return "";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/client.ts
|
|
669
|
+
var _instance;
|
|
670
|
+
var BloomClient = class _BloomClient {
|
|
671
|
+
constructor() {
|
|
672
|
+
}
|
|
673
|
+
// --- Singleton ---
|
|
674
|
+
static instance() {
|
|
675
|
+
if (!_instance) {
|
|
676
|
+
_instance = new _BloomClient();
|
|
677
|
+
}
|
|
678
|
+
return _instance;
|
|
679
|
+
}
|
|
680
|
+
static resetInstance() {
|
|
681
|
+
_instance = void 0;
|
|
682
|
+
}
|
|
683
|
+
// --- Info ---
|
|
684
|
+
// cacheCategory lets the `quote` alias use the 5-minute quote TTL instead of
|
|
685
|
+
// the 24-hour info TTL, so an advertised "current" price is never a day old.
|
|
686
|
+
async getInfo(symbols, fields, cacheCategory = "info") {
|
|
687
|
+
const args = { symbols };
|
|
688
|
+
if (fields) args.fields = fields;
|
|
689
|
+
return callToolCached("get_symbols_metadata", args, cacheCategory);
|
|
690
|
+
}
|
|
691
|
+
// --- News ---
|
|
692
|
+
async getNews(symbols, limit = 15) {
|
|
693
|
+
return callToolCached("get_news_for_symbols", { symbols, limit }, "news");
|
|
694
|
+
}
|
|
695
|
+
// --- Price ---
|
|
696
|
+
async getPrice(symbol, timeframes, includeOhlc = false) {
|
|
697
|
+
return callToolCached("get_price_charts", {
|
|
698
|
+
symbol,
|
|
699
|
+
timeframes,
|
|
700
|
+
include_ohlc: includeOhlc
|
|
701
|
+
}, "quote");
|
|
702
|
+
}
|
|
703
|
+
// --- Financials ---
|
|
704
|
+
async getFinancials(symbol, chartType = "revenue") {
|
|
705
|
+
return callToolCached("get_financial_charts", { symbol, chart_type: chartType }, "financials");
|
|
706
|
+
}
|
|
707
|
+
// --- Earnings ---
|
|
708
|
+
async getEarnings(symbols) {
|
|
709
|
+
return callToolCached("analyze_earnings", { symbols }, "earnings");
|
|
710
|
+
}
|
|
711
|
+
// --- Earnings Calendar ---
|
|
712
|
+
async getEarningsCalendar(params) {
|
|
713
|
+
return callRestCached("/markets/earnings-calendar/", params, "earnings");
|
|
714
|
+
}
|
|
715
|
+
// --- Sentiment ---
|
|
716
|
+
async getSentiment(sections) {
|
|
717
|
+
const args = {};
|
|
718
|
+
if (sections) args.sections = sections;
|
|
719
|
+
return callToolCached("get_market_sentiment", args, "sentiment");
|
|
720
|
+
}
|
|
721
|
+
// --- Market ---
|
|
722
|
+
async getMarket(mapType, period, limit, universe) {
|
|
723
|
+
return callToolCached("get_market_map", {
|
|
724
|
+
map_type: mapType,
|
|
725
|
+
period,
|
|
726
|
+
limit,
|
|
727
|
+
universe
|
|
728
|
+
}, "default");
|
|
729
|
+
}
|
|
730
|
+
// --- Collections ---
|
|
731
|
+
async getCollections(section, limit) {
|
|
732
|
+
return callToolCached("get_collections", { section, limit }, "default");
|
|
733
|
+
}
|
|
734
|
+
// --- Screen ---
|
|
735
|
+
async screen(args) {
|
|
736
|
+
return callToolCached("filter_stocks", args, "default");
|
|
737
|
+
}
|
|
738
|
+
// --- Technicals ---
|
|
739
|
+
async getTechnicals(symbols, sections) {
|
|
740
|
+
const args = { symbols };
|
|
741
|
+
if (sections) args.sections = sections;
|
|
742
|
+
return callToolCached("analyze_technicals", args, "technicals");
|
|
743
|
+
}
|
|
744
|
+
// --- Options ---
|
|
745
|
+
async getOptions(symbol, lookbackDays, weeklySampling, includeGreeks) {
|
|
746
|
+
return callToolCached("analyze_options_history", {
|
|
747
|
+
symbol,
|
|
748
|
+
lookback_days: lookbackDays,
|
|
749
|
+
weekly_sampling: weeklySampling,
|
|
750
|
+
include_greeks_analysis: includeGreeks
|
|
751
|
+
}, "technicals");
|
|
752
|
+
}
|
|
753
|
+
// --- Portfolio ---
|
|
754
|
+
async analyzePortfolio(holdings, strategy) {
|
|
755
|
+
const args = { holdings };
|
|
756
|
+
if (strategy) args.strategy = strategy;
|
|
757
|
+
return callToolCached("analyze_portfolio", args, "default");
|
|
758
|
+
}
|
|
759
|
+
// --- Trades ---
|
|
760
|
+
async evaluateTrades(trades) {
|
|
761
|
+
return callToolCached("analyze_trades", { trades }, "default");
|
|
762
|
+
}
|
|
763
|
+
// --- Catalysts ---
|
|
764
|
+
async getCatalysts(symbols) {
|
|
765
|
+
return callToolCached("generate_catalysts", { symbols }, "default");
|
|
766
|
+
}
|
|
767
|
+
// --- Position Size ---
|
|
768
|
+
async positionSize(args) {
|
|
769
|
+
return callToolCached("calculate_position_size", args, "default");
|
|
770
|
+
}
|
|
771
|
+
// --- Transcript ---
|
|
772
|
+
async searchTranscript(args) {
|
|
773
|
+
return callToolCached("search_transcript", args, "default");
|
|
774
|
+
}
|
|
775
|
+
// --- AI Trades ---
|
|
776
|
+
async getAiTrades(args) {
|
|
777
|
+
return callToolCached("get_ai_trade_ideas", args, "default");
|
|
778
|
+
}
|
|
779
|
+
// --- AI Portfolio ---
|
|
780
|
+
async getAiPortfolio(args) {
|
|
781
|
+
return callToolCached("get_ai_trade_ideas", args, "default");
|
|
782
|
+
}
|
|
783
|
+
// --- Moving Averages ---
|
|
784
|
+
// Folded into get_technicals as the "moving_averages" part. Unwrap the slice
|
|
785
|
+
// back into the {symbol, current_price, indicators} shape callers expect.
|
|
786
|
+
async getMovingAverages(symbol, indicators) {
|
|
787
|
+
const raw = await callToolCached(
|
|
788
|
+
"get_technicals",
|
|
789
|
+
{ symbol, parts: ["moving_averages"], ma_indicators: indicators },
|
|
790
|
+
"technicals"
|
|
791
|
+
);
|
|
792
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
793
|
+
const ma = parsed?.moving_averages;
|
|
794
|
+
if (ma && typeof ma === "object") {
|
|
795
|
+
return { symbol: parsed.symbol, ...ma };
|
|
796
|
+
}
|
|
797
|
+
return parsed;
|
|
798
|
+
}
|
|
799
|
+
// --- Backtest ---
|
|
800
|
+
async backtest(params) {
|
|
801
|
+
return callRestCached("backtest/", params, "default");
|
|
802
|
+
}
|
|
803
|
+
// --- Correlation ---
|
|
804
|
+
async correlation(params) {
|
|
805
|
+
return callRestCached("correlation/", params, "default");
|
|
806
|
+
}
|
|
807
|
+
// --- Risk ---
|
|
808
|
+
async getRisk(symbol) {
|
|
809
|
+
return callRestCached(`investments/${encodeURIComponent(symbol)}/risk_factors/`, void 0, "default");
|
|
810
|
+
}
|
|
811
|
+
// --- Drawdown ---
|
|
812
|
+
async getDrawdown(params) {
|
|
813
|
+
return callRestCached("portfolios/drawdown/", params, "default");
|
|
814
|
+
}
|
|
815
|
+
// --- Macro ---
|
|
816
|
+
async getMacro(args) {
|
|
817
|
+
return callToolCached("get_macro_indicators", args, "default");
|
|
818
|
+
}
|
|
819
|
+
// --- Sectors ---
|
|
820
|
+
async getSectors() {
|
|
821
|
+
return callToolCached("get_sector_performance", {}, "default");
|
|
822
|
+
}
|
|
823
|
+
// --- DCF ---
|
|
824
|
+
async getDcf(symbol) {
|
|
825
|
+
return callToolCached("get_dcf_valuation", { symbol }, "default");
|
|
826
|
+
}
|
|
827
|
+
// --- Holders ---
|
|
828
|
+
async getHolders(symbol) {
|
|
829
|
+
return callToolCached("get_holders", { symbol }, "default");
|
|
830
|
+
}
|
|
831
|
+
// --- Short Interest ---
|
|
832
|
+
async getShortInterest(symbol, limit) {
|
|
833
|
+
return callToolCached("get_short_interest", { symbol, limit }, "default");
|
|
834
|
+
}
|
|
835
|
+
// --- Insider ---
|
|
836
|
+
async getInsider(symbol, limit) {
|
|
837
|
+
return callToolCached("get_insider_trading", { symbol, limit }, "default");
|
|
838
|
+
}
|
|
839
|
+
// --- Dividends ---
|
|
840
|
+
async getDividends(args) {
|
|
841
|
+
return callToolCached("get_dividends", args, "default");
|
|
842
|
+
}
|
|
843
|
+
// --- Splits ---
|
|
844
|
+
async getSplits(args) {
|
|
845
|
+
return callToolCached("get_splits", args, "default");
|
|
846
|
+
}
|
|
847
|
+
// --- IPO ---
|
|
848
|
+
async getIpo(args) {
|
|
849
|
+
return callToolCached("get_ipo_calendar", args, "default");
|
|
850
|
+
}
|
|
851
|
+
// --- Compare ---
|
|
852
|
+
async compare(symbols) {
|
|
853
|
+
return callToolCached("get_symbols_metadata", { symbols }, "info");
|
|
854
|
+
}
|
|
855
|
+
// --- Ratings ---
|
|
856
|
+
async getRatings(symbol, limit) {
|
|
857
|
+
return callToolCached("get_analyst_ratings", { symbol, limit }, "default");
|
|
858
|
+
}
|
|
859
|
+
// --- Stats ---
|
|
860
|
+
async getStats(symbol) {
|
|
861
|
+
return callRestCached(`investments/${encodeURIComponent(symbol)}/stats/`, void 0, "quote");
|
|
862
|
+
}
|
|
863
|
+
// --- Peers ---
|
|
864
|
+
async getPeers(symbol) {
|
|
865
|
+
return callRestCached(`investments/${encodeURIComponent(symbol)}/peers/`, void 0, "quote");
|
|
866
|
+
}
|
|
867
|
+
// --- Bottom Line ---
|
|
868
|
+
async getBottomLine(symbol) {
|
|
869
|
+
return callRestCached(`investments/${encodeURIComponent(symbol)}/bottom_line_v2/`, void 0, "info");
|
|
870
|
+
}
|
|
871
|
+
// --- Insights ---
|
|
872
|
+
async getInsights(symbol, symbols) {
|
|
873
|
+
if (symbol) {
|
|
874
|
+
return callRestCached(`investments/${encodeURIComponent(symbol)}/insights/`, void 0, "default");
|
|
875
|
+
}
|
|
876
|
+
const params = {};
|
|
877
|
+
if (symbols) params.symbols = symbols.join(",");
|
|
878
|
+
return callRestCached("investments/all_insights/", params, "default");
|
|
879
|
+
}
|
|
880
|
+
// --- Arena Activity ---
|
|
881
|
+
async getArenaActivity(model, limit) {
|
|
882
|
+
const params = {};
|
|
883
|
+
if (model) params.model = model;
|
|
884
|
+
if (limit) params.limit = String(limit);
|
|
885
|
+
return callRestCached("api/arena/activity/", params, "default");
|
|
886
|
+
}
|
|
887
|
+
// --- Portfolio News ---
|
|
888
|
+
async getPortfolioNews(symbols) {
|
|
889
|
+
const params = {};
|
|
890
|
+
if (symbols) params.symbols = symbols.join(",");
|
|
891
|
+
return callRestCached("smart-summaries/portfolio-news/", params, "news");
|
|
892
|
+
}
|
|
893
|
+
// --- Collection Returns ---
|
|
894
|
+
async getCollectionReturns(collectionIds, timerange) {
|
|
895
|
+
return callRestCached("collections/returns/", { collection_ids: collectionIds, timerange }, "default");
|
|
896
|
+
}
|
|
897
|
+
};
|
|
309
898
|
|
|
310
899
|
// src/commands/info.ts
|
|
311
900
|
var VALID_FIELDS = [
|
|
@@ -318,15 +907,12 @@ var VALID_FIELDS = [
|
|
|
318
907
|
];
|
|
319
908
|
function registerInfoCommand(program2) {
|
|
320
909
|
addGlobalOptions(
|
|
321
|
-
program2.command("info").description("Company fundamentals, ratings, peers, holdings").argument("<symbols...>", "Ticker symbols (e.g. AAPL MSFT)").option("--fields <fields>", "Comma-separated: " + VALID_FIELDS.join(","))
|
|
910
|
+
program2.command("info").alias("quote").description("Company fundamentals, ratings, peers, holdings").argument("<symbols...>", "Ticker symbols (e.g. AAPL MSFT)").option("--fields <fields>", "Comma-separated: " + VALID_FIELDS.join(","))
|
|
322
911
|
).action(async (symbols, opts) => {
|
|
323
|
-
const
|
|
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
|
}
|
|
@@ -382,10 +968,10 @@ Examples:
|
|
|
382
968
|
$ bloom news AAPL MSFT GOOGL News for multiple stocks
|
|
383
969
|
$ bloom news TSLA --limit 5 Last 5 Tesla articles
|
|
384
970
|
`).action(async (symbols, opts) => {
|
|
385
|
-
const data = await
|
|
386
|
-
symbols
|
|
387
|
-
|
|
388
|
-
|
|
971
|
+
const data = await BloomClient.instance().getNews(
|
|
972
|
+
symbols.map((s) => s.toUpperCase()),
|
|
973
|
+
parseIntArg(opts.limit, "--limit")
|
|
974
|
+
);
|
|
389
975
|
outputResult(data, opts, (d, o) => formatNews(d, o));
|
|
390
976
|
});
|
|
391
977
|
}
|
|
@@ -424,11 +1010,11 @@ Examples:
|
|
|
424
1010
|
$ bloom price MSFT --ohlc Include open/high/low/close
|
|
425
1011
|
$ bloom price AAPL -f csv -o prices.csv Export to CSV file
|
|
426
1012
|
`).action(async (symbol, opts) => {
|
|
427
|
-
const data = await
|
|
428
|
-
symbol
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
1013
|
+
const data = await BloomClient.instance().getPrice(
|
|
1014
|
+
symbol.toUpperCase(),
|
|
1015
|
+
opts.timeframes.split(",").map((t) => t.trim()),
|
|
1016
|
+
!!opts.ohlc
|
|
1017
|
+
);
|
|
432
1018
|
outputResult(data, opts, (d, o) => formatPrices(d, o));
|
|
433
1019
|
});
|
|
434
1020
|
}
|
|
@@ -478,7 +1064,8 @@ function formatPrices(data, opts) {
|
|
|
478
1064
|
}
|
|
479
1065
|
|
|
480
1066
|
// src/commands/financials.ts
|
|
481
|
-
|
|
1067
|
+
import { Option as Option2 } from "commander";
|
|
1068
|
+
var VALID_TYPES = [
|
|
482
1069
|
"revenue",
|
|
483
1070
|
"revenue_growth",
|
|
484
1071
|
"operating_margin",
|
|
@@ -491,18 +1078,18 @@ var CHART_TYPES = [
|
|
|
491
1078
|
];
|
|
492
1079
|
function registerFinancialsCommand(program2) {
|
|
493
1080
|
addGlobalOptions(
|
|
494
|
-
program2.command("financials").description("
|
|
1081
|
+
program2.command("financials").description("Financial charts: revenue, margins, earnings, FCF").argument("<symbol>", "Ticker symbol").addOption(
|
|
1082
|
+
new Option2("--type <type>", "Chart type").choices(VALID_TYPES).default("revenue")
|
|
1083
|
+
)
|
|
495
1084
|
).addHelpText("after", `
|
|
496
1085
|
Examples:
|
|
497
|
-
$ bloom financials AAPL
|
|
498
|
-
$ bloom financials TSLA --type operating_margin
|
|
499
|
-
$ bloom financials MSFT --type free_cash_flow
|
|
500
|
-
$ bloom financials NVDA --type
|
|
1086
|
+
$ bloom financials AAPL Revenue (default)
|
|
1087
|
+
$ bloom financials TSLA --type operating_margin Operating margins
|
|
1088
|
+
$ bloom financials MSFT --type free_cash_flow Free cash flow
|
|
1089
|
+
$ bloom financials NVDA --type revenue_growth Revenue growth rate
|
|
501
1090
|
`).action(async (symbol, opts) => {
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
chart_type: opts.type
|
|
505
|
-
});
|
|
1091
|
+
const sym = symbol.toUpperCase();
|
|
1092
|
+
const data = await BloomClient.instance().getFinancials(sym, opts.type);
|
|
506
1093
|
outputResult(data, opts);
|
|
507
1094
|
});
|
|
508
1095
|
}
|
|
@@ -522,11 +1109,8 @@ Examples:
|
|
|
522
1109
|
$ bloom sentiment All sentiment indicators
|
|
523
1110
|
$ bloom sentiment --sections aaii_sentiment,cnn_fear_greed Just AAII and Fear & Greed
|
|
524
1111
|
`).action(async (opts) => {
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
args.sections = opts.sections.split(",").map((s) => s.trim());
|
|
528
|
-
}
|
|
529
|
-
const data = await callTool("get_market_sentiment", args);
|
|
1112
|
+
const sections = opts.sections?.split(",").map((s) => s.trim());
|
|
1113
|
+
const data = await BloomClient.instance().getSentiment(sections);
|
|
530
1114
|
outputResult(data, opts, (d) => formatSentiment(d));
|
|
531
1115
|
});
|
|
532
1116
|
}
|
|
@@ -565,12 +1149,12 @@ Examples:
|
|
|
565
1149
|
$ bloom market --type top_movers --period 1w Weekly top movers
|
|
566
1150
|
$ bloom market --universe sp500 --limit 10 Top 10 S&P 500 movers
|
|
567
1151
|
`).action(async (opts) => {
|
|
568
|
-
const data = await
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1152
|
+
const data = await BloomClient.instance().getMarket(
|
|
1153
|
+
opts.type,
|
|
1154
|
+
opts.period,
|
|
1155
|
+
parseIntArg(opts.limit, "--limit"),
|
|
1156
|
+
opts.universe
|
|
1157
|
+
);
|
|
574
1158
|
outputResult(data, opts);
|
|
575
1159
|
});
|
|
576
1160
|
}
|
|
@@ -586,10 +1170,10 @@ Examples:
|
|
|
586
1170
|
$ bloom collections --section FAMOUS_INVESTORS Famous investor portfolios
|
|
587
1171
|
$ bloom collections --section TRENDS --limit 3 Top 3 trending collections
|
|
588
1172
|
`).action(async (opts) => {
|
|
589
|
-
const data = await
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1173
|
+
const data = await BloomClient.instance().getCollections(
|
|
1174
|
+
opts.section,
|
|
1175
|
+
parseIntArg(opts.limit, "--limit")
|
|
1176
|
+
);
|
|
593
1177
|
outputResult(data, opts);
|
|
594
1178
|
});
|
|
595
1179
|
}
|
|
@@ -603,7 +1187,7 @@ function registerScreenCommand(program2) {
|
|
|
603
1187
|
const args = {
|
|
604
1188
|
filter_expressions: filters,
|
|
605
1189
|
and_query: !opts.or,
|
|
606
|
-
limit:
|
|
1190
|
+
limit: parseIntArg(opts.limit, "--limit")
|
|
607
1191
|
};
|
|
608
1192
|
if (opts.symbols) {
|
|
609
1193
|
args.symbols = opts.symbols.split(",").map((s) => s.trim().toUpperCase());
|
|
@@ -611,7 +1195,7 @@ function registerScreenCommand(program2) {
|
|
|
611
1195
|
if (opts.fields) {
|
|
612
1196
|
args.fields = opts.fields.split(",").map((f) => f.trim());
|
|
613
1197
|
}
|
|
614
|
-
const data = await
|
|
1198
|
+
const data = await BloomClient.instance().screen(args);
|
|
615
1199
|
outputResult(data, opts, (d, o) => formatScreen(d, o));
|
|
616
1200
|
}
|
|
617
1201
|
);
|
|
@@ -648,42 +1232,52 @@ function formatScreen(data, opts) {
|
|
|
648
1232
|
return makeTable(headers, rows, opts);
|
|
649
1233
|
}
|
|
650
1234
|
|
|
1235
|
+
// src/limits.ts
|
|
1236
|
+
var MAX_ANALYSIS_SYMBOLS = 10;
|
|
1237
|
+
|
|
651
1238
|
// src/commands/earnings.ts
|
|
652
1239
|
function registerEarningsCommand(program2) {
|
|
653
1240
|
addGlobalOptions(
|
|
654
|
-
program2.command("earnings").description("Earnings history, estimates, surprises").argument("<symbols...>", "Ticker symbols (up to
|
|
1241
|
+
program2.command("earnings").description("Earnings history, estimates, surprises").argument("<symbols...>", "Ticker symbols (up to 10)")
|
|
655
1242
|
).addHelpText("after", `
|
|
656
1243
|
Examples:
|
|
657
1244
|
$ bloom earnings AAPL Apple earnings history and surprises
|
|
658
1245
|
$ bloom earnings AAPL MSFT GOOGL Compare earnings for multiple stocks
|
|
659
1246
|
`).action(async (symbols, opts) => {
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
});
|
|
1247
|
+
const syms = symbols.map((s) => s.toUpperCase()).slice(0, MAX_ANALYSIS_SYMBOLS);
|
|
1248
|
+
const data = await BloomClient.instance().getEarnings(syms);
|
|
663
1249
|
outputResult(data, opts);
|
|
664
1250
|
});
|
|
665
1251
|
}
|
|
666
1252
|
|
|
667
1253
|
// src/commands/earnings-calendar.ts
|
|
1254
|
+
function formatEps(value, currency) {
|
|
1255
|
+
if (value == null) return "\u2014";
|
|
1256
|
+
return !currency || currency === "USD" ? `$${value}` : `${value} ${currency}`;
|
|
1257
|
+
}
|
|
668
1258
|
function registerEarningsCalendarCommand(program2) {
|
|
669
1259
|
addGlobalOptions(
|
|
670
1260
|
program2.command("earnings-calendar").description(
|
|
671
1261
|
"Upcoming earnings dates (all companies, or filter by symbols)"
|
|
672
|
-
).argument("[symbols...]", "Ticker symbols (omit to show all)").option("--days <n>", "Look-ahead window in days (1-90)", "28")
|
|
1262
|
+
).argument("[symbols...]", "Ticker symbols (omit to show all)").option("--days <n>", "Look-ahead window in days (1-90)", "28").option(
|
|
1263
|
+
"--popular",
|
|
1264
|
+
"Also include top companies by market cap, not just the given symbols"
|
|
1265
|
+
)
|
|
673
1266
|
).addHelpText("after", `
|
|
674
1267
|
Examples:
|
|
675
1268
|
$ bloom earnings-calendar AAPL MSFT GOOGL Upcoming earnings for these stocks
|
|
676
1269
|
$ bloom earnings-calendar TSLA --days 7 Earnings within the next week
|
|
677
1270
|
$ bloom earnings-calendar NVDA --days 90 Earnings within 90 days
|
|
1271
|
+
$ bloom earnings-calendar AAPL --popular AAPL plus the popular calendar
|
|
678
1272
|
`).action(
|
|
679
1273
|
async (symbols, opts) => {
|
|
680
|
-
const days =
|
|
1274
|
+
const days = parseIntArg(opts.days, "--days", 1, 90);
|
|
681
1275
|
const params = { days: String(days) };
|
|
682
1276
|
if (symbols.length > 0) {
|
|
683
1277
|
params.symbols = symbols.map((s) => s.toUpperCase()).join(",");
|
|
1278
|
+
if (!opts.popular) params.include_popular = "false";
|
|
684
1279
|
}
|
|
685
|
-
const data = await
|
|
686
|
-
"/markets/earnings-calendar/",
|
|
1280
|
+
const data = await BloomClient.instance().getEarningsCalendar(
|
|
687
1281
|
params
|
|
688
1282
|
);
|
|
689
1283
|
const items = data.results ?? [];
|
|
@@ -696,8 +1290,8 @@ Examples:
|
|
|
696
1290
|
r.earnings_date ?? "\u2014",
|
|
697
1291
|
r.symbol,
|
|
698
1292
|
r.name,
|
|
699
|
-
r.eps_estimate
|
|
700
|
-
r.eps_actual
|
|
1293
|
+
formatEps(r.eps_estimate, r.currency),
|
|
1294
|
+
formatEps(r.eps_actual, r.currency)
|
|
701
1295
|
]),
|
|
702
1296
|
o
|
|
703
1297
|
);
|
|
@@ -710,7 +1304,7 @@ Examples:
|
|
|
710
1304
|
var SECTIONS2 = ["technical_indicators", "options_analysis", "insider_activity"];
|
|
711
1305
|
function registerTechnicalsCommand(program2) {
|
|
712
1306
|
addGlobalOptions(
|
|
713
|
-
program2.command("technicals").description("RSI, MAs, patterns, options flow, insider trades").argument("<symbols...>", "Ticker symbols (up to
|
|
1307
|
+
program2.command("technicals").description("RSI, MAs, patterns, options flow, insider trades").argument("<symbols...>", "Ticker symbols (up to 10)").option("--sections <s>", "Comma-separated: " + SECTIONS2.join(","))
|
|
714
1308
|
).addHelpText("after", `
|
|
715
1309
|
Examples:
|
|
716
1310
|
$ bloom technicals AAPL All technical data
|
|
@@ -718,13 +1312,9 @@ Examples:
|
|
|
718
1312
|
$ bloom technicals NVDA --sections options_analysis Options flow only
|
|
719
1313
|
$ bloom technicals AAPL --sections insider_activity Insider trades only
|
|
720
1314
|
`).action(async (symbols, opts) => {
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (opts.sections) {
|
|
725
|
-
args.sections = opts.sections.split(",").map((s) => s.trim());
|
|
726
|
-
}
|
|
727
|
-
const data = await callTool("analyze_technicals", args);
|
|
1315
|
+
const syms = symbols.map((s) => s.toUpperCase()).slice(0, MAX_ANALYSIS_SYMBOLS);
|
|
1316
|
+
const sections = opts.sections?.split(",").map((s) => s.trim());
|
|
1317
|
+
const data = await BloomClient.instance().getTechnicals(syms, sections);
|
|
728
1318
|
outputResult(data, opts);
|
|
729
1319
|
});
|
|
730
1320
|
}
|
|
@@ -741,12 +1331,12 @@ Examples:
|
|
|
741
1331
|
$ bloom options-history AAPL --greeks Include advanced Greeks analysis
|
|
742
1332
|
`).action(
|
|
743
1333
|
async (symbol, opts) => {
|
|
744
|
-
const data = await
|
|
745
|
-
symbol
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1334
|
+
const data = await BloomClient.instance().getOptions(
|
|
1335
|
+
symbol.toUpperCase(),
|
|
1336
|
+
parseIntArg(opts.days, "--days", 7, 90),
|
|
1337
|
+
opts.weekly ?? !opts.daily,
|
|
1338
|
+
!!opts.greeks
|
|
1339
|
+
);
|
|
750
1340
|
outputResult(data, opts);
|
|
751
1341
|
}
|
|
752
1342
|
);
|
|
@@ -761,12 +1351,14 @@ function registerPortfolioCommand(program2) {
|
|
|
761
1351
|
try {
|
|
762
1352
|
holdings = JSON.parse(holdingsStr);
|
|
763
1353
|
} catch {
|
|
764
|
-
console.error(`Error: Invalid JSON for holdings. Use format: '{"AAPL":0.3,"MSFT":0.7}'`);
|
|
765
|
-
process.exit(1);
|
|
766
1354
|
}
|
|
767
|
-
const
|
|
768
|
-
if (
|
|
769
|
-
|
|
1355
|
+
const valid = holdings !== null && typeof holdings === "object" && !Array.isArray(holdings) && Object.keys(holdings).length > 0 && Object.values(holdings).every((w) => typeof w === "number" && Number.isFinite(w));
|
|
1356
|
+
if (!valid) {
|
|
1357
|
+
throw new BloomError(
|
|
1358
|
+
`holdings must be a JSON object of symbol to numeric weight, e.g. '{"AAPL":0.3,"MSFT":0.7}'`
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
const data = await BloomClient.instance().analyzePortfolio(holdings, opts.strategy);
|
|
770
1362
|
outputResult(data, opts);
|
|
771
1363
|
});
|
|
772
1364
|
}
|
|
@@ -781,7 +1373,7 @@ Examples:
|
|
|
781
1373
|
$ bloom trades "Sell MSFT 20% to take profits" "Buy GOOGL 15% on AI momentum"
|
|
782
1374
|
`).action(async (tradeStrs, opts) => {
|
|
783
1375
|
const trades = tradeStrs.map(parseTrade);
|
|
784
|
-
const data = await
|
|
1376
|
+
const data = await BloomClient.instance().evaluateTrades(trades);
|
|
785
1377
|
outputResult(data, opts);
|
|
786
1378
|
});
|
|
787
1379
|
}
|
|
@@ -811,15 +1403,15 @@ Format: "Buy AAPL 30% because thesis" or JSON object`
|
|
|
811
1403
|
// src/commands/catalysts.ts
|
|
812
1404
|
function registerCatalystsCommand(program2) {
|
|
813
1405
|
addGlobalOptions(
|
|
814
|
-
program2.command("catalysts").description("Forward-looking value catalysts").argument("<symbols...>", "Ticker symbols (up to
|
|
1406
|
+
program2.command("catalysts").description("Forward-looking value catalysts").argument("<symbols...>", "Ticker symbols (up to 10)")
|
|
815
1407
|
).addHelpText("after", `
|
|
816
1408
|
Examples:
|
|
817
1409
|
$ bloom catalysts AAPL Upcoming catalysts for Apple
|
|
818
1410
|
$ bloom catalysts TSLA NVDA AMZN Catalysts for multiple stocks
|
|
819
1411
|
`).action(async (symbols, opts) => {
|
|
820
|
-
const data = await
|
|
821
|
-
symbols
|
|
822
|
-
|
|
1412
|
+
const data = await BloomClient.instance().getCatalysts(
|
|
1413
|
+
symbols.map((s) => s.toUpperCase()).slice(0, MAX_ANALYSIS_SYMBOLS)
|
|
1414
|
+
);
|
|
823
1415
|
outputResult(data, opts);
|
|
824
1416
|
});
|
|
825
1417
|
}
|
|
@@ -848,11 +1440,11 @@ function registerPositionCommand(program2) {
|
|
|
848
1440
|
const args = {
|
|
849
1441
|
bull_return: bullReturn,
|
|
850
1442
|
bear_return: bearReturn,
|
|
851
|
-
conviction:
|
|
1443
|
+
conviction: parseIntArg(opts.conviction, "--conviction", 1, 10),
|
|
852
1444
|
portfolio_value: portfolioValue
|
|
853
1445
|
};
|
|
854
1446
|
if (opts.symbol) args.symbol = opts.symbol.toUpperCase();
|
|
855
|
-
const data = await
|
|
1447
|
+
const data = await BloomClient.instance().positionSize(args);
|
|
856
1448
|
outputResult(data, opts);
|
|
857
1449
|
}
|
|
858
1450
|
);
|
|
@@ -861,16 +1453,16 @@ function registerPositionCommand(program2) {
|
|
|
861
1453
|
// src/commands/transcript.ts
|
|
862
1454
|
function registerTranscriptCommand(program2) {
|
|
863
1455
|
addGlobalOptions(
|
|
864
|
-
program2.command("transcript").description("Search earnings call transcripts").argument("<symbol>", "Ticker symbol").argument("<keywords...>", "Keywords to search (OR logic)").option("--quarter <q>", 'Fiscal quarter, e.g. "Q1 2024"').option("--max <n>", "Max results", "10")
|
|
1456
|
+
program2.command("transcript").alias("transcripts").description("Search earnings call transcripts").argument("<symbol>", "Ticker symbol").argument("<keywords...>", "Keywords to search (OR logic)").option("--quarter <q>", 'Fiscal quarter, e.g. "Q1 2024"').option("--max <n>", "Max results", "10")
|
|
865
1457
|
).action(
|
|
866
1458
|
async (symbol, keywords, opts) => {
|
|
867
1459
|
const args = {
|
|
868
1460
|
symbol: symbol.toUpperCase(),
|
|
869
1461
|
keywords,
|
|
870
|
-
max_results:
|
|
1462
|
+
max_results: parseIntArg(opts.max, "--max")
|
|
871
1463
|
};
|
|
872
1464
|
if (opts.quarter) args.fiscal_quarter = opts.quarter;
|
|
873
|
-
const data = await
|
|
1465
|
+
const data = await BloomClient.instance().searchTranscript(args);
|
|
874
1466
|
outputResult(data, opts, (d, o) => formatTranscript(d, o));
|
|
875
1467
|
}
|
|
876
1468
|
);
|
|
@@ -913,11 +1505,11 @@ function registerAiTradesCommand(program2) {
|
|
|
913
1505
|
program2.command("ai-trades").description("Recent trade decisions from AI models competing with real money (InvestingArena)").option("--model <m>", "Filter by AI model: claude|openai|gemini").option("--symbol <s>", "Filter by stock ticker symbol (e.g., AAPL)").option("--limit <n>", "Max recent trades per model (default: 10, max: 50)", "10")
|
|
914
1506
|
).action(async (opts) => {
|
|
915
1507
|
const args = {
|
|
916
|
-
limit:
|
|
1508
|
+
limit: parseIntArg(opts.limit, "--limit", 1, 50)
|
|
917
1509
|
};
|
|
918
1510
|
if (opts.model) args.ai_model = opts.model;
|
|
919
1511
|
if (opts.symbol) args.symbol = opts.symbol.toUpperCase();
|
|
920
|
-
const data = await
|
|
1512
|
+
const data = await BloomClient.instance().getAiTrades(args);
|
|
921
1513
|
if (opts.raw) {
|
|
922
1514
|
outputResult(data, opts);
|
|
923
1515
|
return;
|
|
@@ -945,7 +1537,7 @@ function formatTrades(data, opts) {
|
|
|
945
1537
|
String(t.shares ?? "-"),
|
|
946
1538
|
t.fill_price != null ? `$${Number(t.fill_price).toFixed(2)}` : "-",
|
|
947
1539
|
gain,
|
|
948
|
-
|
|
1540
|
+
truncate(String(t.rationale ?? "-"), 60)
|
|
949
1541
|
];
|
|
950
1542
|
});
|
|
951
1543
|
return makeTable(
|
|
@@ -954,9 +1546,6 @@ function formatTrades(data, opts) {
|
|
|
954
1546
|
opts
|
|
955
1547
|
);
|
|
956
1548
|
}
|
|
957
|
-
function truncate2(s, max) {
|
|
958
|
-
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
959
|
-
}
|
|
960
1549
|
|
|
961
1550
|
// src/commands/ai-portfolio.ts
|
|
962
1551
|
function registerAiPortfolioCommand(program2) {
|
|
@@ -970,7 +1559,7 @@ Examples:
|
|
|
970
1559
|
`).action(async (opts) => {
|
|
971
1560
|
const args = {};
|
|
972
1561
|
if (opts.model) args.ai_model = opts.model;
|
|
973
|
-
const data = await
|
|
1562
|
+
const data = await BloomClient.instance().getAiPortfolio(args);
|
|
974
1563
|
if (opts.raw) {
|
|
975
1564
|
outputResult(data, opts);
|
|
976
1565
|
return;
|
|
@@ -1077,6 +1666,7 @@ function registerMaCommand(program2) {
|
|
|
1077
1666
|
program2.command("ma").description("Moving averages (SMA/EMA) for a symbol").argument("<symbol>", "Ticker symbol (e.g., AAPL)").option("--sma <periods>", "Comma-separated SMA periods (e.g., 50,100,200)").option("--ema <periods>", "Comma-separated EMA periods (e.g., 20,50)").option("--limit <n>", "Number of data points to display (default: 20)", "20")
|
|
1078
1667
|
).action(
|
|
1079
1668
|
async (symbol, opts) => {
|
|
1669
|
+
const limit = parseIntArg(opts.limit, "--limit");
|
|
1080
1670
|
const indicators = [];
|
|
1081
1671
|
if (opts.sma) {
|
|
1082
1672
|
for (const p of opts.sma.split(",")) {
|
|
@@ -1093,12 +1683,10 @@ function registerMaCommand(program2) {
|
|
|
1093
1683
|
if (indicators.length === 0) {
|
|
1094
1684
|
indicators.push("sma_100", "ema_100");
|
|
1095
1685
|
}
|
|
1096
|
-
const
|
|
1097
|
-
symbol
|
|
1686
|
+
const data = await BloomClient.instance().getMovingAverages(
|
|
1687
|
+
symbol.toUpperCase(),
|
|
1098
1688
|
indicators
|
|
1099
|
-
|
|
1100
|
-
const data = await callTool("get_moving_averages", args);
|
|
1101
|
-
const limit = parseInt(opts.limit, 10) || 20;
|
|
1689
|
+
);
|
|
1102
1690
|
let parsed = null;
|
|
1103
1691
|
try {
|
|
1104
1692
|
parsed = typeof data === "string" ? JSON.parse(data) : data;
|
|
@@ -1142,7 +1730,7 @@ function registerBacktestCommand(program2) {
|
|
|
1142
1730
|
addGlobalOptions(
|
|
1143
1731
|
program2.command("backtest").description("Backtest a portfolio against a benchmark").requiredOption("--symbols <s>", "Comma-separated symbols (e.g., AAPL,MSFT)").requiredOption("--allocations <a>", "Comma-separated weights (e.g., 0.5,0.5)").option("--benchmark <b>", "Benchmark symbol (default: SPY)", "SPY").option("--strategy <s>", "Strategy: buy_and_hold", "buy_and_hold")
|
|
1144
1732
|
).action(async (opts) => {
|
|
1145
|
-
const data = await
|
|
1733
|
+
const data = await BloomClient.instance().backtest({
|
|
1146
1734
|
symbols: opts.symbols,
|
|
1147
1735
|
allocations: opts.allocations,
|
|
1148
1736
|
benchmark: opts.benchmark,
|
|
@@ -1169,7 +1757,9 @@ function formatBacktest(data) {
|
|
|
1169
1757
|
]);
|
|
1170
1758
|
}
|
|
1171
1759
|
if (rows.length === 0) return JSON.stringify(data, null, 2);
|
|
1172
|
-
|
|
1760
|
+
const table = makeTable(["Interval", "Return", "Benchmark", "Alpha", "Sharpe", "Max DD"], rows);
|
|
1761
|
+
const warnings = Array.isArray(obj.warnings) ? obj.warnings : [];
|
|
1762
|
+
return [table, ...warnings.map((w) => `Warning: ${w}`)].join("\n");
|
|
1173
1763
|
}
|
|
1174
1764
|
function fmt(v) {
|
|
1175
1765
|
if (v == null || v === "") return "-";
|
|
@@ -1199,7 +1789,7 @@ Examples:
|
|
|
1199
1789
|
};
|
|
1200
1790
|
if (opts.start) params.start_date = opts.start;
|
|
1201
1791
|
if (opts.end) params.end_date = opts.end;
|
|
1202
|
-
const data = await
|
|
1792
|
+
const data = await BloomClient.instance().correlation(params);
|
|
1203
1793
|
outputResult(data, opts, formatCorrelation);
|
|
1204
1794
|
});
|
|
1205
1795
|
}
|
|
@@ -1216,29 +1806,56 @@ function formatCorrelation(data) {
|
|
|
1216
1806
|
}
|
|
1217
1807
|
|
|
1218
1808
|
// src/commands/risk.ts
|
|
1809
|
+
var STALE_DAYS = 456;
|
|
1219
1810
|
function registerRiskCommand(program2) {
|
|
1220
1811
|
addGlobalOptions(
|
|
1221
|
-
program2.command("risk").description("Risk factors
|
|
1812
|
+
program2.command("risk").description("Risk factors from the company's latest ingested SEC 10-K (check filingDate)").argument("<symbol>", "Ticker symbol")
|
|
1222
1813
|
).addHelpText("after", `
|
|
1814
|
+
Each row carries filingDate/periodEnd for its 10-K (null on older rows; use
|
|
1815
|
+
created). A warning goes to stderr when the source is over ~15 months old.
|
|
1816
|
+
|
|
1223
1817
|
Examples:
|
|
1224
1818
|
$ bloom risk AAPL Risk factors for Apple
|
|
1225
1819
|
$ bloom risk TSLA Risk factors for Tesla
|
|
1226
1820
|
`).action(async (symbol, opts) => {
|
|
1227
|
-
const data = await
|
|
1821
|
+
const data = await BloomClient.instance().getRisk(symbol.toUpperCase());
|
|
1822
|
+
const warning = staleWarning(riskRows(data));
|
|
1823
|
+
if (warning && !opts.quiet) console.error(warning);
|
|
1228
1824
|
outputResult(data, opts, formatRisk);
|
|
1229
1825
|
});
|
|
1230
1826
|
}
|
|
1231
|
-
function
|
|
1232
|
-
|
|
1233
|
-
if (data.length === 0) return "(no risk factors found)";
|
|
1234
|
-
const rows = data.map((r) => [
|
|
1235
|
-
String(r.category ?? r.risk_type ?? "-"),
|
|
1236
|
-
truncate3(String(r.description ?? r.text ?? "-"), 80)
|
|
1237
|
-
]);
|
|
1238
|
-
return makeTable(["Category", "Description"], rows);
|
|
1827
|
+
function riskRows(data) {
|
|
1828
|
+
return data?.results ?? [];
|
|
1239
1829
|
}
|
|
1240
|
-
function
|
|
1241
|
-
|
|
1830
|
+
function sourceInfo(rows) {
|
|
1831
|
+
let best = null;
|
|
1832
|
+
for (const r of rows) {
|
|
1833
|
+
const raw = r.filingDate ?? r.created;
|
|
1834
|
+
if (!raw) continue;
|
|
1835
|
+
const date = String(raw).slice(0, 10);
|
|
1836
|
+
if (!best || date > best.date) best = { date, how: r.filingDate ? "filed" : "ingested" };
|
|
1837
|
+
}
|
|
1838
|
+
return best;
|
|
1839
|
+
}
|
|
1840
|
+
function staleWarning(rows) {
|
|
1841
|
+
const src = sourceInfo(rows);
|
|
1842
|
+
if (!src) return null;
|
|
1843
|
+
const ageDays = (Date.now() - Date.parse(src.date)) / 864e5;
|
|
1844
|
+
if (ageDays <= STALE_DAYS) return null;
|
|
1845
|
+
return `Warning: these risk factors come from a 10-K ${src.how} ${src.date} (${Math.floor(ageDays / 30)} months ago) and may be out of date.`;
|
|
1846
|
+
}
|
|
1847
|
+
function formatRisk(data) {
|
|
1848
|
+
const rows = riskRows(data);
|
|
1849
|
+
if (rows.length === 0) return "(no risk factors found)";
|
|
1850
|
+
const src = sourceInfo(rows);
|
|
1851
|
+
const period = rows.find((r) => r.periodEnd)?.periodEnd;
|
|
1852
|
+
const header = `Source: SEC 10-K${src ? ` ${src.how} ${src.date}` : ""}${period ? ` (fiscal year ended ${period})` : ""}`;
|
|
1853
|
+
const table = makeTable(
|
|
1854
|
+
["Risk"],
|
|
1855
|
+
rows.map((r) => [truncate(String(r.keyPoint ?? r.details ?? "-"), 100)])
|
|
1856
|
+
);
|
|
1857
|
+
return `${header}
|
|
1858
|
+
${table}`;
|
|
1242
1859
|
}
|
|
1243
1860
|
|
|
1244
1861
|
// src/commands/drawdown.ts
|
|
@@ -1246,52 +1863,24 @@ function registerDrawdownCommand(program2) {
|
|
|
1246
1863
|
addGlobalOptions(
|
|
1247
1864
|
program2.command("drawdown").description("Portfolio drawdown analysis").requiredOption("--symbols <s>", "Comma-separated symbols (e.g., AAPL,MSFT)").requiredOption("--allocations <a>", "Comma-separated weights (e.g., 0.5,0.5)")
|
|
1248
1865
|
).action(async (opts) => {
|
|
1249
|
-
const data = await
|
|
1866
|
+
const data = await BloomClient.instance().getDrawdown({
|
|
1250
1867
|
symbols: opts.symbols,
|
|
1251
1868
|
allocations: opts.allocations
|
|
1252
1869
|
});
|
|
1253
1870
|
outputResult(data, opts, formatDrawdown);
|
|
1254
1871
|
});
|
|
1255
1872
|
}
|
|
1256
|
-
function formatDrawdown(data) {
|
|
1257
|
-
const
|
|
1258
|
-
if (!
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
if (plotData && plotData.length > 0) {
|
|
1268
|
-
let minDD = 0;
|
|
1269
|
-
let minIdx = -1;
|
|
1270
|
-
for (let i = 0; i < plotData.length; i++) {
|
|
1271
|
-
if (plotData[i].drawdown < minDD) {
|
|
1272
|
-
minDD = plotData[i].drawdown;
|
|
1273
|
-
minIdx = i;
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
if (minIdx >= 0) {
|
|
1277
|
-
troughDate = plotData[minIdx].date;
|
|
1278
|
-
for (let i = minIdx + 1; i < plotData.length; i++) {
|
|
1279
|
-
if (plotData[i].drawdown >= 0) {
|
|
1280
|
-
recoveryDate = plotData[i].date;
|
|
1281
|
-
break;
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
rows.push([
|
|
1287
|
-
interval,
|
|
1288
|
-
d.max_drawdown != null ? `${(Number(d.max_drawdown) * 100).toFixed(2)}%` : "-",
|
|
1289
|
-
troughDate,
|
|
1290
|
-
recoveryDate
|
|
1291
|
-
]);
|
|
1292
|
-
}
|
|
1293
|
-
if (rows.length === 0) return JSON.stringify(data, null, 2);
|
|
1294
|
-
return makeTable(["Interval", "Max Drawdown", "Trough Date", "Recovery"], rows);
|
|
1873
|
+
function formatDrawdown(data, opts) {
|
|
1874
|
+
const intervals = data?.interval_results;
|
|
1875
|
+
if (!intervals) return JSON.stringify(data, null, 2);
|
|
1876
|
+
const rows = Object.entries(intervals).map(([iv, r]) => [iv, fmt2(r.max_drawdown ?? r.error)]);
|
|
1877
|
+
return makeTable(["Interval", "Max Drawdown"], rows, opts);
|
|
1878
|
+
}
|
|
1879
|
+
function fmt2(v) {
|
|
1880
|
+
if (v == null) return "-";
|
|
1881
|
+
const n = Number(v);
|
|
1882
|
+
if (isNaN(n)) return String(v);
|
|
1883
|
+
return `${(n * 100).toFixed(2)}%`;
|
|
1295
1884
|
}
|
|
1296
1885
|
|
|
1297
1886
|
// src/commands/macro.ts
|
|
@@ -1308,7 +1897,7 @@ Examples:
|
|
|
1308
1897
|
if (opts.indicators) {
|
|
1309
1898
|
args.indicators = opts.indicators;
|
|
1310
1899
|
}
|
|
1311
|
-
const data = await
|
|
1900
|
+
const data = await BloomClient.instance().getMacro(args);
|
|
1312
1901
|
outputResult(data, opts, formatMacro);
|
|
1313
1902
|
});
|
|
1314
1903
|
}
|
|
@@ -1345,7 +1934,7 @@ Examples:
|
|
|
1345
1934
|
$ bloom sectors Sector performance across all time periods
|
|
1346
1935
|
$ bloom sectors -f json Output as JSON
|
|
1347
1936
|
`).action(async (opts) => {
|
|
1348
|
-
const data = await
|
|
1937
|
+
const data = await BloomClient.instance().getSectors();
|
|
1349
1938
|
outputResult(data, opts, formatSectors);
|
|
1350
1939
|
});
|
|
1351
1940
|
}
|
|
@@ -1404,7 +1993,7 @@ Examples:
|
|
|
1404
1993
|
$ bloom dcf TSLA DCF valuation for Tesla
|
|
1405
1994
|
`).action(async (symbol, opts) => {
|
|
1406
1995
|
const sym = symbol.toUpperCase();
|
|
1407
|
-
const result = await
|
|
1996
|
+
const result = await BloomClient.instance().getDcf(sym);
|
|
1408
1997
|
outputResult(result, opts, () => formatDcf(result, sym));
|
|
1409
1998
|
});
|
|
1410
1999
|
}
|
|
@@ -1428,39 +2017,27 @@ function formatDcf(data, symbol) {
|
|
|
1428
2017
|
// src/commands/holders.ts
|
|
1429
2018
|
function registerHoldersCommand(program2) {
|
|
1430
2019
|
addGlobalOptions(
|
|
1431
|
-
program2.command("holders").description("Institutional
|
|
2020
|
+
program2.command("holders").description("Institutional holders of a stock").argument("<symbol>", "Ticker symbol")
|
|
1432
2021
|
).addHelpText("after", `
|
|
1433
2022
|
Examples:
|
|
1434
|
-
$ bloom holders AAPL
|
|
1435
|
-
$ bloom holders TSLA
|
|
1436
|
-
$ bloom holders NVDA --limit 5 Top 5 holders
|
|
2023
|
+
$ bloom holders AAPL Top institutional holders of Apple
|
|
2024
|
+
$ bloom holders TSLA Tesla's institutional ownership
|
|
1437
2025
|
`).action(async (symbol, opts) => {
|
|
1438
2026
|
const sym = symbol.toUpperCase();
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
if (opts.type) args.holder_type = opts.type;
|
|
1442
|
-
const data = await callTool("get_holders", args);
|
|
1443
|
-
outputResult(data, opts, () => formatHolders(data, limit));
|
|
2027
|
+
const data = await BloomClient.instance().getHolders(sym);
|
|
2028
|
+
outputResult(data, opts, formatHolders);
|
|
1444
2029
|
});
|
|
1445
2030
|
}
|
|
1446
|
-
function formatHolders(data
|
|
1447
|
-
|
|
1448
|
-
const
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
]);
|
|
1457
|
-
parts.push(`${title}
|
|
1458
|
-
${makeTable(["Holder", "Shares", "Date", "Change"], rows)}`);
|
|
1459
|
-
}
|
|
1460
|
-
};
|
|
1461
|
-
addSection("INSTITUTIONAL HOLDERS", data.institutional);
|
|
1462
|
-
addSection("MUTUAL FUND HOLDERS", data.mutual_fund);
|
|
1463
|
-
return parts.length > 0 ? parts.join("\n\n") : "(no holder data found)";
|
|
2031
|
+
function formatHolders(data) {
|
|
2032
|
+
if (!Array.isArray(data) || data.length === 0) return "(no holders data)";
|
|
2033
|
+
const rows = data.map((h) => [
|
|
2034
|
+
String(h.holder ?? "-"),
|
|
2035
|
+
fmtNum2(h.shares),
|
|
2036
|
+
h.dateReported ? String(h.dateReported).slice(0, 10) : "-",
|
|
2037
|
+
h.change != null ? String(h.change) : "-",
|
|
2038
|
+
h.weightPercent != null ? `${Number(h.weightPercent).toFixed(2)}%` : "-"
|
|
2039
|
+
]);
|
|
2040
|
+
return makeTable(["Holder", "Shares", "Date", "Change", "Weight %"], rows);
|
|
1464
2041
|
}
|
|
1465
2042
|
function fmtNum2(v) {
|
|
1466
2043
|
if (v == null) return "-";
|
|
@@ -1482,8 +2059,8 @@ Examples:
|
|
|
1482
2059
|
$ bloom short-interest AMC --limit 20 Last 20 short interest records
|
|
1483
2060
|
`).action(async (symbol, opts) => {
|
|
1484
2061
|
const sym = symbol.toUpperCase();
|
|
1485
|
-
const limit =
|
|
1486
|
-
const data = await
|
|
2062
|
+
const limit = parseIntArg(opts.limit, "--limit");
|
|
2063
|
+
const data = await BloomClient.instance().getShortInterest(sym, limit);
|
|
1487
2064
|
outputResult(data, opts, formatShort);
|
|
1488
2065
|
});
|
|
1489
2066
|
}
|
|
@@ -1512,33 +2089,28 @@ function fmtNum3(v) {
|
|
|
1512
2089
|
// src/commands/insider.ts
|
|
1513
2090
|
function registerInsiderCommand(program2) {
|
|
1514
2091
|
addGlobalOptions(
|
|
1515
|
-
program2.command("insider").description("Insider trading activity
|
|
2092
|
+
program2.command("insider").description("Insider trading activity").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Max records", "20")
|
|
1516
2093
|
).addHelpText("after", `
|
|
1517
2094
|
Examples:
|
|
1518
2095
|
$ bloom insider AAPL Recent insider trades for Apple
|
|
1519
|
-
$ bloom insider TSLA --limit
|
|
2096
|
+
$ bloom insider TSLA --limit 10 Last 10 insider transactions
|
|
1520
2097
|
`).action(async (symbol, opts) => {
|
|
1521
2098
|
const sym = symbol.toUpperCase();
|
|
1522
|
-
const limit =
|
|
1523
|
-
const data = await
|
|
2099
|
+
const limit = parseIntArg(opts.limit, "--limit");
|
|
2100
|
+
const data = await BloomClient.instance().getInsider(sym, limit);
|
|
1524
2101
|
outputResult(data, opts, formatInsider);
|
|
1525
2102
|
});
|
|
1526
2103
|
}
|
|
1527
2104
|
function formatInsider(data) {
|
|
1528
|
-
if (!Array.isArray(data) || data.length === 0) return "(no insider
|
|
1529
|
-
const rows = data.map((
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
t.price != null ? `$${Number(t.price).toFixed(2)}` : "-",
|
|
1538
|
-
val
|
|
1539
|
-
];
|
|
1540
|
-
});
|
|
1541
|
-
return makeTable(["Date", "Name", "Title", "Type", "Shares", "Price", "Value"], rows);
|
|
2105
|
+
if (!Array.isArray(data) || data.length === 0) return "(no insider data)";
|
|
2106
|
+
const rows = data.map((r) => [
|
|
2107
|
+
String(r.filingDate ?? r.date ?? "-").slice(0, 10),
|
|
2108
|
+
String(r.reportingName ?? r.name ?? "-"),
|
|
2109
|
+
String(r.transactionType ?? r.type ?? "-"),
|
|
2110
|
+
r.securitiesTransacted != null ? fmtNum4(r.securitiesTransacted) : "-",
|
|
2111
|
+
r.price != null ? `$${Number(r.price).toFixed(2)}` : "-"
|
|
2112
|
+
]);
|
|
2113
|
+
return makeTable(["Date", "Name", "Type", "Shares", "Price"], rows);
|
|
1542
2114
|
}
|
|
1543
2115
|
function fmtNum4(v) {
|
|
1544
2116
|
if (v == null) return "-";
|
|
@@ -1558,10 +2130,10 @@ Examples:
|
|
|
1558
2130
|
$ bloom dividends JNJ --from 2024-01-01 Dividends since Jan 2024
|
|
1559
2131
|
`).action(async (symbol, opts) => {
|
|
1560
2132
|
const sym = symbol.toUpperCase();
|
|
1561
|
-
const limit =
|
|
2133
|
+
const limit = parseIntArg(opts.limit, "--limit");
|
|
1562
2134
|
const args = { symbol: sym, limit };
|
|
1563
2135
|
if (opts.from) args.from_date = opts.from;
|
|
1564
|
-
const data = await
|
|
2136
|
+
const data = await BloomClient.instance().getDividends(args);
|
|
1565
2137
|
outputResult(data, opts, formatDividends);
|
|
1566
2138
|
});
|
|
1567
2139
|
}
|
|
@@ -1590,7 +2162,7 @@ Examples:
|
|
|
1590
2162
|
const sym = symbol.toUpperCase();
|
|
1591
2163
|
const args = { symbol: sym };
|
|
1592
2164
|
if (opts.from) args.from_date = opts.from;
|
|
1593
|
-
const data = await
|
|
2165
|
+
const data = await BloomClient.instance().getSplits(args);
|
|
1594
2166
|
outputResult(data, opts, formatSplits);
|
|
1595
2167
|
});
|
|
1596
2168
|
}
|
|
@@ -1606,31 +2178,30 @@ function formatSplits(data) {
|
|
|
1606
2178
|
// src/commands/ipo.ts
|
|
1607
2179
|
function registerIpoCommand(program2) {
|
|
1608
2180
|
addGlobalOptions(
|
|
1609
|
-
program2.command("ipo").description("
|
|
2181
|
+
program2.command("ipo").description("IPO calendar and recent filings").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)")
|
|
1610
2182
|
).addHelpText("after", `
|
|
1611
2183
|
Examples:
|
|
1612
|
-
$ bloom ipo
|
|
1613
|
-
$ bloom ipo --from 2025-01-01
|
|
2184
|
+
$ bloom ipo Upcoming and recent IPOs
|
|
2185
|
+
$ bloom ipo --from 2025-01-01 IPOs since Jan 2025
|
|
1614
2186
|
`).action(async (opts) => {
|
|
1615
2187
|
const args = {};
|
|
1616
2188
|
if (opts.from) args.from_date = opts.from;
|
|
1617
2189
|
if (opts.to) args.to_date = opts.to;
|
|
1618
|
-
const data = await
|
|
1619
|
-
outputResult(data, opts,
|
|
2190
|
+
const data = await BloomClient.instance().getIpo(args);
|
|
2191
|
+
outputResult(data, opts, formatIpo);
|
|
1620
2192
|
});
|
|
1621
2193
|
}
|
|
1622
|
-
function
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
String(
|
|
1627
|
-
String(
|
|
1628
|
-
String(
|
|
1629
|
-
String(
|
|
1630
|
-
|
|
1631
|
-
ipo.shares ? fmtNum5(ipo.shares) : "-"
|
|
2194
|
+
function formatIpo(data) {
|
|
2195
|
+
if (!Array.isArray(data) || data.length === 0) return "(no IPO data)";
|
|
2196
|
+
const rows = data.map((r) => [
|
|
2197
|
+
String(r.date ?? "-"),
|
|
2198
|
+
String(r.symbol ?? "-"),
|
|
2199
|
+
String(r.company ?? r.name ?? "-"),
|
|
2200
|
+
String(r.exchange ?? "-"),
|
|
2201
|
+
r.priceRange ? String(r.priceRange) : "-",
|
|
2202
|
+
r.shares ? fmtNum5(r.shares) : "-"
|
|
1632
2203
|
]);
|
|
1633
|
-
return makeTable(["Date", "
|
|
2204
|
+
return makeTable(["Date", "Symbol", "Company", "Exchange", "Price Range", "Shares"], rows);
|
|
1634
2205
|
}
|
|
1635
2206
|
function fmtNum5(v) {
|
|
1636
2207
|
if (v == null) return "-";
|
|
@@ -1651,7 +2222,7 @@ Examples:
|
|
|
1651
2222
|
$ bloom compare TSLA RIVN LCID Compare EV manufacturers
|
|
1652
2223
|
`).action(async (symbols, opts) => {
|
|
1653
2224
|
const syms = symbols.map((s) => s.toUpperCase()).slice(0, 10);
|
|
1654
|
-
const data = await
|
|
2225
|
+
const data = await BloomClient.instance().compare(syms);
|
|
1655
2226
|
outputResult(data, opts, (d) => formatComparison(d, syms));
|
|
1656
2227
|
});
|
|
1657
2228
|
}
|
|
@@ -1662,12 +2233,10 @@ function formatComparison(data, symbols) {
|
|
|
1662
2233
|
const metrics = [
|
|
1663
2234
|
["Price", "latest_price", "$"],
|
|
1664
2235
|
["Market Cap", "market_cap", "cap"],
|
|
1665
|
-
["P/E", "
|
|
1666
|
-
["EPS", "eps", "$"],
|
|
2236
|
+
["P/E", "price_to_earnings", ""],
|
|
1667
2237
|
["Dividend Yield", "dividend_yield", "%"],
|
|
1668
2238
|
["52W High", "week_52_high", "$"],
|
|
1669
2239
|
["52W Low", "week_52_low", "$"],
|
|
1670
|
-
["Beta", "beta", ""],
|
|
1671
2240
|
["Sector", "sector", ""]
|
|
1672
2241
|
];
|
|
1673
2242
|
const bySymbol = {};
|
|
@@ -1678,15 +2247,15 @@ function formatComparison(data, symbols) {
|
|
|
1678
2247
|
bySymbol[sym] = meta;
|
|
1679
2248
|
}
|
|
1680
2249
|
const headers = ["Metric", ...symbols];
|
|
1681
|
-
const rows = metrics.map(([label, key,
|
|
2250
|
+
const rows = metrics.map(([label, key, fmt3]) => {
|
|
1682
2251
|
const vals = symbols.map((s) => {
|
|
1683
2252
|
const meta = bySymbol[s];
|
|
1684
2253
|
if (!meta) return "-";
|
|
1685
2254
|
const v = meta[key];
|
|
1686
2255
|
if (v == null) return "-";
|
|
1687
|
-
if (
|
|
1688
|
-
if (
|
|
1689
|
-
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);
|
|
1690
2259
|
return String(v);
|
|
1691
2260
|
});
|
|
1692
2261
|
return [label, ...vals];
|
|
@@ -1712,8 +2281,8 @@ Examples:
|
|
|
1712
2281
|
$ bloom ratings NVDA --limit 5 Last 5 analyst actions
|
|
1713
2282
|
`).action(async (symbol, opts) => {
|
|
1714
2283
|
const sym = symbol.toUpperCase();
|
|
1715
|
-
const limit =
|
|
1716
|
-
const result = await
|
|
2284
|
+
const limit = parseIntArg(opts.limit, "--limit");
|
|
2285
|
+
const result = await BloomClient.instance().getRatings(sym, limit);
|
|
1717
2286
|
outputResult(result, opts, () => {
|
|
1718
2287
|
const obj = result;
|
|
1719
2288
|
return formatRatings(obj.grades, obj.estimates);
|
|
@@ -1753,16 +2322,447 @@ function fmtNum6(v) {
|
|
|
1753
2322
|
return `$${n.toLocaleString()}`;
|
|
1754
2323
|
}
|
|
1755
2324
|
|
|
2325
|
+
// src/commands/analytics.ts
|
|
2326
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4 } from "fs";
|
|
2327
|
+
import { dirname as dirname2 } from "path";
|
|
2328
|
+
function parseHoldings(input) {
|
|
2329
|
+
let raw = input;
|
|
2330
|
+
try {
|
|
2331
|
+
raw = readFileSync4(input, "utf8");
|
|
2332
|
+
} catch {
|
|
2333
|
+
}
|
|
2334
|
+
try {
|
|
2335
|
+
const parsed = JSON.parse(raw);
|
|
2336
|
+
if (Array.isArray(parsed)) {
|
|
2337
|
+
return parsed.map((h) => ({
|
|
2338
|
+
symbol: String(h.symbol).toUpperCase(),
|
|
2339
|
+
value: Number(h.value),
|
|
2340
|
+
cost_basis: h.cost_basis !== void 0 ? Number(h.cost_basis) : void 0,
|
|
2341
|
+
shares: h.shares !== void 0 ? Number(h.shares) : void 0
|
|
2342
|
+
}));
|
|
2343
|
+
}
|
|
2344
|
+
return Object.entries(parsed).map(([symbol, value]) => ({
|
|
2345
|
+
symbol: symbol.toUpperCase(),
|
|
2346
|
+
value: Number(value)
|
|
2347
|
+
}));
|
|
2348
|
+
} catch {
|
|
2349
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2350
|
+
return parts.map((p) => {
|
|
2351
|
+
const [sym, val] = p.split("=");
|
|
2352
|
+
if (!sym || !val) {
|
|
2353
|
+
throw new Error(`Invalid holding: ${p}. Use 'AAPL=145000' format.`);
|
|
2354
|
+
}
|
|
2355
|
+
return { symbol: sym.toUpperCase().trim(), value: Number(val) };
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
var DIVIDER = "\u2500".repeat(60);
|
|
2360
|
+
function fmtMoney(n) {
|
|
2361
|
+
if (n >= 1e6) return `$${(n / 1e6).toFixed(2)}M`;
|
|
2362
|
+
if (n >= 1e3) return `$${(n / 1e3).toFixed(1)}K`;
|
|
2363
|
+
return `$${n.toFixed(0)}`;
|
|
2364
|
+
}
|
|
2365
|
+
function fmtPct(n, signed = false) {
|
|
2366
|
+
const v = (n * 100).toFixed(1);
|
|
2367
|
+
return signed && n >= 0 ? `+${v}%` : `${v}%`;
|
|
2368
|
+
}
|
|
2369
|
+
function bar(pct, width = 16) {
|
|
2370
|
+
const filled = Math.round(pct * width);
|
|
2371
|
+
return "\u2588".repeat(Math.max(0, filled)) + " ".repeat(Math.max(0, width - filled));
|
|
2372
|
+
}
|
|
2373
|
+
function renderText(data) {
|
|
2374
|
+
const lines = [];
|
|
2375
|
+
const s = data.summary;
|
|
2376
|
+
const headerParts = [fmtMoney(s.total_value)];
|
|
2377
|
+
if (s.ytd_return !== null && s.ytd_return !== void 0) {
|
|
2378
|
+
headerParts.push(`${fmtPct(s.ytd_return, true)} YTD`);
|
|
2379
|
+
}
|
|
2380
|
+
if (s.vs_spy !== null && s.vs_spy !== void 0) {
|
|
2381
|
+
headerParts.push(`${fmtPct(s.vs_spy, true)} SPY`);
|
|
2382
|
+
}
|
|
2383
|
+
lines.push("");
|
|
2384
|
+
lines.push(" " + headerParts.join(" \u2022 "));
|
|
2385
|
+
lines.push("");
|
|
2386
|
+
lines.push(" " + s.headline_insight);
|
|
2387
|
+
lines.push("");
|
|
2388
|
+
lines.push(" " + DIVIDER);
|
|
2389
|
+
lines.push("");
|
|
2390
|
+
lines.push(" CONCENTRATION");
|
|
2391
|
+
lines.push("");
|
|
2392
|
+
for (const sec of data.concentration.by_sector) {
|
|
2393
|
+
const name = sec.name.padEnd(14);
|
|
2394
|
+
lines.push(` ${name} ${bar(sec.pct)} ${fmtPct(sec.pct)}`);
|
|
2395
|
+
}
|
|
2396
|
+
lines.push("");
|
|
2397
|
+
lines.push(" " + data.concentration.insight);
|
|
2398
|
+
lines.push("");
|
|
2399
|
+
lines.push(" " + DIVIDER);
|
|
2400
|
+
lines.push("");
|
|
2401
|
+
lines.push(" HOW YOU TRADE");
|
|
2402
|
+
lines.push("");
|
|
2403
|
+
if (!data.behavior.available) {
|
|
2404
|
+
lines.push(" " + (data.behavior.reason || "Not enough trade history yet."));
|
|
2405
|
+
} else {
|
|
2406
|
+
if (data.behavior.insight) lines.push(" " + data.behavior.insight);
|
|
2407
|
+
lines.push("");
|
|
2408
|
+
for (const s2 of data.behavior.strengths) lines.push(` \u2713 ${s2.text}`);
|
|
2409
|
+
for (const w of data.behavior.weaknesses) lines.push(` \u26A0 ${w.text}`);
|
|
2410
|
+
}
|
|
2411
|
+
lines.push("");
|
|
2412
|
+
lines.push(" " + DIVIDER);
|
|
2413
|
+
lines.push("");
|
|
2414
|
+
lines.push(" RISK");
|
|
2415
|
+
lines.push("");
|
|
2416
|
+
const r = data.risk;
|
|
2417
|
+
if (r.volatility && r.volatility.value != null) {
|
|
2418
|
+
lines.push(
|
|
2419
|
+
` Volatility ${r.volatility.label.padEnd(8)} ${fmtPct(r.volatility.value)}`
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
if (r.correlation_spy != null) {
|
|
2423
|
+
const label = r.correlation_spy >= 0.8 ? "High" : r.correlation_spy >= 0.5 ? "Med" : "Low";
|
|
2424
|
+
lines.push(
|
|
2425
|
+
` Correlation ${label.padEnd(8)} ${r.correlation_spy.toFixed(2)} to SPY`
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
if (r.max_drawdown != null) {
|
|
2429
|
+
lines.push(` Max drawdown ${fmtPct(r.max_drawdown)}`);
|
|
2430
|
+
}
|
|
2431
|
+
if (r.sharpe != null) {
|
|
2432
|
+
lines.push(` Sharpe ${r.sharpe.toFixed(2)}`);
|
|
2433
|
+
}
|
|
2434
|
+
lines.push("");
|
|
2435
|
+
lines.push(" " + r.insight);
|
|
2436
|
+
lines.push("");
|
|
2437
|
+
lines.push(" " + DIVIDER);
|
|
2438
|
+
lines.push("");
|
|
2439
|
+
lines.push(" WHAT TO WATCH");
|
|
2440
|
+
lines.push("");
|
|
2441
|
+
if (data.watch.alerts.length === 0) {
|
|
2442
|
+
lines.push(" Nothing flagged.");
|
|
2443
|
+
} else {
|
|
2444
|
+
for (const w of data.watch.alerts) {
|
|
2445
|
+
lines.push(` ${w.ticker.padEnd(6)} ${w.reason}`);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
lines.push("");
|
|
2449
|
+
lines.push(" " + DIVIDER);
|
|
2450
|
+
lines.push("");
|
|
2451
|
+
lines.push(" PROJECTION");
|
|
2452
|
+
lines.push("");
|
|
2453
|
+
for (const p of data.projection.points) {
|
|
2454
|
+
if (p.year === 0) continue;
|
|
2455
|
+
lines.push(
|
|
2456
|
+
` ${`${p.year}Y`.padEnd(4)} p10 ${fmtMoney(p.p10)} p50 ${fmtMoney(p.p50)} p90 ${fmtMoney(p.p90)}`
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
lines.push("");
|
|
2460
|
+
lines.push(" " + data.projection.disclaimer);
|
|
2461
|
+
lines.push("");
|
|
2462
|
+
return lines.join("\n");
|
|
2463
|
+
}
|
|
2464
|
+
function renderMarkdown(data) {
|
|
2465
|
+
const lines = [];
|
|
2466
|
+
const s = data.summary;
|
|
2467
|
+
lines.push("# Portfolio Analytics");
|
|
2468
|
+
lines.push("");
|
|
2469
|
+
const hp = [`**${fmtMoney(s.total_value)}**`];
|
|
2470
|
+
if (s.ytd_return != null) hp.push(`${fmtPct(s.ytd_return, true)} YTD`);
|
|
2471
|
+
if (s.vs_spy != null) hp.push(`${fmtPct(s.vs_spy, true)} SPY`);
|
|
2472
|
+
lines.push(hp.join(" \u2022 "));
|
|
2473
|
+
lines.push("");
|
|
2474
|
+
lines.push(`> ${s.headline_insight}`);
|
|
2475
|
+
lines.push("");
|
|
2476
|
+
lines.push("## Concentration");
|
|
2477
|
+
lines.push("");
|
|
2478
|
+
lines.push("| Sector | Allocation | Value |");
|
|
2479
|
+
lines.push("|---|---|---|");
|
|
2480
|
+
for (const sec of data.concentration.by_sector) {
|
|
2481
|
+
lines.push(`| ${sec.name} | ${fmtPct(sec.pct)} | ${fmtMoney(sec.value)} |`);
|
|
2482
|
+
}
|
|
2483
|
+
lines.push("");
|
|
2484
|
+
lines.push(data.concentration.insight);
|
|
2485
|
+
lines.push("");
|
|
2486
|
+
lines.push("## How You Trade");
|
|
2487
|
+
lines.push("");
|
|
2488
|
+
if (!data.behavior.available) {
|
|
2489
|
+
lines.push(data.behavior.reason || "Not enough trade history yet.");
|
|
2490
|
+
} else {
|
|
2491
|
+
if (data.behavior.insight) {
|
|
2492
|
+
lines.push(data.behavior.insight);
|
|
2493
|
+
lines.push("");
|
|
2494
|
+
}
|
|
2495
|
+
for (const x of data.behavior.strengths) lines.push(`- \u2713 ${x.text}`);
|
|
2496
|
+
for (const x of data.behavior.weaknesses) lines.push(`- \u26A0 ${x.text}`);
|
|
2497
|
+
}
|
|
2498
|
+
lines.push("");
|
|
2499
|
+
lines.push("## Risk");
|
|
2500
|
+
lines.push("");
|
|
2501
|
+
const r = data.risk;
|
|
2502
|
+
if (r.volatility && r.volatility.value != null) lines.push(`- Volatility: ${r.volatility.label} (${fmtPct(r.volatility.value)})`);
|
|
2503
|
+
if (r.correlation_spy != null) lines.push(`- Correlation to SPY: ${r.correlation_spy.toFixed(2)}`);
|
|
2504
|
+
if (r.max_drawdown != null) lines.push(`- Max drawdown: ${fmtPct(r.max_drawdown)}`);
|
|
2505
|
+
if (r.sharpe != null) lines.push(`- Sharpe: ${r.sharpe.toFixed(2)}`);
|
|
2506
|
+
lines.push("");
|
|
2507
|
+
lines.push(r.insight);
|
|
2508
|
+
lines.push("");
|
|
2509
|
+
lines.push("## What to Watch");
|
|
2510
|
+
lines.push("");
|
|
2511
|
+
if (data.watch.alerts.length === 0) {
|
|
2512
|
+
lines.push("Nothing flagged.");
|
|
2513
|
+
} else {
|
|
2514
|
+
for (const w of data.watch.alerts) {
|
|
2515
|
+
lines.push(`- **${w.ticker}** \u2014 ${w.reason}`);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
lines.push("");
|
|
2519
|
+
lines.push("## Projection");
|
|
2520
|
+
lines.push("");
|
|
2521
|
+
lines.push("| Year | p10 | p50 | p90 |");
|
|
2522
|
+
lines.push("|---|---|---|---|");
|
|
2523
|
+
for (const p of data.projection.points) {
|
|
2524
|
+
lines.push(`| ${p.year}Y | ${fmtMoney(p.p10)} | ${fmtMoney(p.p50)} | ${fmtMoney(p.p90)} |`);
|
|
2525
|
+
}
|
|
2526
|
+
lines.push("");
|
|
2527
|
+
lines.push(`*${data.projection.disclaimer}*`);
|
|
2528
|
+
return lines.join("\n");
|
|
2529
|
+
}
|
|
2530
|
+
function emit2(content, opts) {
|
|
2531
|
+
if (opts.output) {
|
|
2532
|
+
mkdirSync5(dirname2(opts.output), { recursive: true });
|
|
2533
|
+
writeFileSync5(opts.output, content, "utf8");
|
|
2534
|
+
if (!opts.quiet) console.error(`Wrote ${opts.output}`);
|
|
2535
|
+
} else {
|
|
2536
|
+
console.log(content);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
function registerAnalyticsCommand(program2) {
|
|
2540
|
+
addGlobalOptions(
|
|
2541
|
+
program2.command("analytics").description(
|
|
2542
|
+
"One-page portfolio analytics: concentration, risk, watch, projection"
|
|
2543
|
+
).argument(
|
|
2544
|
+
"<holdings>",
|
|
2545
|
+
"Holdings as JSON, file path, or shorthand (e.g. 'AAPL=145000,MSFT=91000')"
|
|
2546
|
+
).option(
|
|
2547
|
+
"--section <s>",
|
|
2548
|
+
"Show only one section: concentration, behavior, risk, watch, projection"
|
|
2549
|
+
).option("--md", "Output as markdown (alias for --format markdown)")
|
|
2550
|
+
).action(
|
|
2551
|
+
async (holdingsInput, opts) => {
|
|
2552
|
+
let holdings;
|
|
2553
|
+
try {
|
|
2554
|
+
holdings = parseHoldings(holdingsInput);
|
|
2555
|
+
} catch (e) {
|
|
2556
|
+
console.error(`Error parsing holdings: ${e.message}`);
|
|
2557
|
+
process.exit(1);
|
|
2558
|
+
}
|
|
2559
|
+
const result = await callTool("analyze_portfolio", {
|
|
2560
|
+
holdings,
|
|
2561
|
+
parts: [
|
|
2562
|
+
"summary",
|
|
2563
|
+
"concentration",
|
|
2564
|
+
"asset_class",
|
|
2565
|
+
"geography",
|
|
2566
|
+
"behavior",
|
|
2567
|
+
"risk",
|
|
2568
|
+
"watch",
|
|
2569
|
+
"projection"
|
|
2570
|
+
]
|
|
2571
|
+
});
|
|
2572
|
+
if (result && typeof result === "object" && "error" in result && result.error) {
|
|
2573
|
+
console.error(`Error: ${result.error}`);
|
|
2574
|
+
process.exit(1);
|
|
2575
|
+
}
|
|
2576
|
+
const data = result;
|
|
2577
|
+
if (opts.raw) {
|
|
2578
|
+
emit2(JSON.stringify(data, null, 2), opts);
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
let payload = data;
|
|
2582
|
+
if (opts.section) {
|
|
2583
|
+
const sec = opts.section.toLowerCase();
|
|
2584
|
+
const map = {
|
|
2585
|
+
summary: data.summary,
|
|
2586
|
+
concentration: data.concentration,
|
|
2587
|
+
behavior: data.behavior,
|
|
2588
|
+
risk: data.risk,
|
|
2589
|
+
watch: data.watch,
|
|
2590
|
+
projection: data.projection
|
|
2591
|
+
};
|
|
2592
|
+
if (!(sec in map)) {
|
|
2593
|
+
console.error(
|
|
2594
|
+
`Unknown section: ${sec}. Choose: summary, concentration, behavior, risk, watch, projection`
|
|
2595
|
+
);
|
|
2596
|
+
process.exit(1);
|
|
2597
|
+
}
|
|
2598
|
+
payload = map[sec];
|
|
2599
|
+
}
|
|
2600
|
+
const fmt3 = opts.md ? "markdown" : opts.format ?? resolveFormat(opts);
|
|
2601
|
+
if (fmt3 === "json") {
|
|
2602
|
+
emit2(JSON.stringify(payload, null, 2), opts);
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
if (fmt3 === "markdown") {
|
|
2606
|
+
if (opts.section) {
|
|
2607
|
+
emit2(JSON.stringify(payload, null, 2), opts);
|
|
2608
|
+
} else {
|
|
2609
|
+
emit2(renderMarkdown(data), opts);
|
|
2610
|
+
}
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
if (opts.section) {
|
|
2614
|
+
emit2(JSON.stringify(payload, null, 2), opts);
|
|
2615
|
+
} else {
|
|
2616
|
+
emit2(renderText(data), opts);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// src/commands/stats.ts
|
|
2623
|
+
function registerStatsCommand(program2) {
|
|
2624
|
+
addGlobalOptions(
|
|
2625
|
+
program2.command("stats").description("Key stats: beta, 52-week range, volume, P/E, dividend yield").argument("<symbol>", "Ticker symbol")
|
|
2626
|
+
).addHelpText("after", `
|
|
2627
|
+
Examples:
|
|
2628
|
+
$ bloom stats AAPL Key statistics for Apple
|
|
2629
|
+
$ bloom stats TSLA -f json Stats as JSON
|
|
2630
|
+
`).action(async (symbol, opts) => {
|
|
2631
|
+
const data = await BloomClient.instance().getStats(symbol.toUpperCase());
|
|
2632
|
+
outputResult(data, opts);
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
// src/commands/peers.ts
|
|
2637
|
+
function registerPeersCommand(program2) {
|
|
2638
|
+
addGlobalOptions(
|
|
2639
|
+
program2.command("peers").description("Peer companies and competitors").argument("<symbol>", "Ticker symbol")
|
|
2640
|
+
).addHelpText("after", `
|
|
2641
|
+
Examples:
|
|
2642
|
+
$ bloom peers AAPL Companies similar to Apple
|
|
2643
|
+
$ bloom peers TSLA Tesla's peer group
|
|
2644
|
+
`).action(async (symbol, opts) => {
|
|
2645
|
+
const data = await BloomClient.instance().getPeers(symbol.toUpperCase());
|
|
2646
|
+
outputResult(data, opts, formatPeers);
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
function formatPeers(data, opts) {
|
|
2650
|
+
const peers = data?.results;
|
|
2651
|
+
if (!Array.isArray(peers)) return JSON.stringify(data, null, 2);
|
|
2652
|
+
const num = (v, f) => v == null || v === "" || isNaN(Number(v)) ? "-" : f(Number(v));
|
|
2653
|
+
const pct = (n) => `${(n * 100).toFixed(2)}%`;
|
|
2654
|
+
const cap = (n) => n >= 1e12 ? `$${(n / 1e12).toFixed(2)}T` : n >= 1e9 ? `$${(n / 1e9).toFixed(2)}B` : `$${(n / 1e6).toFixed(2)}M`;
|
|
2655
|
+
const rows = peers.map((p) => [
|
|
2656
|
+
String(p.symbol ?? ""),
|
|
2657
|
+
truncate(String(p.name ?? ""), 32),
|
|
2658
|
+
String(p.sector || p.category || ""),
|
|
2659
|
+
num(p.latestPrice, (n) => `$${n.toFixed(2)}`),
|
|
2660
|
+
num(p.changePercent1d, pct),
|
|
2661
|
+
num(p.changePercent1y, pct),
|
|
2662
|
+
num(p.priceToEarnings, (n) => n.toFixed(2)),
|
|
2663
|
+
num(p.marketCap, cap)
|
|
2664
|
+
]);
|
|
2665
|
+
return makeTable(["Symbol", "Name", "Sector", "Price", "1D", "1Y", "P/E", "Mkt Cap"], rows, opts);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// src/commands/bottom-line.ts
|
|
2669
|
+
function registerBottomLineCommand(program2) {
|
|
2670
|
+
addGlobalOptions(
|
|
2671
|
+
program2.command("bottom-line").description("Buy/sell/hold verdict with reasoning").argument("<symbol>", "Ticker symbol")
|
|
2672
|
+
).addHelpText("after", `
|
|
2673
|
+
Examples:
|
|
2674
|
+
$ bloom bottom-line AAPL Bloom's bottom line on Apple
|
|
2675
|
+
$ bloom bottom-line NVDA Buy/sell/hold verdict for NVIDIA
|
|
2676
|
+
`).action(async (symbol, opts) => {
|
|
2677
|
+
const data = await BloomClient.instance().getBottomLine(symbol.toUpperCase());
|
|
2678
|
+
outputResult(data, opts, formatBottomLine);
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
function formatBottomLine(data, opts) {
|
|
2682
|
+
const bl = data?.results;
|
|
2683
|
+
if (!bl?.verdict) return JSON.stringify(data, null, 2);
|
|
2684
|
+
const rows = bl.details.map((d) => [d.rating, d.title, d.descriptions.map((x) => `- ${x}`).join("\n")]);
|
|
2685
|
+
const table = makeTable(["Rating", "Area", "Reasoning"], rows, opts);
|
|
2686
|
+
return [`${bl.symbol}: ${bl.verdict}`, ...bl.lead ? [bl.lead] : [], "", table].join("\n");
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// src/commands/insights.ts
|
|
2690
|
+
function registerInsightsCommand(program2) {
|
|
2691
|
+
addGlobalOptions(
|
|
2692
|
+
program2.command("insights").description("AI-generated insights for stocks").argument("[symbols...]", "Ticker symbols (omit for all recent)")
|
|
2693
|
+
).addHelpText("after", `
|
|
2694
|
+
Examples:
|
|
2695
|
+
$ bloom insights AAPL Insights for Apple
|
|
2696
|
+
$ bloom insights AAPL MSFT Insights for multiple stocks
|
|
2697
|
+
$ bloom insights All recent insights
|
|
2698
|
+
`).action(async (symbols, opts) => {
|
|
2699
|
+
if (symbols.length === 1) {
|
|
2700
|
+
const data = await BloomClient.instance().getInsights(symbols[0].toUpperCase());
|
|
2701
|
+
outputResult(data, opts);
|
|
2702
|
+
} else {
|
|
2703
|
+
const syms = symbols.length > 0 ? symbols.map((s) => s.toUpperCase()) : void 0;
|
|
2704
|
+
const data = await BloomClient.instance().getInsights(void 0, syms);
|
|
2705
|
+
outputResult(data, opts);
|
|
2706
|
+
}
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// src/commands/arena-activity.ts
|
|
2711
|
+
function registerArenaActivityCommand(program2) {
|
|
2712
|
+
addGlobalOptions(
|
|
2713
|
+
program2.command("arena-activity").description("Live AI arena trade activity feed").option("--model <m>", "Filter by model: claude, openai, gemini").option("--limit <n>", "Max results", "20")
|
|
2714
|
+
).addHelpText("after", `
|
|
2715
|
+
Examples:
|
|
2716
|
+
$ bloom arena-activity All recent arena trades
|
|
2717
|
+
$ bloom arena-activity --model claude Claude's trades only
|
|
2718
|
+
$ bloom arena-activity --limit 5 Last 5 trades
|
|
2719
|
+
`).action(async (opts) => {
|
|
2720
|
+
const limit = parseIntArg(opts.limit, "--limit");
|
|
2721
|
+
const data = await BloomClient.instance().getArenaActivity(opts.model, limit);
|
|
2722
|
+
outputResult(data, opts);
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// src/commands/portfolio-news.ts
|
|
2727
|
+
function registerPortfolioNewsCommand(program2) {
|
|
2728
|
+
addGlobalOptions(
|
|
2729
|
+
program2.command("portfolio-news").description("Smart news summary for portfolio symbols").argument("[symbols...]", "Ticker symbols")
|
|
2730
|
+
).addHelpText("after", `
|
|
2731
|
+
Examples:
|
|
2732
|
+
$ bloom portfolio-news AAPL MSFT GOOG News digest for holdings
|
|
2733
|
+
$ bloom portfolio-news News for all watched symbols
|
|
2734
|
+
`).action(async (symbols, opts) => {
|
|
2735
|
+
const syms = symbols.length > 0 ? symbols.map((s) => s.toUpperCase()) : void 0;
|
|
2736
|
+
const data = await BloomClient.instance().getPortfolioNews(syms);
|
|
2737
|
+
outputResult(data, opts);
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
// src/commands/collection-returns.ts
|
|
2742
|
+
function registerCollectionReturnsCommand(program2) {
|
|
2743
|
+
addGlobalOptions(
|
|
2744
|
+
program2.command("collection-returns").description("Performance returns for the given collections").requiredOption("--ids <ids>", "Comma-separated collection IDs (from `bloom collections`)").option("--timerange <range>", "1d|1w|1m|3m|ytd|1y|5y", "ytd")
|
|
2745
|
+
).addHelpText("after", `
|
|
2746
|
+
Examples:
|
|
2747
|
+
$ bloom collection-returns --ids 113 YTD return for collection 113
|
|
2748
|
+
$ bloom collection-returns --ids 113,114 --timerange 1y
|
|
2749
|
+
$ bloom collection-returns --ids 113 -f table As table
|
|
2750
|
+
`).action(async (opts) => {
|
|
2751
|
+
const data = await BloomClient.instance().getCollectionReturns(opts.ids, opts.timerange);
|
|
2752
|
+
outputResult(data, opts);
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
|
|
1756
2756
|
// src/index.ts
|
|
1757
|
-
var program = new
|
|
2757
|
+
var program = new Command3();
|
|
1758
2758
|
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1759
2759
|
var pkgPath = path.join(__dirname, "..", "package.json");
|
|
1760
|
-
var compiledVersion = "0.1.
|
|
2760
|
+
var compiledVersion = "0.1.7".length > 0 ? "0.1.7" : void 0;
|
|
1761
2761
|
var envVersion = process.env.BLOOM_CLI_VERSION ?? process.env.npm_package_version;
|
|
1762
2762
|
var fallbackVersion = compiledVersion ?? "unknown";
|
|
1763
2763
|
var cliVersion = envVersion ?? fallbackVersion;
|
|
1764
2764
|
try {
|
|
1765
|
-
const pkg = JSON.parse(
|
|
2765
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1766
2766
|
if (typeof pkg.version === "string") {
|
|
1767
2767
|
cliVersion = pkg.version;
|
|
1768
2768
|
}
|
|
@@ -1771,7 +2771,24 @@ try {
|
|
|
1771
2771
|
if (cliVersion === "0.0.0") {
|
|
1772
2772
|
cliVersion = fallbackVersion;
|
|
1773
2773
|
}
|
|
1774
|
-
|
|
2774
|
+
var config = loadConfig();
|
|
2775
|
+
if (config.apiBase !== "https://api.getbloom.app") {
|
|
2776
|
+
setApiBase(config.apiBase);
|
|
2777
|
+
}
|
|
2778
|
+
setCacheDir(config.cacheDir);
|
|
2779
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
2780
|
+
for (const [key, value] of Object.entries(program.opts())) {
|
|
2781
|
+
if (program.getOptionValueSource(key) === "cli" && actionCommand.getOptionValueSource(key) !== "cli") {
|
|
2782
|
+
actionCommand.setOptionValueWithSource(key, value, "cli");
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
const opts = actionCommand.opts();
|
|
2786
|
+
if (opts.debug) setDebug(true);
|
|
2787
|
+
if (opts.cache === false) setNoCache(true);
|
|
2788
|
+
});
|
|
2789
|
+
addGlobalOptions(
|
|
2790
|
+
program.name("bloom").description("Bloom CLI \u2014 financial data for coding agents and humans").version(cliVersion).enablePositionalOptions()
|
|
2791
|
+
);
|
|
1775
2792
|
registerAuthCommand(program);
|
|
1776
2793
|
registerInfoCommand(program);
|
|
1777
2794
|
registerNewsCommand(program);
|
|
@@ -1808,7 +2825,50 @@ registerSplitsCommand(program);
|
|
|
1808
2825
|
registerIpoCommand(program);
|
|
1809
2826
|
registerCompareCommand(program);
|
|
1810
2827
|
registerRatingsCommand(program);
|
|
2828
|
+
registerAnalyticsCommand(program);
|
|
2829
|
+
registerStatsCommand(program);
|
|
2830
|
+
registerPeersCommand(program);
|
|
2831
|
+
registerBottomLineCommand(program);
|
|
2832
|
+
registerInsightsCommand(program);
|
|
2833
|
+
registerArenaActivityCommand(program);
|
|
2834
|
+
registerPortfolioNewsCommand(program);
|
|
2835
|
+
registerCollectionReturnsCommand(program);
|
|
2836
|
+
var cacheCmd = program.command("cache").description("Manage disk cache");
|
|
2837
|
+
cacheCmd.command("clear").description("Clear all cached data").action(() => {
|
|
2838
|
+
const { cleared } = clearCache();
|
|
2839
|
+
console.log(`Cleared ${cleared} cached items.`);
|
|
2840
|
+
});
|
|
2841
|
+
cacheCmd.command("stats").description("Show cache statistics").action(() => {
|
|
2842
|
+
const stats = cacheStats();
|
|
2843
|
+
console.log(`Cache directory: ${stats.cacheDir}`);
|
|
2844
|
+
console.log(`Items: ${stats.items} (${stats.expired} expired)`);
|
|
2845
|
+
console.log(`Total size: ${(stats.totalBytes / 1024).toFixed(1)} KB`);
|
|
2846
|
+
if (Object.keys(stats.byCategory).length > 0) {
|
|
2847
|
+
console.log("By category:");
|
|
2848
|
+
for (const [cat, count] of Object.entries(stats.byCategory)) {
|
|
2849
|
+
console.log(` ${cat}: ${count}`);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
});
|
|
1811
2853
|
program.parseAsync().catch((err) => {
|
|
1812
|
-
|
|
2854
|
+
if (err instanceof AuthError) {
|
|
2855
|
+
console.error(`Error: ${err.message}`);
|
|
2856
|
+
console.error("Run `bloom auth login` to authenticate.");
|
|
2857
|
+
process.exit(1);
|
|
2858
|
+
}
|
|
2859
|
+
if (err instanceof RateLimitError) {
|
|
2860
|
+
console.error("Rate limited. Try again in a moment.");
|
|
2861
|
+
process.exit(2);
|
|
2862
|
+
}
|
|
2863
|
+
if (err instanceof TickerNotFoundError) {
|
|
2864
|
+
console.error(`Ticker not found: ${err.ticker}`);
|
|
2865
|
+
process.exit(1);
|
|
2866
|
+
}
|
|
2867
|
+
if (err instanceof BloomError) {
|
|
2868
|
+
console.error(`Error: ${err.message}`);
|
|
2869
|
+
process.exit(1);
|
|
2870
|
+
}
|
|
2871
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2872
|
+
console.error("Error:", msg);
|
|
1813
2873
|
process.exit(1);
|
|
1814
2874
|
});
|