@bloomai/cli 0.1.0 → 0.1.1

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
@@ -1,67 +1,124 @@
1
1
  # Bloom CLI
2
2
 
3
- A command-line tool for accessing Bloom financial data. Designed for coding agents that need to download data to files and use bash tools (jq, grep, awk) to extract what they need, instead of loading everything into LLM context.
3
+ Financial data CLI for coding agents. Downloads data to files so you can use bash (jq, grep, awk) to extract what you need instead of loading everything into context.
4
4
 
5
5
  ## Setup
6
6
 
7
7
  ```bash
8
- cd cli
9
- npm install
10
- npm run build
8
+ npx @bloomai/cli --help
9
+ # Or install globally:
10
+ npm install -g @bloomai/cli
11
+
11
12
  export BLOOM_API_KEY=<your-key>
12
- node dist/index.js --help
13
+ # Or: bloom auth login
13
14
  ```
14
15
 
15
- ## Usage
16
+ ## Quick Research Workflow
16
17
 
17
18
  ```bash
18
- # Company fundamentals
19
- bloom info AAPL MSFT
20
-
21
- # Download to file for analysis
22
- bloom info AAPL -o /tmp/aapl.json
23
- jq '.symbols[0].basic_metadata | {name, latest_price, pe_ratio}' /tmp/aapl.json
19
+ # 1. Find what you need
20
+ bloom info AAPL # fundamentals, ratings, peers
21
+ bloom info AAPL --fields basic_metadata,bottom_line_ratings # specific sections only
24
22
 
25
- # Market data
26
- bloom sentiment
27
- bloom market --type top_movers
28
- bloom news AAPL --limit 5
23
+ # 2. Download to files for analysis
24
+ bloom info AAPL -o /tmp/bloom/aapl-info.json
25
+ bloom news AAPL --limit 10 -o /tmp/bloom/aapl-news.json
26
+ bloom price AAPL --timeframes 1m,1y -o /tmp/bloom/aapl-price.json
27
+ bloom financials AAPL --type revenue -o /tmp/bloom/aapl-revenue.json
29
28
 
30
- # Screening
31
- bloom screen "market_cap > 100B" "pe_ratio < 25"
32
-
33
- # Analysis
34
- bloom earnings AAPL
35
- bloom technicals AAPL
36
- bloom catalysts AAPL
29
+ # 3. Use bash to extract what matters
30
+ jq '.symbols[0].basic_metadata | {name, latest_price, sector, pe_ratio}' /tmp/bloom/aapl-info.json
31
+ jq '.articles[] | .headline' /tmp/bloom/aapl-news.json
32
+ jq '.data[] | select(.revenue_growth > 0.1)' /tmp/bloom/aapl-revenue.json
37
33
  ```
38
34
 
39
- See [SKILL.md](./SKILL.md) for complete workflow patterns for coding agents.
40
-
41
35
  ## Commands
42
36
 
