@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 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.3".length > 0 ? "0.1.3" : void 0;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloomai/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "CLI tool for Bloom financial data - download market data to files for analysis",
5
5
  "type": "module",
6
6
  "bin": {