@bloomai/cli 0.1.0

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.
@@ -0,0 +1,77 @@
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 registerScreenCommand(program: Command): void {
7
+ addGlobalOptions(
8
+ program
9
+ .command("screen")
10
+ .description("Quantitative stock screener")
11
+ .argument("<filters...>", 'Filter expressions, e.g. "market_cap > 10B" "pe_ratio < 25"')
12
+ .option("--or", "Use OR logic (default: AND)")
13
+ .option("--limit <n>", "Max results", "50")
14
+ .option("--symbols <s>", "Comma-separated symbols to filter from")
15
+ .option("--fields <f>", "Comma-separated fields (use * for all)")
16
+ ).action(
17
+ async (
18
+ filters: string[],
19
+ opts: OutputOptions & {
20
+ or?: boolean;
21
+ limit: string;
22
+ symbols?: string;
23
+ fields?: string;
24
+ }
25
+ ) => {
26
+ const args: Record<string, unknown> = {
27
+ filter_expressions: filters,
28
+ and_query: !opts.or,
29
+ limit: parseIntSafe(opts.limit, 50),
30
+ };
31
+ if (opts.symbols) {
32
+ args.symbols = opts.symbols.split(",").map((s) => s.trim().toUpperCase());
33
+ }
34
+ if (opts.fields) {
35
+ args.fields = opts.fields.split(",").map((f) => f.trim());
36
+ }
37
+ const data = await callTool("filter_stocks", args);
38
+ outputResult(data, opts, (d, o) => formatScreen(d, o));
39
+ }
40
+ );
41
+ }
42
+
43
+ function formatScreen(data: unknown, opts: OutputOptions): string {
44
+ if (!data || typeof data !== "object") return String(data);
45
+ const obj = data as Record<string, unknown>;
46
+
47
+ let stocks: Record<string, unknown>[] = [];
48
+ if (Array.isArray(data)) {
49
+ stocks = data as Record<string, unknown>[];
50
+ } else {
51
+ for (const val of Object.values(obj)) {
52
+ if (Array.isArray(val) && val.length > 0) {
53
+ stocks = val as Record<string, unknown>[];
54
+ break;
55
+ }
56
+ }
57
+ }
58
+
59
+ if (stocks.length === 0) return "(no matches)";
60
+
61
+ const headers = Object.keys(stocks[0]).filter(
62
+ (k) => !k.startsWith("_") && k !== "id"
63
+ );
64
+ const rows = stocks.map((s) =>
65
+ headers.map((h) => {
66
+ const v = s[h];
67
+ if (typeof v === "number") {
68
+ if (Math.abs(v) >= 1e9) return `${(v / 1e9).toFixed(1)}B`;
69
+ if (Math.abs(v) >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
70
+ return v.toFixed(2);
71
+ }
72
+ return String(v ?? "");
73
+ })
74
+ );
75
+
76
+ return makeTable(headers, rows, opts);
77
+ }
@@ -0,0 +1,51 @@
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 SECTIONS = [
7
+ "aaii_sentiment",
8
+ "cnn_fear_greed",
9
+ "volatility_analysis",
10
+ "momentum_analysis",
11
+ ];
12
+
13
+ export function registerSentimentCommand(program: Command): void {
14
+ addGlobalOptions(
15
+ program
16
+ .command("sentiment")
17
+ .description("Market mood: AAII, Fear&Greed, VIX, momentum")
18
+ .option("--sections <s>", "Comma-separated: " + SECTIONS.join(","))
19
+ ).action(async (opts: OutputOptions & { sections?: string }) => {
20
+ const args: Record<string, unknown> = {};
21
+ if (opts.sections) {
22
+ args.sections = opts.sections.split(",").map((s) => s.trim());
23
+ }
24
+ const data = await callTool("get_market_sentiment", args);
25
+ outputResult(data, opts, (d) => formatSentiment(d));
26
+ });
27
+ }
28
+
29
+ function formatSentiment(data: unknown): string {
30
+ if (!data || typeof data !== "object") return String(data);
31
+ const obj = data as Record<string, unknown>;
32
+ const lines: string[] = [];
33
+
34
+ for (const [section, val] of Object.entries(obj)) {
35
+ if (section === "timestamp" || section === "source") continue;
36
+ if (!val || typeof val !== "object") continue;
37
+ lines.push(`\n=== ${section.replace(/_/g, " ").toUpperCase()} ===`);
38
+ for (const [k, v] of Object.entries(val as Record<string, unknown>)) {
39
+ if (v && typeof v === "object" && !Array.isArray(v)) {
40
+ lines.push(` ${k}:`);
41
+ for (const [ik, iv] of Object.entries(v as Record<string, unknown>)) {
42
+ lines.push(` ${ik}: ${iv}`);
43
+ }
44
+ } else {
45
+ lines.push(` ${k}: ${v}`);
46
+ }
47
+ }
48
+ }
49
+
50
+ return lines.join("\n") || JSON.stringify(data, null, 2);
51
+ }
@@ -0,0 +1,25 @@
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 SECTIONS = ["technical_indicators", "options_analysis", "insider_activity"];
7
+
8
+ export function registerTechnicalsCommand(program: Command): void {
9
+ addGlobalOptions(
10
+ program
11
+ .command("technicals")
12
+ .description("RSI, MAs, patterns, options flow, insider trades")
13
+ .argument("<symbols...>", "Ticker symbols (up to 5)")
14
+ .option("--sections <s>", "Comma-separated: " + SECTIONS.join(","))
15
+ ).action(async (symbols: string[], opts: OutputOptions & { sections?: string }) => {
16
+ const args: Record<string, unknown> = {
17
+ symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5),
18
+ };
19
+ if (opts.sections) {
20
+ args.sections = opts.sections.split(",").map((s) => s.trim());
21
+ }
22
+ const data = await callTool("analyze_technicals", args);
23
+ outputResult(data, opts);
24
+ });
25
+ }
@@ -0,0 +1,41 @@
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 registerTradesCommand(program: Command): void {
7
+ addGlobalOptions(
8
+ program
9
+ .command("trades")
10
+ .description("AI trade evaluation")
11
+ .argument("<trades...>", 'Trade descriptions: "Buy AAPL 30% because..." "Sell MSFT 20% because..."')
12
+ ).action(async (tradeStrs: string[], opts: OutputOptions) => {
13
+ const trades = tradeStrs.map(parseTrade);
14
+ const data = await callTool("analyze_trades", { trades });
15
+ outputResult(data, opts);
16
+ });
17
+ }
18
+
19
+ function parseTrade(str: string): Record<string, unknown> {
20
+ const match = str.match(
21
+ /^(buy|sell|hold)\s+([A-Z]+)\s+(\d+)%?\s+(?:because\s+)?(.+)$/i
22
+ );
23
+ if (match) {
24
+ return {
25
+ action: match[1].toLowerCase(),
26
+ symbol: match[2].toUpperCase(),
27
+ portfolio_weight: parseInt(match[3], 10) / 100,
28
+ thesis: match[4],
29
+ };
30
+ }
31
+
32
+ try {
33
+ return JSON.parse(str);
34
+ } catch {
35
+ console.error(
36
+ `Error: Could not parse trade "${str}"\n` +
37
+ 'Format: "Buy AAPL 30% because thesis" or JSON object'
38
+ );
39
+ process.exit(1);
40
+ }
41
+ }
@@ -0,0 +1,69 @@
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 registerTranscriptCommand(program: Command): void {
7
+ addGlobalOptions(
8
+ program
9
+ .command("transcript")
10
+ .description("Search earnings call transcripts")
11
+ .argument("<symbol>", "Ticker symbol")
12
+ .argument("<keywords...>", "Keywords to search (OR logic)")
13
+ .option("--quarter <q>", 'Fiscal quarter, e.g. "Q1 2024"')
14
+ .option("--max <n>", "Max results", "10")
15
+ ).action(
16
+ async (
17
+ symbol: string,
18
+ keywords: string[],
19
+ opts: OutputOptions & { quarter?: string; max: string }
20
+ ) => {
21
+ const args: Record<string, unknown> = {
22
+ symbol: symbol.toUpperCase(),
23
+ keywords,
24
+ max_results: parseIntSafe(opts.max, 10),
25
+ };
26
+ if (opts.quarter) args.fiscal_quarter = opts.quarter;
27
+
28
+ const data = await callTool("search_transcript", args);
29
+ outputResult(data, opts, (d, o) => formatTranscript(d, o));
30
+ }
31
+ );
32
+ }
33
+
34
+ function formatTranscript(data: unknown, opts: OutputOptions): string {
35
+ if (!data || typeof data !== "object") return String(data);
36
+ const obj = data as Record<string, unknown>;
37
+
38
+ let matches: Record<string, unknown>[] = [];
39
+ if (Array.isArray(data)) {
40
+ matches = data as Record<string, unknown>[];
41
+ } else {
42
+ for (const val of Object.values(obj)) {
43
+ if (Array.isArray(val) && val.length > 0) {
44
+ matches = val as Record<string, unknown>[];
45
+ break;
46
+ }
47
+ // Check one level deeper (e.g. transcript_search_results.matches)
48
+ if (val && typeof val === "object" && !Array.isArray(val)) {
49
+ for (const nested of Object.values(val as Record<string, unknown>)) {
50
+ if (Array.isArray(nested) && nested.length > 0) {
51
+ matches = nested as Record<string, unknown>[];
52
+ break;
53
+ }
54
+ }
55
+ }
56
+ if (matches.length > 0) break;
57
+ }
58
+ }
59
+
60
+ if (matches.length === 0) return "(no matches)";
61
+
62
+ const rows = matches.map((m) => [
63
+ String(m.speaker || m.name || ""),
64
+ String(m.role || m.title || ""),
65
+ String(m.content_with_highlights || m.text || m.content || m.exact_quote || m.quote || "").slice(0, 120),
66
+ ]);
67
+
68
+ return makeTable(["Speaker", "Role", "Text"], rows, opts);
69
+ }
package/src/format.ts ADDED
@@ -0,0 +1,179 @@
1
+ import Table from "cli-table3";
2
+ import { writeFileSync, mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+
5
+ export interface OutputOptions {
6
+ format?: "json" | "csv" | "table";
7
+ output?: string;
8
+ quiet?: boolean;
9
+ raw?: boolean;
10
+ }
11
+
12
+ export function resolveFormat(opts: OutputOptions): "json" | "csv" | "table" {
13
+ if (opts.format) return opts.format;
14
+ if (opts.output || !process.stdout.isTTY) return "json";
15
+ return "table";
16
+ }
17
+
18
+ export function outputResult(
19
+ data: unknown,
20
+ opts: OutputOptions,
21
+ tableFormatter?: (data: unknown, opts: OutputOptions) => string
22
+ ): void {
23
+ if (opts.raw) {
24
+ emit(JSON.stringify(data, null, 2), opts);
25
+ return;
26
+ }
27
+
28
+ const fmt = resolveFormat(opts);
29
+
30
+ if (fmt === "json") {
31
+ emit(JSON.stringify(data, null, 2), opts);
32
+ return;
33
+ }
34
+
35
+ if (fmt === "csv") {
36
+ emit(toCSV(data), opts);
37
+ return;
38
+ }
39
+
40
+ // table format
41
+ if (tableFormatter) {
42
+ emit(tableFormatter(data, opts), opts);
43
+ } else {
44
+ emit(autoTable(data, opts), opts);
45
+ }
46
+ }
47
+
48
+ function emit(content: string, opts: OutputOptions): void {
49
+ if (opts.output) {
50
+ mkdirSync(dirname(opts.output), { recursive: true });
51
+ writeFileSync(opts.output, content + "\n");
52
+ if (!opts.quiet) {
53
+ console.error(`Written to ${opts.output}`);
54
+ }
55
+ } else {
56
+ console.log(content);
57
+ }
58
+ }
59
+
60
+ function toCSV(data: unknown): string {
61
+ const rows = toRows(data);
62
+ if (rows.length === 0) return "";
63
+ const headers = Object.keys(rows[0]);
64
+ const lines = [headers.join(",")];
65
+ for (const row of rows) {
66
+ lines.push(
67
+ headers
68
+ .map((h) => {
69
+ const val = String(row[h] ?? "");
70
+ return val.includes(",") || val.includes('"') || val.includes("\n")
71
+ ? `"${val.replace(/"/g, '""')}"`
72
+ : val;
73
+ })
74
+ .join(",")
75
+ );
76
+ }
77
+ return lines.join("\n");
78
+ }
79
+
80
+ function toRows(data: unknown): Record<string, unknown>[] {
81
+ const flatten = (v: unknown) => flattenObj(v);
82
+ if (Array.isArray(data)) return data.map(flatten);
83
+ if (data && typeof data === "object") {
84
+ const obj = data as Record<string, unknown>;
85
+ // Prefer arrays of objects over arrays of primitives
86
+ for (const key of Object.keys(obj)) {
87
+ const val = obj[key];
88
+ if (
89
+ Array.isArray(val) &&
90
+ val.length > 0 &&
91
+ typeof val[0] === "object" &&
92
+ val[0] !== null
93
+ ) {
94
+ return val.map(flatten);
95
+ }
96
+ }
97
+ // Fallback: any non-empty array
98
+ for (const key of Object.keys(obj)) {
99
+ if (Array.isArray(obj[key]) && (obj[key] as unknown[]).length > 0) {
100
+ return (obj[key] as unknown[]).map(flatten);
101
+ }
102
+ }
103
+ return [flattenObj(obj)];
104
+ }
105
+ return [{ value: data }];
106
+ }
107
+
108
+ function flattenObj(
109
+ obj: unknown,
110
+ prefix = ""
111
+ ): Record<string, unknown> {
112
+ if (!obj || typeof obj !== "object") return { value: obj };
113
+ const result: Record<string, unknown> = {};
114
+ for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
115
+ const key = prefix ? `${prefix}.${k}` : k;
116
+ if (v && typeof v === "object" && !Array.isArray(v)) {
117
+ Object.assign(result, flattenObj(v, key));
118
+ } else if (Array.isArray(v)) {
119
+ if (v.length <= 3) {
120
+ result[key] = v
121
+ .map((item) =>
122
+ item && typeof item === "object" ? JSON.stringify(item) : String(item)
123
+ )
124
+ .join(", ");
125
+ } else {
126
+ result[key] = `[${v.length} items]`;
127
+ }
128
+ } else {
129
+ result[key] = v;
130
+ }
131
+ }
132
+ return result;
133
+ }
134
+
135
+ export function autoTable(data: unknown, opts?: OutputOptions): string {
136
+ const rows = toRows(data);
137
+ if (rows.length === 0) return "(no data)";
138
+
139
+ const headers = Object.keys(rows[0]);
140
+ const table = new Table({
141
+ head: opts?.quiet ? [] : headers,
142
+ style: { head: ["cyan"] },
143
+ wordWrap: true,
144
+ });
145
+
146
+ for (const row of rows) {
147
+ table.push(headers.map((h) => truncate(String(row[h] ?? ""), 80)));
148
+ }
149
+
150
+ return table.toString();
151
+ }
152
+
153
+ export function makeTable(
154
+ headers: string[],
155
+ rows: string[][],
156
+ opts?: { quiet?: boolean }
157
+ ): string {
158
+ const table = new Table({
159
+ head: opts?.quiet ? [] : headers,
160
+ style: { head: ["cyan"] },
161
+ wordWrap: true,
162
+ });
163
+ for (const row of rows) {
164
+ table.push(row);
165
+ }
166
+ return table.toString();
167
+ }
168
+
169
+ function truncate(s: string, max: number): string {
170
+ return s.length > max ? s.slice(0, max - 1) + "…" : s;
171
+ }
172
+
173
+ /**
174
+ * Parse a string as an integer, returning `fallback` if the result is NaN.
175
+ */
176
+ export function parseIntSafe(value: string, fallback: number): number {
177
+ const n = parseInt(value, 10);
178
+ return Number.isNaN(n) ? fallback : n;
179
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from "commander";
2
+
3
+ export function addGlobalOptions(cmd: Command): Command {
4
+ return cmd
5
+ .option("-o, --output <file>", "Write output to file")
6
+ .option("-f, --format <fmt>", "Output format: json, csv, table")
7
+ .option("-q, --quiet", "Suppress headers, data only")
8
+ .option("--raw", "Raw JSON from API");
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { Command } from "commander";
2
+ import { registerAuthCommand } from "./commands/auth.js";
3
+ import { registerInfoCommand } from "./commands/info.js";
4
+ import { registerNewsCommand } from "./commands/news.js";
5
+ import { registerPriceCommand } from "./commands/price.js";
6
+ import { registerFinancialsCommand } from "./commands/financials.js";
7
+ import { registerSentimentCommand } from "./commands/sentiment.js";
8
+ import { registerMarketCommand } from "./commands/market.js";
9
+ import { registerCollectionsCommand } from "./commands/collections.js";
10
+ import { registerScreenCommand } from "./commands/screen.js";
11
+ import { registerEarningsCommand } from "./commands/earnings.js";
12
+ import { registerTechnicalsCommand } from "./commands/technicals.js";
13
+ import { registerOptionsCommand } from "./commands/options.js";
14
+ import { registerPortfolioCommand } from "./commands/portfolio.js";
15
+ import { registerTradesCommand } from "./commands/trades.js";
16
+ import { registerCatalystsCommand } from "./commands/catalysts.js";
17
+ import { registerPositionCommand } from "./commands/position.js";
18
+ import { registerTranscriptCommand } from "./commands/transcript.js";
19
+
20
+ const program = new Command();
21
+
22
+ program
23
+ .name("bloom")
24
+ .description("Bloom CLI — financial data for coding agents and humans")
25
+ .version("0.1.0");
26
+
27
+ // Register all commands (each command adds its own global options via addGlobalOptions)
28
+ registerAuthCommand(program);
29
+ registerInfoCommand(program);
30
+ registerNewsCommand(program);
31
+ registerPriceCommand(program);
32
+ registerFinancialsCommand(program);
33
+ registerSentimentCommand(program);
34
+ registerMarketCommand(program);
35
+ registerCollectionsCommand(program);
36
+ registerScreenCommand(program);
37
+ registerEarningsCommand(program);
38
+ registerTechnicalsCommand(program);
39
+ registerOptionsCommand(program);
40
+ registerPortfolioCommand(program);
41
+ registerTradesCommand(program);
42
+ registerCatalystsCommand(program);
43
+ registerPositionCommand(program);
44
+ registerTranscriptCommand(program);
45
+
46
+ program.parseAsync().catch((err) => {
47
+ console.error("Error:", err.message || err);
48
+ process.exit(1);
49
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "declaration": true,
12
+ "resolveJsonModule": true
13
+ },
14
+ "include": ["src/**/*"]
15
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm"],
6
+ target: "node20",
7
+ clean: true,
8
+ splitting: false,
9
+ banner: {
10
+ js: "#!/usr/bin/env node",
11
+ },
12
+ });