@bloomai/cli 0.1.2 → 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 +64 -0
- package/SKILL.md +33 -0
- package/dist/index.js +351 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,12 +46,25 @@ 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"` |
|
|
52
53
|
| `catalysts` | Forward-looking value drivers | `bloom catalysts AAPL MSFT` |
|
|
53
54
|
| `position-size` | Kelly criterion sizing | `bloom position-size --bull 0.6 --bear -0.15 --conviction 8 --value 100000` |
|
|
54
55
|
| `transcript` | Search earnings call transcripts | `bloom transcript AAPL "AI" "services"` |
|
|
56
|
+
| `ai-trades` | Recent AI model trade decisions (InvestingArena) | `bloom ai-trades --model claude --limit 5` |
|
|
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 |
|
|
55
68
|
|
|
56
69
|
## Global Flags
|
|
57
70
|
|
|
@@ -115,6 +128,57 @@ bloom transcript AAPL "AI" "services" "margins" --max 20 -o /tmp/bloom/transcrip
|
|
|
115
128
|
jq '.matches[] | {speaker, text}' /tmp/bloom/transcript.json
|
|
116
129
|
```
|
|
117
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
|
+
|
|
146
|
+
### AI Arena (InvestingArena — 3 AIs with real money)
|
|
147
|
+
```bash
|
|
148
|
+
# All AI models' recent trades
|
|
149
|
+
bloom ai-trades -o /tmp/bloom/ai-trades.json
|
|
150
|
+
|
|
151
|
+
# Claude's last 5 trades
|
|
152
|
+
bloom ai-trades --model claude --limit 5
|
|
153
|
+
|
|
154
|
+
# All trades on NVDA across all models
|
|
155
|
+
bloom ai-trades --symbol NVDA -o /tmp/bloom/nvda-ai-trades.json
|
|
156
|
+
|
|
157
|
+
# Full portfolio snapshot for all 3 models
|
|
158
|
+
bloom ai-portfolio -o /tmp/bloom/ai-portfolio.json
|
|
159
|
+
|
|
160
|
+
# Just Gemini's portfolio
|
|
161
|
+
bloom ai-portfolio --model gemini
|
|
162
|
+
|
|
163
|
+
# Extract model returns
|
|
164
|
+
jq '[.[] | {model: .ai_model, return_pct, account_value}]' /tmp/bloom/ai-portfolio.json
|
|
165
|
+
```
|
|
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
|
+
|
|
118
182
|
## Tips
|
|
119
183
|
|
|
120
184
|
- Use `-o` to download data to files, then `jq` to extract. Saves context tokens.
|
package/SKILL.md
CHANGED
|
@@ -48,6 +48,18 @@ jq '.data[] | select(.revenue_growth > 0.1)' /tmp/bloom/aapl-revenue.json
|
|
|
48
48
|
| `catalysts` | Forward-looking value drivers | `bloom catalysts AAPL MSFT` |
|
|
49
49
|
| `position-size` | Kelly criterion sizing | `bloom position-size --bull 0.6 --bear -0.15 --conviction 8 --value 100000` |
|
|
50
50
|
| `transcript` | Search earnings call transcripts | `bloom transcript AAPL "AI" "services"` |
|
|
51
|
+
| `ai-trades` | Recent AI model trade decisions (InvestingArena) | `bloom ai-trades --model claude --limit 5` |
|
|
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 |
|
|
51
63
|
|
|
52
64
|
## Global Flags
|
|
53
65
|
|
|
@@ -111,6 +123,27 @@ bloom transcript AAPL "AI" "services" "margins" --max 20 -o /tmp/bloom/transcrip
|
|
|
111
123
|
jq '.matches[] | {speaker, text}' /tmp/bloom/transcript.json
|
|
112
124
|
```
|
|
113
125
|
|
|
126
|
+
### AI Arena (InvestingArena — 3 AIs with real money)
|
|
127
|
+
```bash
|
|
128
|
+
# All AI models' recent trades
|
|
129
|
+
bloom ai-trades -o /tmp/bloom/ai-trades.json
|
|
130
|
+
|
|
131
|
+
# Claude's last 5 trades
|
|
132
|
+
bloom ai-trades --model claude --limit 5
|
|
133
|
+
|
|
134
|
+
# All trades on NVDA across all models
|
|
135
|
+
bloom ai-trades --symbol NVDA -o /tmp/bloom/nvda-ai-trades.json
|
|
136
|
+
|
|
137
|
+
# Full portfolio snapshot for all 3 models
|
|
138
|
+
bloom ai-portfolio -o /tmp/bloom/ai-portfolio.json
|
|
139
|
+
|
|
140
|
+
# Just Gemini's portfolio
|
|
141
|
+
bloom ai-portfolio --model gemini
|
|
142
|
+
|
|
143
|
+
# Extract model returns
|
|
144
|
+
jq '[.[] | {model: .ai_model, return_pct, account_value}]' /tmp/bloom/ai-portfolio.json
|
|
145
|
+
```
|
|
146
|
+
|
|
114
147
|
## Tips
|
|
115
148
|
|
|
116
149
|
- Use `-o` to download data to files, then `jq` to extract. Saves context tokens.
|
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
|
});
|
|
@@ -782,12 +785,350 @@ function formatTranscript(data, opts) {
|
|
|
782
785
|
return makeTable(["Speaker", "Role", "Text"], rows, opts);
|
|
783
786
|
}
|
|
784
787
|
|
|
788
|
+
// src/commands/ai-trades.ts
|
|
789
|
+
function registerAiTradesCommand(program2) {
|
|
790
|
+
addGlobalOptions(
|
|
791
|
+
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")
|
|
792
|
+
).action(async (opts) => {
|
|
793
|
+
const args = {
|
|
794
|
+
limit: parseIntSafe(opts.limit, 10)
|
|
795
|
+
};
|
|
796
|
+
if (opts.model) args.ai_model = opts.model;
|
|
797
|
+
if (opts.symbol) args.symbol = opts.symbol.toUpperCase();
|
|
798
|
+
const data = await callTool("get_ai_trade_ideas", args);
|
|
799
|
+
if (opts.raw) {
|
|
800
|
+
outputResult(data, opts);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const obj = data;
|
|
804
|
+
const trades = obj.recent_trades ?? data;
|
|
805
|
+
outputResult(trades, opts, (d, o) => formatTrades(d, o));
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
function formatTrades(data, opts) {
|
|
809
|
+
if (!data || !Array.isArray(data)) {
|
|
810
|
+
if (data && typeof data === "object") {
|
|
811
|
+
return JSON.stringify(data, null, 2);
|
|
812
|
+
}
|
|
813
|
+
return String(data ?? "(no trades)");
|
|
814
|
+
}
|
|
815
|
+
if (data.length === 0) return "(no trades found)";
|
|
816
|
+
const rows = data.map((trade) => {
|
|
817
|
+
const t = trade;
|
|
818
|
+
const gain = t.realized_gain_loss != null ? `$${Number(t.realized_gain_loss).toFixed(2)}` : "-";
|
|
819
|
+
return [
|
|
820
|
+
String(t.ai_model ?? t.model ?? "-"),
|
|
821
|
+
String(t.action ?? "-").toUpperCase(),
|
|
822
|
+
String(t.symbol ?? "-"),
|
|
823
|
+
String(t.shares ?? "-"),
|
|
824
|
+
t.fill_price != null ? `$${Number(t.fill_price).toFixed(2)}` : "-",
|
|
825
|
+
gain,
|
|
826
|
+
truncate2(String(t.rationale ?? "-"), 60)
|
|
827
|
+
];
|
|
828
|
+
});
|
|
829
|
+
return makeTable(
|
|
830
|
+
["Model", "Action", "Symbol", "Shares", "Fill Price", "Realized P/L", "Rationale"],
|
|
831
|
+
rows,
|
|
832
|
+
opts
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
function truncate2(s, max) {
|
|
836
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/commands/ai-portfolio.ts
|
|
840
|
+
function registerAiPortfolioCommand(program2) {
|
|
841
|
+
addGlobalOptions(
|
|
842
|
+
program2.command("ai-portfolio").description("Current portfolio positions held by AI models (InvestingArena live accounts)").option("--model <m>", "Filter by AI model: claude|openai|gemini")
|
|
843
|
+
).action(async (opts) => {
|
|
844
|
+
const args = {};
|
|
845
|
+
if (opts.model) args.ai_model = opts.model;
|
|
846
|
+
const data = await callTool("get_ai_trade_ideas", args);
|
|
847
|
+
if (opts.raw) {
|
|
848
|
+
outputResult(data, opts);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const obj = data;
|
|
852
|
+
const portfolios = obj.portfolios ?? data;
|
|
853
|
+
outputResult(portfolios, opts, (d, o) => formatPortfolios(d, o));
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
function formatPortfolios(data, opts) {
|
|
857
|
+
if (!data || !Array.isArray(data)) {
|
|
858
|
+
if (data && typeof data === "object") {
|
|
859
|
+
return JSON.stringify(data, null, 2);
|
|
860
|
+
}
|
|
861
|
+
return String(data ?? "(no portfolio data)");
|
|
862
|
+
}
|
|
863
|
+
if (data.length === 0) return "(no portfolio data found)";
|
|
864
|
+
const lines = [];
|
|
865
|
+
for (const portfolio of data) {
|
|
866
|
+
const p = portfolio;
|
|
867
|
+
const modelName = String(p.ai_model ?? p.model ?? "Unknown").toUpperCase();
|
|
868
|
+
lines.push(`
|
|
869
|
+
=== ${modelName} ===`);
|
|
870
|
+
const acctValue = p.account_value != null ? `$${Number(p.account_value).toFixed(2)}` : "-";
|
|
871
|
+
const cash = p.cash != null ? `$${Number(p.cash).toFixed(2)}` : "-";
|
|
872
|
+
const returnPct = p.return_pct != null ? `${Number(p.return_pct).toFixed(2)}%` : "-";
|
|
873
|
+
const pnl = p.total_pnl != null ? `$${Number(p.total_pnl).toFixed(2)}` : "-";
|
|
874
|
+
lines.push(` Account Value: ${acctValue} | Cash: ${cash} | Return: ${returnPct} | Total P&L: ${pnl}`);
|
|
875
|
+
const positions = p.positions;
|
|
876
|
+
if (Array.isArray(positions) && positions.length > 0) {
|
|
877
|
+
lines.push("");
|
|
878
|
+
const rows = positions.map((pos) => {
|
|
879
|
+
const position = pos;
|
|
880
|
+
const unrealizedPnl = position.unrealized_pnl != null ? `$${Number(position.unrealized_pnl).toFixed(2)}` : "-";
|
|
881
|
+
const allocation = position.allocation_pct != null ? `${Number(position.allocation_pct).toFixed(1)}%` : "-";
|
|
882
|
+
const avgCost = position.avg_cost != null ? `$${Number(position.avg_cost).toFixed(2)}` : "-";
|
|
883
|
+
return [
|
|
884
|
+
String(position.symbol ?? "-"),
|
|
885
|
+
String(position.qty ?? "-"),
|
|
886
|
+
avgCost,
|
|
887
|
+
unrealizedPnl,
|
|
888
|
+
allocation
|
|
889
|
+
];
|
|
890
|
+
});
|
|
891
|
+
lines.push(
|
|
892
|
+
makeTable(["Symbol", "Qty", "Avg Cost", "Unrealized P/L", "Allocation"], rows, opts)
|
|
893
|
+
);
|
|
894
|
+
} else {
|
|
895
|
+
lines.push(" (no open positions)");
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return lines.join("\n");
|
|
899
|
+
}
|
|
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
|
+
|
|
785
1113
|
// src/index.ts
|
|
786
1114
|
var program = new Command();
|
|
787
1115
|
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
788
1116
|
var pkgPath = path.join(__dirname, "..", "package.json");
|
|
789
|
-
var
|
|
790
|
-
|
|
1117
|
+
var compiledVersion = "0.1.4".length > 0 ? "0.1.4" : void 0;
|
|
1118
|
+
var envVersion = process.env.BLOOM_CLI_VERSION ?? process.env.npm_package_version;
|
|
1119
|
+
var fallbackVersion = compiledVersion ?? "unknown";
|
|
1120
|
+
var cliVersion = envVersion ?? fallbackVersion;
|
|
1121
|
+
try {
|
|
1122
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
|
|
1123
|
+
if (typeof pkg.version === "string") {
|
|
1124
|
+
cliVersion = pkg.version;
|
|
1125
|
+
}
|
|
1126
|
+
} catch {
|
|
1127
|
+
}
|
|
1128
|
+
if (cliVersion === "0.0.0") {
|
|
1129
|
+
cliVersion = fallbackVersion;
|
|
1130
|
+
}
|
|
1131
|
+
program.name("bloom").description("Bloom CLI \u2014 financial data for coding agents and humans").version(cliVersion);
|
|
791
1132
|
registerAuthCommand(program);
|
|
792
1133
|
registerInfoCommand(program);
|
|
793
1134
|
registerNewsCommand(program);
|
|
@@ -805,6 +1146,11 @@ registerTradesCommand(program);
|
|
|
805
1146
|
registerCatalystsCommand(program);
|
|
806
1147
|
registerPositionCommand(program);
|
|
807
1148
|
registerTranscriptCommand(program);
|
|
1149
|
+
registerAiTradesCommand(program);
|
|
1150
|
+
registerAiPortfolioCommand(program);
|
|
1151
|
+
registerMaCommand(program);
|
|
1152
|
+
registerSearchCommand(program);
|
|
1153
|
+
registerScrapeCommand(program);
|
|
808
1154
|
program.parseAsync().catch((err) => {
|
|
809
1155
|
console.error("Error:", err.message || err);
|
|
810
1156
|
process.exit(1);
|