@invinite-org/invinite-data-cli 0.4.0 → 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 (84) hide show
  1. package/README.md +88 -802
  2. package/claude-plugin/agents/invinite-data.md +1 -1
  3. package/claude-plugin/commands/analyze-earnings-call.md +3 -3
  4. package/claude-plugin/skills/invinite-data-cli/SKILL.md +444 -187
  5. package/dist/src/cli.d.ts +6 -0
  6. package/dist/src/cli.js +21 -31
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/client/api-client.d.ts +2 -4
  9. package/dist/src/client/api-client.js +3 -9
  10. package/dist/src/client/api-client.js.map +1 -1
  11. package/dist/src/client/pagination.d.ts +10 -0
  12. package/dist/src/client/pagination.js +40 -0
  13. package/dist/src/client/pagination.js.map +1 -1
  14. package/dist/src/commands/earnings.d.ts +0 -3
  15. package/dist/src/commands/earnings.js +46 -524
  16. package/dist/src/commands/earnings.js.map +1 -1
  17. package/dist/src/generated/cli-manifest.json +4983 -0
  18. package/dist/src/hand-written-commands.d.ts +43 -0
  19. package/dist/src/hand-written-commands.js +21 -0
  20. package/dist/src/hand-written-commands.js.map +1 -0
  21. package/dist/src/shared/errors.d.ts +5 -0
  22. package/dist/src/shared/errors.js +12 -0
  23. package/dist/src/shared/errors.js.map +1 -1
  24. package/dist/src/shared/options.d.ts +0 -13
  25. package/dist/src/shared/options.js +0 -13
  26. package/dist/src/shared/options.js.map +1 -1
  27. package/dist/src/spec-commands.d.ts +10 -0
  28. package/dist/src/spec-commands.js +207 -0
  29. package/dist/src/spec-commands.js.map +1 -0
  30. package/dist/src/spec-flags.d.ts +6 -0
  31. package/dist/src/spec-flags.js +9 -0
  32. package/dist/src/spec-flags.js.map +1 -0
  33. package/dist/src/spec-manifest.d.ts +382 -0
  34. package/dist/src/spec-manifest.js +65 -0
  35. package/dist/src/spec-manifest.js.map +1 -0
  36. package/package.json +7 -3
  37. package/dist/src/commands/company.d.ts +0 -2
  38. package/dist/src/commands/company.js +0 -220
  39. package/dist/src/commands/company.js.map +0 -1
  40. package/dist/src/commands/earnings-selector.d.ts +0 -15
  41. package/dist/src/commands/earnings-selector.js +0 -29
  42. package/dist/src/commands/earnings-selector.js.map +0 -1
  43. package/dist/src/commands/filings.d.ts +0 -2
  44. package/dist/src/commands/filings.js +0 -139
  45. package/dist/src/commands/filings.js.map +0 -1
  46. package/dist/src/commands/financials.d.ts +0 -2
  47. package/dist/src/commands/financials.js +0 -162
  48. package/dist/src/commands/financials.js.map +0 -1
  49. package/dist/src/commands/health.d.ts +0 -2
  50. package/dist/src/commands/health.js +0 -18
  51. package/dist/src/commands/health.js.map +0 -1
  52. package/dist/src/commands/insider-trades.d.ts +0 -2
  53. package/dist/src/commands/insider-trades.js +0 -56
  54. package/dist/src/commands/insider-trades.js.map +0 -1
  55. package/dist/src/commands/ipos.d.ts +0 -2
  56. package/dist/src/commands/ipos.js +0 -42
  57. package/dist/src/commands/ipos.js.map +0 -1
  58. package/dist/src/commands/market.d.ts +0 -2
  59. package/dist/src/commands/market.js +0 -34
  60. package/dist/src/commands/market.js.map +0 -1
  61. package/dist/src/commands/metadata.d.ts +0 -2
  62. package/dist/src/commands/metadata.js +0 -39
  63. package/dist/src/commands/metadata.js.map +0 -1
  64. package/dist/src/commands/metrics.d.ts +0 -2
  65. package/dist/src/commands/metrics.js +0 -158
  66. package/dist/src/commands/metrics.js.map +0 -1
  67. package/dist/src/commands/ownership.d.ts +0 -2
  68. package/dist/src/commands/ownership.js +0 -152
  69. package/dist/src/commands/ownership.js.map +0 -1
  70. package/dist/src/commands/segments.d.ts +0 -2
  71. package/dist/src/commands/segments.js +0 -64
  72. package/dist/src/commands/segments.js.map +0 -1
  73. package/dist/src/commands/usage.d.ts +0 -38
  74. package/dist/src/commands/usage.js +0 -82
  75. package/dist/src/commands/usage.js.map +0 -1
  76. package/dist/src/commands/webhooks.d.ts +0 -16
  77. package/dist/src/commands/webhooks.js +0 -186
  78. package/dist/src/commands/webhooks.js.map +0 -1
  79. package/dist/src/formatters/sparkline.d.ts +0 -2
  80. package/dist/src/formatters/sparkline.js +0 -11
  81. package/dist/src/formatters/sparkline.js.map +0 -1
  82. package/dist/src/formatters/tree-formatter.d.ts +0 -11
  83. package/dist/src/formatters/tree-formatter.js +0 -30
  84. package/dist/src/formatters/tree-formatter.js.map +0 -1
