1dr-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.
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "1dr-cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "1dr": "./dist/index.js"
7
+ },
8
+ "scripts": {
9
+ "dev": "bun run src/index.ts",
10
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun",
11
+ "compile": "bun build ./src/index.ts --compile --outfile 1dr"
12
+ },
13
+ "dependencies": {
14
+ "commander": "^13.1.0"
15
+ }
16
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,153 @@
1
+ import { CREDENTIALS_PATH, ensureConfigDir, loadConfig, type Credentials } from './config';
2
+
3
+ const CLIENT_ID = '1dr-cli';
4
+ const POLL_INTERVAL_MS = 5_000;
5
+
6
+ export async function login(): Promise<void> {
7
+ const config = await loadConfig();
8
+ const base = config.baseUrl;
9
+
10
+ // Step 1: Request device + user code
11
+ const codeRes = await fetch(`${base}/api/auth/device/code`, {
12
+ method: 'POST',
13
+ headers: { 'Content-Type': 'application/json' },
14
+ body: JSON.stringify({ client_id: CLIENT_ID })
15
+ });
16
+
17
+ if (!codeRes.ok) {
18
+ const err = await codeRes.json().catch(() => null);
19
+ throw new Error(`Failed to start device flow: ${err?.error_description || codeRes.statusText}`);
20
+ }
21
+
22
+ const codeData = (await codeRes.json()) as {
23
+ device_code: string;
24
+ user_code: string;
25
+ verification_uri: string;
26
+ verification_uri_complete: string;
27
+ expires_in: number;
28
+ interval: number;
29
+ };
30
+
31
+ console.log();
32
+ console.log(` Open this URL in your browser:`);
33
+ console.log(` \x1b[1m\x1b[36m${codeData.verification_uri_complete}\x1b[0m`);
34
+ console.log();
35
+ console.log(` Or go to \x1b[36m${codeData.verification_uri}\x1b[0m and enter code:`);
36
+ console.log(` \x1b[1m\x1b[33m${codeData.user_code}\x1b[0m`);
37
+ console.log();
38
+ console.log(` Waiting for authorization...`);
39
+
40
+ // Step 2: Poll for token
41
+ const deadline = Date.now() + codeData.expires_in * 1000;
42
+ const interval = Math.max((codeData.interval || 5) * 1000, POLL_INTERVAL_MS);
43
+
44
+ while (Date.now() < deadline) {
45
+ await sleep(interval);
46
+
47
+ const tokenRes = await fetch(`${base}/api/auth/device/token`, {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: JSON.stringify({
51
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
52
+ device_code: codeData.device_code,
53
+ client_id: CLIENT_ID
54
+ })
55
+ });
56
+
57
+ if (tokenRes.ok) {
58
+ const tokenData = (await tokenRes.json()) as {
59
+ access_token: string;
60
+ token_type: string;
61
+ expires_in: number;
62
+ scope: string;
63
+ };
64
+
65
+ await saveCredentials({
66
+ accessToken: tokenData.access_token,
67
+ expiresIn: tokenData.expires_in,
68
+ obtainedAt: Date.now()
69
+ });
70
+
71
+ console.log(` \x1b[32mAuthenticated successfully!\x1b[0m`);
72
+ return;
73
+ }
74
+
75
+ const err = (await tokenRes.json().catch(() => null)) as {
76
+ error?: string;
77
+ error_description?: string;
78
+ } | null;
79
+
80
+ if (err?.error === 'authorization_pending') {
81
+ continue; // still waiting
82
+ }
83
+ if (err?.error === 'slow_down') {
84
+ await sleep(5000); // back off
85
+ continue;
86
+ }
87
+ if (err?.error === 'access_denied') {
88
+ throw new Error('Authorization denied by user');
89
+ }
90
+ if (err?.error === 'expired_token') {
91
+ throw new Error('Device code expired. Please try again.');
92
+ }
93
+
94
+ throw new Error(`Token error: ${err?.error_description || err?.error || 'unknown'}`);
95
+ }
96
+
97
+ throw new Error('Device code expired. Please try again.');
98
+ }
99
+
100
+ export async function logout(): Promise<void> {
101
+ const { unlink } = await import('fs/promises');
102
+ try {
103
+ await unlink(CREDENTIALS_PATH);
104
+ console.log('Logged out successfully.');
105
+ } catch {
106
+ console.log('Not currently logged in.');
107
+ }
108
+ }
109
+
110
+ export async function getToken(): Promise<string> {
111
+ const creds = await loadCredentials();
112
+ if (!creds) {
113
+ throw new Error('Not logged in. Run `1dr login` first.');
114
+ }
115
+
116
+ // Check if expired (with 60s buffer)
117
+ const expiresAt = creds.obtainedAt + creds.expiresIn * 1000;
118
+ if (Date.now() > expiresAt - 60_000) {
119
+ throw new Error('Session expired. Run `1dr login` to re-authenticate.');
120
+ }
121
+
122
+ return creds.accessToken;
123
+ }
124
+
125
+ async function loadCredentials(): Promise<Credentials | null> {
126
+ try {
127
+ const raw = await Bun.file(CREDENTIALS_PATH).text();
128
+ return JSON.parse(raw) as Credentials;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Bump obtainedAt to now — keeps local expiry in sync with
136
+ * the server-side session extension that happens on every request.
137
+ */
138
+ export async function refreshCredentials(): Promise<void> {
139
+ const creds = await loadCredentials();
140
+ if (creds) {
141
+ creds.obtainedAt = Date.now();
142
+ await saveCredentials(creds);
143
+ }
144
+ }
145
+
146
+ async function saveCredentials(creds: Credentials): Promise<void> {
147
+ await ensureConfigDir();
148
+ await Bun.write(CREDENTIALS_PATH, JSON.stringify(creds, null, 2) + '\n');
149
+ }
150
+
151
+ function sleep(ms: number): Promise<void> {
152
+ return new Promise((resolve) => setTimeout(resolve, ms));
153
+ }
package/src/client.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { getToken, refreshCredentials } from './auth';
2
+ import { loadConfig } from './config';
3
+
4
+ type ApiResponse<T> = { data: T } | { error: { code: string; message: string; details?: unknown } };
5
+
6
+ export async function api<T>(path: string, options?: RequestInit): Promise<T> {
7
+ const config = await loadConfig();
8
+ const token = await getToken();
9
+
10
+ const url = `${config.baseUrl}${path}`;
11
+ const res = await fetch(url, {
12
+ ...options,
13
+ headers: {
14
+ 'Authorization': `Bearer ${token}`,
15
+ 'Content-Type': 'application/json',
16
+ ...options?.headers
17
+ }
18
+ });
19
+
20
+ if (res.status === 401) {
21
+ throw new Error('Session expired. Run `1dr login` to re-authenticate.');
22
+ }
23
+
24
+ // Server extends session on every authenticated request,
25
+ // so bump local expiry to stay in sync
26
+ await refreshCredentials();
27
+
28
+ const body = (await res.json()) as ApiResponse<T>;
29
+
30
+ if ('error' in body) {
31
+ throw new Error(`${body.error.code}: ${body.error.message}`);
32
+ }
33
+
34
+ return body.data;
35
+ }
36
+
37
+ export function buildQuery(params: Record<string, string | number | undefined>): string {
38
+ const entries = Object.entries(params).filter(
39
+ (entry): entry is [string, string | number] => entry[1] !== undefined
40
+ );
41
+ if (entries.length === 0) return '';
42
+ return '?' + new URLSearchParams(entries.map(([k, v]) => [k, String(v)])).toString();
43
+ }
@@ -0,0 +1,18 @@
1
+ import { loadConfig, saveConfig } from '../config';
2
+
3
+ export async function configCommand(opts: { baseUrl?: string; json?: boolean }) {
4
+ if (opts.baseUrl) {
5
+ await saveConfig({ baseUrl: opts.baseUrl });
6
+ console.log(`Base URL set to: ${opts.baseUrl}`);
7
+ return;
8
+ }
9
+
10
+ const config = await loadConfig();
11
+
12
+ if (opts.json) {
13
+ console.log(JSON.stringify(config, null, 2));
14
+ return;
15
+ }
16
+
17
+ console.log(` Base URL: ${config.baseUrl}`);
18
+ }
@@ -0,0 +1,10 @@
1
+ import { login } from '../auth';
2
+
3
+ export async function loginCommand() {
4
+ try {
5
+ await login();
6
+ } catch (err) {
7
+ console.error(`Login failed: ${(err as Error).message}`);
8
+ process.exit(1);
9
+ }
10
+ }
@@ -0,0 +1,5 @@
1
+ import { logout } from '../auth';
2
+
3
+ export async function logoutCommand() {
4
+ await logout();
5
+ }
@@ -0,0 +1,13 @@
1
+ import { loadConfig, setMode } from '../config';
2
+
3
+ export async function devCommand() {
4
+ await setMode(true);
5
+ const config = await loadConfig();
6
+ console.log(` \x1b[33m[DEV]\x1b[0m Switched to dev mode → ${config.baseUrl}`);
7
+ }
8
+
9
+ export async function prodCommand() {
10
+ await setMode(false);
11
+ const config = await loadConfig();
12
+ console.log(` Switched to prod mode → ${config.baseUrl}`);
13
+ }
@@ -0,0 +1,283 @@
1
+ import { api, buildQuery } from '../client';
2
+
3
+ type CompanySearchResult = {
4
+ exchangeTicker: string | null;
5
+ ticker: string | null;
6
+ exchange: string | null;
7
+ yahooTicker: string | null;
8
+ name: string | null;
9
+ sector: string | null;
10
+ industry: string | null;
11
+ country: string | null;
12
+ };
13
+
14
+ type TranscriptListItem = {
15
+ id: number;
16
+ headline: string | null;
17
+ date: string | null;
18
+ type: string | null;
19
+ year: number | null;
20
+ ticker: string | null;
21
+ exchange: string | null;
22
+ exchangeTicker: string | null;
23
+ yahooTicker: string | null;
24
+ companyName: string | null;
25
+ sector: string | null;
26
+ industry: string | null;
27
+ country: string | null;
28
+ usdmarketcap: number | null;
29
+ snippets: string[];
30
+ };
31
+
32
+ type TranscriptListResponse = {
33
+ items: TranscriptListItem[];
34
+ cursor: string | null;
35
+ };
36
+
37
+ type TranscriptDetail = {
38
+ id: number;
39
+ headline: string | null;
40
+ date: string | null;
41
+ type: string | null;
42
+ year: number | null;
43
+ ticker: string | null;
44
+ exchange: string | null;
45
+ exchangeTicker: string | null;
46
+ yahooTicker: string | null;
47
+ companyName: string | null;
48
+ components: Array<{
49
+ order: number;
50
+ text: string | null;
51
+ speaker: string | null;
52
+ speakerTypeId: number | null;
53
+ componentTypeId: number | null;
54
+ }>;
55
+ };
56
+
57
+ type CompanyTranscriptItem = {
58
+ id: number;
59
+ headline: string | null;
60
+ date: string | null;
61
+ type: string | null;
62
+ year: number | null;
63
+ };
64
+
65
+ type CompanyTranscriptsResponse = {
66
+ company: CompanySearchResult | null;
67
+ items: CompanyTranscriptItem[];
68
+ cursor: string | null;
69
+ };
70
+
71
+ type LastUpdatedResponse = {
72
+ lastUpdated: string | null;
73
+ };
74
+
75
+ // ── Company search ──────────────────────────────────────────────────
76
+
77
+ export async function companySearchCommand(query: string, opts: { json?: boolean }) {
78
+ try {
79
+ const q = buildQuery({ q: query });
80
+ const results = await api<CompanySearchResult[]>(`/api/v1/transcripts/company-search${q}`);
81
+
82
+ if (opts.json) {
83
+ console.log(JSON.stringify(results, null, 2));
84
+ return;
85
+ }
86
+
87
+ if (results.length === 0) {
88
+ console.log('No companies found.');
89
+ return;
90
+ }
91
+
92
+ for (const c of results) {
93
+ console.log(
94
+ ` \x1b[1m${c.exchangeTicker || c.ticker || '???'}\x1b[0m ${c.name || ''} \x1b[2m${c.sector || ''} | ${c.country || ''}\x1b[0m`
95
+ );
96
+ if (c.yahooTicker && c.yahooTicker !== c.ticker) {
97
+ console.log(` yahoo: ${c.yahooTicker}`);
98
+ }
99
+ }
100
+ } catch (err) {
101
+ console.error((err as Error).message);
102
+ process.exit(1);
103
+ }
104
+ }
105
+
106
+ // ── List transcripts ────────────────────────────────────────────────
107
+
108
+ export async function listCommand(opts: {
109
+ limit?: string;
110
+ cursor?: string;
111
+ types?: string;
112
+ year?: string;
113
+ dateFrom?: string;
114
+ dateTo?: string;
115
+ search?: string;
116
+ yahooTicker?: string;
117
+ json?: boolean;
118
+ }) {
119
+ try {
120
+ const q = buildQuery({
121
+ limit: opts.limit,
122
+ cursor: opts.cursor,
123
+ types: opts.types,
124
+ year: opts.year,
125
+ dateFrom: opts.dateFrom,
126
+ dateTo: opts.dateTo,
127
+ search: opts.search,
128
+ yahooTicker: opts.yahooTicker
129
+ });
130
+
131
+ const result = await api<TranscriptListResponse>(`/api/v1/transcripts${q}`);
132
+
133
+ if (opts.json) {
134
+ console.log(JSON.stringify(result, null, 2));
135
+ return;
136
+ }
137
+
138
+ if (result.items.length === 0) {
139
+ console.log('No transcripts found.');
140
+ return;
141
+ }
142
+
143
+ for (const t of result.items) {
144
+ const date = t.date ? new Date(t.date).toLocaleDateString() : 'n/a';
145
+ console.log(
146
+ ` \x1b[1m${t.id}\x1b[0m ${date} \x1b[36m${t.exchangeTicker || t.ticker || '???'}\x1b[0m ${t.headline || ''}`
147
+ );
148
+ if (t.snippets.length > 0) {
149
+ for (const s of t.snippets.slice(0, 2)) {
150
+ const clean = s.replace(/<\/?mark>/g, '');
151
+ console.log(` \x1b[2m${clean.slice(0, 120)}...\x1b[0m`);
152
+ }
153
+ }
154
+ }
155
+
156
+ console.log(`\n ${result.items.length} result(s)`);
157
+ if (result.cursor) {
158
+ console.log(` \x1b[2mNext page: --cursor ${result.cursor}\x1b[0m`);
159
+ }
160
+ } catch (err) {
161
+ console.error((err as Error).message);
162
+ process.exit(1);
163
+ }
164
+ }
165
+
166
+ // ── Get single transcript ───────────────────────────────────────────
167
+
168
+ export async function getCommand(id: string, opts: { json?: boolean }) {
169
+ try {
170
+ const transcript = await api<TranscriptDetail>(`/api/v1/transcripts/${id}`);
171
+
172
+ if (opts.json) {
173
+ console.log(JSON.stringify(transcript, null, 2));
174
+ return;
175
+ }
176
+
177
+ console.log(`\n \x1b[1m${transcript.headline}\x1b[0m`);
178
+ console.log(
179
+ ` ${transcript.exchangeTicker || transcript.ticker} | ${transcript.type} | ${transcript.date ? new Date(transcript.date).toLocaleDateString() : ''}\n`
180
+ );
181
+
182
+ for (const c of transcript.components) {
183
+ if (c.speaker) {
184
+ console.log(`\x1b[1m\x1b[33m${c.speaker}:\x1b[0m`);
185
+ }
186
+ if (c.text) {
187
+ console.log(c.text);
188
+ console.log();
189
+ }
190
+ }
191
+ } catch (err) {
192
+ console.error((err as Error).message);
193
+ process.exit(1);
194
+ }
195
+ }
196
+
197
+ // ── Company transcripts ─────────────────────────────────────────────
198
+
199
+ export async function companyCommand(
200
+ ticker: string,
201
+ opts: {
202
+ limit?: string;
203
+ cursor?: string;
204
+ timeRange?: string;
205
+ types?: string;
206
+ year?: string;
207
+ dateFrom?: string;
208
+ dateTo?: string;
209
+ json?: boolean;
210
+ }
211
+ ) {
212
+ try {
213
+ const q = buildQuery({
214
+ limit: opts.limit,
215
+ cursor: opts.cursor,
216
+ timeRange: opts.timeRange,
217
+ types: opts.types,
218
+ year: opts.year,
219
+ dateFrom: opts.dateFrom,
220
+ dateTo: opts.dateTo
221
+ });
222
+
223
+ const result = await api<CompanyTranscriptsResponse>(
224
+ `/api/v1/transcripts/company/${encodeURIComponent(ticker)}${q}`
225
+ );
226
+
227
+ if (opts.json) {
228
+ console.log(JSON.stringify(result, null, 2));
229
+ return;
230
+ }
231
+
232
+ if (!result.company) {
233
+ console.log(`No company found for ticker: ${ticker}`);
234
+ return;
235
+ }
236
+
237
+ console.log(
238
+ `\n \x1b[1m${result.company.name}\x1b[0m (${result.company.exchangeTicker || result.company.ticker})\n`
239
+ );
240
+
241
+ if (result.items.length === 0) {
242
+ console.log(' No transcripts found.');
243
+ return;
244
+ }
245
+
246
+ for (const t of result.items) {
247
+ const date = t.date ? new Date(t.date).toLocaleDateString() : 'n/a';
248
+ console.log(
249
+ ` \x1b[1m${t.id}\x1b[0m ${date} ${t.type || ''} ${t.year || ''} ${t.headline || ''}`
250
+ );
251
+ }
252
+
253
+ console.log(`\n ${result.items.length} transcript(s)`);
254
+ if (result.cursor) {
255
+ console.log(` \x1b[2mNext page: --cursor ${result.cursor}\x1b[0m`);
256
+ }
257
+ } catch (err) {
258
+ console.error((err as Error).message);
259
+ process.exit(1);
260
+ }
261
+ }
262
+
263
+ // ── Last updated ────────────────────────────────────────────────────
264
+
265
+ export async function lastUpdatedCommand(opts: { json?: boolean }) {
266
+ try {
267
+ const result = await api<LastUpdatedResponse>(`/api/v1/transcripts/last-updated`);
268
+
269
+ if (opts.json) {
270
+ console.log(JSON.stringify(result, null, 2));
271
+ return;
272
+ }
273
+
274
+ if (result.lastUpdated) {
275
+ console.log(` Last updated: ${new Date(result.lastUpdated).toLocaleString()}`);
276
+ } else {
277
+ console.log(' No transcripts in database.');
278
+ }
279
+ } catch (err) {
280
+ console.error((err as Error).message);
281
+ process.exit(1);
282
+ }
283
+ }
@@ -0,0 +1,36 @@
1
+ import { getToken } from '../auth';
2
+ import { loadConfig } from '../config';
3
+
4
+ export async function whoamiCommand(opts: { json?: boolean }) {
5
+ try {
6
+ const config = await loadConfig();
7
+ const token = await getToken();
8
+
9
+ const res = await fetch(`${config.baseUrl}/api/auth/get-session`, {
10
+ headers: { Authorization: `Bearer ${token}` }
11
+ });
12
+
13
+ if (!res.ok) {
14
+ throw new Error('Failed to get session. Try `1dr login` again.');
15
+ }
16
+
17
+ const data = (await res.json()) as {
18
+ user: { id: string; name: string; email: string; level: number; username?: string };
19
+ session: { id: string; expiresAt: string };
20
+ };
21
+
22
+ if (opts.json) {
23
+ console.log(JSON.stringify(data, null, 2));
24
+ return;
25
+ }
26
+
27
+ console.log(` Name: ${data.user.name}`);
28
+ console.log(` Email: ${data.user.email}`);
29
+ console.log(` Username: ${data.user.username || '(not set)'}`);
30
+ console.log(` Level: ${data.user.level}`);
31
+ console.log(` Expires: ${new Date(data.session.expiresAt).toLocaleString()}`);
32
+ } catch (err) {
33
+ console.error((err as Error).message);
34
+ process.exit(1);
35
+ }
36
+ }
package/src/config.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+
4
+ const CONFIG_DIR = join(homedir(), '.config', '1dr');
5
+ export const CREDENTIALS_PATH = join(CONFIG_DIR, 'credentials.json');
6
+ export const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
7
+
8
+ export type Config = {
9
+ baseUrl: string;
10
+ dev?: boolean;
11
+ };
12
+
13
+ export type Credentials = {
14
+ accessToken: string;
15
+ expiresIn: number;
16
+ obtainedAt: number; // epoch ms
17
+ };
18
+
19
+ const PROD_URL = 'https://firstdraftresearch.com';
20
+ const DEV_URL = 'http://localhost:5173';
21
+
22
+ const DEFAULT_CONFIG: Config = {
23
+ baseUrl: PROD_URL,
24
+ dev: false
25
+ };
26
+
27
+ export async function ensureConfigDir() {
28
+ const { mkdir } = await import('fs/promises');
29
+ await mkdir(CONFIG_DIR, { recursive: true });
30
+ }
31
+
32
+ export async function loadConfig(): Promise<Config> {
33
+ try {
34
+ const raw = await Bun.file(CONFIG_PATH).text();
35
+ const stored = JSON.parse(raw);
36
+ const config = { ...DEFAULT_CONFIG, ...stored };
37
+ // Resolve baseUrl from dev flag unless explicitly overridden
38
+ if (!stored.baseUrl) {
39
+ config.baseUrl = config.dev ? DEV_URL : PROD_URL;
40
+ }
41
+ return config;
42
+ } catch {
43
+ return DEFAULT_CONFIG;
44
+ }
45
+ }
46
+
47
+ export async function saveConfig(config: Partial<Config>) {
48
+ await ensureConfigDir();
49
+ const existing = await loadConfig();
50
+ const merged = { ...existing, ...config };
51
+ await Bun.write(CONFIG_PATH, JSON.stringify(merged, null, 2) + '\n');
52
+ }
53
+
54
+ /** Switch dev/prod mode — clears any manual baseUrl override */
55
+ export async function setMode(dev: boolean) {
56
+ await ensureConfigDir();
57
+ await Bun.write(CONFIG_PATH, JSON.stringify({ dev }, null, 2) + '\n');
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from 'commander';
3
+ import { loginCommand } from './commands/login';
4
+ import { logoutCommand } from './commands/logout';
5
+ import { whoamiCommand } from './commands/whoami';
6
+ import {
7
+ companySearchCommand,
8
+ listCommand,
9
+ getCommand,
10
+ companyCommand,
11
+ lastUpdatedCommand
12
+ } from './commands/transcripts';
13
+ import { configCommand } from './commands/config';
14
+ import { devCommand, prodCommand } from './commands/mode';
15
+ import { loadConfig } from './config';
16
+
17
+ const program = new Command();
18
+
19
+ program.name('1dr').description('First Draft Research CLI').version('0.1.0');
20
+
21
+ // Show [DEV] indicator before every command when in dev mode
22
+ program.hook('preAction', async (_thisCmd, actionCmd) => {
23
+ if (['dev', 'prod'].includes(actionCmd.name())) return;
24
+ const config = await loadConfig();
25
+ if (config.dev) {
26
+ console.log(` \x1b[33m[DEV]\x1b[0m ${config.baseUrl}`);
27
+ }
28
+ });
29
+
30
+ // Auth
31
+ program.command('login').description('Authenticate via browser (device flow)').action(loginCommand);
32
+ program.command('logout').description('Remove stored credentials').action(logoutCommand);
33
+ program
34
+ .command('whoami')
35
+ .description('Show current user info')
36
+ .option('--json', 'Output as JSON')
37
+ .action(whoamiCommand);
38
+
39
+ // Config
40
+ program
41
+ .command('config')
42
+ .description('View or set configuration')
43
+ .option('--base-url <url>', 'Set the API base URL')
44
+ .option('--json', 'Output as JSON')
45
+ .action(configCommand);
46
+
47
+ // Mode
48
+ program.command('dev').description('Switch to dev mode (localhost:5173)').action(devCommand);
49
+ program.command('prod').description('Switch to prod mode (firstdraftresearch.com)').action(prodCommand);
50
+
51
+ // Transcripts
52
+ const transcripts = program
53
+ .command('transcripts')
54
+ .alias('t')
55
+ .description('Search and read earnings call transcripts');
56
+
57
+ transcripts
58
+ .command('search <query>')
59
+ .description('Search companies by ticker or name (returns exchange:ticker)')
60
+ .option('--json', 'Output as JSON')
61
+ .action(companySearchCommand);
62
+
63
+ transcripts
64
+ .command('list')
65
+ .description('List transcripts by date (cursored, max 500)')
66
+ .option('-l, --limit <n>', 'Max results (max 500)', '50')
67
+ .option('--cursor <date>', 'Cursor from previous page (ISO date)')
68
+ .option('-s, --search <term>', 'Full-text search')
69
+ .option('--yahoo-ticker <ticker>', 'Filter by yahoo ticker (e.g. AAPL, 2330.TW)')
70
+ .option('--types <list>', 'Filter by call types (Q1,Q2,Q3,Q4,CONF,OTHER,CMD)')
71
+ .option('-y, --year <year>', 'Filter by fiscal year')
72
+ .option('--date-from <date>', 'Start date (ISO)')
73
+ .option('--date-to <date>', 'End date (ISO)')
74
+ .option('--json', 'Output as JSON')
75
+ .action(listCommand);
76
+
77
+ transcripts
78
+ .command('get <id>')
79
+ .description('Read a full transcript')
80
+ .option('--json', 'Output as JSON')
81
+ .action(getCommand);
82
+
83
+ transcripts
84
+ .command('company <ticker>')
85
+ .description('List transcripts for a company (yahoo ticker)')
86
+ .option('-l, --limit <n>', 'Max results (max 500)', '50')
87
+ .option('--cursor <date>', 'Cursor from previous page (ISO date)')
88
+ .option('-t, --time-range <range>', 'Time range: 1M,3M,6M,YTD,1Y,2Y,5Y,Max')
89
+ .option('--types <list>', 'Filter by call types (Q1,Q2,Q3,Q4,CONF,OTHER,CMD)')
90
+ .option('-y, --year <year>', 'Filter by fiscal year')
91
+ .option('--date-from <date>', 'Start date (ISO)')
92
+ .option('--date-to <date>', 'End date (ISO)')
93
+ .option('--json', 'Output as JSON')
94
+ .action(companyCommand);
95
+
96
+ transcripts
97
+ .command('last-updated')
98
+ .description('Show when the transcript database was last updated')
99
+ .option('--json', 'Output as JSON')
100
+ .action(lastUpdatedCommand);
101
+
102
+ program.parse();
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "types": ["bun-types"]
12
+ },
13
+ "include": ["src"]
14
+ }