43
- | Command | Description |
44
- |---------|-------------|
45
- | `info` | Company fundamentals, ratings, peers |
46
- | `news` | Recent news headlines |
47
- | `price` | Historical price data |
48
- | `financials` | Revenue, margins, cash flow |
49
- | `sentiment` | Market mood (AAII, Fear&Greed, VIX) |
50
- | `market` | Market map, sectors, movers |
51
- | `collections` | Curated stock lists |
52
- | `screen` | Filter stocks by metrics |
53
- | `earnings` | Earnings history & estimates |
54
- | `technicals` | RSI, MAs, patterns, options |
55
- | `options-history` | Options sentiment over time |
56
- | `portfolio` | Portfolio risk analysis |
57
- | `trades` | AI trade evaluation |
58
- | `catalysts` | Forward-looking value drivers |
59
- | `position-size` | Kelly criterion sizing |
60
- | `transcript` | Search earnings call transcripts |
37
+ | Command | What it does | Example |
38
+ |---------|-------------|---------|
39
+ | `info` | Company fundamentals, ratings, peers | `bloom info AAPL MSFT` |
40
+ | `news` | Recent news headlines | `bloom news AAPL --limit 5` |
41
+ | `price` | Historical price data | `bloom price AAPL --timeframes 1d,1m,1y` |
42
+ | `financials` | Revenue, margins, cash flow | `bloom financials AAPL --type revenue` |
43
+ | `sentiment` | Market mood (AAII, Fear&Greed, VIX) | `bloom sentiment` |
44
+ | `market` | Market map, sectors, movers | `bloom market --type top_movers` |
45
+ | `collections` | Curated stock lists by theme | `bloom collections --section VALUE` |
46
+ | `screen` | Filter stocks by metrics | `bloom screen "market_cap > 100B" "pe_ratio < 25"` |
47
+ | `earnings` | Earnings history & estimates | `bloom earnings AAPL` |
48
+ | `technicals` | RSI, MAs, patterns, options flow | `bloom technicals AAPL` |
49
+ | `options-history` | Options sentiment over time | `bloom options-history AAPL --days 30` |
50
+ | `portfolio` | Portfolio risk analysis | `bloom portfolio '{"AAPL":0.5,"MSFT":0.5}'` |
51
+ | `trades` | AI trade evaluation | `bloom trades "Buy AAPL: strong AI catalysts"` |
52
+ | `catalysts` | Forward-looking value drivers | `bloom catalysts AAPL MSFT` |
53
+ | `position-size` | Kelly criterion sizing | `bloom position-size --bull 0.6 --bear -0.15 --conviction 8 --value 100000` |
54
+ | `transcript` | Search earnings call transcripts | `bloom transcript AAPL "AI" "services"` |
61
55
 
62
56
  ## Global Flags
63
57
 
64
- - `-o, --output <file>` — write to file
65
- - `-f, --format json|csv|table` — output format
66
- - `-q, --quiet` — data only
58
+ Every command supports:
59
+ - `-o, --output <file>` — write to file instead of stdout
60
+ - `-f, --format json|csv|table` — output format (default: json)
61
+ - `-q, --quiet` — suppress headers, data only
67
62
  - `--raw` — raw API response
63
+
64
+ ## Common Patterns
65
+
66
+ ### Compare Multiple Stocks
67
+ ```bash
68
+ mkdir -p /tmp/bloom/compare
69
+ for ticker in AAPL MSFT GOOG AMZN; do
70
+ bloom info $ticker -o /tmp/bloom/compare/$ticker.json
71
+ done
72
+ # Extract key metrics
73
+ for f in /tmp/bloom/compare/*.json; do
74
+ ticker=$(basename $f .json)
75
+ pe=$(jq -r '.symbols[0].basic_metadata.pe_ratio // "N/A"' $f)
76
+ price=$(jq -r '.symbols[0].basic_metadata.latest_price' $f)
77
+ echo "$ticker: \$$price (P/E: $pe)"
78
+ done
79
+ ```
80
+
81
+ ### Screen → Deep Dive
82
+ ```bash
83
+ # Find cheap large-caps
84
+ bloom screen "market_cap > 50B" "pe_ratio < 20" "dividend_yield > 2" -o /tmp/bloom/screened.json
85
+
86
+ # Get the tickers
87
+ tickers=$(jq -r '.results[].symbol' /tmp/bloom/screened.json | head -5 | tr '\n' ' ')
88
+
89
+ # Deep dive on each
90
+ for t in $tickers; do
91
+ bloom earnings $t -o /tmp/bloom/earnings-$t.json
92
+ bloom catalysts $t -o /tmp/bloom/catalysts-$t.json
93
+ done
94
+ ```
95
+
96
+ ### Market Check
97
+ ```bash
98
+ bloom sentiment -o /tmp/bloom/sentiment.json
99
+ bloom market --type major_indexes -o /tmp/bloom/indexes.json
100
+ bloom market --type top_movers --limit 10 -o /tmp/bloom/movers.json
101
+
102
+ # Quick summary
103
+ jq '{fear_greed: .cnn_fear_greed.index_value, level: .cnn_fear_greed.level, vix: .volatility_analysis.assessment}' /tmp/bloom/sentiment.json
104
+ ```
105
+
106
+ ### Portfolio Review
107
+ ```bash
108
+ bloom portfolio '{"AAPL":0.3,"MSFT":0.25,"GOOG":0.25,"AMZN":0.2}' --strategy "tech growth" -o /tmp/bloom/portfolio.json
109
+ jq '.risk_metrics' /tmp/bloom/portfolio.json
110
+ ```
111
+
112
+ ### Earnings Research
113
+ ```bash
114
+ bloom transcript AAPL "AI" "services" "margins" --max 20 -o /tmp/bloom/transcript.json
115
+ jq '.matches[] | {speaker, text}' /tmp/bloom/transcript.json
116
+ ```
117
+
118
+ ## Tips
119
+
120
+ - Use `-o` to download data to files, then `jq` to extract. Saves context tokens.
121
+ - Batch multiple tickers: `bloom info AAPL MSFT GOOG` (up to 10).
122
+ - Use `bloom screen` first to narrow your universe, then deep-dive with other commands.
123
+ - `--fields` on `bloom info` reduces response size when you only need specific sections.
124
+ - Pipe to `jq -r '.field'` for clean values usable in scripts.
package/SKILL.md CHANGED
@@ -39,7 +39,7 @@ jq '.data[] | select(.revenue_growth > 0.1)' /tmp/bloom/aapl-revenue.json
39
39
  | `sentiment` | Market mood (AAII, Fear&Greed, VIX) | `bloom sentiment` |
