sentisense 0.51.0 → 0.53.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/dist/cli.cjs CHANGED
@@ -77,6 +77,14 @@ var UnknownTickerError = class extends Error {
77
77
  this.ticker = ticker;
78
78
  }
79
79
  };
80
+ var NoMatchError = class extends Error {
81
+ constructor(query2, hint) {
82
+ super(`nothing matches "${query2}".`);
83
+ this.name = "NoMatchError";
84
+ this.query = query2;
85
+ this.hint = hint;
86
+ }
87
+ };
80
88
  var MissingKeyError = class extends Error {
81
89
  constructor() {
82
90
  super("no API key configured.");
@@ -109,6 +117,12 @@ function classify(error, command) {
109
117
  ]
110
118
  };
111
119
  }
120
+ if (error instanceof NoMatchError) {
121
+ return {
122
+ exitCode: EXIT.NOT_FOUND,
123
+ lines: [`error: ${error.message}`, `next: ${error.hint}`]
124
+ };
125
+ }
112
126
  if (error instanceof CliUsageError) {
113
127
  return {
114
128
  exitCode: EXIT.USAGE,
@@ -184,48 +198,6 @@ function classify(error, command) {
184
198
  };
185
199
  }
186
200
 
187
- // src/cli/ticker.ts
188
- function rejectSurplus(args, name, allowed) {
189
- if (args.positionals.length <= allowed) return;
190
- throw new CliUsageError(
191
- `${name} takes one ticker, and got ${args.positionals.length}.`,
192
- "run it once per ticker. Only quote accepts more than one."
193
- );
194
- }
195
- function oneTicker(args, name) {
196
- rejectSurplus(args, name, 1);
197
- const ticker = args.positionals[0];
198
- if (!ticker) {
199
- throw new CliUsageError(
200
- `${name} needs a ticker.`,
201
- `for example: sentisense ${name} NVDA`
202
- );
203
- }
204
- return ticker.toUpperCase();
205
- }
206
- function optionalTicker(args, name) {
207
- rejectSurplus(args, name, 1);
208
- return args.positionals[0]?.toUpperCase();
209
- }
210
- function rejectPositionals(args, name) {
211
- if (args.positionals.length === 0) return;
212
- throw new CliUsageError(
213
- `${name} takes no ticker, and got ${args.positionals.length}.`,
214
- `run "sentisense help ${name}" for what it accepts.`
215
- );
216
- }
217
- async function verifyTickerOnEmpty(api, ticker) {
218
- try {
219
- await api.stocks.getQuote(ticker);
220
- return void 0;
221
- } catch (error) {
222
- if (error instanceof NotFoundError) throw new UnknownTickerError(ticker);
223
- return `could not verify ${ticker}, so the empty result is unconfirmed`;
224
- }
225
- }
226
- var EMPTY_VERIFY_NOTE = "An empty result verifies the ticker before reporting no data, so a typo exits 4 rather";
227
- var EMPTY_VERIFY_NOTE_2 = "than looking like a company with nothing to report.";
228
-
229
201
  // src/cli/render/doc.ts
230
202
  function cell(text, tone) {
231
203
  return tone ? { text, tone } : { text };
@@ -322,34 +294,264 @@ function truncate(text, max) {
322
294
  return `${text.slice(0, Math.max(0, max - 3))}...`;
323
295
  }
324
296
 
297
+ // src/cli/commands/analyst.ts
298
+ var DEFAULT_CALLS = 25;
299
+ var NOT_A_SCORECARD = "Call history, not accuracy scoring: no hit rate, no ranking, and nothing here rates the person.";
300
+ function oneSlug(positionals) {
301
+ if (positionals.length > 1) {
302
+ throw new CliUsageError(
303
+ `analyst takes one slug, and got ${positionals.length}.`,
304
+ "run it once per analyst."
305
+ );
306
+ }
307
+ const raw = positionals[0];
308
+ if (!raw) {
309
+ throw new CliUsageError(
310
+ "analyst needs a slug.",
311
+ 'for example: sentisense analyst dan-ives. Slugs come from "sentisense analysts <ticker> --coverage".'
312
+ );
313
+ }
314
+ if (/\s/.test(raw)) {
315
+ throw new CliUsageError(
316
+ `analyst takes a slug, not a name, and got "${raw}".`,
317
+ 'slugs are lowercase and hyphenated, such as dan-ives. Every named analyst in "sentisense analysts <ticker> --coverage" carries one.'
318
+ );
319
+ }
320
+ return raw.toLowerCase();
321
+ }
322
+ var analystCommand = {
323
+ name: "analyst",
324
+ summary: "One analyst: the firms they publish under, what they cover, and their calls",
325
+ usage: "sentisense analyst <slug> [--calls] [--limit N]",
326
+ examples: [
327
+ "sentisense analyst dan-ives",
328
+ "sentisense analyst dan-ives --calls",
329
+ "sentisense analyst dan-ives --calls --limit 50 --full",
330
+ "sentisense analyst dan-ives --json"
331
+ ],
332
+ notes: [
333
+ "Takes a slug, not a name. Slugs are lowercase and hyphenated, and every named analyst",
334
+ 'in "sentisense analysts <ticker> --coverage" carries the one that addresses them here.',
335
+ "--calls appends their price target notes, newest first.",
336
+ "firstSeen and lastSeen bound the notes on record, not employment: the most recent firm",
337
+ "is where they last published, not necessarily where they work today.",
338
+ NOT_A_SCORECARD,
339
+ "A slug that matches no analyst exits 4."
340
+ ],
341
+ flags: {
342
+ calls: { type: "boolean", describe: "Append their price target notes, newest first" },
343
+ limit: { type: "number", placeholder: "N", describe: `Calls to request, 1 to 200 (default ${DEFAULT_CALLS})` }
344
+ },
345
+ async run({ args, client, full }) {
346
+ const slug = oneSlug(args.positionals);
347
+ const wantCalls = args.flags.calls === true;
348
+ const limit = typeof args.flags.limit === "number" ? args.flags.limit : void 0;
349
+ const api = client();
350
+ const profileEnvelope = await api.analyst.profile(slug);
351
+ const callsEnvelope = wantCalls ? await api.analyst.calls(slug, limit === void 0 ? void 0 : { limit }) : null;
352
+ const data = profileEnvelope.data;
353
+ const blocks = [
354
+ {
355
+ kind: "head",
356
+ title: field("analyst", data?.name ?? slug),
357
+ subtitle: field("slug", data?.slug ?? slug),
358
+ right: fields(
359
+ field("notes", String(data?.noteCount ?? 0)),
360
+ field("tickers", String(data?.tickerCount ?? 0))
361
+ )
362
+ },
363
+ {
364
+ kind: "facts",
365
+ items: fields(
366
+ field("Most recent firm", data?.mostRecentFirm ?? "n/a"),
367
+ field("First seen", data?.firstSeen ?? "n/a"),
368
+ field("Last seen", data?.lastSeen ?? "n/a")
369
+ )
370
+ }
371
+ ];
372
+ const firms = data?.firms ?? [];
373
+ if (firms.length > 0) {
374
+ blocks.push({ kind: "blank" });
375
+ blocks.push({
376
+ kind: "table",
377
+ head: ["FIRM", "FIRST NOTE", "LAST NOTE", "CURRENT"],
378
+ rows: firms.map((firm) => [
379
+ cell(truncate(firm.firm, full ? 40 : 28)),
380
+ cell(firm.firstSeen),
381
+ cell(firm.lastSeen),
382
+ cell(firm.mostRecent ? "yes" : "")
383
+ ])
384
+ });
385
+ }
386
+ const book = data?.coverage ?? [];
387
+ if (book.length > 0) {
388
+ const shown = full ? book : book.slice(0, 10);
389
+ blocks.push({ kind: "blank" });
390
+ blocks.push({
391
+ kind: "table",
392
+ head: ["TICKER", "NOTES", "LAST NOTE", "TARGET", "FIRM"],
393
+ align: ["left", "right", "left", "right", "left"],
394
+ rows: shown.map((entry) => [
395
+ cell(entry.ticker),
396
+ cell(String(entry.noteCount)),
397
+ cell(entry.lastNote ?? ""),
398
+ cell(entry.latestPriceTarget === null ? "" : money(entry.latestPriceTarget)),
399
+ cell(truncate(entry.latestFirm ?? "", full ? 40 : 24))
400
+ ])
401
+ });
402
+ const total = profileEnvelope.totalCount ?? data?.tickerCount ?? book.length;
403
+ if (shown.length < total) {
404
+ blocks.push({
405
+ kind: "text",
406
+ text: `Showing ${shown.length} of ${total} covered tickers. Add --full for the rest.`,
407
+ tone: "dim"
408
+ });
409
+ }
410
+ }
411
+ if (callsEnvelope) {
412
+ const calls = callsEnvelope.data ?? [];
413
+ blocks.push({ kind: "blank" });
414
+ if (calls.length === 0) {
415
+ blocks.push({ kind: "text", text: "No price target notes on record for this analyst." });
416
+ } else {
417
+ const shown = full ? calls : calls.slice(0, 15);
418
+ blocks.push({
419
+ kind: "table",
420
+ head: ["DATE", "TICKER", "FIRM", "TARGET", "PRICE THEN", "PUBLISHER"],
421
+ align: ["left", "left", "left", "right", "right", "left"],
422
+ rows: shown.map((call) => [
423
+ cell(call.publishedDate),
424
+ cell(call.ticker),
425
+ cell(truncate(call.firm, full ? 40 : 22)),
426
+ cell(call.priceTarget === null ? "" : money(call.priceTarget)),
427
+ cell(call.priceWhenPosted === null ? "" : money(call.priceWhenPosted)),
428
+ cell(truncate(call.newsPublisher ?? "", full ? 40 : 20))
429
+ ])
430
+ });
431
+ const total = callsEnvelope.totalCount ?? calls.length;
432
+ if (shown.length < total) {
433
+ blocks.push({
434
+ kind: "text",
435
+ text: `Showing ${shown.length} of ${total} calls. Add --full, or --limit to ask for more.`,
436
+ tone: "dim"
437
+ });
438
+ }
439
+ }
440
+ if (callsEnvelope.isPreview) {
441
+ blocks.push({
442
+ kind: "text",
443
+ text: "Preview response: a free key reads the first 25 calls, a PRO key pages the whole history.",
444
+ tone: "dim"
445
+ });
446
+ }
447
+ }
448
+ if (profileEnvelope.isPreview) {
449
+ blocks.push({
450
+ kind: "text",
451
+ text: "Preview response: the coverage book is trimmed to the 5 most recent tickers on a free key.",
452
+ tone: "dim"
453
+ });
454
+ }
455
+ blocks.push({ kind: "text", text: NOT_A_SCORECARD, tone: "dim" });
456
+ return {
457
+ json: callsEnvelope ? { profile: profileEnvelope, calls: callsEnvelope } : profileEnvelope,
458
+ doc: doc(...blocks)
459
+ };
460
+ }
461
+ };
462
+
463
+ // src/cli/ticker.ts
464
+ function rejectSurplus(args, name, allowed) {
465
+ if (args.positionals.length <= allowed) return;
466
+ throw new CliUsageError(
467
+ `${name} takes one ticker, and got ${args.positionals.length}.`,
468
+ "run it once per ticker. Only quote accepts more than one."
469
+ );
470
+ }
471
+ function oneTicker(args, name) {
472
+ rejectSurplus(args, name, 1);
473
+ const ticker = args.positionals[0];
474
+ if (!ticker) {
475
+ throw new CliUsageError(
476
+ `${name} needs a ticker.`,
477
+ `for example: sentisense ${name} NVDA`
478
+ );
479
+ }
480
+ return ticker.toUpperCase();
481
+ }
482
+ function optionalTicker(args, name) {
483
+ rejectSurplus(args, name, 1);
484
+ return args.positionals[0]?.toUpperCase();
485
+ }
486
+ function rejectPositionals(args, name) {
487
+ if (args.positionals.length === 0) return;
488
+ throw new CliUsageError(
489
+ `${name} takes no ticker, and got ${args.positionals.length}.`,
490
+ `run "sentisense help ${name}" for what it accepts.`
491
+ );
492
+ }
493
+ async function verifyTickerOnEmpty(api, ticker) {
494
+ try {
495
+ await api.stocks.getQuote(ticker);
496
+ return void 0;
497
+ } catch (error) {
498
+ if (error instanceof NotFoundError) throw new UnknownTickerError(ticker);
499
+ return `could not verify ${ticker}, so the empty result is unconfirmed`;
500
+ }
501
+ }
502
+ var EMPTY_VERIFY_NOTE = "An empty result verifies the ticker before reporting no data, so a typo exits 4 rather";
503
+ var EMPTY_VERIFY_NOTE_2 = "than looking like a company with nothing to report.";
504
+
325
505
  // src/cli/commands/analysts.ts
326
506
  var DEFAULT_ACTIONS = 5;
507
+ var DEFAULT_COVERAGE_ROWS = 15;
327
508
  function actionTone(actionType) {
328
509
  if (actionType === "UPGRADE") return "up";
329
510
  if (actionType === "DOWNGRADE") return "down";
330
511
  return void 0;
331
512
  }
513
+ function primaryAnalyst(row) {
514
+ const named = row.latestNote?.analyst;
515
+ const match = (named ? row.analysts.find((analyst) => analyst.name === named) : void 0) ?? row.analysts[0];
516
+ const name = match?.name ?? named ?? "";
517
+ const extra = row.analysts.length > 1 ? ` +${row.analysts.length - 1}` : "";
518
+ return { name: name ? `${name}${extra}` : "", slug: match?.slug ?? "" };
519
+ }
332
520
  var analystsCommand = {
333
521
  name: "analysts",
334
522
  summary: "Wall Street consensus, price target band, and recent rating changes",
335
- usage: "sentisense analysts <ticker> [--days N]",
523
+ usage: "sentisense analysts <ticker> [--coverage] [--days N] [--limit N]",
336
524
  examples: [
337
525
  "sentisense analysts NVDA",
338
526
  "sentisense analysts NVDA --days 180 --full",
527
+ "sentisense analysts NVDA --coverage",
528
+ "sentisense analysts NVDA --coverage --days 180 --limit 30",
339
529
  "sentisense analysts NVDA --json"
340
530
  ],
341
531
  notes: [
342
532
  "The price target band and analyst count come back in full on every key.",
343
533
  "The buy / hold / sell distribution and the longer action history are PRO.",
344
- "A ticker with no analyst coverage exits 4 rather than returning an empty band."
534
+ "A ticker with no analyst coverage exits 4 rather than returning an empty band.",
535
+ "--coverage answers who covers the ticker instead: one row per firm, the analyst that",
536
+ "firm last published under, their latest target, and the firm's current rating. The slug",
537
+ 'on a row is what "sentisense analyst <slug>" takes.',
538
+ "A firm can cover a ticker on a rating action alone, so a blank target is a real reading",
539
+ "rather than missing data, and a blank analyst means the note named nobody.",
540
+ "--limit trims the rows printed under --coverage; the API returns the whole book either",
541
+ "way, and --full prints all of it."
345
542
  ],
346
543
  flags: {
347
- days: { type: "number", placeholder: "N", describe: "Days of rating history (default 90)" }
544
+ coverage: { type: "boolean", describe: "Show who covers the ticker, one row per firm" },
545
+ days: { type: "number", placeholder: "N", describe: "Days of rating history (default 90)" },
546
+ limit: { type: "number", placeholder: "N", describe: `Firm rows to print with --coverage (default ${DEFAULT_COVERAGE_ROWS})` }
348
547
  },
349
548
  async run({ args, client, full }) {
350
549
  const ticker = oneTicker(args, "analysts");
351
550
  const lookbackDays = typeof args.flags.days === "number" ? args.flags.days : void 0;
352
551
  const api = client();
552
+ if (args.flags.coverage === true) {
553
+ return coverageRun({ api, ticker, lookbackDays, full, args });
554
+ }
353
555
  const notes = [];
354
556
  const [consensus, actions] = await Promise.all([
355
557
  api.analyst.consensus(ticker),
@@ -428,6 +630,91 @@ var analystsCommand = {
428
630
  return { json: { consensus, actions }, doc: doc(...blocks), notes };
429
631
  }
430
632
  };
633
+ async function coverageRun({ api, ticker, lookbackDays, full, args }) {
634
+ const envelope = await api.analyst.coverage(
635
+ ticker,
636
+ lookbackDays === void 0 ? void 0 : { lookbackDays }
637
+ );
638
+ const data = envelope.data;
639
+ const rows = data?.coverage ?? [];
640
+ const limit = typeof args.flags.limit === "number" ? args.flags.limit : DEFAULT_COVERAGE_ROWS;
641
+ const shown = full ? rows : rows.slice(0, Math.max(1, limit));
642
+ const blocks = [
643
+ {
644
+ kind: "head",
645
+ title: field("ticker", ticker),
646
+ right: fields(
647
+ field("firms", String(data?.firmCount ?? rows.length)),
648
+ field("named analysts", String(data?.namedAnalystCount ?? 0)),
649
+ data?.windowDays === void 0 ? void 0 : field("window", `${data.windowDays}d`)
650
+ )
651
+ }
652
+ ];
653
+ const buckets = data?.ratingBuckets;
654
+ if (buckets) {
655
+ blocks.push({
656
+ kind: "facts",
657
+ items: fields(
658
+ field("Buy", String(buckets.buy), buckets.buy > 0 ? "up" : void 0),
659
+ field("Hold", String(buckets.hold)),
660
+ field("Sell", String(buckets.sell), buckets.sell > 0 ? "down" : void 0),
661
+ field("Unrated", String(buckets.unrated)),
662
+ field("Total", String(buckets.total))
663
+ )
664
+ });
665
+ }
666
+ blocks.push({
667
+ kind: "facts",
668
+ items: fields(
669
+ field("Notes", String(data?.noteCount ?? 0)),
670
+ field("Attributed", String(data?.attributedNoteCount ?? 0)),
671
+ field("Unattributed", String(data?.unattributedNoteCount ?? 0)),
672
+ field("Rating only", String(data?.ratingOnlyFirmCount ?? 0)),
673
+ data?.asOf === void 0 ? void 0 : field("As of", data.asOf)
674
+ )
675
+ });
676
+ if (shown.length === 0) {
677
+ blocks.push({ kind: "text", text: "No firm published a target or a rating in this window." });
678
+ } else {
679
+ blocks.push({ kind: "blank" });
680
+ blocks.push({
681
+ kind: "table",
682
+ head: ["FIRM", "ANALYST", "SLUG", "TARGET", "DATE", "RATING", "ACTION"],
683
+ align: ["left", "left", "left", "right", "left", "left", "left"],
684
+ rows: shown.map((row) => {
685
+ const analyst = primaryAnalyst(row);
686
+ return [
687
+ cell(truncate(row.firm, full ? 40 : 24)),
688
+ cell(truncate(analyst.name, full ? 32 : 20)),
689
+ cell(truncate(analyst.slug, full ? 32 : 20)),
690
+ cell(row.latestNote?.priceTarget === null || row.latestNote?.priceTarget === void 0 ? "" : money(row.latestNote.priceTarget)),
691
+ cell(row.latestNote?.publishedDate ?? row.firmRating?.date ?? ""),
692
+ cell(row.firmRating?.rating ?? ""),
693
+ cell(row.firmRating?.actionType ?? "", actionTone(row.firmRating?.actionType))
694
+ ];
695
+ })
696
+ });
697
+ const total = data?.firmCount ?? rows.length;
698
+ if (shown.length < total) {
699
+ blocks.push({
700
+ kind: "text",
701
+ text: `Showing ${shown.length} of ${total} firms. Add --full for the rest.`,
702
+ tone: "dim"
703
+ });
704
+ }
705
+ }
706
+ if (data?.attributionNote) {
707
+ blocks.push({ kind: "text", text: data.attributionNote, tone: "dim" });
708
+ }
709
+ if (envelope.isPreview) {
710
+ blocks.push({
711
+ kind: "text",
712
+ text: "Preview response: the counts describe the whole window, the rows are trimmed on a free key.",
713
+ tone: "dim"
714
+ });
715
+ }
716
+ return { json: envelope, doc: doc(...blocks) };
717
+ }
431
718
 
432
719
  // src/cli/config.ts
433
720
  var import_node_fs = require("fs");
@@ -1037,7 +1324,7 @@ var flowsCommand = {
1037
1324
  };
1038
1325
 
1039
1326
  // src/version.ts
1040
- var VERSION = "0.51.0";
1327
+ var VERSION = "0.53.0";
1041
1328
 
1042
1329
  // src/resources/analyst.ts
1043
1330
  var Analyst = class {
@@ -1206,8 +1493,8 @@ var Documents = class {
1206
1493
  );
1207
1494
  }
1208
1495
  /** Smart search with natural language query parsing. The rows are in `documents`. */
1209
- async search(query, options) {
1210
- return this.client.get("/api/v1/documents/search", { query, ...options });
1496
+ async search(query2, options) {
1497
+ return this.client.get("/api/v1/documents/search", { query: query2, ...options });
1211
1498
  }
1212
1499
  /** Get latest document metrics from a source type. The rows are in `documents`. */
1213
1500
  async getBySource(source, options) {
@@ -1220,7 +1507,14 @@ var Documents = class {
1220
1507
  async getStories(options) {
1221
1508
  return this.client.get("/api/v1/documents/stories", options);
1222
1509
  }
1223
- /** Get full story detail by cluster ID. */
1510
+ /**
1511
+ * Get full story detail by cluster ID.
1512
+ *
1513
+ * Deliberately untyped: narrow it yourself. The response carries `storySource` and
1514
+ * `isLive` alongside the story body, plus a `timeline` array of dated updates,
1515
+ * newest first and empty when the story has none. {@link StoryTimelineEntry} is
1516
+ * exported for that array.
1517
+ */
1224
1518
  async getStoryDetail(clusterId) {
1225
1519
  return this.client.get(`/api/v1/documents/stories/${encodeURIComponent(clusterId)}`);
1226
1520
  }
@@ -1675,6 +1969,22 @@ var KB = class {
1675
1969
  async getPopularEntities() {
1676
1970
  return this.client.get("/api/v1/kb/entities/popular");
1677
1971
  }
1972
+ /**
1973
+ * Resolve a name, alias, ticker or slug to the entities we track, best match first.
1974
+ *
1975
+ * This is resolution, not enumeration: the query must be at least 2 characters and the
1976
+ * result count is capped, so it answers "which handle did the user mean" rather than
1977
+ * dumping the graph. Use it when someone typed "Tesla" and the rest of your code needs
1978
+ * `TSLA`, or when you need the `urlSlug` an entity's metric series is addressed by.
1979
+ *
1980
+ * Returns a bare array, not a `PreviewResponse` envelope, and an empty array is the
1981
+ * normal answer for a query that matches nothing.
1982
+ *
1983
+ * @param q What the user typed. At least 2 characters, or the API answers 400.
1984
+ */
1985
+ async searchEntities(q, options) {
1986
+ return this.client.get("/api/v1/kb/entities/search", { q, ...options });
1987
+ }
1678
1988
  };
1679
1989
 
1680
1990
  // src/resources/marketMood.ts
@@ -3622,6 +3932,90 @@ var screenCommand = {
3622
3932
  }
3623
3933
  };
3624
3934
 
3935
+ // src/cli/commands/search.ts
3936
+ var TYPES = ["person", "company", "product", "organization", "etf", "topic", "country"];
3937
+ function query(positionals) {
3938
+ const text = positionals.join(" ").trim();
3939
+ if (!text) {
3940
+ throw new CliUsageError("search needs something to look for.", "for example: sentisense search Tesla");
3941
+ }
3942
+ if (text.length < 2) {
3943
+ throw new CliUsageError(
3944
+ `search needs at least 2 characters, and got "${text}".`,
3945
+ "for example: sentisense search Tesla"
3946
+ );
3947
+ }
3948
+ return text;
3949
+ }
3950
+ var searchCommand = {
3951
+ name: "search",
3952
+ summary: "Resolve a name, alias, ticker or slug to the entities we track",
3953
+ usage: "sentisense search <name> [--type <kind>] [--limit N]",
3954
+ examples: [
3955
+ "sentisense search Tesla",
3956
+ "sentisense search 'Elon Musk' --type person",
3957
+ "sentisense search nvidia --limit 5",
3958
+ "sentisense search Tesla --json"
3959
+ ],
3960
+ notes: [
3961
+ "For when you have a name and the rest of your pipeline needs a symbol: search Tesla,",
3962
+ "get TSLA, then run quote or sentiment on it.",
3963
+ "The slug column is the handle the metric endpoints address an entity by, which is the",
3964
+ "only place to get it for a person, product or topic that has no ticker.",
3965
+ `--type narrows to one kind: ${TYPES.join(", ")}.`,
3966
+ "Ranking is the API's, and a company can sort below its own products, so reach for",
3967
+ "--type company when what you want is the issuer. A query that matches nothing exits 4."
3968
+ ],
3969
+ flags: {
3970
+ type: { type: "string", placeholder: "kind", describe: `Narrow to one kind: ${TYPES.join(", ")}` },
3971
+ limit: { type: "number", placeholder: "N", describe: "Matches to return, 1 to 25 (default 10)" }
3972
+ },
3973
+ async run({ args, client, full }) {
3974
+ const text = query(args.positionals);
3975
+ const type = typeof args.flags.type === "string" ? args.flags.type.toLowerCase() : void 0;
3976
+ if (type && !TYPES.includes(type)) {
3977
+ throw new CliUsageError(
3978
+ `--type takes one of ${TYPES.join(", ")}, got "${type}".`,
3979
+ "for example: --type company"
3980
+ );
3981
+ }
3982
+ const limit = typeof args.flags.limit === "number" ? args.flags.limit : void 0;
3983
+ const api = client();
3984
+ const results = await api.kb.searchEntities(text, {
3985
+ ...type ? { type } : {},
3986
+ ...limit === void 0 ? {} : { limit }
3987
+ });
3988
+ if (results.length === 0) {
3989
+ throw new NoMatchError(
3990
+ text,
3991
+ type ? `nothing of type ${type} matched. Try dropping --type, or fewer characters.` : "try fewer characters, another spelling, or the ticker itself."
3992
+ );
3993
+ }
3994
+ const blocks = [
3995
+ {
3996
+ kind: "head",
3997
+ title: field("query", text),
3998
+ right: fields(
3999
+ field("matches", String(results.length)),
4000
+ type === void 0 ? void 0 : field("type", type)
4001
+ )
4002
+ },
4003
+ { kind: "blank" },
4004
+ {
4005
+ kind: "table",
4006
+ head: ["SYMBOL", "NAME", "TYPE", "SLUG"],
4007
+ rows: results.map((hit) => [
4008
+ cell(hit.ticker ?? "", hit.ticker ? "accent" : void 0),
4009
+ cell(truncate(hit.name, full ? 60 : 34)),
4010
+ cell(hit.type ?? ""),
4011
+ cell(truncate(hit.urlSlug ?? "", full ? 60 : 34))
4012
+ ])
4013
+ }
4014
+ ];
4015
+ return { json: results, doc: doc(...blocks) };
4016
+ }
4017
+ };
4018
+
3625
4019
  // src/cli/commands/sentiment.ts
3626
4020
  var DEFAULT_DAYS = 30;
3627
4021
  function toneForDirection(value) {
@@ -3784,6 +4178,7 @@ var COMMANDS = [
3784
4178
  sentimentCommand,
3785
4179
  moodCommand,
3786
4180
  analystsCommand,
4181
+ analystCommand,
3787
4182
  earningsCommand,
3788
4183
  insidersCommand,
3789
4184
  insightsCommand,
@@ -3791,7 +4186,8 @@ var COMMANDS = [
3791
4186
  newsCommand,
3792
4187
  flowsCommand,
3793
4188
  optionsCommand,
3794
- screenCommand
4189
+ screenCommand,
4190
+ searchCommand
3795
4191
  ];
3796
4192
  var COMMAND_NAMES = COMMANDS.map((command) => command.name);
3797
4193
  function findCommand(name) {
@@ -3806,7 +4202,8 @@ var EXAMPLE = {
3806
4202
  quote: "sentisense quote NVDA AAPL",
3807
4203
  sentiment: "sentisense sentiment NVDA --days 30",
3808
4204
  mood: "sentisense mood",
3809
- analysts: "sentisense analysts NVDA",
4205
+ analysts: "sentisense analysts NVDA --coverage",
4206
+ analyst: "sentisense analyst dan-ives --calls",
3810
4207
  earnings: "sentisense earnings --week next",
3811
4208
  insiders: "sentisense insiders NVDA",
3812
4209
  insights: "sentisense insights NVDA",
@@ -3814,7 +4211,8 @@ var EXAMPLE = {
3814
4211
  news: "sentisense news NVDA",
3815
4212
  flows: "sentisense flows NVDA",
3816
4213
  options: "sentisense options NVDA",
3817
- screen: "sentisense screen --filter SENTI_SCORE_7D:GTE:13"
4214
+ screen: "sentisense screen --filter SENTI_SCORE_7D:GTE:13",
4215
+ search: "sentisense search Tesla"
3818
4216
  };
3819
4217
  function pad(text, width) {
3820
4218
  return text.length >= width ? text : text + " ".repeat(width - text.length);
package/dist/index.cjs CHANGED
@@ -255,7 +255,14 @@ var Documents = class {
255
255
  async getStories(options) {
256
256
  return this.client.get("/api/v1/documents/stories", options);
257
257
  }
258
- /** Get full story detail by cluster ID. */
258
+ /**
259
+ * Get full story detail by cluster ID.
260
+ *
261
+ * Deliberately untyped: narrow it yourself. The response carries `storySource` and
262
+ * `isLive` alongside the story body, plus a `timeline` array of dated updates,
263
+ * newest first and empty when the story has none. {@link StoryTimelineEntry} is
264
+ * exported for that array.
265
+ */
259
266
  async getStoryDetail(clusterId) {
260
267
  return this.client.get(`/api/v1/documents/stories/${encodeURIComponent(clusterId)}`);
261
268
  }
@@ -710,6 +717,22 @@ var KB = class {
710
717
  async getPopularEntities() {
711
718
  return this.client.get("/api/v1/kb/entities/popular");
712
719
  }
720
+ /**
721
+ * Resolve a name, alias, ticker or slug to the entities we track, best match first.
722
+ *
723
+ * This is resolution, not enumeration: the query must be at least 2 characters and the
724
+ * result count is capped, so it answers "which handle did the user mean" rather than
725
+ * dumping the graph. Use it when someone typed "Tesla" and the rest of your code needs
726
+ * `TSLA`, or when you need the `urlSlug` an entity's metric series is addressed by.
727
+ *
728
+ * Returns a bare array, not a `PreviewResponse` envelope, and an empty array is the
729
+ * normal answer for a query that matches nothing.
730
+ *
731
+ * @param q What the user typed. At least 2 characters, or the API answers 400.
732
+ */
733
+ async searchEntities(q, options) {
734
+ return this.client.get("/api/v1/kb/entities/search", { q, ...options });
735
+ }
713
736
  };
714
737
 
715
738
  // src/resources/marketMood.ts
@@ -1226,7 +1249,7 @@ var Trackers = class {
1226
1249
  };
1227
1250
 
1228
1251
  // src/version.ts
1229
- var VERSION = "0.51.0";
1252
+ var VERSION = "0.53.0";
1230
1253
 
1231
1254
  // src/client.ts
1232
1255
  var DEFAULT_BASE_URL = "https://app.sentisense.ai";