@invinite-org/invinite-data-cli 0.3.4 → 0.5.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/README.md +89 -751
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/agents/invinite-data.md +1 -1
- package/claude-plugin/commands/analyze-earnings-call.md +5 -4
- package/claude-plugin/skills/invinite-data-cli/SKILL.md +446 -180
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +21 -31
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client/api-client.d.ts +2 -4
- package/dist/src/client/api-client.js +3 -9
- package/dist/src/client/api-client.js.map +1 -1
- package/dist/src/client/pagination.d.ts +10 -0
- package/dist/src/client/pagination.js +40 -0
- package/dist/src/client/pagination.js.map +1 -1
- package/dist/src/commands/earnings.d.ts +4 -3
- package/dist/src/commands/earnings.js +163 -466
- package/dist/src/commands/earnings.js.map +1 -1
- package/dist/src/generated/cli-manifest.json +4983 -0
- package/dist/src/hand-written-commands.d.ts +43 -0
- package/dist/src/hand-written-commands.js +21 -0
- package/dist/src/hand-written-commands.js.map +1 -0
- package/dist/src/shared/errors.d.ts +5 -0
- package/dist/src/shared/errors.js +12 -0
- package/dist/src/shared/errors.js.map +1 -1
- package/dist/src/shared/options.d.ts +0 -13
- package/dist/src/shared/options.js +0 -13
- package/dist/src/shared/options.js.map +1 -1
- package/dist/src/spec-commands.d.ts +10 -0
- package/dist/src/spec-commands.js +207 -0
- package/dist/src/spec-commands.js.map +1 -0
- package/dist/src/spec-flags.d.ts +6 -0
- package/dist/src/spec-flags.js +9 -0
- package/dist/src/spec-flags.js.map +1 -0
- package/dist/src/spec-manifest.d.ts +382 -0
- package/dist/src/spec-manifest.js +65 -0
- package/dist/src/spec-manifest.js.map +1 -0
- package/package.json +7 -3
- package/dist/src/commands/company.d.ts +0 -2
- package/dist/src/commands/company.js +0 -220
- package/dist/src/commands/company.js.map +0 -1
- package/dist/src/commands/earnings-selector.d.ts +0 -15
- package/dist/src/commands/earnings-selector.js +0 -29
- package/dist/src/commands/earnings-selector.js.map +0 -1
- package/dist/src/commands/filings.d.ts +0 -2
- package/dist/src/commands/filings.js +0 -139
- package/dist/src/commands/filings.js.map +0 -1
- package/dist/src/commands/financials.d.ts +0 -2
- package/dist/src/commands/financials.js +0 -162
- package/dist/src/commands/financials.js.map +0 -1
- package/dist/src/commands/health.d.ts +0 -2
- package/dist/src/commands/health.js +0 -18
- package/dist/src/commands/health.js.map +0 -1
- package/dist/src/commands/insider-trades.d.ts +0 -2
- package/dist/src/commands/insider-trades.js +0 -56
- package/dist/src/commands/insider-trades.js.map +0 -1
- package/dist/src/commands/ipos.d.ts +0 -2
- package/dist/src/commands/ipos.js +0 -42
- package/dist/src/commands/ipos.js.map +0 -1
- package/dist/src/commands/market.d.ts +0 -2
- package/dist/src/commands/market.js +0 -34
- package/dist/src/commands/market.js.map +0 -1
- package/dist/src/commands/metadata.d.ts +0 -2
- package/dist/src/commands/metadata.js +0 -39
- package/dist/src/commands/metadata.js.map +0 -1
- package/dist/src/commands/metrics.d.ts +0 -2
- package/dist/src/commands/metrics.js +0 -158
- package/dist/src/commands/metrics.js.map +0 -1
- package/dist/src/commands/ownership.d.ts +0 -2
- package/dist/src/commands/ownership.js +0 -152
- package/dist/src/commands/ownership.js.map +0 -1
- package/dist/src/commands/segments.d.ts +0 -2
- package/dist/src/commands/segments.js +0 -64
- package/dist/src/commands/segments.js.map +0 -1
- package/dist/src/commands/usage.d.ts +0 -38
- package/dist/src/commands/usage.js +0 -82
- package/dist/src/commands/usage.js.map +0 -1
- package/dist/src/commands/webhooks.d.ts +0 -16
- package/dist/src/commands/webhooks.js +0 -186
- package/dist/src/commands/webhooks.js.map +0 -1
- package/dist/src/formatters/sparkline.d.ts +0 -2
- package/dist/src/formatters/sparkline.js +0 -11
- package/dist/src/formatters/sparkline.js.map +0 -1
- package/dist/src/formatters/tree-formatter.d.ts +0 -11
- package/dist/src/formatters/tree-formatter.js +0 -30
- package/dist/src/formatters/tree-formatter.js.map +0 -1
|
@@ -4,53 +4,66 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { apiGet } from '../client/api-client.js';
|
|
5
5
|
import { downloadToFile, ensureDir, extensionFor, writeTextFile } from '../client/download.js';
|
|
6
6
|
import { fetchAllPages } from '../client/pagination.js';
|
|
7
|
-
import { withErrorHandling } from '../shared/errors.js';
|
|
7
|
+
import { apiErrorHint, withErrorHandling } from '../shared/errors.js';
|
|
8
8
|
import { output } from '../shared/output.js';
|
|
9
|
-
import { splitList } from '../shared/options.js';
|
|
10
9
|
import { setJsonMode } from '../shared/spinner.js';
|
|
11
10
|
import { formatJson } from '../formatters/json-formatter.js';
|
|
12
11
|
import { formatKeyValue, formatTable, printPaginationHint } from '../formatters/table-formatter.js';
|
|
13
|
-
import { flattenRows } from '../formatters/flatten.js';
|
|
14
12
|
import { formatMs } from '../formatters/time.js';
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The hand-written `invd earnings` commands: each writes a file or renders text, so none is one REST call plus one
|
|
15
|
+
* view (`src/hand-written-commands.ts` rosters them). Every other `earnings` action is generated from the manifest
|
|
16
|
+
* and attached to the same group (`src/spec-commands.ts`).
|
|
17
|
+
*/
|
|
16
18
|
const BASE = '/v1/earnings-calls';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
/** IDA's `event_type` values (the spec's enum on the selector operations). */
|
|
20
|
+
const EVENT_TYPES = ['earnings_call', 'investor_day', 'conference', 'agm', 'other'];
|
|
21
|
+
/**
|
|
22
|
+
* The event selector, as the generated commands spell it (the spec's query parameters): `--event-id`, or `-i` with
|
|
23
|
+
* `--year` + `--quarter` or `--date`. An incomplete selector is the API's own 400.
|
|
24
|
+
*/
|
|
25
|
+
function selectorOptions(cmd) {
|
|
26
|
+
return cmd
|
|
27
|
+
.option('--event-id <event_id>', 'Event ID (wins over the other selectors)')
|
|
28
|
+
.option('-i, --identifier <ticker>', 'Ticker or CIK (with --year + --quarter, or --date)')
|
|
29
|
+
.option('--year <n>', 'Fiscal year of the call (with --quarter)')
|
|
30
|
+
.option('--quarter <n>', 'Fiscal quarter of the call (with --year)')
|
|
31
|
+
.option('--date <date>', 'Day of the call (YYYY-MM-DD): a calendar day in New York time (America/New_York); a call without a confirmed time is listed on its announced date')
|
|
32
|
+
.addOption(new Option('--event-type <type>', 'Event type of the selected call').choices(EVENT_TYPES));
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
function selectorParams(opts) {
|
|
35
|
+
return {
|
|
36
|
+
event_id: opts.eventId,
|
|
37
|
+
identifier: opts.identifier,
|
|
38
|
+
year: opts.year,
|
|
39
|
+
quarter: opts.quarter,
|
|
40
|
+
date: opts.date,
|
|
41
|
+
event_type: opts.eventType,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const run = (fn) => withErrorHandling(fn, apiErrorHint);
|
|
45
|
+
/** `--all`: follows `next_url` and concatenates the `documents` pages. */
|
|
46
|
+
async function allDocuments(data, all) {
|
|
47
|
+
if (!all)
|
|
35
48
|
return data;
|
|
36
|
-
return fetchAllPages(data,
|
|
49
|
+
return fetchAllPages(data, page => page.next_url, (accumulated, page) => ({
|
|
37
50
|
...page,
|
|
38
|
-
|
|
51
|
+
documents: [...accumulated.documents, ...page.documents],
|
|
39
52
|
}));
|
|
40
53
|
}
|
|
41
54
|
function eventLine(event) {
|
|
42
55
|
if (!event)
|
|
43
56
|
return '';
|
|
44
57
|
const day = event.conference_date ?? event.scheduled_at ?? '';
|
|
45
|
-
|
|
58
|
+
// A non-earnings event (investor day, conference, AGM) by its type and title.
|
|
59
|
+
const kind = event.event_type && event.event_type !== 'earnings_call'
|
|
60
|
+
? [event.event_type.replaceAll('_', ' '), event.event_title].filter(Boolean).join(': ')
|
|
61
|
+
: undefined;
|
|
62
|
+
return [event.symbol ?? event.cik, event.company_name, kind, day].filter(Boolean).join(' · ');
|
|
46
63
|
}
|
|
47
64
|
function printSaved(saved) {
|
|
48
65
|
console.log(chalk.green(`Saved ${saved.path} (${saved.bytes.toLocaleString('en-US')} bytes)`));
|
|
49
66
|
}
|
|
50
|
-
/** `*_ms` numbers → `m:ss` for key / value output. */
|
|
51
|
-
function humanizeMs(row) {
|
|
52
|
-
return Object.fromEntries(Object.entries(row).map(([key, value]) => [key, key.endsWith('_ms') && typeof value === 'number' ? formatMs(value) : value]));
|
|
53
|
-
}
|
|
54
67
|
function speakerName(turn) {
|
|
55
68
|
return turn.name ?? `Speaker ${turn.speaker_label}`;
|
|
56
69
|
}
|
|
@@ -80,10 +93,25 @@ export function skippedDocumentWarning(doc) {
|
|
|
80
93
|
const source = doc.source_url ? `; source: ${doc.source_url}` : '';
|
|
81
94
|
return `Not saved: ${doc.kind} ${doc.document_id} has no download URL${source}`;
|
|
82
95
|
}
|
|
96
|
+
/** `company-documents --output`: `<year | undated>-<kind>-<document_id>.<ext>` (the public shape has no content hash). */
|
|
97
|
+
export function companyDocumentFileName(doc) {
|
|
98
|
+
return `${doc.year ?? 'undated'}-${doc.kind}-${doc.document_id}.${extensionFor(doc.content_type)}`;
|
|
99
|
+
}
|
|
100
|
+
/** Plain text of a pages JSON (`{ pages: [{ n, title, text }] }`); anything else → empty string. */
|
|
101
|
+
export function pagesText(pagesJson) {
|
|
102
|
+
const pages = pagesJson?.pages;
|
|
103
|
+
if (!Array.isArray(pages))
|
|
104
|
+
return '';
|
|
105
|
+
return pages
|
|
106
|
+
.map((page, index) => {
|
|
107
|
+
const n = typeof page?.n === 'number' ? page.n : index + 1;
|
|
108
|
+
const title = typeof page?.title === 'string' && page.title.trim() ? ` · ${page.title}` : '';
|
|
109
|
+
return `## Page ${n}${title}\n\n${typeof page?.text === 'string' ? page.text : ''}`;
|
|
110
|
+
})
|
|
111
|
+
.join('\n\n');
|
|
112
|
+
}
|
|
83
113
|
export function registerEarningsCommands(program) {
|
|
84
|
-
const earnings = program
|
|
85
|
-
.command('earnings')
|
|
86
|
-
.description('Earnings calls: calendar, transcripts, audio, slides, guidance, Q&A, search');
|
|
114
|
+
const earnings = program.commands.find(command => command.name() === 'earnings') ?? program.command('earnings');
|
|
87
115
|
const globalOpts = () => {
|
|
88
116
|
const opts = program.opts();
|
|
89
117
|
if (opts.json)
|
|
@@ -91,104 +119,6 @@ export function registerEarningsCommands(program) {
|
|
|
91
119
|
return opts;
|
|
92
120
|
};
|
|
93
121
|
const selectorCommand = (name, description) => selectorOptions(earnings.command(name).description(description));
|
|
94
|
-
// ── Calendar / events ──────────────────────────────────────────────
|
|
95
|
-
earnings
|
|
96
|
-
.command('calendar')
|
|
97
|
-
.description('Upcoming and recent earnings calls across companies')
|
|
98
|
-
.option('--from <date>', 'First New York day (YYYY-MM-DD, default today)')
|
|
99
|
-
.option('--to <date>', 'Last New York day (YYYY-MM-DD, default from + 14 days, at most 92)')
|
|
100
|
-
.option('--status <list>', 'Date statuses, comma-separated (estimated, unconfirmed, confirmed, postponed, cancelled)')
|
|
101
|
-
.addOption(new Option('--session <session>', 'Session').choices(['bmo', 'amc', 'intraday']))
|
|
102
|
-
.option('--exchange <exchange>', 'Exchange')
|
|
103
|
-
.addOption(new Option('--event-type <type>', 'Event type (default earnings_call; all = every type)').choices(EVENT_TYPE_FILTERS))
|
|
104
|
-
.addOption(new Option('-s, --sort <dir>', 'Sort direction').choices(['asc', 'desc']))
|
|
105
|
-
.option('-l, --limit <n>', 'Max results (1-500)')
|
|
106
|
-
.option('--offset <n>', 'Offset')
|
|
107
|
-
.action(run(async (cmdOpts) => {
|
|
108
|
-
const opts = globalOpts();
|
|
109
|
-
const data = await paginate(await apiGet({
|
|
110
|
-
path: `${BASE}/calendar`,
|
|
111
|
-
params: {
|
|
112
|
-
from: cmdOpts.from,
|
|
113
|
-
to: cmdOpts.to,
|
|
114
|
-
// IDA's `status` is an array parameter: one `status=` per value, never one comma string.
|
|
115
|
-
status: splitList(cmdOpts.status),
|
|
116
|
-
session: cmdOpts.session,
|
|
117
|
-
exchange: cmdOpts.exchange,
|
|
118
|
-
event_type: cmdOpts.eventType,
|
|
119
|
-
sort: cmdOpts.sort,
|
|
120
|
-
limit: cmdOpts.limit,
|
|
121
|
-
offset: cmdOpts.offset,
|
|
122
|
-
},
|
|
123
|
-
}), 'events', opts.all);
|
|
124
|
-
output(data, opts, () => {
|
|
125
|
-
console.log(chalk.bold(`\nEarnings calendar ${data.from} … ${data.to}\n`));
|
|
126
|
-
formatTable(data.events, EVENT_COLUMNS);
|
|
127
|
-
printPaginationHint(data.next_url);
|
|
128
|
-
});
|
|
129
|
-
}));
|
|
130
|
-
earnings
|
|
131
|
-
.command('events')
|
|
132
|
-
.description("A company's earnings calls")
|
|
133
|
-
.requiredOption('-i, --identifier <ticker>', 'Ticker or CIK')
|
|
134
|
-
.option('--from <date>', 'Calls on or after this New York day (YYYY-MM-DD)')
|
|
135
|
-
.option('--to <date>', 'Calls on or before this New York day (YYYY-MM-DD)')
|
|
136
|
-
.addOption(new Option('--event-type <type>', 'Event type (default all)').choices(EVENT_TYPE_FILTERS))
|
|
137
|
-
.option('--observations', 'Include the date / time observations per call')
|
|
138
|
-
.addOption(new Option('-s, --sort <dir>', 'Sort direction').choices(['asc', 'desc']))
|
|
139
|
-
.option('-l, --limit <n>', 'Max results (1-200)')
|
|
140
|
-
.option('--offset <n>', 'Offset')
|
|
141
|
-
.action(run(async (cmdOpts) => {
|
|
142
|
-
const opts = globalOpts();
|
|
143
|
-
const data = await paginate(await apiGet({
|
|
144
|
-
path: `${BASE}/events`,
|
|
145
|
-
params: {
|
|
146
|
-
identifier: cmdOpts.identifier,
|
|
147
|
-
from: cmdOpts.from,
|
|
148
|
-
to: cmdOpts.to,
|
|
149
|
-
event_type: cmdOpts.eventType,
|
|
150
|
-
include: cmdOpts.observations ? 'observations' : undefined,
|
|
151
|
-
sort: cmdOpts.sort,
|
|
152
|
-
limit: cmdOpts.limit,
|
|
153
|
-
offset: cmdOpts.offset,
|
|
154
|
-
},
|
|
155
|
-
}), 'events', opts.all);
|
|
156
|
-
output(data, opts, () => {
|
|
157
|
-
console.log(chalk.bold(`\n${data.symbol} (${data.cik}) Earnings Calls\n`));
|
|
158
|
-
if (!data.covered)
|
|
159
|
-
console.log(chalk.yellow('Not covered by the earnings-calls pipeline.\n'));
|
|
160
|
-
formatTable(data.events, EVENT_COLUMNS);
|
|
161
|
-
printPaginationHint(data.next_url);
|
|
162
|
-
});
|
|
163
|
-
}));
|
|
164
|
-
earnings
|
|
165
|
-
.command('live')
|
|
166
|
-
.description('Calls being captured right now')
|
|
167
|
-
.action(run(async () => {
|
|
168
|
-
const opts = globalOpts();
|
|
169
|
-
const data = await apiGet({ path: `${BASE}/live` });
|
|
170
|
-
output(data, opts, () => {
|
|
171
|
-
console.log(chalk.bold(`\nLive earnings calls (as of ${data.as_of})\n`));
|
|
172
|
-
formatTable(data.events, [...EVENT_COLUMNS, 'live_audio']);
|
|
173
|
-
});
|
|
174
|
-
}));
|
|
175
|
-
earnings
|
|
176
|
-
.command('event')
|
|
177
|
-
.description('One earnings call with its date / time observations')
|
|
178
|
-
.argument('<event_id>', 'Event ID')
|
|
179
|
-
.action(run(async (eventId) => {
|
|
180
|
-
const opts = globalOpts();
|
|
181
|
-
const data = await apiGet({ path: `${BASE}/event/${encodeURIComponent(eventId)}` });
|
|
182
|
-
output(data, opts, () => {
|
|
183
|
-
const { observations, ...event } = data;
|
|
184
|
-
console.log(chalk.bold(`\n${eventLine(event)}\n`));
|
|
185
|
-
formatKeyValue(event);
|
|
186
|
-
if (observations?.length) {
|
|
187
|
-
console.log(chalk.bold('\nObservations\n'));
|
|
188
|
-
formatTable(observations, ['observed_source', 'observed_at', 'starts_at', 'tz', 'status_hint', 'announcement_kind']);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
}));
|
|
192
122
|
// ── Transcript ─────────────────────────────────────────────────────
|
|
193
123
|
selectorCommand('transcript', 'Transcript of a call')
|
|
194
124
|
.addOption(new Option('--format <format>', 'Transcript format').choices(['speakers', 'text', 'words', 'sections', 'cleaned']))
|
|
@@ -199,7 +129,6 @@ export function registerEarningsCommands(program) {
|
|
|
199
129
|
.option('--force', 'Overwrite an existing --output file')
|
|
200
130
|
.action(run(async (cmdOpts) => {
|
|
201
131
|
const opts = globalOpts();
|
|
202
|
-
validateSelector(cmdOpts);
|
|
203
132
|
const data = await apiGet({
|
|
204
133
|
path: `${BASE}/transcript`,
|
|
205
134
|
params: { ...selectorParams(cmdOpts), format: cmdOpts.format, version: cmdOpts.transcriptVersion, as_of: cmdOpts.asOf },
|
|
@@ -223,141 +152,12 @@ export function registerEarningsCommands(program) {
|
|
|
223
152
|
console.log(text);
|
|
224
153
|
});
|
|
225
154
|
}));
|
|
226
|
-
selectorCommand('versions', 'Published transcript versions of a call')
|
|
227
|
-
.action(run(async (cmdOpts) => {
|
|
228
|
-
const opts = globalOpts();
|
|
229
|
-
validateSelector(cmdOpts);
|
|
230
|
-
const data = await apiGet({
|
|
231
|
-
path: `${BASE}/transcript/versions`,
|
|
232
|
-
params: selectorParams(cmdOpts),
|
|
233
|
-
});
|
|
234
|
-
output(data, opts, () => {
|
|
235
|
-
console.log(chalk.bold(`\nTranscript versions (current: ${data.current_version})\n`));
|
|
236
|
-
formatTable(data.versions, ['version', 'version_kind', 'published_at', 'word_count', 'change_note', 'corrected_by']);
|
|
237
|
-
});
|
|
238
|
-
}));
|
|
239
|
-
selectorCommand('transcript-diff', 'Word diff between two transcript versions')
|
|
240
|
-
.requiredOption('--from <n>', 'Older version')
|
|
241
|
-
.option('--to <n>', 'Newer version (default: the current one)')
|
|
242
|
-
.action(run(async (cmdOpts) => {
|
|
243
|
-
const opts = globalOpts();
|
|
244
|
-
validateSelector(cmdOpts);
|
|
245
|
-
const data = await apiGet({
|
|
246
|
-
path: `${BASE}/transcript/diff`,
|
|
247
|
-
params: { ...selectorParams(cmdOpts), from: cmdOpts.from, to: cmdOpts.to },
|
|
248
|
-
});
|
|
249
|
-
output(data, opts, () => {
|
|
250
|
-
console.log(chalk.bold(`\nTranscript diff v${data.from} → v${data.to}\n`));
|
|
251
|
-
formatTable(data.changes.map(c => ({ ...c, at_ms: formatMs(c.at_ms) })), ['op', 'at_ms', 'text']);
|
|
252
|
-
console.log(`\n+${data.stats.added_words} / -${data.stats.removed_words} words${data.truncated ? chalk.yellow(' (truncated)') : ''}`);
|
|
253
|
-
});
|
|
254
|
-
}));
|
|
255
|
-
selectorCommand('qa', 'Q&A pairs of a call')
|
|
256
|
-
.option('-l, --limit <n>', 'Max pairs (1-200)')
|
|
257
|
-
.option('--offset <n>', 'Offset')
|
|
258
|
-
.action(run(async (cmdOpts) => {
|
|
259
|
-
const opts = globalOpts();
|
|
260
|
-
validateSelector(cmdOpts);
|
|
261
|
-
const data = await paginate(await apiGet({
|
|
262
|
-
path: `${BASE}/qa`,
|
|
263
|
-
params: { ...selectorParams(cmdOpts), limit: cmdOpts.limit, offset: cmdOpts.offset },
|
|
264
|
-
}), 'pairs', opts.all);
|
|
265
|
-
output(data, opts, () => {
|
|
266
|
-
if (data.pairs.length === 0)
|
|
267
|
-
console.log(chalk.yellow('No results.'));
|
|
268
|
-
for (const pair of data.pairs) {
|
|
269
|
-
const analyst = pair.analyst ?? {};
|
|
270
|
-
const firm = analyst.firm ? ` (${analyst.firm})` : '';
|
|
271
|
-
const followUp = pair.is_follow_up ? chalk.dim(' follow-up') : '';
|
|
272
|
-
console.log(chalk.bold(`\n#${pair.seq} ${speakerName(analyst)}${firm}`) + followUp + chalk.dim(` [${formatMs(pair.question_start_ms)}]`));
|
|
273
|
-
console.log(` Q: ${pair.question}`);
|
|
274
|
-
const answeredBy = (pair.answers ?? []).map(speakerName).join(', ');
|
|
275
|
-
console.log(` A${answeredBy ? ` (${answeredBy})` : ''}: ${pair.answer}`);
|
|
276
|
-
}
|
|
277
|
-
printPaginationHint(data.next_url);
|
|
278
|
-
});
|
|
279
|
-
}));
|
|
280
|
-
selectorCommand('participants', 'Speakers of a call')
|
|
281
|
-
.option('--evidence', 'Include the quotes each name was resolved from')
|
|
282
|
-
.action(run(async (cmdOpts) => {
|
|
283
|
-
const opts = globalOpts();
|
|
284
|
-
validateSelector(cmdOpts);
|
|
285
|
-
const data = await apiGet({
|
|
286
|
-
path: `${BASE}/participants`,
|
|
287
|
-
params: { ...selectorParams(cmdOpts), include: cmdOpts.evidence ? 'evidence' : undefined },
|
|
288
|
-
});
|
|
289
|
-
output(data, opts, () => {
|
|
290
|
-
console.log(chalk.bold('\nParticipants\n'));
|
|
291
|
-
formatTable(data.participants, ['speaker_label', 'name', 'title', 'firm', 'role', 'confidence', 'resolved_by']);
|
|
292
|
-
if (cmdOpts.evidence) {
|
|
293
|
-
for (const p of data.participants.filter(p => p.evidence?.length)) {
|
|
294
|
-
console.log(chalk.bold(`\n${speakerName(p)}`));
|
|
295
|
-
for (const e of p.evidence)
|
|
296
|
-
console.log(` [${formatMs(e.start_ms)}] "${e.quote}"`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
if (data.participation_events?.length) {
|
|
300
|
-
console.log(chalk.bold('\nParticipation flags\n'));
|
|
301
|
-
formatTable(data.participation_events, ['kind', 'person_id', 'event_id']);
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
}));
|
|
305
|
-
// ── People ─────────────────────────────────────────────────────────
|
|
306
|
-
earnings
|
|
307
|
-
.command('analysts')
|
|
308
|
-
.description("Analysts on a company's calls")
|
|
309
|
-
.requiredOption('-i, --identifier <ticker>', 'Ticker or CIK')
|
|
310
|
-
.option('-l, --limit <n>', 'Max results (1-100)')
|
|
311
|
-
.option('--offset <n>', 'Offset')
|
|
312
|
-
.action(run(async (cmdOpts) => {
|
|
313
|
-
const opts = globalOpts();
|
|
314
|
-
const data = await paginate(await apiGet({
|
|
315
|
-
path: `${BASE}/analysts`,
|
|
316
|
-
params: { identifier: cmdOpts.identifier, limit: cmdOpts.limit, offset: cmdOpts.offset },
|
|
317
|
-
}), 'analysts', opts.all);
|
|
318
|
-
output(data, opts, () => {
|
|
319
|
-
console.log(chalk.bold(`\n${data.symbol} (${data.cik}) Analysts\n`));
|
|
320
|
-
formatTable(data.analysts, ['name', 'firm', 'appearances', 'questions', 'first_seen', 'last_seen', 'person_id']);
|
|
321
|
-
printPaginationHint(data.next_url);
|
|
322
|
-
});
|
|
323
|
-
}));
|
|
324
|
-
earnings
|
|
325
|
-
.command('person')
|
|
326
|
-
.description("One person's call appearances")
|
|
327
|
-
.argument('<person_id>', 'Person ID')
|
|
328
|
-
.option('-l, --limit <n>', 'Max appearances (1-200)')
|
|
329
|
-
.option('--offset <n>', 'Offset')
|
|
330
|
-
.action(run(async (personId, cmdOpts) => {
|
|
331
|
-
const opts = globalOpts();
|
|
332
|
-
const data = await paginate(await apiGet({
|
|
333
|
-
path: `${BASE}/people/${encodeURIComponent(personId)}`,
|
|
334
|
-
params: { limit: cmdOpts.limit, offset: cmdOpts.offset },
|
|
335
|
-
}), 'appearances', opts.all);
|
|
336
|
-
output(data, opts, () => {
|
|
337
|
-
console.log(chalk.bold(`\n${data.name ?? data.person_id}\n`));
|
|
338
|
-
formatKeyValue({
|
|
339
|
-
kind: data.kind,
|
|
340
|
-
current_firm: data.current_firm,
|
|
341
|
-
current_title: data.current_title,
|
|
342
|
-
appearance_count: data.appearance_count,
|
|
343
|
-
aliases: data.aliases,
|
|
344
|
-
});
|
|
345
|
-
console.log(chalk.bold('\nAppearances\n'));
|
|
346
|
-
formatTable(data.appearances, [
|
|
347
|
-
'scheduled_at', 'symbol', 'company_name', 'fiscal_year', 'fiscal_quarter',
|
|
348
|
-
'role', 'title', 'firm', 'word_count', 'question_count', 'event_id',
|
|
349
|
-
]);
|
|
350
|
-
printPaginationHint(data.next_url);
|
|
351
|
-
});
|
|
352
|
-
}));
|
|
353
|
-
// ── Press release / extractions / guidance ─────────────────────────
|
|
354
155
|
selectorCommand('press-release', 'Results press release of a call')
|
|
355
156
|
.option('--body', 'Include the release text')
|
|
356
157
|
.option('-o, --output <file>', 'Save the release text to a file')
|
|
357
158
|
.option('--force', 'Overwrite an existing --output file')
|
|
358
159
|
.action(run(async (cmdOpts) => {
|
|
359
160
|
const opts = globalOpts();
|
|
360
|
-
validateSelector(cmdOpts);
|
|
361
161
|
const data = await apiGet({
|
|
362
162
|
path: `${BASE}/press-release`,
|
|
363
163
|
params: { ...selectorParams(cmdOpts), include: cmdOpts.body || cmdOpts.output ? 'body' : undefined },
|
|
@@ -387,173 +187,12 @@ export function registerEarningsCommands(program) {
|
|
|
387
187
|
console.log(`\n${data.body_text}`);
|
|
388
188
|
});
|
|
389
189
|
}));
|
|
390
|
-
selectorCommand('extractions', 'Guidance or KPI extractions of a call')
|
|
391
|
-
.addOption(new Option('--kind <kind>', 'guidance (default) or kpi').choices(['guidance', 'kpi']))
|
|
392
|
-
.addOption(new Option('--review <filter>', 'Guidance review filter').choices(['approved', 'auto', 'all']))
|
|
393
|
-
.option('-l, --limit <n>', 'Max items (1-500)')
|
|
394
|
-
.option('--offset <n>', 'Offset')
|
|
395
|
-
.action(run(async (cmdOpts) => {
|
|
396
|
-
const opts = globalOpts();
|
|
397
|
-
validateSelector(cmdOpts);
|
|
398
|
-
const first = await apiGet({
|
|
399
|
-
path: `${BASE}/extractions`,
|
|
400
|
-
params: {
|
|
401
|
-
...selectorParams(cmdOpts),
|
|
402
|
-
kind: cmdOpts.kind,
|
|
403
|
-
review: cmdOpts.review,
|
|
404
|
-
limit: cmdOpts.limit,
|
|
405
|
-
offset: cmdOpts.offset,
|
|
406
|
-
},
|
|
407
|
-
});
|
|
408
|
-
const data = await paginate(first, first.kind === 'kpi' ? 'kpis' : 'guidance', opts.all);
|
|
409
|
-
output(data, opts, () => {
|
|
410
|
-
console.log(chalk.bold(`\n${eventLine(data.event)} · ${data.kind === 'kpi' ? 'KPIs' : 'Guidance'}\n`));
|
|
411
|
-
if (data.kind === 'kpi') {
|
|
412
|
-
const columns = ['metric_normalized', 'value_text', 'period_label', 'speaker.name', 'origin', 'filing_link.form', 'filing_link.matched_value'];
|
|
413
|
-
formatTable(flattenRows(data.kpis ?? [], columns), columns);
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
formatTable(data.guidance ?? [], [
|
|
417
|
-
'metric_normalized', 'period_label', 'low', 'high', 'midpoint', 'unit',
|
|
418
|
-
'basis', 'direction', 'confidence', 'review_status',
|
|
419
|
-
]);
|
|
420
|
-
}
|
|
421
|
-
printPaginationHint(data.next_url);
|
|
422
|
-
});
|
|
423
|
-
}));
|
|
424
|
-
earnings
|
|
425
|
-
.command('guidance-history')
|
|
426
|
-
.description("A company's guidance for one metric across calls")
|
|
427
|
-
.requiredOption('-i, --identifier <ticker>', 'Ticker or CIK')
|
|
428
|
-
.requiredOption('--metric <metric>', 'Normalized metric (e.g. revenue, eps_non_gaap, segment:iphone)')
|
|
429
|
-
.addOption(new Option('--period-kind <kind>', 'Period kind').choices(['quarter', 'fiscal_year', 'half', 'other']))
|
|
430
|
-
.addOption(new Option('--basis <basis>', 'Basis').choices(['gaap', 'non_gaap', 'unknown']))
|
|
431
|
-
.option('-l, --limit <n>', 'Max series entries (1-40)')
|
|
432
|
-
.action(run(async (cmdOpts) => {
|
|
433
|
-
const opts = globalOpts();
|
|
434
|
-
const data = await apiGet({
|
|
435
|
-
path: `${BASE}/guidance/history`,
|
|
436
|
-
params: {
|
|
437
|
-
identifier: cmdOpts.identifier,
|
|
438
|
-
metric: cmdOpts.metric,
|
|
439
|
-
period_kind: cmdOpts.periodKind,
|
|
440
|
-
basis: cmdOpts.basis,
|
|
441
|
-
limit: cmdOpts.limit,
|
|
442
|
-
},
|
|
443
|
-
});
|
|
444
|
-
output(data, opts, () => {
|
|
445
|
-
console.log(chalk.bold(`\n${data.symbol} (${data.cik}) ${data.metric_normalized} guidance history\n`));
|
|
446
|
-
const columns = ['conference_date', 'period_label', 'low', 'high', 'midpoint', 'direction', 'change_vs_prior.midpoint_delta'];
|
|
447
|
-
formatTable(flattenRows(data.series, columns), columns);
|
|
448
|
-
});
|
|
449
|
-
}));
|
|
450
|
-
// ── Derived ────────────────────────────────────────────────────────
|
|
451
|
-
selectorCommand('diff', 'What changed against the prior call')
|
|
452
|
-
.addOption(new Option('--vs <base>', 'Comparison base').choices(['prior']))
|
|
453
|
-
.action(run(async (cmdOpts) => {
|
|
454
|
-
const opts = globalOpts();
|
|
455
|
-
validateSelector(cmdOpts);
|
|
456
|
-
const data = await apiGet({
|
|
457
|
-
path: `${BASE}/diff`,
|
|
458
|
-
params: { ...selectorParams(cmdOpts), vs: cmdOpts.vs },
|
|
459
|
-
});
|
|
460
|
-
output(data, opts, () => {
|
|
461
|
-
console.log(chalk.bold(`\n${eventLine(data.event)} vs ${data.prior_event?.conference_date ?? 'prior call'}\n`));
|
|
462
|
-
formatKeyValue({
|
|
463
|
-
prior_event_id: data.prior_event?.event_id,
|
|
464
|
-
similarity: data.similarity,
|
|
465
|
-
tone_shift: data.tone_shift,
|
|
466
|
-
hedging_level: data.hedging?.level,
|
|
467
|
-
hedging_change: data.hedging?.change,
|
|
468
|
-
uncertainty_level: data.uncertainty?.level,
|
|
469
|
-
uncertainty_change: data.uncertainty?.change,
|
|
470
|
-
numeric_specificity_level: data.numeric_specificity?.level,
|
|
471
|
-
numeric_specificity_change: data.numeric_specificity?.change,
|
|
472
|
-
slide_page_delta: data.slide_diff?.page_delta,
|
|
473
|
-
slide_new_titles: data.slide_diff?.new_titles,
|
|
474
|
-
slide_dropped_titles: data.slide_diff?.dropped_titles,
|
|
475
|
-
});
|
|
476
|
-
for (const [label, topics] of [['New topics', data.new_topics], ['Dropped topics', data.dropped_topics]]) {
|
|
477
|
-
if (!topics?.length)
|
|
478
|
-
continue;
|
|
479
|
-
console.log(chalk.bold(`\n${label}\n`));
|
|
480
|
-
formatTable(topics.map((t) => ({ ...t, start_ms: formatMs(t.start_ms) })), ['topic', 'start_ms', 'evidence']);
|
|
481
|
-
}
|
|
482
|
-
if (data.summary)
|
|
483
|
-
console.log(`\n${data.summary}`);
|
|
484
|
-
});
|
|
485
|
-
}));
|
|
486
|
-
selectorCommand('metrics', 'Call structure metrics (length, Q&A share, questions, …)')
|
|
487
|
-
.action(run(async (cmdOpts) => {
|
|
488
|
-
const opts = globalOpts();
|
|
489
|
-
validateSelector(cmdOpts);
|
|
490
|
-
const data = await apiGet({ path: `${BASE}/metrics`, params: selectorParams(cmdOpts) });
|
|
491
|
-
output(data, opts, () => {
|
|
492
|
-
const { event, ...metrics } = data;
|
|
493
|
-
console.log(chalk.bold(`\n${eventLine(event)} · metrics\n`));
|
|
494
|
-
formatKeyValue(humanizeMs(metrics));
|
|
495
|
-
});
|
|
496
|
-
}));
|
|
497
|
-
selectorCommand('prosody', 'Speech timing / pitch / energy per speaker')
|
|
498
|
-
.addOption(new Option('--section <section>', 'Section').choices(['all', 'prepared', 'qa']))
|
|
499
|
-
.action(run(async (cmdOpts) => {
|
|
500
|
-
const opts = globalOpts();
|
|
501
|
-
validateSelector(cmdOpts);
|
|
502
|
-
const data = await apiGet({
|
|
503
|
-
path: `${BASE}/prosody`,
|
|
504
|
-
params: { ...selectorParams(cmdOpts), section: cmdOpts.section },
|
|
505
|
-
});
|
|
506
|
-
output(data, opts, () => {
|
|
507
|
-
console.log(chalk.bold(`\n${eventLine(data.event)} · prosody (${data.section})\n`));
|
|
508
|
-
const columns = [
|
|
509
|
-
'speaker.name', 'speaker.role', 'section', 'speech_rate_wpm', 'pause_count', 'pause_mean_ms',
|
|
510
|
-
'filler_rate', 'time_to_answer_mean_ms', 'pitch_mean_hz', 'energy_mean_db', 'spoken_ms',
|
|
511
|
-
'word_count', 'acoustic_available',
|
|
512
|
-
];
|
|
513
|
-
formatTable(flattenRows(data.speakers, columns).map(row => ({ ...row, spoken_ms: formatMs(row.spoken_ms) })), columns);
|
|
514
|
-
});
|
|
515
|
-
}));
|
|
516
|
-
selectorCommand('price-attribution', 'Minute-level price moves during a call next to what was said (coincidence, not cause)')
|
|
517
|
-
.action(run(async (cmdOpts) => {
|
|
518
|
-
const opts = globalOpts();
|
|
519
|
-
validateSelector(cmdOpts);
|
|
520
|
-
const data = await apiGet({ path: `${BASE}/price-attribution`, params: selectorParams(cmdOpts) });
|
|
521
|
-
output(data, opts, () => {
|
|
522
|
-
const { event, buckets, top_minutes: topMinutes, live, settled, ...summary } = data;
|
|
523
|
-
console.log(chalk.bold(`\n${eventLine(event)} · price moves during the call\n`));
|
|
524
|
-
console.log(chalk.dim('Temporal coincidence, not attribution of cause; pre-market and after-hours bars are sparse.\n'));
|
|
525
|
-
if (live === true) {
|
|
526
|
-
console.log(chalk.yellow('LIVE — minute rows so far, not settled; the post-call run replaces them\n'));
|
|
527
|
-
}
|
|
528
|
-
else if (settled === false) {
|
|
529
|
-
console.log(chalk.yellow('Not settled — read before the bars settled; a later run may replace them\n'));
|
|
530
|
-
}
|
|
531
|
-
// While live the summary fields are null: print only what is known (bar_count = rows so far).
|
|
532
|
-
formatKeyValue(humanizeMs(Object.fromEntries(Object.entries(summary).filter(([, value]) => value !== null && value !== undefined))));
|
|
533
|
-
const columns = ['bucket', 'return_bps', 'cumulative_return_bps', 'said'];
|
|
534
|
-
const said = (row) => {
|
|
535
|
-
const text = (row.utterance_refs ?? [])
|
|
536
|
-
.map((ref) => `[${formatMs(ref.start_ms)}] ${ref.text}`)
|
|
537
|
-
.join(' ');
|
|
538
|
-
return text.length > SEARCH_TEXT_MAX ? `${text.slice(0, SEARCH_TEXT_MAX - 1)}…` : text;
|
|
539
|
-
};
|
|
540
|
-
if (topMinutes?.length) {
|
|
541
|
-
console.log(chalk.bold('\nTop minutes\n'));
|
|
542
|
-
formatTable(topMinutes.map((row) => ({ ...row, said: said(row) })), ['bucket', 'return_bps', 'said']);
|
|
543
|
-
}
|
|
544
|
-
if (buckets?.length) {
|
|
545
|
-
console.log(chalk.bold('\nMinutes\n'));
|
|
546
|
-
formatTable(buckets.map((row) => ({ ...row, said: said(row) })), columns);
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
}));
|
|
550
190
|
// ── Media ──────────────────────────────────────────────────────────
|
|
551
191
|
selectorCommand('audio', 'Call recording (signed URL, or --output to save it)')
|
|
552
192
|
.option('-o, --output <file>', 'Save the recording (e.g. call.mp3)')
|
|
553
193
|
.option('--force', 'Overwrite an existing --output file')
|
|
554
194
|
.action(run(async (cmdOpts) => {
|
|
555
195
|
const opts = globalOpts();
|
|
556
|
-
validateSelector(cmdOpts);
|
|
557
196
|
const data = await apiGet({ path: `${BASE}/audio`, params: selectorParams(cmdOpts) });
|
|
558
197
|
const saved = cmdOpts.output
|
|
559
198
|
? await downloadToFile(data.url, cmdOpts.output, { force: cmdOpts.force, spinnerText: 'Downloading audio...' })
|
|
@@ -578,7 +217,6 @@ export function registerEarningsCommands(program) {
|
|
|
578
217
|
.option('--force', 'Overwrite an existing --output file')
|
|
579
218
|
.action(run(async (cmdOpts) => {
|
|
580
219
|
const opts = globalOpts();
|
|
581
|
-
validateSelector(cmdOpts);
|
|
582
220
|
const data = await apiGet({
|
|
583
221
|
path: `${BASE}/slides`,
|
|
584
222
|
params: { ...selectorParams(cmdOpts), include: cmdOpts.markdown ? 'markdown' : undefined, pages: cmdOpts.pages },
|
|
@@ -621,8 +259,7 @@ export function registerEarningsCommands(program) {
|
|
|
621
259
|
.option('--force', 'Overwrite existing files in --output')
|
|
622
260
|
.action(run(async (cmdOpts) => {
|
|
623
261
|
const opts = globalOpts();
|
|
624
|
-
|
|
625
|
-
const data = await paginate(await apiGet({
|
|
262
|
+
const data = await allDocuments(await apiGet({
|
|
626
263
|
path: `${BASE}/documents`,
|
|
627
264
|
params: {
|
|
628
265
|
...selectorParams(cmdOpts),
|
|
@@ -631,7 +268,7 @@ export function registerEarningsCommands(program) {
|
|
|
631
268
|
limit: cmdOpts.limit,
|
|
632
269
|
offset: cmdOpts.offset,
|
|
633
270
|
},
|
|
634
|
-
}),
|
|
271
|
+
}), opts.all);
|
|
635
272
|
const saved = [];
|
|
636
273
|
const skipped = [];
|
|
637
274
|
if (cmdOpts.output) {
|
|
@@ -678,61 +315,121 @@ export function registerEarningsCommands(program) {
|
|
|
678
315
|
}
|
|
679
316
|
}
|
|
680
317
|
}));
|
|
681
|
-
// ──
|
|
318
|
+
// ── Company documents (IR library) ─────────────────────────────────
|
|
682
319
|
earnings
|
|
683
|
-
.command('
|
|
684
|
-
.description('
|
|
685
|
-
.requiredOption('-
|
|
686
|
-
.option('--
|
|
687
|
-
.option('
|
|
688
|
-
.option('--
|
|
689
|
-
.option('--
|
|
690
|
-
.
|
|
691
|
-
.option('-l, --limit <n>', 'Max
|
|
320
|
+
.command('company-documents')
|
|
321
|
+
.description("A company's IR documents (annual reports, proxy, ESG, investor-day decks, letters, webcast pages)")
|
|
322
|
+
.requiredOption('-i, --identifier <ticker>', 'Ticker or CIK')
|
|
323
|
+
.option('--section <list>', 'IR sections, comma-separated (quarterly_results, events_archive, presentations_archive, annual_reports, proxy, sec_filings, press_releases, esg, governance, faq, other)')
|
|
324
|
+
.option('--kind <list>', 'Kinds, comma-separated (annual_report, proxy_statement, esg_report, investor_day_deck, conference_presentation, shareholder_letter, fact_sheet, webcast, …)')
|
|
325
|
+
.option('--year <n>', 'Year the document belongs to')
|
|
326
|
+
.option('--period <period>', 'Period within the year (Q1-Q4, H1, H2, FY)')
|
|
327
|
+
.option('--event-id <event_id>', 'Only the documents of this event (investor day, conference, AGM)')
|
|
328
|
+
.option('-l, --limit <n>', 'Max results (1-100)')
|
|
329
|
+
.option('--offset <n>', 'Offset')
|
|
330
|
+
.option('-o, --output <dir>', 'Save every stored file into this directory as <year>-<kind>-<document_id>.<ext>; a link-only row (webcast) is reported on stderr, exit 1 only when none could be saved')
|
|
331
|
+
.option('--force', 'Overwrite existing files in --output')
|
|
692
332
|
.action(run(async (cmdOpts) => {
|
|
693
333
|
const opts = globalOpts();
|
|
694
|
-
const data = await apiGet({
|
|
695
|
-
path: `${BASE}/
|
|
334
|
+
const data = await allDocuments(await apiGet({
|
|
335
|
+
path: `${BASE}/company-documents`,
|
|
696
336
|
params: {
|
|
697
|
-
q: cmdOpts.query,
|
|
698
|
-
event_id: cmdOpts.event,
|
|
699
337
|
identifier: cmdOpts.identifier,
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
338
|
+
section: cmdOpts.section,
|
|
339
|
+
kind: cmdOpts.kind,
|
|
340
|
+
year: cmdOpts.year,
|
|
341
|
+
period: cmdOpts.period,
|
|
342
|
+
event_id: cmdOpts.eventId,
|
|
703
343
|
limit: cmdOpts.limit,
|
|
344
|
+
offset: cmdOpts.offset,
|
|
704
345
|
},
|
|
705
|
-
});
|
|
346
|
+
}), opts.all);
|
|
347
|
+
const saved = [];
|
|
348
|
+
const skipped = [];
|
|
349
|
+
if (cmdOpts.output) {
|
|
350
|
+
const dir = await ensureDir(cmdOpts.output);
|
|
351
|
+
for (const doc of data.documents) {
|
|
352
|
+
if (!doc.url) {
|
|
353
|
+
// Link-only rows (webcast replay pages) carry no signed URL: say so, never skip silently.
|
|
354
|
+
skipped.push(doc);
|
|
355
|
+
console.error(chalk.yellow(skippedDocumentWarning(doc)));
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const { path, bytes } = await downloadToFile(doc.url, join(dir, companyDocumentFileName(doc)), {
|
|
359
|
+
force: cmdOpts.force,
|
|
360
|
+
spinnerText: `Downloading ${doc.kind}...`,
|
|
361
|
+
});
|
|
362
|
+
saved.push({ document_id: doc.document_id, kind: doc.kind, path, bytes });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
706
365
|
output(data, opts, () => {
|
|
707
|
-
console.log(chalk.bold(`\
|
|
708
|
-
|
|
709
|
-
formatTable(
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
audio: Boolean(data.hits[i].audio_url),
|
|
716
|
-
};
|
|
717
|
-
}), columns);
|
|
366
|
+
console.log(chalk.bold(`\n${data.company?.symbol ?? cmdOpts.identifier} (${data.company?.cik ?? ''}) IR documents\n`));
|
|
367
|
+
console.log(chalk.dim('Company-published material, shown as published.\n'));
|
|
368
|
+
formatTable(data.documents, ['document_id', 'kind', 'section', 'year', 'period', 'title', 'pages']);
|
|
369
|
+
if (cmdOpts.output) {
|
|
370
|
+
console.log(chalk.bold('\nSaved\n'));
|
|
371
|
+
formatTable(saved, ['document_id', 'kind', 'path', 'bytes']);
|
|
372
|
+
}
|
|
373
|
+
printPaginationHint(data.next_url);
|
|
718
374
|
});
|
|
375
|
+
if (cmdOpts.output) {
|
|
376
|
+
if (skipped.length)
|
|
377
|
+
console.error(chalk.yellow(`${saved.length} of ${data.documents.length} documents saved; ${skipped.length} skipped (no download URL)`));
|
|
378
|
+
if (saved.length === 0 && data.documents.length > 0) {
|
|
379
|
+
throw new Error(`None of the ${data.documents.length} documents could be downloaded`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
719
382
|
}));
|
|
720
|
-
|
|
383
|
+
earnings
|
|
384
|
+
.command('document')
|
|
385
|
+
.description('One company IR document (metadata and signed URLs, or --pages for its text)')
|
|
386
|
+
.requiredOption('--document-id <document_id>', 'Document ID (from company-documents or document-search)')
|
|
387
|
+
.option('--pages', 'Print the per-page text')
|
|
388
|
+
.option('-o, --output <file>', 'Save the file (or, with --pages, the text)')
|
|
389
|
+
.option('--force', 'Overwrite an existing --output file')
|
|
721
390
|
.action(run(async (cmdOpts) => {
|
|
722
391
|
const opts = globalOpts();
|
|
723
|
-
|
|
724
|
-
|
|
392
|
+
const data = await apiGet({
|
|
393
|
+
path: `${BASE}/company-documents/${encodeURIComponent(cmdOpts.documentId)}`,
|
|
394
|
+
params: { include: cmdOpts.pages ? 'pages' : undefined },
|
|
395
|
+
});
|
|
396
|
+
const doc = data.document;
|
|
397
|
+
let text;
|
|
398
|
+
if (cmdOpts.pages) {
|
|
399
|
+
// Inline up to 2 MB; else the signed pages file (a bearer URL: plain GET, no Authorization header).
|
|
400
|
+
let pagesJson = doc.pages_json;
|
|
401
|
+
if (pagesJson === undefined && doc.pages_url) {
|
|
402
|
+
const response = await fetch(doc.pages_url);
|
|
403
|
+
if (!response.ok)
|
|
404
|
+
throw new Error(`Fetching the page text failed with status ${response.status}`);
|
|
405
|
+
pagesJson = await response.json();
|
|
406
|
+
// `--json` then carries the text too, as with the inline answer.
|
|
407
|
+
doc.pages_json = pagesJson;
|
|
408
|
+
}
|
|
409
|
+
text = pagesText(pagesJson);
|
|
410
|
+
if (!text)
|
|
411
|
+
throw new Error('No text extracted for this document yet');
|
|
412
|
+
}
|
|
413
|
+
let saved;
|
|
414
|
+
if (cmdOpts.output) {
|
|
415
|
+
if (text !== undefined) {
|
|
416
|
+
saved = await writeTextFile(cmdOpts.output, text, { force: cmdOpts.force });
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
if (!doc.url)
|
|
420
|
+
throw new Error(`No stored file for this document; source: ${doc.source_url}`);
|
|
421
|
+
saved = await downloadToFile(doc.url, cmdOpts.output, { force: cmdOpts.force, spinnerText: `Downloading ${doc.kind}...` });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
725
424
|
output(data, opts, () => {
|
|
726
|
-
console.log(chalk.bold(`\n${
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
if (data.url)
|
|
735
|
-
console.log(chalk.dim(`\nPlay with: ffplay "${data.url}" or vlc "${data.url}"`));
|
|
425
|
+
console.log(chalk.bold(`\n${doc.title ?? doc.kind} · ${data.company?.symbol ?? ''}\n`));
|
|
426
|
+
// The metadata without the inline text; a saved file prints its path instead of the signed URLs.
|
|
427
|
+
const hidden = new Set(saved ? ['pages_json', 'url', 'pages_url'] : ['pages_json']);
|
|
428
|
+
formatKeyValue(Object.fromEntries(Object.entries(doc).filter(([key]) => !hidden.has(key))));
|
|
429
|
+
if (saved)
|
|
430
|
+
printSaved(saved);
|
|
431
|
+
else if (text !== undefined)
|
|
432
|
+
console.log(`\n${text}`);
|
|
736
433
|
});
|
|
737
434
|
}));
|
|
738
435
|
}
|