@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.
Files changed (85) hide show
  1. package/README.md +89 -751
  2. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  3. package/claude-plugin/agents/invinite-data.md +1 -1
  4. package/claude-plugin/commands/analyze-earnings-call.md +5 -4
  5. package/claude-plugin/skills/invinite-data-cli/SKILL.md +446 -180
  6. package/dist/src/cli.d.ts +6 -0
  7. package/dist/src/cli.js +21 -31
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/client/api-client.d.ts +2 -4
  10. package/dist/src/client/api-client.js +3 -9
  11. package/dist/src/client/api-client.js.map +1 -1
  12. package/dist/src/client/pagination.d.ts +10 -0
  13. package/dist/src/client/pagination.js +40 -0
  14. package/dist/src/client/pagination.js.map +1 -1
  15. package/dist/src/commands/earnings.d.ts +4 -3
  16. package/dist/src/commands/earnings.js +163 -466
  17. package/dist/src/commands/earnings.js.map +1 -1
  18. package/dist/src/generated/cli-manifest.json +4983 -0
  19. package/dist/src/hand-written-commands.d.ts +43 -0
  20. package/dist/src/hand-written-commands.js +21 -0
  21. package/dist/src/hand-written-commands.js.map +1 -0
  22. package/dist/src/shared/errors.d.ts +5 -0
  23. package/dist/src/shared/errors.js +12 -0
  24. package/dist/src/shared/errors.js.map +1 -1
  25. package/dist/src/shared/options.d.ts +0 -13
  26. package/dist/src/shared/options.js +0 -13
  27. package/dist/src/shared/options.js.map +1 -1
  28. package/dist/src/spec-commands.d.ts +10 -0
  29. package/dist/src/spec-commands.js +207 -0
  30. package/dist/src/spec-commands.js.map +1 -0
  31. package/dist/src/spec-flags.d.ts +6 -0
  32. package/dist/src/spec-flags.js +9 -0
  33. package/dist/src/spec-flags.js.map +1 -0
  34. package/dist/src/spec-manifest.d.ts +382 -0
  35. package/dist/src/spec-manifest.js +65 -0
  36. package/dist/src/spec-manifest.js.map +1 -0
  37. package/package.json +7 -3
  38. package/dist/src/commands/company.d.ts +0 -2
  39. package/dist/src/commands/company.js +0 -220
  40. package/dist/src/commands/company.js.map +0 -1
  41. package/dist/src/commands/earnings-selector.d.ts +0 -15
  42. package/dist/src/commands/earnings-selector.js +0 -29
  43. package/dist/src/commands/earnings-selector.js.map +0 -1
  44. package/dist/src/commands/filings.d.ts +0 -2
  45. package/dist/src/commands/filings.js +0 -139
  46. package/dist/src/commands/filings.js.map +0 -1
  47. package/dist/src/commands/financials.d.ts +0 -2
  48. package/dist/src/commands/financials.js +0 -162
  49. package/dist/src/commands/financials.js.map +0 -1
  50. package/dist/src/commands/health.d.ts +0 -2
  51. package/dist/src/commands/health.js +0 -18
  52. package/dist/src/commands/health.js.map +0 -1
  53. package/dist/src/commands/insider-trades.d.ts +0 -2
  54. package/dist/src/commands/insider-trades.js +0 -56
  55. package/dist/src/commands/insider-trades.js.map +0 -1
  56. package/dist/src/commands/ipos.d.ts +0 -2
  57. package/dist/src/commands/ipos.js +0 -42
  58. package/dist/src/commands/ipos.js.map +0 -1
  59. package/dist/src/commands/market.d.ts +0 -2
  60. package/dist/src/commands/market.js +0 -34
  61. package/dist/src/commands/market.js.map +0 -1
  62. package/dist/src/commands/metadata.d.ts +0 -2
  63. package/dist/src/commands/metadata.js +0 -39
  64. package/dist/src/commands/metadata.js.map +0 -1
  65. package/dist/src/commands/metrics.d.ts +0 -2
  66. package/dist/src/commands/metrics.js +0 -158
  67. package/dist/src/commands/metrics.js.map +0 -1
  68. package/dist/src/commands/ownership.d.ts +0 -2
  69. package/dist/src/commands/ownership.js +0 -152
  70. package/dist/src/commands/ownership.js.map +0 -1
  71. package/dist/src/commands/segments.d.ts +0 -2
  72. package/dist/src/commands/segments.js +0 -64
  73. package/dist/src/commands/segments.js.map +0 -1
  74. package/dist/src/commands/usage.d.ts +0 -38
  75. package/dist/src/commands/usage.js +0 -82
  76. package/dist/src/commands/usage.js.map +0 -1
  77. package/dist/src/commands/webhooks.d.ts +0 -16
  78. package/dist/src/commands/webhooks.js +0 -186
  79. package/dist/src/commands/webhooks.js.map +0 -1
  80. package/dist/src/formatters/sparkline.d.ts +0 -2
  81. package/dist/src/formatters/sparkline.js +0 -11
  82. package/dist/src/formatters/sparkline.js.map +0 -1
  83. package/dist/src/formatters/tree-formatter.d.ts +0 -11
  84. package/dist/src/formatters/tree-formatter.js +0 -30
  85. 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