@@ -4,38 +4,51 @@ 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', 'event_type', 'event_title', '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) {
@@ -51,10 +64,6 @@ function eventLine(event) {
51
64
  function printSaved(saved) {
52
65
  console.log(chalk.green(`Saved ${saved.path} (${saved.bytes.toLocaleString('en-US')} bytes)`));
53
66
  }
54
- /** `*_ms` numbers → `m:ss` for key / value output. */
55
- function humanizeMs(row) {
56
- return Object.fromEntries(Object.entries(row).map(([key, value]) => [key, key.endsWith('_ms') && typeof value === 'number' ? formatMs(value) : value]));
57
- }
58
67
  function speakerName(turn) {
59
68
  return turn.name ?? `Speaker ${turn.speaker_label}`;
60
69
  }
@@ -101,15 +110,8 @@ export function pagesText(pagesJson) {
101
110
  })
102
111
  .join('\n\n');
103
112
  }
104
- /** A table cell of at most `SEARCH_TEXT_MAX` characters. */
105
- function clip(value) {
106
- const text = String(value ?? '');
107
- return text.length > SEARCH_TEXT_MAX ? `${text.slice(0, SEARCH_TEXT_MAX - 1)}…` : text;
108
- }
109
113
  export function registerEarningsCommands(program) {
110
- const earnings = program
111
- .command('earnings')
112
- .description('Earnings calls: calendar, transcripts, audio, slides, guidance, Q&A, search');
114
+ const earnings = program.commands.find(command => command.name() === 'earnings') ?? program.command('earnings');
113
115
  const globalOpts = () => {
114
116
  const opts = program.opts();
115
117
  if (opts.json)
@@ -117,104 +119,6 @@ export function registerEarningsCommands(program) {
117
119
  return opts;
118
120
  };
119
121
  const selectorCommand = (name, description) => selectorOptions(earnings.command(name).description(description));
120
- // ── Calendar / events ──────────────────────────────────────────────
121
- earnings
122
- .command('calendar')
123
- .description('Upcoming and recent earnings calls across companies')
124
- .option('--from <date>', 'First New York day (YYYY-MM-DD, default today)')
125
- .option('--to <date>', 'Last New York day (YYYY-MM-DD, default from + 14 days, at most 92)')
126
- .option('--status <list>', 'Date statuses, comma-separated (estimated, unconfirmed, confirmed, postponed, cancelled)')
127
- .addOption(new Option('--session <session>', 'Session').choices(['bmo', 'amc', 'intraday']))
128
- .option('--exchange <exchange>', 'Exchange')
129
- .addOption(new Option('--event-type <type>', 'Event type (default earnings_call; all = every type)').choices(EVENT_TYPE_FILTERS))
130
- .addOption(new Option('-s, --sort <dir>', 'Sort direction').choices(['asc', 'desc']))
131
- .option('-l, --limit <n>', 'Max results (1-500)')
132
- .option('--offset <n>', 'Offset')
133
- .action(run(async (cmdOpts) => {
134
- const opts = globalOpts();
135
- const data = await paginate(await apiGet({
136
- path: `${BASE}/calendar`,
137
- params: {
138
- from: cmdOpts.from,
139
- to: cmdOpts.to,
140
- // IDA's `status` is an array parameter: one `status=` per value, never one comma string.
141
- status: splitList(cmdOpts.status),
142
- session: cmdOpts.session,
143
- exchange: cmdOpts.exchange,
144
- event_type: cmdOpts.eventType,
145
- sort: cmdOpts.sort,
146
- limit: cmdOpts.limit,
147
- offset: cmdOpts.offset,
148
- },
149
- }), 'events', opts.all);
150
- output(data, opts, () => {
151
- console.log(chalk.bold(`\nEarnings calendar ${data.from} … ${data.to}\n`));
152
- formatTable(data.events, EVENT_COLUMNS);
153
- printPaginationHint(data.next_url);
154
- });
155
- }));
156
- earnings
157
- .command('events')
158
- .description("A company's earnings calls")
159
- .requiredOption('-i, --identifier <ticker>', 'Ticker or CIK')
160
- .option('--from <date>', 'Calls on or after this New York day (YYYY-MM-DD)')
161
- .option('--to <date>', 'Calls on or before this New York day (YYYY-MM-DD)')
162
- .addOption(new Option('--event-type <type>', 'Event type (default all)').choices(EVENT_TYPE_FILTERS))
163
- .option('--observations', 'Include the date / time observations per call')
164
- .addOption(new Option('-s, --sort <dir>', 'Sort direction').choices(['asc', 'desc']))
165
- .option('-l, --limit <n>', 'Max results (1-200)')
166
- .option('--offset <n>', 'Offset')
167
- .action(run(async (cmdOpts) => {
168
- const opts = globalOpts();
169
- const data = await paginate(await apiGet({
170
- path: `${BASE}/events`,
171
- params: {
172
- identifier: cmdOpts.identifier,
173
- from: cmdOpts.from,
174
- to: cmdOpts.to,
175
- event_type: cmdOpts.eventType,
176
- include: cmdOpts.observations ? 'observations' : undefined,
177
- sort: cmdOpts.sort,
178
- limit: cmdOpts.limit,
179
- offset: cmdOpts.offset,
180
- },
181
- }), 'events', opts.all);
182
- output(data, opts, () => {
183
- console.log(chalk.bold(`\n${data.symbol} (${data.cik}) Earnings Calls\n`));
184
- if (!data.covered)
185
- console.log(chalk.yellow('Not covered by the earnings-calls pipeline.\n'));
186
- formatTable(data.events, EVENT_COLUMNS);
187
- printPaginationHint(data.next_url);
188
- });
189
- }));
190
- earnings
191
- .command('live')
192
- .description('Calls being captured right now')
193
- .action(run(async () => {
194
- const opts = globalOpts();
195
- const data = await apiGet({ path: `${BASE}/live` });
196
- output(data, opts, () => {
197
- console.log(chalk.bold(`\nLive earnings calls (as of ${data.as_of})\n`));
198
- formatTable(data.events, [...EVENT_COLUMNS, 'live_audio']);
199
- });
200
- }));
201
- earnings
202
- .command('event')
203
- .description('One earnings call with its date / time observations')
204
- .argument('<event_id>', 'Event ID')
205
- .action(run(async (eventId) => {
206
- const opts = globalOpts();
207
- const data = await apiGet({ path: `${BASE}/event/${encodeURIComponent(eventId)}` });
208
- output(data, opts, () => {
209
- const { observations, ...event } = data;
210
- console.log(chalk.bold(`\n${eventLine(event)}\n`));
211
- formatKeyValue(event);
212
- if (observations?.length) {
213
- console.log(chalk.bold('\nObservations\n'));
214
- formatTable(observations, ['observed_source', 'observed_at', 'starts_at', 'tz', 'status_hint', 'announcement_kind']);
215
- }
216
- });
217
- }));
218
122
  // ── Transcript ─────────────────────────────────────────────────────
219
123
  selectorCommand('transcript', 'Transcript of a call')
220
124
  .addOption(new Option('--format <format>', 'Transcript format').choices(['speakers', 'text', 'words', 'sections', 'cleaned']))
@@ -225,7 +129,6 @@ export function registerEarningsCommands(program) {
225
129
  .option('--force', 'Overwrite an existing --output file')
226
130
  .action(run(async (cmdOpts) => {
227
131
  const opts = globalOpts();
228
- validateSelector(cmdOpts);
229
132
  const data = await apiGet({
230
133
  path: `${BASE}/transcript`,
231
134
  params: { ...selectorParams(cmdOpts), format: cmdOpts.format, version: cmdOpts.transcriptVersion, as_of: cmdOpts.asOf },
@@ -249,141 +152,12 @@ export function registerEarningsCommands(program) {
249
152
  console.log(text);
250
153
  });
251
154
  }));
252
- selectorCommand('versions', 'Published transcript versions of a call')
253
- .action(run(async (cmdOpts) => {
254
- const opts = globalOpts();
255
- validateSelector(cmdOpts);
256
- const data = await apiGet({
257
- path: `${BASE}/transcript/versions`,
258
- params: selectorParams(cmdOpts),
259
- });
260
- output(data, opts, () => {
261
- console.log(chalk.bold(`\nTranscript versions (current: ${data.current_version})\n`));
262
- formatTable(data.versions, ['version', 'version_kind', 'published_at', 'word_count', 'change_note', 'corrected_by']);
263
- });
264
- }));
265
- selectorCommand('transcript-diff', 'Word diff between two transcript versions')
266
- .requiredOption('--from <n>', 'Older version')
267
- .option('--to <n>', 'Newer version (default: the current one)')
268
- .action(run(async (cmdOpts) => {
269
- const opts = globalOpts();
270
- validateSelector(cmdOpts);
271
- const data = await apiGet({
272
- path: `${BASE}/transcript/diff`,
273
- params: { ...selectorParams(cmdOpts), from: cmdOpts.from, to: cmdOpts.to },
274
- });
275
- output(data, opts, () => {
276
- console.log(chalk.bold(`\nTranscript diff v${data.from} → v${data.to}\n`));
277
- formatTable(data.changes.map(c => ({ ...c, at_ms: formatMs(c.at_ms) })), ['op', 'at_ms', 'text']);
278
- console.log(`\n+${data.stats.added_words} / -${data.stats.removed_words} words${data.truncated ? chalk.yellow(' (truncated)') : ''}`);
279
- });
280
- }));
281
- selectorCommand('qa', 'Q&A pairs of a call')
282
- .option('-l, --limit <n>', 'Max pairs (1-200)')
283
- .option('--offset <n>', 'Offset')
284
- .action(run(async (cmdOpts) => {
285
- const opts = globalOpts();
286
- validateSelector(cmdOpts);
287
- const data = await paginate(await apiGet({
288
- path: `${BASE}/qa`,
289
- params: { ...selectorParams(cmdOpts), limit: cmdOpts.limit, offset: cmdOpts.offset },
290
- }), 'pairs', opts.all);
291
- output(data, opts, () => {
292
- if (data.pairs.length === 0)
293
- console.log(chalk.yellow('No results.'));
294
- for (const pair of data.pairs) {
295
- const analyst = pair.analyst ?? {};
296
- const firm = analyst.firm ? ` (${analyst.firm})` : '';
297
- const followUp = pair.is_follow_up ? chalk.dim(' follow-up') : '';
298
- console.log(chalk.bold(`\n#${pair.seq} ${speakerName(analyst)}${firm}`) + followUp + chalk.dim(` [${formatMs(pair.question_start_ms)}]`));
299
- console.log(` Q: ${pair.question}`);
300
- const answeredBy = (pair.answers ?? []).map(speakerName).join(', ');
301
- console.log(` A${answeredBy ? ` (${answeredBy})` : ''}: ${pair.answer}`);
302
- }
303
- printPaginationHint(data.next_url);
304
- });
305
- }));
306
- selectorCommand('participants', 'Speakers of a call')
307
- .option('--evidence', 'Include the quotes each name was resolved from')
308
- .action(run(async (cmdOpts) => {
309
- const opts = globalOpts();
310
- validateSelector(cmdOpts);
311
- const data = await apiGet({
312
- path: `${BASE}/participants`,
313
- params: { ...selectorParams(cmdOpts), include: cmdOpts.evidence ? 'evidence' : undefined },
314
- });
315
- output(data, opts, () => {
316
- console.log(chalk.bold('\nParticipants\n'));
317
- formatTable(data.participants, ['speaker_label', 'name', 'title', 'firm', 'role', 'confidence', 'resolved_by']);
318
- if (cmdOpts.evidence) {
319
- for (const p of data.participants.filter(p => p.evidence?.length)) {
320
- console.log(chalk.bold(`\n${speakerName(p)}`));
321
- for (const e of p.evidence)
322
- console.log(` [${formatMs(e.start_ms)}] "${e.quote}"`);
323
- }
324
- }
325
- if (data.participation_events?.length) {
326
- console.log(chalk.bold('\nParticipation flags\n'));
327
- formatTable(data.participation_events, ['kind', 'person_id', 'event_id']);
328
- }
329
- });
330
- }));
331
- // ── People ─────────────────────────────────────────────────────────
332
- earnings
333
- .command('analysts')
334
- .description("Analysts on a company's calls")
335
- .requiredOption('-i, --identifier <ticker>', 'Ticker or CIK')
336
- .option('-l, --limit <n>', 'Max results (1-100)')
337
- .option('--offset <n>', 'Offset')
338
- .action(run(async (cmdOpts) => {
339
- const opts = globalOpts();
340
- const data = await paginate(await apiGet({
341
- path: `${BASE}/analysts`,
342
- params: { identifier: cmdOpts.identifier, limit: cmdOpts.limit, offset: cmdOpts.offset },
343
- }), 'analysts', opts.all);
344
- output(data, opts, () => {
345
- console.log(chalk.bold(`\n${data.symbol} (${data.cik}) Analysts\n`));
346
- formatTable(data.analysts, ['name', 'firm', 'appearances', 'questions', 'first_seen', 'last_seen', 'person_id']);
347
- printPaginationHint(data.next_url);
348
- });
349
- }));
350
- earnings
351
- .command('person')
352
- .description("One person's call appearances")
353
- .argument('<person_id>', 'Person ID')
354
- .option('-l, --limit <n>', 'Max appearances (1-200)')
355
- .option('--offset <n>', 'Offset')
356
- .action(run(async (personId, cmdOpts) => {
357
- const opts = globalOpts();
358
- const data = await paginate(await apiGet({
359
- path: `${BASE}/people/${encodeURIComponent(personId)}`,
360
- params: { limit: cmdOpts.limit, offset: cmdOpts.offset },
361
- }), 'appearances', opts.all);
362
- output(data, opts, () => {
363
- console.log(chalk.bold(`\n${data.name ?? data.person_id}\n`));
364
- formatKeyValue({
365
- kind: data.kind,
366
- current_firm: data.current_firm,
367
- current_title: data.current_title,
368
- appearance_count: data.appearance_count,
369
- aliases: data.aliases,
370
- });
371
- console.log(chalk.bold('\nAppearances\n'));
372
- formatTable(data.appearances, [
373
- 'scheduled_at', 'symbol', 'company_name', 'fiscal_year', 'fiscal_quarter',
374
- 'role', 'title', 'firm', 'word_count', 'question_count', 'event_id',
375
- ]);
376
- printPaginationHint(data.next_url);
377
- });
378
- }));
379
- // ── Press release / extractions / guidance ─────────────────────────
380
155
  selectorCommand('press-release', 'Results press release of a call')
381
156
  .option('--body', 'Include the release text')
382
157
  .option('-o, --output <file>', 'Save the release text to a file')
383
158
  .option('--force', 'Overwrite an existing --output file')
384
159
  .action(run(async (cmdOpts) => {
385
160
  const opts = globalOpts();
386
- validateSelector(cmdOpts);
387
161
  const data = await apiGet({
388
162
  path: `${BASE}/press-release`,
389
163
  params: { ...selectorParams(cmdOpts), include: cmdOpts.body || cmdOpts.output ? 'body' : undefined },
@@ -413,173 +187,12 @@ export function registerEarningsCommands(program) {
413
187
  console.log(`\n${data.body_text}`);
414
188
  });
415
189
  }));
416
- selectorCommand('extractions', 'Guidance or KPI extractions of a call')
417
- .addOption(new Option('--kind <kind>', 'guidance (default) or kpi').choices(['guidance', 'kpi']))
418
- .addOption(new Option('--review <filter>', 'Guidance review filter').choices(['approved', 'auto', 'all']))
419
- .option('-l, --limit <n>', 'Max items (1-500)')
420
- .option('--offset <n>', 'Offset')
421
- .action(run(async (cmdOpts) => {
422
- const opts = globalOpts();
423
- validateSelector(cmdOpts);
424
- const first = await apiGet({
425
- path: `${BASE}/extractions`,
426
- params: {
427
- ...selectorParams(cmdOpts),
428
- kind: cmdOpts.kind,
429
- review: cmdOpts.review,
430
- limit: cmdOpts.limit,
431
- offset: cmdOpts.offset,
432
- },
433
- });
434
- const data = await paginate(first, first.kind === 'kpi' ? 'kpis' : 'guidance', opts.all);
435
- output(data, opts, () => {
436
- console.log(chalk.bold(`\n${eventLine(data.event)} · ${data.kind === 'kpi' ? 'KPIs' : 'Guidance'}\n`));
437
- if (data.kind === 'kpi') {
438
- const columns = ['metric_normalized', 'value_text', 'period_label', 'speaker.name', 'origin', 'filing_link.form', 'filing_link.matched_value'];
439
- formatTable(flattenRows(data.kpis ?? [], columns), columns);
440
- }
441
- else {
442
- formatTable(data.guidance ?? [], [
443
- 'metric_normalized', 'period_label', 'low', 'high', 'midpoint', 'unit',
444
- 'basis', 'direction', 'confidence', 'review_status',
445
- ]);
446
- }
447
- printPaginationHint(data.next_url);
448
- });
449
- }));
450
- earnings
451
- .command('guidance-history')
452
- .description("A company's guidance for one metric across calls")
453
- .requiredOption('-i, --identifier <ticker>', 'Ticker or CIK')
454
- .requiredOption('--metric <metric>', 'Normalized metric (e.g. revenue, eps_non_gaap, segment:iphone)')
455
- .addOption(new Option('--period-kind <kind>', 'Period kind').choices(['quarter', 'fiscal_year', 'half', 'other']))
456
- .addOption(new Option('--basis <basis>', 'Basis').choices(['gaap', 'non_gaap', 'unknown']))
457
- .option('-l, --limit <n>', 'Max series entries (1-40)')
458
- .action(run(async (cmdOpts) => {
459
- const opts = globalOpts();
460
- const data = await apiGet({
461
- path: `${BASE}/guidance/history`,
462
- params: {
463
- identifier: cmdOpts.identifier,
464
- metric: cmdOpts.metric,
465
- period_kind: cmdOpts.periodKind,
466
- basis: cmdOpts.basis,
467
- limit: cmdOpts.limit,
468
- },
469
- });
470
- output(data, opts, () => {
471
- console.log(chalk.bold(`\n${data.symbol} (${data.cik}) ${data.metric_normalized} guidance history\n`));
472
- const columns = ['conference_date', 'period_label', 'low', 'high', 'midpoint', 'direction', 'change_vs_prior.midpoint_delta'];
473
- formatTable(flattenRows(data.series, columns), columns);
474
- });
475
- }));
476
- // ── Derived ────────────────────────────────────────────────────────
477
- selectorCommand('diff', 'What changed against the prior call')
478
- .addOption(new Option('--vs <base>', 'Comparison base').choices(['prior']))
479
- .action(run(async (cmdOpts) => {
480
- const opts = globalOpts();
481
- validateSelector(cmdOpts);
482
- const data = await apiGet({
483
- path: `${BASE}/diff`,
484
- params: { ...selectorParams(cmdOpts), vs: cmdOpts.vs },
485
- });
486
- output(data, opts, () => {
487
- console.log(chalk.bold(`\n${eventLine(data.event)} vs ${data.prior_event?.conference_date ?? 'prior call'}\n`));
488
- formatKeyValue({
489
- prior_event_id: data.prior_event?.event_id,
490
- similarity: data.similarity,
491
- tone_shift: data.tone_shift,
492
- hedging_level: data.hedging?.level,
493
- hedging_change: data.hedging?.change,
494
- uncertainty_level: data.uncertainty?.level,
495
- uncertainty_change: data.uncertainty?.change,
496
- numeric_specificity_level: data.numeric_specificity?.level,
497
- numeric_specificity_change: data.numeric_specificity?.change,
498
- slide_page_delta: data.slide_diff?.page_delta,
499
- slide_new_titles: data.slide_diff?.new_titles,
500
- slide_dropped_titles: data.slide_diff?.dropped_titles,
501
- });
502
- for (const [label, topics] of [['New topics', data.new_topics], ['Dropped topics', data.dropped_topics]]) {
503
- if (!topics?.length)
504
- continue;
505
- console.log(chalk.bold(`\n${label}\n`));
506
- formatTable(topics.map((t) => ({ ...t, start_ms: formatMs(t.start_ms) })), ['topic', 'start_ms', 'evidence']);
507
- }
508
- if (data.summary)
509
- console.log(`\n${data.summary}`);
510
- });
511
- }));
512
- selectorCommand('metrics', 'Call structure metrics (length, Q&A share, questions, …)')
513
- .action(run(async (cmdOpts) => {
514
- const opts = globalOpts();
515
- validateSelector(cmdOpts);
516
- const data = await apiGet({ path: `${BASE}/metrics`, params: selectorParams(cmdOpts) });
517
- output(data, opts, () => {
518
- const { event, ...metrics } = data;
519
- console.log(chalk.bold(`\n${eventLine(event)} · metrics\n`));
520
- formatKeyValue(humanizeMs(metrics));
521
- });
522
- }));
523
- selectorCommand('prosody', 'Speech timing / pitch / energy per speaker')
524
- .addOption(new Option('--section <section>', 'Section').choices(['all', 'prepared', 'qa']))
525
- .action(run(async (cmdOpts) => {
526
- const opts = globalOpts();
527
- validateSelector(cmdOpts);
528
- const data = await apiGet({
529
- path: `${BASE}/prosody`,
530
- params: { ...selectorParams(cmdOpts), section: cmdOpts.section },
531
- });
532
- output(data, opts, () => {
533
- console.log(chalk.bold(`\n${eventLine(data.event)} · prosody (${data.section})\n`));
534
- const columns = [
535
- 'speaker.name', 'speaker.role', 'section', 'speech_rate_wpm', 'pause_count', 'pause_mean_ms',
536
- 'filler_rate', 'time_to_answer_mean_ms', 'pitch_mean_hz', 'energy_mean_db', 'spoken_ms',
537
- 'word_count', 'acoustic_available',
538
- ];
539
- formatTable(flattenRows(data.speakers, columns).map(row => ({ ...row, spoken_ms: formatMs(row.spoken_ms) })), columns);
540
- });
541
- }));
542
- selectorCommand('price-attribution', 'Minute-level price moves during a call next to what was said (coincidence, not cause)')
543
- .action(run(async (cmdOpts) => {
544
- const opts = globalOpts();
545
- validateSelector(cmdOpts);
546
- const data = await apiGet({ path: `${BASE}/price-attribution`, params: selectorParams(cmdOpts) });
547
- output(data, opts, () => {
548
- const { event, buckets, top_minutes: topMinutes, live, settled, ...summary } = data;
549
- console.log(chalk.bold(`\n${eventLine(event)} · price moves during the call\n`));
550
- console.log(chalk.dim('Temporal coincidence, not attribution of cause; pre-market and after-hours bars are sparse.\n'));
551
- if (live === true) {
552
- console.log(chalk.yellow('LIVE — minute rows so far, not settled; the post-call run replaces them\n'));
553
- }
554
- else if (settled === false) {
555
- console.log(chalk.yellow('Not settled — read before the bars settled; a later run may replace them\n'));
556
- }
557
- // While live the summary fields are null: print only what is known (bar_count = rows so far).
558
- formatKeyValue(humanizeMs(Object.fromEntries(Object.entries(summary).filter(([, value]) => value !== null && value !== undefined))));
559
- const columns = ['bucket', 'return_bps', 'cumulative_return_bps', 'said'];
560
- const said = (row) => {
561
- const text = (row.utterance_refs ?? [])
562
- .map((ref) => `[${formatMs(ref.start_ms)}] ${ref.text}`)
563
- .join(' ');
564
- return text.length > SEARCH_TEXT_MAX ? `${text.slice(0, SEARCH_TEXT_MAX - 1)}…` : text;
565
- };
566
- if (topMinutes?.length) {
567
- console.log(chalk.bold('\nTop minutes\n'));
568
- formatTable(topMinutes.map((row) => ({ ...row, said: said(row) })), ['bucket', 'return_bps', 'said']);
569
- }
570
- if (buckets?.length) {
571
- console.log(chalk.bold('\nMinutes\n'));
572
- formatTable(buckets.map((row) => ({ ...row, said: said(row) })), columns);
573
- }
574
- });
575
- }));
576
190
  // ── Media ──────────────────────────────────────────────────────────
577
191
  selectorCommand('audio', 'Call recording (signed URL, or --output to save it)')
578
192
  .option('-o, --output <file>', 'Save the recording (e.g. call.mp3)')
579
193
  .option('--force', 'Overwrite an existing --output file')
580
194
  .action(run(async (cmdOpts) => {
581
195
  const opts = globalOpts();
582
- validateSelector(cmdOpts);
583
196
  const data = await apiGet({ path: `${BASE}/audio`, params: selectorParams(cmdOpts) });
584
197
  const saved = cmdOpts.output
585
198
  ? await downloadToFile(data.url, cmdOpts.output, { force: cmdOpts.force, spinnerText: 'Downloading audio...' })
@@ -604,7 +217,6 @@ export function registerEarningsCommands(program) {
604
217
  .option('--force', 'Overwrite an existing --output file')
605
218
  .action(run(async (cmdOpts) => {
606
219
  const opts = globalOpts();
607
- validateSelector(cmdOpts);
608
220
  const data = await apiGet({
609
221
  path: `${BASE}/slides`,
610
222
  params: { ...selectorParams(cmdOpts), include: cmdOpts.markdown ? 'markdown' : undefined, pages: cmdOpts.pages },
@@ -647,8 +259,7 @@ export function registerEarningsCommands(program) {
647
259
  .option('--force', 'Overwrite existing files in --output')
648
260
  .action(run(async (cmdOpts) => {
649
261
  const opts = globalOpts();
650
- validateSelector(cmdOpts);
651
- const data = await paginate(await apiGet({
262
+ const data = await allDocuments(await apiGet({
652
263
  path: `${BASE}/documents`,
653
264
  params: {
654
265
  ...selectorParams(cmdOpts),
@@ -657,7 +268,7 @@ export function registerEarningsCommands(program) {
657
268
  limit: cmdOpts.limit,
658
269
  offset: cmdOpts.offset,
659
270
  },
660
- }), 'documents', opts.all);
271
+ }), opts.all);
661
272
  const saved = [];
662
273
  const skipped = [];
663
274
  if (cmdOpts.output) {
@@ -704,47 +315,6 @@ export function registerEarningsCommands(program) {
704
315
  }
705
316
  }
706
317
  }));
707
- // ── Search / live stream ───────────────────────────────────────────
708
- earnings
709
- .command('search')
710
- .description('Semantic search over call transcripts and slides')
711
- .requiredOption('-q, --query <text>', 'Search text')
712
- .option('--event <event_id>', 'Only this call')
713
- .option('-i, --identifier <ticker>', 'Only this company')
714
- .option('--from <date>', 'First New York call day (YYYY-MM-DD)')
715
- .option('--to <date>', 'Last New York call day (YYYY-MM-DD)')
716
- .addOption(new Option('--type <type>', 'Passage type').choices(['transcript', 'qa', 'preparedRemarks', 'presentation']))
717
- .addOption(new Option('--event-type <type>', 'Event type (default earnings_call; all = every type)').choices(EVENT_TYPE_FILTERS))
718
- .option('-l, --limit <n>', 'Max passages (1-8)')
719
- .action(run(async (cmdOpts) => {
720
- const opts = globalOpts();
721
- const data = await apiGet({
722
- path: `${BASE}/search`,
723
- params: {
724
- q: cmdOpts.query,
725
- event_id: cmdOpts.event,
726
- identifier: cmdOpts.identifier,
727
- from: cmdOpts.from,
728
- to: cmdOpts.to,
729
- type: cmdOpts.type,
730
- event_type: cmdOpts.eventType,
731
- limit: cmdOpts.limit,
732
- },
733
- });
734
- output(data, opts, () => {
735
- console.log(chalk.bold(`\nSearch: ${data.query}\n`));
736
- const columns = ['score', 'type', 'symbol', 'conference_date', 'speaker.name', 'start_ms', 'text', 'audio'];
737
- formatTable(flattenRows(data.hits, columns).map((row, i) => {
738
- const text = String(row.text ?? '');
739
- return {
740
- ...row,
741
- start_ms: formatMs(row.start_ms),
742
- text: text.length > SEARCH_TEXT_MAX ? `${text.slice(0, SEARCH_TEXT_MAX - 1)}…` : text,
743
- audio: Boolean(data.hits[i].audio_url),
744
- };
745
- }), columns);
746
- });
747
- }));
748
318
  // ── Company documents (IR library) ─────────────────────────────────
749
319
  earnings
750
320
  .command('company-documents')
@@ -754,14 +324,14 @@ export function registerEarningsCommands(program) {
754
324
  .option('--kind <list>', 'Kinds, comma-separated (annual_report, proxy_statement, esg_report, investor_day_deck, conference_presentation, shareholder_letter, fact_sheet, webcast, …)')
755
325
  .option('--year <n>', 'Year the document belongs to')
756
326
  .option('--period <period>', 'Period within the year (Q1-Q4, H1, H2, FY)')
757
- .option('--event <event_id>', 'Only the documents of this event (investor day, conference, AGM)')
327
+ .option('--event-id <event_id>', 'Only the documents of this event (investor day, conference, AGM)')
758
328
  .option('-l, --limit <n>', 'Max results (1-100)')
759
329
  .option('--offset <n>', 'Offset')
760
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')
761
331
  .option('--force', 'Overwrite existing files in --output')
762
332
  .action(run(async (cmdOpts) => {
763
333
  const opts = globalOpts();
764
- const data = await paginate(await apiGet({
334
+ const data = await allDocuments(await apiGet({
765
335
  path: `${BASE}/company-documents`,
766
336
  params: {
767
337
  identifier: cmdOpts.identifier,
@@ -769,11 +339,11 @@ export function registerEarningsCommands(program) {
769
339
  kind: cmdOpts.kind,
770
340
  year: cmdOpts.year,
771
341
  period: cmdOpts.period,
772
- event_id: cmdOpts.event,
342
+ event_id: cmdOpts.eventId,
773
343
  limit: cmdOpts.limit,
774
344
  offset: cmdOpts.offset,
775
345
  },
776
- }), 'documents', opts.all);
346
+ }), opts.all);
777
347
  const saved = [];
778
348
  const skipped = [];
779
349
  if (cmdOpts.output) {
@@ -813,14 +383,14 @@ export function registerEarningsCommands(program) {
813
383
  earnings
814
384
  .command('document')
815
385
  .description('One company IR document (metadata and signed URLs, or --pages for its text)')
816
- .argument('<document_id>', 'Document ID (from company-documents or document-search)')
386
+ .requiredOption('--document-id <document_id>', 'Document ID (from company-documents or document-search)')
817
387
  .option('--pages', 'Print the per-page text')
818
388
  .option('-o, --output <file>', 'Save the file (or, with --pages, the text)')
819
389
  .option('--force', 'Overwrite an existing --output file')
820
- .action(run(async (documentId, cmdOpts) => {
390
+ .action(run(async (cmdOpts) => {
821
391
  const opts = globalOpts();
822
392
  const data = await apiGet({
823
- path: `${BASE}/company-documents/${encodeURIComponent(documentId)}`,
393
+ path: `${BASE}/company-documents/${encodeURIComponent(cmdOpts.documentId)}`,
824
394
  params: { include: cmdOpts.pages ? 'pages' : undefined },
825
395
  });
826
396
  const doc = data.document;
@@ -862,53 +432,5 @@ export function registerEarningsCommands(program) {
862
432
  console.log(`\n${text}`);
863
433
  });
864
434
  }));
865
- earnings
866
- .command('document-search')
867
- .description('Semantic search over company documents (call documents and IR libraries): passages with page numbers')
868
- .requiredOption('-q, --query <text>', 'Search text')
869
- .option('-i, --identifier <ticker>', 'Only this company')
870
- .option('--kind <list>', 'Kinds, comma-separated (annual_report, proxy_statement, cfo_commentary, …)')
871
- .option('--section <list>', 'IR sections, comma-separated (company documents only)')
872
- .option('--year <n>', 'Only documents of this year')
873
- .addOption(new Option('--table <table>', 'Only call documents or only IR libraries').choices(['event_documents', 'company_documents']))
874
- .option('-l, --limit <n>', 'Max passages (1-20)')
875
- .action(run(async (cmdOpts) => {
876
- const opts = globalOpts();
877
- const data = await apiGet({
878
- path: `${BASE}/documents/search`,
879
- params: {
880
- q: cmdOpts.query,
881
- identifier: cmdOpts.identifier,
882
- kind: cmdOpts.kind,
883
- section: cmdOpts.section,
884
- year: cmdOpts.year,
885
- table: cmdOpts.table,
886
- limit: cmdOpts.limit,
887
- },
888
- });
889
- output(data, opts, () => {
890
- console.log(chalk.bold(`\nDocument search: ${data.query}\n`));
891
- console.log(chalk.dim('Passages from company-published documents, not verified facts.\n'));
892
- formatTable(data.hits.map(hit => ({ ...hit, title: clip(hit.title), text: clip(hit.text) })), ['score', 'kind', 'year', 'page', 'symbol', 'title', 'text', 'document_id']);
893
- });
894
- }));
895
- selectorCommand('stream', 'Live audio stream URL of a call in progress (HLS)')
896
- .action(run(async (cmdOpts) => {
897
- const opts = globalOpts();
898
- validateSelector(cmdOpts);
899
- const data = await apiGet({ path: `${BASE}/live/stream`, params: selectorParams(cmdOpts) });
900
- output(data, opts, () => {
901
- console.log(chalk.bold(`\n${eventLine(data.event)} · live stream\n`));
902
- formatKeyValue({
903
- status: data.status,
904
- url: data.url,
905
- expires_at: data.expires_at,
906
- call_t0: data.call_t0,
907
- segment_seconds: data.segment_seconds,
908
- });
909
- if (data.url)
910
- console.log(chalk.dim(`\nPlay with: ffplay "${data.url}" or vlc "${data.url}"`));
911
- });
912
- }));
913
435
  }
914
436
  //# sourceMappingURL=earnings.js.map