40
40
  | `market` | Market map, sectors, movers | `bloom market --type top_movers` |
41
41
  | `collections` | Curated stock lists by theme | `bloom collections --section VALUE` |
42
- | `screen` | Filter stocks by metrics | `bloom screen "market_cap > 100B" "pe < 25"` |
42
+ | `screen` | Filter stocks by metrics | `bloom screen "market_cap > 100B" "pe_ratio < 25"` |
43
43
  | `earnings` | Earnings history & estimates | `bloom earnings AAPL` |
44
44
  | `technicals` | RSI, MAs, patterns, options flow | `bloom technicals AAPL` |
45
45
  | `options-history` | Options sentiment over time | `bloom options-history AAPL --days 30` |
@@ -107,7 +107,7 @@ jq '.risk_metrics' /tmp/bloom/portfolio.json
107
107
 
108
108
  ### Earnings Research
109
109
  ```bash
110
- bloom transcript AAPL "AI" "services" "margins" --max-results 20 -o /tmp/bloom/transcript.json
110
+ bloom transcript AAPL "AI" "services" "margins" --max 20 -o /tmp/bloom/transcript.json
111
111
  jq '.matches[] | {speaker, text}' /tmp/bloom/transcript.json
112
112
  ```
113
113
 
package/package.json CHANGED
@@ -1,16 +1,27 @@
1
1
  {
2
2
  "name": "@bloomai/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "CLI tool for Bloom financial data - download market data to files for analysis",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "bloom": "./dist/index.js"
8
8
  },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "SKILL.md"
13
+ ],
9
14
  "scripts": {
10
15
  "build": "tsup",
11
16
  "dev": "tsup --watch"
12
17
  },
13
- "keywords": ["finance", "stocks", "cli", "bloom", "market-data"],
18
+ "keywords": [
19
+ "finance",
20
+ "stocks",
21
+ "cli",
22
+ "bloom",
23
+ "market-data"
24
+ ],
14
25
  "license": "MIT",
