@bloomai/cli 0.1.3 → 0.1.4
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 +41 -0
- package/SKILL.md +10 -0
- package/dist/index.js +222 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ jq '.data[] | select(.revenue_growth > 0.1)' /tmp/bloom/aapl-revenue.json
|
|
|
46
46
|
| `screen` | Filter stocks by metrics | `bloom screen "market_cap > 100B" "pe_ratio < 25"` |
|
|
47
47
|
| `earnings` | Earnings history & estimates | `bloom earnings AAPL` |
|
|
48
48
|
| `technicals` | RSI, MAs, patterns, options flow | `bloom technicals AAPL` |
|
|
49
|
+
| `ma` | Raw moving average data (SMA/EMA) | `bloom ma AAPL --sma 50,100,200 --ema 20` |
|
|
49
50
|
| `options-history` | Options sentiment over time | `bloom options-history AAPL --days 30` |
|
|
50
51
|
| `portfolio` | Portfolio risk analysis | `bloom portfolio '{"AAPL":0.5,"MSFT":0.5}'` |
|
|
51
52
|
| `trades` | AI trade evaluation | `bloom trades "Buy AAPL: strong AI catalysts"` |
|
|
@@ -54,6 +55,16 @@ jq '.data[] | select(.revenue_growth > 0.1)' /tmp/bloom/aapl-revenue.json
|
|
|
54
55
|
| `transcript` | Search earnings call transcripts | `bloom transcript AAPL "AI" "services"` |
|
|
55
56
|
| `ai-trades` | Recent AI model trade decisions (InvestingArena) | `bloom ai-trades --model claude --limit 5` |
|
|
56
57
|
| `ai-portfolio` | AI model portfolio positions & performance | `bloom ai-portfolio --model openai` |
|
|
58
|
+
| `search` | Web search via Serper (Google) | `bloom search "nvidia earnings Q1 2026"` |
|
|
59
|
+
| `scrape` | Scrape a web page via Firecrawl | `bloom scrape https://example.com` |
|
|
60
|
+
|
|
61
|
+
## Environment Variables
|
|
62
|
+
|
|
63
|
+
| Variable | Required for | Description |
|
|
64
|
+
|----------|-------------|-------------|
|
|
65
|
+
| `BLOOM_API_KEY` | All financial commands | Bloom API key (`bloom auth login`) |
|
|
66
|
+
| `SERPER_API_KEY` | `bloom search` | Serper.dev API key for web search |
|
|
67
|
+
| `FIRECRAWL_API_KEY` | `bloom scrape` | Firecrawl API key for web scraping |
|
|
57
68
|
|
|
58
69
|
## Global Flags
|
|
59
70
|
|
|
@@ -117,6 +128,21 @@ bloom transcript AAPL "AI" "services" "margins" --max 20 -o /tmp/bloom/transcrip
|
|
|
117
128
|
jq '.matches[] | {speaker, text}' /tmp/bloom/transcript.json
|
|
118
129
|
```
|
|
119
130
|
|
|
131
|
+
### Moving Averages
|
|
132
|
+
```bash
|
|
133
|
+
# Default: 100-day SMA + 100-day EMA
|
|
134
|
+
bloom ma AAPL
|
|
135
|
+
|
|
136
|
+
# Custom periods
|
|
137
|
+
bloom ma AAPL --sma 50,100,200 --ema 20,100
|
|
138
|
+
|
|
139
|
+
# Last 50 data points, save to file
|
|
140
|
+
bloom ma AAPL --sma 200 --limit 50 -o /tmp/bloom/aapl-ma.json
|
|
141
|
+
|
|
142
|
+
# Extract latest values
|
|
143
|
+
jq '.indicators | to_entries[] | {(.key): .value | to_entries | last | .value}' /tmp/bloom/aapl-ma.json
|
|
144
|
+
```
|
|
145
|
+
|
|
120
146
|
### AI Arena (InvestingArena — 3 AIs with real money)
|
|
121
147
|
```bash
|
|
122
148
|
# All AI models' recent trades
|
|
@@ -138,6 +164,21 @@ bloom ai-portfolio --model gemini
|
|
|
138
164
|
jq '[.[] | {model: .ai_model, return_pct, account_value}]' /tmp/bloom/ai-portfolio.json
|
|
139
165
|
```
|
|
140
166
|
|
|
167
|
+
### Web Research (Search + Scrape)
|
|
168
|
+
```bash
|
|
169
|
+
# Search the web
|
|
170
|
+
bloom search "nvidia earnings Q1 2026" --limit 5 -o /tmp/bloom/search.json
|
|
171
|
+
|
|
172
|
+
# Search for news specifically
|
|
173
|
+
bloom search "AAPL product launch" --type news --limit 10
|
|
174
|
+
|
|
175
|
+
# Scrape a page for full content
|
|
176
|
+
bloom scrape https://example.com/article -o /tmp/bloom/article.md
|
|
177
|
+
|
|
178
|
+
# Truncate long pages
|
|
179
|
+
bloom scrape https://example.com/article --max-chars 5000
|
|
180
|
+
```
|
|
181
|
+
|
|
141
182
|
## Tips
|
|
142
183
|
|
|
143
184
|
- Use `-o` to download data to files, then `jq` to extract. Saves context tokens.
|
package/SKILL.md
CHANGED
|
@@ -50,6 +50,16 @@ jq '.data[] | select(.revenue_growth > 0.1)' /tmp/bloom/aapl-revenue.json
|
|
|
50
50
|
| `transcript` | Search earnings call transcripts | `bloom transcript AAPL "AI" "services"` |
|
|
51
51
|
| `ai-trades` | Recent AI model trade decisions (InvestingArena) | `bloom ai-trades --model claude --limit 5` |
|
|
52
52
|
| `ai-portfolio` | AI model portfolio positions & performance | `bloom ai-portfolio --model openai` |
|
|
53
|
+
| `search` | Web search via Serper (Google) | `bloom search "nvidia earnings Q1 2026"` |
|
|
54
|
+
| `scrape` | Scrape a web page via Firecrawl | `bloom scrape https://example.com` |
|
|
55
|
+
|
|
56
|
+
## Environment Variables
|
|
57
|
+
|
|
58
|
+
| Variable | Required for | Description |
|
|
59
|
+
|----------|-------------|-------------|
|
|
60
|
+
| `BLOOM_API_KEY` | All financial commands | Bloom API key (`bloom auth login`) |
|
|
61
|
+
| `SERPER_API_KEY` | `bloom search` | Serper.dev API key for web search |
|
|
62
|
+
| `FIRECRAWL_API_KEY` | `bloom scrape` | Firecrawl API key for web scraping |
|
|
53
63
|
|
|
54
64
|
## Global Flags
|
|
55
65
|
|
package/dist/index.js
CHANGED
|
@@ -452,7 +452,9 @@ var CHART_TYPES = [
|
|
|
452
452
|
"net_earnings",
|
|
453
453
|
"net_earnings_growth",
|
|
454
454
|
"free_cash_flow",
|
|
455
|
-
"free_cash_flow_growth"
|
|
455
|
+
"free_cash_flow_growth",
|
|
456
|
+
"stock_based_compensation",
|
|
457
|
+
"stock_based_compensation_growth"
|
|
456
458
|
];
|
|
457
459
|
function registerFinancialsCommand(program2) {
|
|
458
460
|
addGlobalOptions(
|
|
@@ -511,12 +513,13 @@ function formatSentiment(data) {
|
|
|
511
513
|
// src/commands/market.ts
|
|
512
514
|
function registerMarketCommand(program2) {
|
|
513
515
|
addGlobalOptions(
|
|
514
|
-
program2.command("market").description("Market performance, sectors, global").option("--type <type>", "top_movers|sector_breakdown|global_markets|major_indexes", "top_movers").option("--period <p>", "1d|1w|1m|3m|6m|ytd|1y|5y", "1d").option("--limit <n>", "Max stocks for top_movers", "24")
|
|
516
|
+
program2.command("market").description("Market performance, sectors, global").option("--type <type>", "top_movers|sector_breakdown|global_markets|major_indexes", "top_movers").option("--period <p>", "1d|1w|1m|3m|6m|ytd|1y|5y", "1d").option("--limit <n>", "Max stocks for top_movers", "24").option("--universe <u>", "all|sp500|international|crypto|community", "all")
|
|
515
517
|
).action(async (opts) => {
|
|
516
518
|
const data = await callTool("get_market_map", {
|
|
517
519
|
map_type: opts.type,
|
|
518
520
|
period: opts.period,
|
|
519
|
-
limit: parseIntSafe(opts.limit, 24)
|
|
521
|
+
limit: parseIntSafe(opts.limit, 24),
|
|
522
|
+
universe: opts.universe
|
|
520
523
|
});
|
|
521
524
|
outputResult(data, opts);
|
|
522
525
|
});
|
|
@@ -895,11 +898,223 @@ function formatPortfolios(data, opts) {
|
|
|
895
898
|
return lines.join("\n");
|
|
896
899
|
}
|
|
897
900
|
|
|
901
|
+
// src/commands/ma.ts
|
|
902
|
+
function formatTable(data, limit) {
|
|
903
|
+
const { symbol, current_price, indicators } = data;
|
|
904
|
+
const labels = Object.keys(indicators);
|
|
905
|
+
if (labels.length === 0) {
|
|
906
|
+
console.log(`No indicator data returned for ${symbol}`);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const allDates = /* @__PURE__ */ new Set();
|
|
910
|
+
for (const label of labels) {
|
|
911
|
+
const ind = indicators[label];
|
|
912
|
+
if ("error" in ind) continue;
|
|
913
|
+
for (const date of Object.keys(ind)) {
|
|
914
|
+
allDates.add(date);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const sortedDates = Array.from(allDates).sort((a, b) => b.localeCompare(a)).slice(0, limit).reverse();
|
|
918
|
+
if (sortedDates.length === 0) {
|
|
919
|
+
console.log(`No data points available for ${symbol}`);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const dateWidth = 12;
|
|
923
|
+
const colWidth = 12;
|
|
924
|
+
const header = "Date".padEnd(dateWidth) + labels.map((l) => l.toUpperCase().padStart(colWidth)).join("");
|
|
925
|
+
const divider = "-".repeat(dateWidth + colWidth * labels.length);
|
|
926
|
+
console.log(`
|
|
927
|
+
${symbol}${current_price != null ? ` (current: $${current_price.toFixed(2)})` : ""}`);
|
|
928
|
+
console.log(divider);
|
|
929
|
+
console.log(header);
|
|
930
|
+
console.log(divider);
|
|
931
|
+
for (const date of sortedDates) {
|
|
932
|
+
let row = date.padEnd(dateWidth);
|
|
933
|
+
for (const label of labels) {
|
|
934
|
+
const ind = indicators[label];
|
|
935
|
+
if ("error" in ind) {
|
|
936
|
+
row += "N/A".padStart(colWidth);
|
|
937
|
+
} else {
|
|
938
|
+
const val = ind[date];
|
|
939
|
+
row += (val != null ? val.toFixed(2) : "\u2014").padStart(colWidth);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
console.log(row);
|
|
943
|
+
}
|
|
944
|
+
console.log(divider);
|
|
945
|
+
console.log(`Showing ${sortedDates.length} data point(s)
|
|
946
|
+
`);
|
|
947
|
+
}
|
|
948
|
+
function registerMaCommand(program2) {
|
|
949
|
+
addGlobalOptions(
|
|
950
|
+
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")
|
|
951
|
+
).action(
|
|
952
|
+
async (symbol, opts) => {
|
|
953
|
+
const indicators = [];
|
|
954
|
+
if (opts.sma) {
|
|
955
|
+
for (const p of opts.sma.split(",")) {
|
|
956
|
+
const period = p.trim();
|
|
957
|
+
if (period) indicators.push(`sma_${period}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (opts.ema) {
|
|
961
|
+
for (const p of opts.ema.split(",")) {
|
|
962
|
+
const period = p.trim();
|
|
963
|
+
if (period) indicators.push(`ema_${period}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (indicators.length === 0) {
|
|
967
|
+
indicators.push("sma_100", "ema_100");
|
|
968
|
+
}
|
|
969
|
+
const args = {
|
|
970
|
+
symbol: symbol.toUpperCase(),
|
|
971
|
+
indicators
|
|
972
|
+
};
|
|
973
|
+
const data = await callTool("get_moving_averages", args);
|
|
974
|
+
const limit = parseInt(opts.limit, 10) || 20;
|
|
975
|
+
let parsed = null;
|
|
976
|
+
try {
|
|
977
|
+
parsed = typeof data === "string" ? JSON.parse(data) : data;
|
|
978
|
+
} catch {
|
|
979
|
+
}
|
|
980
|
+
if (parsed?.indicators && typeof parsed.indicators === "object") {
|
|
981
|
+
const allDates = /* @__PURE__ */ new Set();
|
|
982
|
+
for (const ind of Object.values(parsed.indicators)) {
|
|
983
|
+
if ("error" in ind) continue;
|
|
984
|
+
for (const date of Object.keys(ind)) {
|
|
985
|
+
allDates.add(date);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const keepDates = new Set(
|
|
989
|
+
Array.from(allDates).sort((a, b) => b.localeCompare(a)).slice(0, limit)
|
|
990
|
+
);
|
|
991
|
+
for (const [label, ind] of Object.entries(parsed.indicators)) {
|
|
992
|
+
if ("error" in ind) continue;
|
|
993
|
+
const filtered = {};
|
|
994
|
+
for (const [date, val] of Object.entries(ind)) {
|
|
995
|
+
if (keepDates.has(date)) filtered[date] = val;
|
|
996
|
+
}
|
|
997
|
+
parsed.indicators[label] = filtered;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (opts.output || opts.format || opts.raw || !process.stdout.isTTY) {
|
|
1001
|
+
outputResult(parsed ?? data, opts);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (parsed?.indicators && typeof parsed.indicators === "object") {
|
|
1005
|
+
formatTable(parsed, limit);
|
|
1006
|
+
} else {
|
|
1007
|
+
outputResult(data, opts);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/commands/search.ts
|
|
1014
|
+
function registerSearchCommand(program2) {
|
|
1015
|
+
addGlobalOptions(
|
|
1016
|
+
program2.command("search").description("Web search via Serper (Google)").argument("<query...>", "Search query (joined into one string)").option("--limit <n>", "Max results (default: 10)", "10").option("--type <type>", "Search type: web, news, images (default: web)", "web")
|
|
1017
|
+
).action(async (query, opts) => {
|
|
1018
|
+
const apiKey = process.env.SERPER_API_KEY;
|
|
1019
|
+
if (!apiKey) {
|
|
1020
|
+
console.error("Error: SERPER_API_KEY environment variable is required.");
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
const q = query.join(" ");
|
|
1024
|
+
const limit = parseIntSafe(opts.limit, 10);
|
|
1025
|
+
const searchType = opts.type || "web";
|
|
1026
|
+
const validTypes = /* @__PURE__ */ new Set(["web", "news", "images"]);
|
|
1027
|
+
if (!validTypes.has(searchType)) {
|
|
1028
|
+
console.error(`Error: Invalid search type "${searchType}". Must be one of: web, news, images`);
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
}
|
|
1031
|
+
const endpoint = searchType === "web" ? "search" : searchType;
|
|
1032
|
+
const response = await fetch(`https://google.serper.dev/${endpoint}`, {
|
|
1033
|
+
method: "POST",
|
|
1034
|
+
headers: {
|
|
1035
|
+
"X-API-KEY": apiKey,
|
|
1036
|
+
"Content-Type": "application/json"
|
|
1037
|
+
},
|
|
1038
|
+
body: JSON.stringify({ q, num: limit })
|
|
1039
|
+
});
|
|
1040
|
+
if (!response.ok) {
|
|
1041
|
+
const text = await response.text();
|
|
1042
|
+
console.error(`Serper API error (${response.status}): ${text}`);
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
const data = await response.json();
|
|
1046
|
+
outputResult(data, opts, (d, o) => formatSearch(d, o));
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
function formatSearch(data, opts) {
|
|
1050
|
+
if (!data || typeof data !== "object") return String(data);
|
|
1051
|
+
const obj = data;
|
|
1052
|
+
const organic = obj.organic || obj.news || obj.images || [];
|
|
1053
|
+
if (!Array.isArray(organic) || organic.length === 0) return "(no results)";
|
|
1054
|
+
const rows = organic.map((r) => [
|
|
1055
|
+
String(r.position ?? ""),
|
|
1056
|
+
String(r.title || "").slice(0, 60),
|
|
1057
|
+
String(r.link || "").slice(0, 60),
|
|
1058
|
+
String(r.snippet || r.description || "").slice(0, 80)
|
|
1059
|
+
]);
|
|
1060
|
+
return makeTable(["#", "Title", "URL", "Snippet"], rows, opts);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/commands/scrape.ts
|
|
1064
|
+
function registerScrapeCommand(program2) {
|
|
1065
|
+
addGlobalOptions(
|
|
1066
|
+
program2.command("scrape").description("Scrape a web page via Firecrawl").argument("<url>", "URL to scrape").option("--max-chars <n>", "Truncate output to N characters")
|
|
1067
|
+
).action(async (url, opts) => {
|
|
1068
|
+
if (opts.maxChars !== void 0 && isNaN(parseInt(opts.maxChars, 10))) {
|
|
1069
|
+
console.error("Error: --max-chars must be a number");
|
|
1070
|
+
process.exit(1);
|
|
1071
|
+
}
|
|
1072
|
+
const apiKey = process.env.FIRECRAWL_API_KEY;
|
|
1073
|
+
if (!apiKey) {
|
|
1074
|
+
console.error("Error: FIRECRAWL_API_KEY environment variable is required.");
|
|
1075
|
+
process.exit(1);
|
|
1076
|
+
}
|
|
1077
|
+
const response = await fetch("https://api.firecrawl.dev/v1/scrape", {
|
|
1078
|
+
method: "POST",
|
|
1079
|
+
headers: {
|
|
1080
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1081
|
+
"Content-Type": "application/json"
|
|
1082
|
+
},
|
|
1083
|
+
body: JSON.stringify({ url, formats: ["markdown"] })
|
|
1084
|
+
});
|
|
1085
|
+
if (!response.ok) {
|
|
1086
|
+
const text = await response.text();
|
|
1087
|
+
console.error(`Firecrawl API error (${response.status}): ${text}`);
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
const data = await response.json();
|
|
1091
|
+
outputResult(data, opts, (d, o) => formatScrape(d, o, opts.maxChars));
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
function formatScrape(data, _opts, maxChars) {
|
|
1095
|
+
if (!data || typeof data !== "object") return String(data);
|
|
1096
|
+
const obj = data;
|
|
1097
|
+
const inner = obj.data || obj;
|
|
1098
|
+
const content = String(inner.markdown || inner.text || inner.content || "(no content)");
|
|
1099
|
+
const title = inner.metadata ? String(inner.metadata.title || "") : "";
|
|
1100
|
+
const header = title ? `# ${title}
|
|
1101
|
+
|
|
1102
|
+
` : "";
|
|
1103
|
+
const body = header + content;
|
|
1104
|
+
if (maxChars) {
|
|
1105
|
+
const limit = parseIntSafe(maxChars, body.length);
|
|
1106
|
+
if (body.length > limit) {
|
|
1107
|
+
return body.slice(0, limit) + "\n\n... (truncated)";
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return body;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
898
1113
|
// src/index.ts
|
|
899
1114
|
var program = new Command();
|
|
900
1115
|
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
901
1116
|
var pkgPath = path.join(__dirname, "..", "package.json");
|
|
902
|
-
var compiledVersion = "0.1.
|
|
1117
|
+
var compiledVersion = "0.1.4".length > 0 ? "0.1.4" : void 0;
|
|
903
1118
|
var envVersion = process.env.BLOOM_CLI_VERSION ?? process.env.npm_package_version;
|
|
904
1119
|
var fallbackVersion = compiledVersion ?? "unknown";
|
|
905
1120
|
var cliVersion = envVersion ?? fallbackVersion;
|
|
@@ -933,6 +1148,9 @@ registerPositionCommand(program);
|
|
|
933
1148
|
registerTranscriptCommand(program);
|
|
934
1149
|
registerAiTradesCommand(program);
|
|
935
1150
|
registerAiPortfolioCommand(program);
|
|
1151
|
+
registerMaCommand(program);
|
|
1152
|
+
registerSearchCommand(program);
|
|
1153
|
+
registerScrapeCommand(program);
|
|
936
1154
|
program.parseAsync().catch((err) => {
|
|
937
1155
|
console.error("Error:", err.message || err);
|
|
938
1156
|
process.exit(1);
|