@invinite-org/invinite-data-cli 0.1.21 → 0.2.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 +314 -6
- package/claude-plugin/.claude-plugin/plugin.json +13 -5
- package/claude-plugin/.mcp.json +8 -0
- package/claude-plugin/agents/invinite-data.md +6 -2
- package/claude-plugin/commands/analyze-earnings-call.md +24 -0
- package/claude-plugin/commands/analyze-filing.md +15 -0
- package/claude-plugin/commands/compare-companies.md +15 -0
- package/claude-plugin/commands/financial-research.md +2 -1
- package/claude-plugin/commands/ownership-review.md +14 -0
- package/claude-plugin/skills/invinite-data-cli/SKILL.md +119 -8
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +22 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client/api-client.d.ts +13 -1
- package/dist/src/client/api-client.js +46 -13
- package/dist/src/client/api-client.js.map +1 -1
- package/dist/src/client/download.d.ts +23 -0
- package/dist/src/client/download.js +116 -0
- package/dist/src/client/download.js.map +1 -0
- package/dist/src/client/earnings-live.d.ts +79 -0
- package/dist/src/client/earnings-live.js +156 -0
- package/dist/src/client/earnings-live.js.map +1 -0
- package/dist/src/client/pagination.js +2 -1
- package/dist/src/client/pagination.js.map +1 -1
- package/dist/src/client/websocket-client.d.ts +12 -1
- package/dist/src/client/websocket-client.js +114 -37
- package/dist/src/client/websocket-client.js.map +1 -1
- package/dist/src/commands/earnings-selector.d.ts +15 -0
- package/dist/src/commands/earnings-selector.js +29 -0
- package/dist/src/commands/earnings-selector.js.map +1 -0
- package/dist/src/commands/earnings.d.ts +13 -0
- package/dist/src/commands/earnings.js +705 -0
- package/dist/src/commands/earnings.js.map +1 -0
- package/dist/src/commands/plugin.d.ts +11 -0
- package/dist/src/commands/plugin.js +23 -8
- package/dist/src/commands/plugin.js.map +1 -1
- package/dist/src/commands/webhooks.d.ts +6 -0
- package/dist/src/commands/webhooks.js +144 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/ws.js +23 -3
- package/dist/src/commands/ws.js.map +1 -1
- package/dist/src/formatters/flatten.d.ts +4 -0
- package/dist/src/formatters/flatten.js +15 -0
- package/dist/src/formatters/flatten.js.map +1 -0
- package/dist/src/formatters/time.d.ts +2 -0
- package/dist/src/formatters/time.js +14 -0
- package/dist/src/formatters/time.js.map +1 -0
- package/dist/src/shared/errors.d.ts +2 -1
- package/dist/src/shared/errors.js +5 -1
- package/dist/src/shared/errors.js.map +1 -1
- package/dist/src/shared/options.d.ts +2 -0
- package/dist/src/shared/options.js +6 -0
- package/dist/src/shared/options.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { apiGet } from '../client/api-client.js';
|
|
5
|
+
import { downloadToFile, ensureDir, extensionFor, writeTextFile } from '../client/download.js';
|
|
6
|
+
import { fetchAllPages } from '../client/pagination.js';
|
|
7
|
+
import { withErrorHandling } from '../shared/errors.js';
|
|
8
|
+
import { output } from '../shared/output.js';
|
|
9
|
+
import { splitList } from '../shared/options.js';
|
|
10
|
+
import { setJsonMode } from '../shared/spinner.js';
|
|
11
|
+
import { formatJson } from '../formatters/json-formatter.js';
|
|
12
|
+
import { formatKeyValue, formatTable, printPaginationHint } from '../formatters/table-formatter.js';
|
|
13
|
+
import { flattenRows } from '../formatters/flatten.js';
|
|
14
|
+
import { formatMs } from '../formatters/time.js';
|
|
15
|
+
import { selectorOptions, selectorParams, validateSelector } from './earnings-selector.js';
|
|
16
|
+
const BASE = '/v1/earnings-calls';
|
|
17
|
+
const EVENT_COLUMNS = [
|
|
18
|
+
'scheduled_at', 'symbol', 'company_name', 'fiscal_year', 'fiscal_quarter', 'session',
|
|
19
|
+
'date_status', 'capture_status', 'has_transcript', 'has_audio', 'has_slides', 'event_id',
|
|
20
|
+
];
|
|
21
|
+
const SEARCH_TEXT_MAX = 120;
|
|
22
|
+
/** IDA's `event_type` filter values (`EVENT_TYPE_FILTERS`): one stored type, or `all`. */
|
|
23
|
+
const EVENT_TYPE_FILTERS = ['earnings_call', 'investor_day', 'conference', 'agm', 'other', 'all'];
|
|
24
|
+
/** One dim line after a 403 whose message names the plan the route needs. */
|
|
25
|
+
export function planHint(error) {
|
|
26
|
+
if (error.statusCode !== 403)
|
|
27
|
+
return undefined;
|
|
28
|
+
const tier = /requires (?:the )?(\w+) plan/i.exec(error.message)?.[1];
|
|
29
|
+
return tier ? `This route needs the ${tier} plan; see invd earnings --help` : undefined;
|
|
30
|
+
}
|
|
31
|
+
const run = (fn) => withErrorHandling(fn, planHint);
|
|
32
|
+
/** `--all`: follows `next_url` and concatenates the page arrays under `key`. */
|
|
33
|
+
async function paginate(data, key, all) {
|
|
34
|
+
if (!all || !data.next_url)
|
|
35
|
+
return data;
|
|
36
|
+
return fetchAllPages(data, d => d.next_url, (acc, page) => ({
|
|
37
|
+
...page,
|
|
38
|
+
[key]: [...(acc[key] ?? []), ...(page[key] ?? [])],
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
function eventLine(event) {
|
|
42
|
+
if (!event)
|
|
43
|
+
return '';
|
|
44
|
+
const day = event.conference_date ?? event.scheduled_at ?? '';
|
|
45
|
+
return [event.symbol ?? event.cik, event.company_name, day].filter(Boolean).join(' · ');
|
|
46
|
+
}
|
|
47
|
+
function printSaved(saved) {
|
|
48
|
+
console.log(chalk.green(`Saved ${saved.path} (${saved.bytes.toLocaleString('en-US')} bytes)`));
|
|
49
|
+
}
|
|
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
|
+
function speakerName(turn) {
|
|
55
|
+
return turn.name ?? `Speaker ${turn.speaker_label}`;
|
|
56
|
+
}
|
|
57
|
+
/** "Q&A starts at m:ss" for the transcript header; undefined without a Q&A boundary. */
|
|
58
|
+
export function qaStartLine(qaStartMs) {
|
|
59
|
+
return typeof qaStartMs === 'number' ? `Q&A starts at ${formatMs(qaStartMs)}` : undefined;
|
|
60
|
+
}
|
|
61
|
+
/** Plain-text rendering of a transcript `data` (console and `--output`); `words` stays JSON. */
|
|
62
|
+
export function transcriptText(data, format) {
|
|
63
|
+
if (typeof data === 'string')
|
|
64
|
+
return data;
|
|
65
|
+
if (format === 'words')
|
|
66
|
+
return JSON.stringify(data, null, 2);
|
|
67
|
+
if (format === 'sections') {
|
|
68
|
+
const sections = data;
|
|
69
|
+
return [
|
|
70
|
+
'## Prepared remarks', '', sections.prepared_remarks ?? '—', '',
|
|
71
|
+
'## Questions and answers', '', sections.questions_and_answers ?? '—',
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
|
74
|
+
return data
|
|
75
|
+
.map(turn => `${turn.start_ms === null || turn.start_ms === undefined ? '' : `[${formatMs(turn.start_ms)}] `}${speakerName(turn)}: ${turn.text}`)
|
|
76
|
+
.join('\n\n');
|
|
77
|
+
}
|
|
78
|
+
/** `documents --output`: the warning for a document without a signed URL. */
|
|
79
|
+
export function skippedDocumentWarning(doc) {
|
|
80
|
+
const source = doc.source_url ? `; source: ${doc.source_url}` : '';
|
|
81
|
+
return `Not saved: ${doc.kind} ${doc.document_id} has no download URL${source}`;
|
|
82
|
+
}
|
|
83
|
+
export function registerEarningsCommands(program) {
|
|
84
|
+
const earnings = program
|
|
85
|
+
.command('earnings')
|
|
86
|
+
.description('Earnings calls: calendar, transcripts, audio, slides, guidance, Q&A, search');
|
|
87
|
+
const globalOpts = () => {
|
|
88
|
+
const opts = program.opts();
|
|
89
|
+
if (opts.json)
|
|
90
|
+
setJsonMode(true);
|
|
91
|
+
return opts;
|
|
92
|
+
};
|
|
93
|
+
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
|
+
// ── Transcript ─────────────────────────────────────────────────────
|
|
193
|
+
selectorCommand('transcript', 'Transcript of a call')
|
|
194
|
+
.addOption(new Option('--format <format>', 'Transcript format').choices(['speakers', 'text', 'words', 'sections', 'cleaned']))
|
|
195
|
+
// Not `--version`: the root program's `-V, --version` is recognised after subcommands too.
|
|
196
|
+
.option('--transcript-version <n>', 'A specific published version (see `invd earnings versions`)')
|
|
197
|
+
.option('--as-of <iso>', 'The version current at this instant (ISO 8601)')
|
|
198
|
+
.option('-o, --output <file>', 'Save the transcript to a file')
|
|
199
|
+
.option('--force', 'Overwrite an existing --output file')
|
|
200
|
+
.action(run(async (cmdOpts) => {
|
|
201
|
+
const opts = globalOpts();
|
|
202
|
+
validateSelector(cmdOpts);
|
|
203
|
+
const data = await apiGet({
|
|
204
|
+
path: `${BASE}/transcript`,
|
|
205
|
+
params: { ...selectorParams(cmdOpts), format: cmdOpts.format, version: cmdOpts.transcriptVersion, as_of: cmdOpts.asOf },
|
|
206
|
+
});
|
|
207
|
+
const text = transcriptText(data.data, data.format);
|
|
208
|
+
const saved = cmdOpts.output ? await writeTextFile(cmdOpts.output, text, { force: cmdOpts.force }) : undefined;
|
|
209
|
+
output(data, opts, () => {
|
|
210
|
+
console.log(chalk.bold(`\n${eventLine(data.event)} · version ${data.version} (${data.version_kind}) · ${data.published_at}\n`));
|
|
211
|
+
const qaStart = qaStartLine(data.qa_start_ms);
|
|
212
|
+
if (qaStart)
|
|
213
|
+
console.log(chalk.dim(`${qaStart}\n`));
|
|
214
|
+
if (saved) {
|
|
215
|
+
printSaved(saved);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (data.format === 'words') {
|
|
219
|
+
console.log(chalk.yellow('The words format is JSON only; use --json to pipe it.\n'));
|
|
220
|
+
formatJson(data.data);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
console.log(text);
|
|
224
|
+
});
|
|
225
|
+
}));
|
|
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
|
+
selectorCommand('press-release', 'Results press release of a call')
|
|
355
|
+
.option('--body', 'Include the release text')
|
|
356
|
+
.option('-o, --output <file>', 'Save the release text to a file')
|
|
357
|
+
.option('--force', 'Overwrite an existing --output file')
|
|
358
|
+
.action(run(async (cmdOpts) => {
|
|
359
|
+
const opts = globalOpts();
|
|
360
|
+
validateSelector(cmdOpts);
|
|
361
|
+
const data = await apiGet({
|
|
362
|
+
path: `${BASE}/press-release`,
|
|
363
|
+
params: { ...selectorParams(cmdOpts), include: cmdOpts.body || cmdOpts.output ? 'body' : undefined },
|
|
364
|
+
});
|
|
365
|
+
let saved;
|
|
366
|
+
if (cmdOpts.output) {
|
|
367
|
+
if (!data.body_text)
|
|
368
|
+
throw new Error('The press release has no text to save');
|
|
369
|
+
saved = await writeTextFile(cmdOpts.output, data.body_text, { force: cmdOpts.force });
|
|
370
|
+
}
|
|
371
|
+
output(data, opts, () => {
|
|
372
|
+
console.log(chalk.bold(`\n${data.title ?? 'Press release'}\n`));
|
|
373
|
+
formatKeyValue({
|
|
374
|
+
published_at: data.published_at,
|
|
375
|
+
origin: data.origin,
|
|
376
|
+
url: data.url,
|
|
377
|
+
accession_number: data.accession_number,
|
|
378
|
+
trust_tier: data.trust_tier,
|
|
379
|
+
});
|
|
380
|
+
if (data.participants?.length) {
|
|
381
|
+
console.log(chalk.bold('\nParticipants\n'));
|
|
382
|
+
formatTable(data.participants, ['name', 'title', 'role']);
|
|
383
|
+
}
|
|
384
|
+
if (saved)
|
|
385
|
+
printSaved(saved);
|
|
386
|
+
if (cmdOpts.body && data.body_text)
|
|
387
|
+
console.log(`\n${data.body_text}`);
|
|
388
|
+
});
|
|
389
|
+
}));
|
|
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
|
+
// ── Media ──────────────────────────────────────────────────────────
|
|
517
|
+
selectorCommand('audio', 'Call recording (signed URL, or --output to save it)')
|
|
518
|
+
.option('-o, --output <file>', 'Save the recording (e.g. call.mp3)')
|
|
519
|
+
.option('--force', 'Overwrite an existing --output file')
|
|
520
|
+
.action(run(async (cmdOpts) => {
|
|
521
|
+
const opts = globalOpts();
|
|
522
|
+
validateSelector(cmdOpts);
|
|
523
|
+
const data = await apiGet({ path: `${BASE}/audio`, params: selectorParams(cmdOpts) });
|
|
524
|
+
const saved = cmdOpts.output
|
|
525
|
+
? await downloadToFile(data.url, cmdOpts.output, { force: cmdOpts.force, spinnerText: 'Downloading audio...' })
|
|
526
|
+
: undefined;
|
|
527
|
+
output(data, opts, () => {
|
|
528
|
+
console.log(chalk.bold(`\n${eventLine(data.event)} · audio\n`));
|
|
529
|
+
formatKeyValue({
|
|
530
|
+
duration: formatMs(data.duration_seconds === null ? null : data.duration_seconds * 1000),
|
|
531
|
+
bytes: data.bytes,
|
|
532
|
+
codec: data.codec,
|
|
533
|
+
capture_path: data.capture_path,
|
|
534
|
+
...(saved ? {} : { expires_at: data.expires_at, url: data.url }),
|
|
535
|
+
});
|
|
536
|
+
if (saved)
|
|
537
|
+
printSaved(saved);
|
|
538
|
+
});
|
|
539
|
+
}));
|
|
540
|
+
selectorCommand('slides', 'Slide deck (signed URL, or --output to save the PDF)')
|
|
541
|
+
.option('--markdown', "Include each page's extracted text")
|
|
542
|
+
.option('--pages <range>', 'Page or range, e.g. 3 or 1-5')
|
|
543
|
+
.option('-o, --output <file>', 'Save the deck PDF (e.g. deck.pdf)')
|
|
544
|
+
.option('--force', 'Overwrite an existing --output file')
|
|
545
|
+
.action(run(async (cmdOpts) => {
|
|
546
|
+
const opts = globalOpts();
|
|
547
|
+
validateSelector(cmdOpts);
|
|
548
|
+
const data = await apiGet({
|
|
549
|
+
path: `${BASE}/slides`,
|
|
550
|
+
params: { ...selectorParams(cmdOpts), include: cmdOpts.markdown ? 'markdown' : undefined, pages: cmdOpts.pages },
|
|
551
|
+
});
|
|
552
|
+
let saved;
|
|
553
|
+
if (cmdOpts.output) {
|
|
554
|
+
if (!data.deck?.url)
|
|
555
|
+
throw new Error('No slide deck to save for this call');
|
|
556
|
+
saved = await downloadToFile(data.deck.url, cmdOpts.output, { force: cmdOpts.force, spinnerText: 'Downloading deck...' });
|
|
557
|
+
}
|
|
558
|
+
output(data, opts, () => {
|
|
559
|
+
console.log(chalk.bold(`\n${eventLine(data.event)} · slides\n`));
|
|
560
|
+
if (data.deck) {
|
|
561
|
+
formatKeyValue({
|
|
562
|
+
pages: data.deck.pages,
|
|
563
|
+
origin: data.deck.origin,
|
|
564
|
+
source_url: data.deck.source_url,
|
|
565
|
+
trust_tier: data.deck.trust_tier,
|
|
566
|
+
...(saved ? {} : { expires_at: data.deck.expires_at, url: data.deck.url }),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (saved)
|
|
570
|
+
printSaved(saved);
|
|
571
|
+
console.log();
|
|
572
|
+
formatTable(data.slides ?? [], ['page', 'title']);
|
|
573
|
+
if (cmdOpts.markdown) {
|
|
574
|
+
for (const slide of data.slides ?? []) {
|
|
575
|
+
if (slide.markdown)
|
|
576
|
+
console.log(`\n${chalk.bold(`Page ${slide.page}`)}\n${slide.markdown}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
}));
|
|
581
|
+
selectorCommand('documents', 'Investor documents of a call (CFO commentary, data sheets, letters, filings)')
|
|
582
|
+
.option('--kind <list>', 'Kinds, comma-separated (cfo_commentary, financial_data, shareholder_letter, fact_sheet, company_transcript, sec_filing, infographic, other)')
|
|
583
|
+
.option('--body', 'Include the extracted text')
|
|
584
|
+
.option('-l, --limit <n>', 'Max results (1-100)')
|
|
585
|
+
.option('--offset <n>', 'Offset')
|
|
586
|
+
.option('-o, --output <dir>', 'Save every document into this directory as <kind>-<document_id>.<ext>; one without a download URL (link-only sec_filing) is reported on stderr, exit 1 only when none could be saved')
|
|
587
|
+
.option('--force', 'Overwrite existing files in --output')
|
|
588
|
+
.action(run(async (cmdOpts) => {
|
|
589
|
+
const opts = globalOpts();
|
|
590
|
+
validateSelector(cmdOpts);
|
|
591
|
+
const data = await paginate(await apiGet({
|
|
592
|
+
path: `${BASE}/documents`,
|
|
593
|
+
params: {
|
|
594
|
+
...selectorParams(cmdOpts),
|
|
595
|
+
kind: cmdOpts.kind,
|
|
596
|
+
include: cmdOpts.body ? 'body' : undefined,
|
|
597
|
+
limit: cmdOpts.limit,
|
|
598
|
+
offset: cmdOpts.offset,
|
|
599
|
+
},
|
|
600
|
+
}), 'documents', opts.all);
|
|
601
|
+
const saved = [];
|
|
602
|
+
const skipped = [];
|
|
603
|
+
if (cmdOpts.output) {
|
|
604
|
+
const dir = await ensureDir(cmdOpts.output);
|
|
605
|
+
for (const doc of data.documents) {
|
|
606
|
+
if (!doc.url) {
|
|
607
|
+
// Link-only rows (sec_filing) and failed downloads carry no signed URL: say so, never skip silently.
|
|
608
|
+
skipped.push(doc);
|
|
609
|
+
console.error(chalk.yellow(skippedDocumentWarning(doc)));
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const file = join(dir, `${doc.kind}-${doc.document_id}.${extensionFor(doc.content_type)}`);
|
|
613
|
+
const { path, bytes } = await downloadToFile(doc.url, file, { force: cmdOpts.force, spinnerText: `Downloading ${doc.kind}...` });
|
|
614
|
+
saved.push({ document_id: doc.document_id, kind: doc.kind, path, bytes });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
output(data, opts, () => {
|
|
618
|
+
console.log(chalk.bold(`\n${eventLine(data.event)} · documents\n`));
|
|
619
|
+
formatTable(data.documents, ['kind', 'title', 'content_type', 'bytes', 'pages', 'trust_tier', 'origin', 'expires_at', 'document_id']);
|
|
620
|
+
if (cmdOpts.output) {
|
|
621
|
+
console.log(chalk.bold('\nSaved\n'));
|
|
622
|
+
formatTable(saved, ['document_id', 'kind', 'path', 'bytes']);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
const linked = data.documents.filter(doc => doc.url);
|
|
626
|
+
if (linked.length)
|
|
627
|
+
console.log(chalk.bold('\nURLs\n'));
|
|
628
|
+
for (const doc of linked)
|
|
629
|
+
console.log(` ${doc.document_id} ${doc.url}`);
|
|
630
|
+
}
|
|
631
|
+
if (cmdOpts.body) {
|
|
632
|
+
for (const doc of data.documents.filter(doc => doc.body_text)) {
|
|
633
|
+
console.log(`\n${chalk.bold(doc.title ?? doc.kind)}\n${doc.body_text}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
printPaginationHint(data.next_url);
|
|
637
|
+
});
|
|
638
|
+
if (cmdOpts.output) {
|
|
639
|
+
// stderr, like the per-document warnings, so `--json` output stays parseable.
|
|
640
|
+
if (skipped.length)
|
|
641
|
+
console.error(chalk.yellow(`${saved.length} of ${data.documents.length} documents saved; ${skipped.length} skipped (no download URL)`));
|
|
642
|
+
if (saved.length === 0 && data.documents.length > 0) {
|
|
643
|
+
throw new Error(`None of the ${data.documents.length} documents could be downloaded`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}));
|
|
647
|
+
// ── Search / live stream ───────────────────────────────────────────
|
|
648
|
+
earnings
|
|
649
|
+
.command('search')
|
|
650
|
+
.description('Semantic search over call transcripts and slides')
|
|
651
|
+
.requiredOption('-q, --query <text>', 'Search text')
|
|
652
|
+
.option('--event <event_id>', 'Only this call')
|
|
653
|
+
.option('-i, --identifier <ticker>', 'Only this company')
|
|
654
|
+
.option('--from <date>', 'First New York call day (YYYY-MM-DD)')
|
|
655
|
+
.option('--to <date>', 'Last New York call day (YYYY-MM-DD)')
|
|
656
|
+
.addOption(new Option('--type <type>', 'Passage type').choices(['transcript', 'qa', 'preparedRemarks', 'presentation']))
|
|
657
|
+
.option('-l, --limit <n>', 'Max passages (1-8)')
|
|
658
|
+
.action(run(async (cmdOpts) => {
|
|
659
|
+
const opts = globalOpts();
|
|
660
|
+
const data = await apiGet({
|
|
661
|
+
path: `${BASE}/search`,
|
|
662
|
+
params: {
|
|
663
|
+
q: cmdOpts.query,
|
|
664
|
+
event_id: cmdOpts.event,
|
|
665
|
+
identifier: cmdOpts.identifier,
|
|
666
|
+
from: cmdOpts.from,
|
|
667
|
+
to: cmdOpts.to,
|
|
668
|
+
type: cmdOpts.type,
|
|
669
|
+
limit: cmdOpts.limit,
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
output(data, opts, () => {
|
|
673
|
+
console.log(chalk.bold(`\nSearch: ${data.query}\n`));
|
|
674
|
+
const columns = ['score', 'type', 'symbol', 'conference_date', 'speaker.name', 'start_ms', 'text', 'audio'];
|
|
675
|
+
formatTable(flattenRows(data.hits, columns).map((row, i) => {
|
|
676
|
+
const text = String(row.text ?? '');
|
|
677
|
+
return {
|
|
678
|
+
...row,
|
|
679
|
+
start_ms: formatMs(row.start_ms),
|
|
680
|
+
text: text.length > SEARCH_TEXT_MAX ? `${text.slice(0, SEARCH_TEXT_MAX - 1)}…` : text,
|
|
681
|
+
audio: Boolean(data.hits[i].audio_url),
|
|
682
|
+
};
|
|
683
|
+
}), columns);
|
|
684
|
+
});
|
|
685
|
+
}));
|
|
686
|
+
selectorCommand('stream', 'Live audio stream URL of a call in progress (HLS)')
|
|
687
|
+
.action(run(async (cmdOpts) => {
|
|
688
|
+
const opts = globalOpts();
|
|
689
|
+
validateSelector(cmdOpts);
|
|
690
|
+
const data = await apiGet({ path: `${BASE}/live/stream`, params: selectorParams(cmdOpts) });
|
|
691
|
+
output(data, opts, () => {
|
|
692
|
+
console.log(chalk.bold(`\n${eventLine(data.event)} · live stream\n`));
|
|
693
|
+
formatKeyValue({
|
|
694
|
+
status: data.status,
|
|
695
|
+
url: data.url,
|
|
696
|
+
expires_at: data.expires_at,
|
|
697
|
+
call_t0: data.call_t0,
|
|
698
|
+
segment_seconds: data.segment_seconds,
|
|
699
|
+
});
|
|
700
|
+
if (data.url)
|
|
701
|
+
console.log(chalk.dim(`\nPlay with: ffplay "${data.url}" or vlc "${data.url}"`));
|
|
702
|
+
});
|
|
703
|
+
}));
|
|
704
|
+
}
|
|
705
|
+
//# sourceMappingURL=earnings.js.map
|