15
26
  "dependencies": {
16
27
  "cli-table3": "^0.6.5",
package/src/api.ts DELETED
@@ -1,67 +0,0 @@
1
- import { requireApiKey } from "./auth.js";
2
-
3
- const MCP_URL = "https://api.getbloom.app/mcp/";
4
-
5
- let requestId = 0;
6
-
7
- interface McpResponse {
8
- jsonrpc: string;
9
- id: number;
10
- result?: {
11
- content: Array<{ type: string; text: string }>;
12
- };
13
- error?: {
14
- code: number;
15
- message: string;
16
- };
17
- }
18
-
19
- export async function callTool(
20
- toolName: string,
21
- args: Record<string, unknown>
22
- ): Promise<unknown> {
23
- const apiKey = requireApiKey();
24
- requestId++;
25
-
26
- const body = {
27
- jsonrpc: "2.0",
28
- method: "tools/call",
29
- params: {
30
- name: toolName,
31
- arguments: args,
32
- },
33
- id: requestId,
34
- };
35
-
36
- const res = await fetch(MCP_URL, {
37
- method: "POST",
38
- headers: {
39
- Authorization: `Bearer ${apiKey}`,
40
- "Content-Type": "application/json",
41
- },
42
- body: JSON.stringify(body),
43
- });
44
-
45
- if (!res.ok) {
46
- const text = await res.text().catch(() => "");
47
- throw new Error(`API error ${res.status}: ${text || res.statusText}`);
48
- }
49
-
50
- const data = (await res.json()) as McpResponse;
51
-
52
- if (data.error) {
53
- throw new Error(`MCP error ${data.error.code}: ${data.error.message}`);
54
- }
55
-
56
- if (!data.result?.content?.length) {
57
- throw new Error("Empty response from API");
58
- }
59
-
60
- // The MCP response wraps the data as a JSON string inside content[0].text
61
- const text = data.result.content[0].text;
62
- try {
63
- return JSON.parse(text);
64
- } catch {
65
- return text;
66
- }
67
- }
package/src/auth.ts DELETED
@@ -1,83 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { homedir } from "node:os";
4
-
5
- const CONFIG_DIR = join(homedir(), ".config", "bloom-cli");
6
- const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
-
8
- interface Config {
9
- api_key?: string;
10
- }
11
-
12
- function readConfig(): Config {
13
- if (!existsSync(CONFIG_FILE)) return {};
14
- try {
15
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
16
- } catch {
17
- return {};
18
- }
19
- }
20
-
21
- function writeConfig(config: Config): void {
22
- try {
23
- mkdirSync(CONFIG_DIR, { recursive: true });
24
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
25
- mode: 0o600,
26
- });
27
- } catch (err) {
28
- const msg = err instanceof Error ? err.message : String(err);
29
- console.error(
30
- `Error: Could not write config to ${CONFIG_FILE}: ${msg}\n` +
31
- `Try using BLOOM_API_KEY environment variable instead.`
32
- );
33
- process.exit(1);
34
- }
35
- }
36
-
37
- export function getApiKey(): string | undefined {
38
- return process.env.BLOOM_API_KEY || readConfig().api_key;
39
- }
40
-
41
- export function requireApiKey(): string {
42
- const key = getApiKey();
43
- if (!key) {
44
- console.error(
45
- "Error: No API key found. Set BLOOM_API_KEY or run: bloom auth login"
46
- );
47
- process.exit(1);
48
- }
49
- return key;
50
- }
51
-
52
- export function login(apiKey: string): void {
53
- const config = readConfig();
54
- config.api_key = apiKey;
55
- writeConfig(config);
56
- console.log("API key saved to " + CONFIG_FILE);
57
- }
58
-
59
- export function logout(): void {
60
- if (existsSync(CONFIG_FILE)) {
61
- const config = readConfig();
62
- delete config.api_key;
63
- writeConfig(config);
64
- }
65
- console.log("Logged out. API key cleared.");
66
- }
67
-
68
- export function status(): void {
69
- const envKey = process.env.BLOOM_API_KEY;
70
- const storedKey = readConfig().api_key;
71
- if (envKey) {
72
- console.log(`Authenticated via BLOOM_API_KEY (${maskKey(envKey)})`);
73
- } else if (storedKey) {
74
- console.log(`Authenticated via ${CONFIG_FILE} (${maskKey(storedKey)})`);
75
- } else {
76
- console.log("Not authenticated. Set BLOOM_API_KEY or run: bloom auth login");
77
- }
78
- }
79
-
80
- function maskKey(key: string): string {
81
- if (key.length <= 8) return "****";
82
- return key.slice(0, 4) + "..." + key.slice(-4);
83
- }
@@ -1,45 +0,0 @@
1
- import { Command } from "commander";
2
- import { login, logout, status } from "../auth.js";
3
- import { createInterface } from "node:readline";
4
-
5
- export function registerAuthCommand(program: Command): void {
6
- const auth = program.command("auth").description("Manage API authentication");
7
-
8
- auth
9
- .command("login")
10
- .description("Store API key")
11
- .argument("[key]", "API key (or pass via stdin)")
12
- .action(async (key?: string) => {
13
- if (!key) {
14
- const rl = createInterface({
15
- input: process.stdin,
16
- output: process.stderr,
17
- });
18
- key = await new Promise<string>((resolve) => {
19
- rl.question("API key: ", (answer) => {
20
- rl.close();
21
- resolve(answer.trim());
22
- });
23
- });
24
- }
25
- if (!key) {
26
- console.error("Error: No API key provided");
27
- process.exit(1);
28
- }
29
- login(key);
30
- });
31
-
32
- auth
33
- .command("status")
34
- .description("Show current auth state")
35
- .action(() => {
36
- status();
37
- });
38
-
39
- auth
40
- .command("logout")
41
- .description("Clear stored credentials")
42
- .action(() => {
43
- logout();
44
- });
45
- }
@@ -1,18 +0,0 @@
1
- import { Command } from "commander";
2
- import { callTool } from "../api.js";
3
- import { outputResult, OutputOptions } from "../format.js";
4
- import { addGlobalOptions } from "../global-opts.js";
5
-
6
- export function registerCatalystsCommand(program: Command): void {
7
- addGlobalOptions(
8
- program
9
- .command("catalysts")
10
- .description("Forward-looking value catalysts")
11
- .argument("<symbols...>", "Ticker symbols (up to 5)")
12
- ).action(async (symbols: string[], opts: OutputOptions) => {
13
- const data = await callTool("generate_catalysts", {
14
- symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5),
15
- });
16
- outputResult(data, opts);
17
- });
18
- }
@@ -1,20 +0,0 @@
1
- import { Command } from "commander";
2
- import { callTool } from "../api.js";
3
- import { outputResult, OutputOptions, parseIntSafe } from "../format.js";
4
- import { addGlobalOptions } from "../global-opts.js";
5
-
6
- export function registerCollectionsCommand(program: Command): void {
7
- addGlobalOptions(
8
- program
9
- .command("collections")
10
- .description("Curated stock collections by theme")
11
- .option("--section <s>", "GET_STARTED|VALUE|TRENDS|BEAT_UP|FAMOUS_INVESTORS|HIGH_FLYERS|SECTORS|GEO|LEADERBOARDS", "GET_STARTED")
12
- .option("--limit <n>", "Max collections", "6")
13
- ).action(async (opts: OutputOptions & { section: string; limit: string }) => {
14
- const data = await callTool("get_collections", {
15
- section: opts.section,
16
- limit: parseIntSafe(opts.limit, 6),
17
- });
18
- outputResult(data, opts);
19
- });
20
- }
@@ -1,18 +0,0 @@
1
- import { Command } from "commander";
2
- import { callTool } from "../api.js";
3
- import { outputResult, OutputOptions } from "../format.js";
4
- import { addGlobalOptions } from "../global-opts.js";
5
-
6
- export function registerEarningsCommand(program: Command): void {
7
- addGlobalOptions(
8
- program
9
- .command("earnings")
10
- .description("Earnings history, estimates, surprises")
11
- .argument("<symbols...>", "Ticker symbols (up to 5)")
12
- ).action(async (symbols: string[], opts: OutputOptions) => {
13
- const data = await callTool("analyze_earnings", {
14
- symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5),
15
- });
16
- outputResult(data, opts);
17
- });
18
- }
@@ -1,30 +0,0 @@
1
- import { Command } from "commander";
2
- import { callTool } from "../api.js";
3
- import { outputResult, OutputOptions } from "../format.js";
4
- import { addGlobalOptions } from "../global-opts.js";
5
-
6
- const CHART_TYPES = [
7
- "revenue",
8
- "revenue_growth",
9
- "operating_margin",
10
- "net_earnings",
11
- "net_earnings_growth",
12
- "free_cash_flow",
13
- "free_cash_flow_growth",
14
- ];
15
-
16
- export function registerFinancialsCommand(program: Command): void {
17
- addGlobalOptions(
18
- program
19
- .command("financials")
20
- .description("Quarterly/annual financial data with YoY growth")
21
- .argument("<symbol>", "Ticker symbol")
22
- .option("--type <type>", "Chart type: " + CHART_TYPES.join(", "), "revenue")
23
- ).action(async (symbol: string, opts: OutputOptions & { type: string }) => {
24
- const data = await callTool("get_financial_charts", {
25
- symbol: symbol.toUpperCase(),
26
- chart_type: opts.type,
27
- });
28
- outputResult(data, opts);
29
- });
30
- }
@@ -1,79 +0,0 @@
1
- import { Command } from "commander";
2
- import { callTool } from "../api.js";
3
- import { outputResult, OutputOptions } from "../format.js";
4
- import { addGlobalOptions } from "../global-opts.js";
5
-
6
- const VALID_FIELDS = [
7
- "basic_metadata",
8
- "bottom_line_ratings",
9
- "embedded_expectations",
10
- "peers",
11
- "key_events",
12
- "holdings",
13
- ];
14
-
15
- export function registerInfoCommand(program: Command): void {
16
- addGlobalOptions(
17
- program
18
- .command("info")
19
- .description("Company fundamentals, ratings, peers, holdings")
20
- .argument("<symbols...>", "Ticker symbols (e.g. AAPL MSFT)")
21
- .option("--fields <fields>", "Comma-separated: " + VALID_FIELDS.join(","))
22
- ).action(async (symbols: string[], opts: OutputOptions & { fields?: string }) => {
23
- const args: Record<string, unknown> = {
24
- symbols: symbols.map((s) => s.toUpperCase()),
25
- };
26
- if (opts.fields) {
27
- args.fields = opts.fields.split(",").map((f) => f.trim());
28
- }
29
- const data = await callTool("get_symbols_metadata", args);
30
- outputResult(data, opts, (d) => formatInfo(d));
31
- });
32
- }
33
-
34
- function formatInfo(data: unknown): string {
35
- if (!data || typeof data !== "object") return String(data);
36
- const obj = data as Record<string, unknown>;
37
- const sections: string[] = [];
38
-
39
- for (const [key, val] of Object.entries(obj)) {
40
- if (key === "timestamp" || key === "source") continue;
41
- if (val && typeof val === "object") {
42
- sections.push(`\n=== ${key.toUpperCase()} ===`);
43
- sections.push(formatSection(val as Record<string, unknown>));
44
- }
45
- }
46
-
47
- return sections.length > 0 ? sections.join("\n") : JSON.stringify(data, null, 2);
48
- }
49
-
50
- function formatSection(obj: Record<string, unknown>, indent = ""): string {
51
- const lines: string[] = [];
52
- for (const [k, v] of Object.entries(obj)) {
53
- if (v && typeof v === "object" && !Array.isArray(v)) {
54
- lines.push(`${indent}${k}:`);
55
- lines.push(formatSection(v as Record<string, unknown>, indent + " "));
56
- } else if (Array.isArray(v)) {
57
- if (v.length === 0) continue;
58
- if (typeof v[0] === "string" || typeof v[0] === "number") {
59
- lines.push(`${indent}${k}: ${v.join(", ")}`);
60
- } else {
61
- lines.push(`${indent}${k}:`);
62
- for (const item of v) {
63
- if (typeof item === "object" && item) {
64
- const summary = Object.entries(item as Record<string, unknown>)
65
- .slice(0, 4)
66
- .map(([ik, iv]) => `${ik}=${iv}`)
67
- .join(" ");
68
- lines.push(`${indent} - ${summary}`);
69
- } else {
70
- lines.push(`${indent} - ${item}`);
71
- }
72
- }
73
- }
74
- } else {
75
- lines.push(`${indent}${k}: ${v}`);
76
- }
77
- }
78
- return lines.join("\n");
79
- }
@@ -1,22 +0,0 @@
1
- import { Command } from "commander";
2
- import { callTool } from "../api.js";
3
- import { outputResult, OutputOptions, parseIntSafe } from "../format.js";
4
- import { addGlobalOptions } from "../global-opts.js";
5
-
6
- export function registerMarketCommand(program: Command): void {
7
- addGlobalOptions(
8
- program
9
- .command("market")
10
- .description("Market performance, sectors, global")
11
- .option("--type <type>", "top_movers|sector_breakdown|global_markets|major_indexes", "top_movers")
12
- .option("--period <p>", "1d|1w|1m|3m|6m|ytd|1y|5y", "1d")
13
- .option("--limit <n>", "Max stocks for top_movers", "24")
14
- ).action(async (opts: OutputOptions & { type: string; period: string; limit: string }) => {
15
- const data = await callTool("get_market_map", {
16
- map_type: opts.type,
17
- period: opts.period,
18
- limit: parseIntSafe(opts.limit, 24),
19
- });
20
- outputResult(data, opts);
21
- });
22
- }
@@ -1,48 +0,0 @@
1
- import { Command } from "commander";
2
- import { callTool } from "../api.js";
3
- import { outputResult, OutputOptions, makeTable, parseIntSafe } from "../format.js";
4
- import { addGlobalOptions } from "../global-opts.js";
5
-
6
- export function registerNewsCommand(program: Command): void {
7
- addGlobalOptions(
8
- program
9
- .command("news")
10
- .description("Recent news from Benzinga channels")
11
- .argument("<symbols...>", "Ticker symbols")
12
- .option("--limit <n>", "Max articles (default: 15)", "15")
13
- ).action(async (symbols: string[], opts: OutputOptions & { limit: string }) => {
14
- const data = await callTool("get_news_for_symbols", {
15
- symbols: symbols.map((s) => s.toUpperCase()),
16
- limit: parseIntSafe(opts.limit, 15),
17
- });
18
- outputResult(data, opts, (d, o) => formatNews(d, o));
19
- });
20
- }
21
-
22
- function formatNews(data: unknown, opts: OutputOptions): string {
23
- if (!data || typeof data !== "object") return String(data);
24
- const obj = data as Record<string, unknown>;
25
-
26
- let articles: Record<string, unknown>[] = [];
27
- if (Array.isArray(data)) {
28
- articles = data as Record<string, unknown>[];
29
- } else {
30
- for (const val of Object.values(obj)) {
31
- if (Array.isArray(val) && val.length > 0) {
32
- articles = val as Record<string, unknown>[];
33
- break;
34
- }
35
- }
36
- }
37
-
38
- if (articles.length === 0) return "(no articles)";
39
-
40
- const rows = articles.map((a) => [
41
- String(a.date || a.published || a.created || "").slice(0, 16),
42
- String(a.title || a.headline || "").slice(0, 80),
43
- String(a.symbols || a.tickers || a.stocks || "").slice(0, 20),
44
- String(a.channel || a.source || ""),
45
- ]);
46
-
47
- return makeTable(["Date", "Title", "Symbols", "Channel"], rows, opts);
48
- }