- import { selectorOptions, selectorParams, validateSelector } from './earnings-selector.js';
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
- 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;
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
- 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)
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, d => d.next_url, (acc, page) => ({
49
+ return fetchAllPages(data, page => page.next_url, (accumulated, page) => ({
37
50
  ...page,
38
- [key]: [...(acc[key] ?? []), ...(page[key] ?? [])],
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
- return [event.symbol ?? event.cik, event.company_name, day].filter(Boolean).join(' · ');
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
- validateSelector(cmdOpts);
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
- }), 'documents', opts.all);
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
- // ── Search / live stream ───────────────────────────────────────────
318
+ // ── Company documents (IR library) ─────────────────────────────────
682
319
  earnings
683
- .command('search')
684
- .description('Semantic search over call transcripts and slides')
685
- .requiredOption('-q, --query <text>', 'Search text')
686
- .option('--event <event_id>', 'Only this call')
687
- .option('-i, --identifier <ticker>', 'Only this company')
688
- .option('--from <date>', 'First New York call day (YYYY-MM-DD)')
689
- .option('--to <date>', 'Last New York call day (YYYY-MM-DD)')
690
- .addOption(new Option('--type <type>', 'Passage type').choices(['transcript', 'qa', 'preparedRemarks', 'presentation']))
691
- .option('-l, --limit <n>', 'Max passages (1-8)')
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}/search`,
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
- from: cmdOpts.from,
701
- to: cmdOpts.to,
702
- type: cmdOpts.type,
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(`\nSearch: ${data.query}\n`));
708
- const columns = ['score', 'type', 'symbol', 'conference_date', 'speaker.name', 'start_ms', 'text', 'audio'];
709
- formatTable(flattenRows(data.hits, columns).map((row, i) => {
710
- const text = String(row.text ?? '');
711
- return {
712
- ...row,
713
- start_ms: formatMs(row.start_ms),
714
- text: text.length > SEARCH_TEXT_MAX ? `${text.slice(0, SEARCH_TEXT_MAX - 1)}…` : text,
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
- selectorCommand('stream', 'Live audio stream URL of a call in progress (HLS)')
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
- validateSelector(cmdOpts);
724
- const data = await apiGet({ path: `${BASE}/live/stream`, params: selectorParams(cmdOpts) });
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${eventLine(data.event)} · live stream\n`));
727
- formatKeyValue({
728
- status: data.status,
729
- url: data.url,
730
- expires_at: data.expires_at,
731
- call_t0: data.call_t0,
732
- segment_seconds: data.segment_seconds,
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
  }