sentisense 0.51.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,7 +71,8 @@ npx -y sentisense@latest health
71
71
  | `quote <ticker>...` | Price, day range, 52-week range, market cap, P/E. One request per ticker |
72
72
  | `sentiment <ticker>` | SentiSense Score, tone, attention, per-source breakdown, `--days N` history |
73
73
  | `mood` | Composite market sentiment, the signals behind it, and the sector map |
74
- | `analysts <ticker>` | Consensus, price target band, recent upgrades and downgrades |
74
+ | `analysts <ticker>` | Consensus, price target band, recent upgrades and downgrades. `--coverage` for who covers it, by firm |
75
+ | `analyst <slug>` | One analyst: their firms, their coverage book, and `--calls` for their price target notes |
75
76
  | `earnings [ticker]` | Forward calendar with no ticker, per-quarter analysis with one (`earnings AAPL`) |
76
77
  | `insiders <ticker>` | Filed Form 4 transactions, including whether they were pre-planned |
77
78
  | `insights <ticker>` | Generated signals, filterable by `--urgency` and `--type` |
@@ -80,9 +81,22 @@ npx -y sentisense@latest health
80
81
  | `flows [ticker]` | Institutional 13F flows, or one ticker's holders and notable changes |
81
82
  | `options <ticker>` | End-of-day options positioning, IV rank, walls, unusual contracts |
82
83
  | `screen --filter ...` | Screen the universe on Score, analyst, technical, and price fields |
84
+ | `search <name>` | Resolve a name, alias, ticker or slug to a symbol and the entity handle |
83
85
 
84
86
  Run `sentisense help <command>` for its flags and examples.
85
87
 
88
+ Three of these chain into each other. Start from a name, land on a person:
89
+
90
+ ```bash
91
+ npx -y sentisense@latest search Tesla --type company # "Tesla" -> TSLA, plus the entity slug
92
+ npx -y sentisense@latest analysts NVDA --coverage # who covers it, by firm, with analyst slugs
93
+ npx -y sentisense@latest analyst quinn-bolton --calls # that analyst's firms, book, and notes
94
+ ```
95
+
96
+ `analyst` takes a slug, not a name: slugs are lowercase and hyphenated, and every named analyst in a `--coverage` row carries the one that addresses them. A name is rejected before a request is spent. What comes back is call history, not accuracy scoring: there is no hit rate, no ranking, and nothing in it rates the person.
97
+
98
+ Ranking on `search` is the API's own, and a company can sort below its own products, so pass `--type company` when what you want is the issuer.
99
+
86
100
  ### Output modes
87
101
 
88
102
  Readable in a terminal, plain text when piped, and exact API JSON on request:
@@ -474,8 +488,18 @@ All five are optional: a response served before they shipped omits them. For the
474
488
  ```typescript
475
489
  client.marketMood.get() // Composite market sentiment with sub-signals
476
490
  client.kb.getPopularEntities() // Most-tracked entities
491
+ client.kb.searchEntities("Tesla") // Resolve a name, alias, ticker or slug to what we track
477
492
  ```
478
493
 
494
+ Entity search is resolution, not enumeration: the query must be at least 2 characters, the match count is capped at 25, and it returns a bare `EntitySearchResult[]` rather than a `PreviewResponse` envelope. Each hit carries `name`, `type`, the `ticker` for a listed entity (`null` for everything else), and the `urlSlug` the metric endpoints address that entity by, which is the only way to get a handle for a person, product or topic with no ticker.
495
+
496
+ ```typescript
497
+ const hits = await client.kb.searchEntities("Tesla", { type: "company", limit: 5 });
498
+ const symbol = hits.find((hit) => hit.ticker)?.ticker; // "TSLA"
499
+ ```
500
+
501
+ An empty array is the normal answer for a query that matches nothing, so branch on `length` rather than catching.
502
+
479
503
  ### Screener
480
504
 
481
505
  Filter the tracked universe on the SentiSense Score, attention, analyst consensus, technicals and price in one query. Screening on analyst ratings alone is something a dozen free tools do; screening on analyst ratings *where the Score disagrees* is not.
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.52.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) {
@@ -1675,6 +1962,22 @@ var KB = class {
1675
1962
  async getPopularEntities() {
1676
1963
  return this.client.get("/api/v1/kb/entities/popular");
1677
1964
  }
1965
+ /**
1966
+ * Resolve a name, alias, ticker or slug to the entities we track, best match first.
1967
+ *
1968
+ * This is resolution, not enumeration: the query must be at least 2 characters and the
1969
+ * result count is capped, so it answers "which handle did the user mean" rather than
1970
+ * dumping the graph. Use it when someone typed "Tesla" and the rest of your code needs
1971
+ * `TSLA`, or when you need the `urlSlug` an entity's metric series is addressed by.
1972
+ *
1973
+ * Returns a bare array, not a `PreviewResponse` envelope, and an empty array is the
1974
+ * normal answer for a query that matches nothing.
1975
+ *
1976
+ * @param q What the user typed. At least 2 characters, or the API answers 400.
1977
+ */
1978
+ async searchEntities(q, options) {
1979
+ return this.client.get("/api/v1/kb/entities/search", { q, ...options });
1980
+ }
1678
1981
  };
1679
1982
 
1680
1983
  // src/resources/marketMood.ts
@@ -3622,6 +3925,90 @@ var screenCommand = {
3622
3925
  }
3623
3926
  };
3624
3927
 
3928
+ // src/cli/commands/search.ts
3929
+ var TYPES = ["person", "company", "product", "organization", "etf", "topic", "country"];
3930
+ function query(positionals) {
3931
+ const text = positionals.join(" ").trim();
3932
+ if (!text) {
3933
+ throw new CliUsageError("search needs something to look for.", "for example: sentisense search Tesla");
3934
+ }
3935
+ if (text.length < 2) {
3936
+ throw new CliUsageError(
3937
+ `search needs at least 2 characters, and got "${text}".`,
3938
+ "for example: sentisense search Tesla"
3939
+ );
3940
+ }
3941
+ return text;
3942
+ }
3943
+ var searchCommand = {
3944
+ name: "search",
3945
+ summary: "Resolve a name, alias, ticker or slug to the entities we track",
3946
+ usage: "sentisense search <name> [--type <kind>] [--limit N]",
3947
+ examples: [
3948
+ "sentisense search Tesla",
3949
+ "sentisense search 'Elon Musk' --type person",
3950
+ "sentisense search nvidia --limit 5",
3951
+ "sentisense search Tesla --json"
3952
+ ],
3953
+ notes: [
3954
+ "For when you have a name and the rest of your pipeline needs a symbol: search Tesla,",
3955
+ "get TSLA, then run quote or sentiment on it.",
3956
+ "The slug column is the handle the metric endpoints address an entity by, which is the",
3957
+ "only place to get it for a person, product or topic that has no ticker.",
3958
+ `--type narrows to one kind: ${TYPES.join(", ")}.`,
3959
+ "Ranking is the API's, and a company can sort below its own products, so reach for",
3960
+ "--type company when what you want is the issuer. A query that matches nothing exits 4."
3961
+ ],
3962
+ flags: {
3963
+ type: { type: "string", placeholder: "kind", describe: `Narrow to one kind: ${TYPES.join(", ")}` },
3964
+ limit: { type: "number", placeholder: "N", describe: "Matches to return, 1 to 25 (default 10)" }
3965
+ },
3966
+ async run({ args, client, full }) {
3967
+ const text = query(args.positionals);
3968
+ const type = typeof args.flags.type === "string" ? args.flags.type.toLowerCase() : void 0;
3969
+ if (type && !TYPES.includes(type)) {
3970
+ throw new CliUsageError(
3971
+ `--type takes one of ${TYPES.join(", ")}, got "${type}".`,
3972
+ "for example: --type company"
3973
+ );
3974
+ }
3975
+ const limit = typeof args.flags.limit === "number" ? args.flags.limit : void 0;
3976
+ const api = client();
3977
+ const results = await api.kb.searchEntities(text, {
3978
+ ...type ? { type } : {},
3979
+ ...limit === void 0 ? {} : { limit }
3980
+ });
3981
+ if (results.length === 0) {
3982
+ throw new NoMatchError(
3983
+ text,
3984
+ type ? `nothing of type ${type} matched. Try dropping --type, or fewer characters.` : "try fewer characters, another spelling, or the ticker itself."
3985
+ );
3986
+ }
3987
+ const blocks = [
3988
+ {
3989
+ kind: "head",
3990
+ title: field("query", text),
3991
+ right: fields(
3992
+ field("matches", String(results.length)),
3993
+ type === void 0 ? void 0 : field("type", type)
3994
+ )
3995
+ },
3996
+ { kind: "blank" },
3997
+ {
3998
+ kind: "table",
3999
+ head: ["SYMBOL", "NAME", "TYPE", "SLUG"],
4000
+ rows: results.map((hit) => [
4001
+ cell(hit.ticker ?? "", hit.ticker ? "accent" : void 0),
4002
+ cell(truncate(hit.name, full ? 60 : 34)),
4003
+ cell(hit.type ?? ""),
4004
+ cell(truncate(hit.urlSlug ?? "", full ? 60 : 34))
4005
+ ])
4006
+ }
4007
+ ];
4008
+ return { json: results, doc: doc(...blocks) };
4009
+ }
4010
+ };
4011
+
3625
4012
  // src/cli/commands/sentiment.ts
3626
4013
  var DEFAULT_DAYS = 30;
3627
4014
  function toneForDirection(value) {
@@ -3784,6 +4171,7 @@ var COMMANDS = [
3784
4171
  sentimentCommand,
3785
4172
  moodCommand,
3786
4173
  analystsCommand,
4174
+ analystCommand,
3787
4175
  earningsCommand,
3788
4176
  insidersCommand,
3789
4177
  insightsCommand,
@@ -3791,7 +4179,8 @@ var COMMANDS = [
3791
4179
  newsCommand,
3792
4180
  flowsCommand,
3793
4181
  optionsCommand,
3794
- screenCommand
4182
+ screenCommand,
4183
+ searchCommand
3795
4184
  ];
3796
4185
  var COMMAND_NAMES = COMMANDS.map((command) => command.name);
3797
4186
  function findCommand(name) {
@@ -3806,7 +4195,8 @@ var EXAMPLE = {
3806
4195
  quote: "sentisense quote NVDA AAPL",
3807
4196
  sentiment: "sentisense sentiment NVDA --days 30",
3808
4197
  mood: "sentisense mood",
3809
- analysts: "sentisense analysts NVDA",
4198
+ analysts: "sentisense analysts NVDA --coverage",
4199
+ analyst: "sentisense analyst dan-ives --calls",
3810
4200
  earnings: "sentisense earnings --week next",
3811
4201
  insiders: "sentisense insiders NVDA",
3812
4202
  insights: "sentisense insights NVDA",
@@ -3814,7 +4204,8 @@ var EXAMPLE = {
3814
4204
  news: "sentisense news NVDA",
3815
4205
  flows: "sentisense flows NVDA",
3816
4206
  options: "sentisense options NVDA",
3817
- screen: "sentisense screen --filter SENTI_SCORE_7D:GTE:13"
4207
+ screen: "sentisense screen --filter SENTI_SCORE_7D:GTE:13",
4208
+ search: "sentisense search Tesla"
3818
4209
  };
3819
4210
  function pad(text, width) {
3820
4211
  return text.length >= width ? text : text + " ".repeat(width - text.length);
package/dist/index.cjs CHANGED
@@ -710,6 +710,22 @@ var KB = class {
710
710
  async getPopularEntities() {
711
711
  return this.client.get("/api/v1/kb/entities/popular");
712
712
  }
713
+ /**
714
+ * Resolve a name, alias, ticker or slug to the entities we track, best match first.
715
+ *
716
+ * This is resolution, not enumeration: the query must be at least 2 characters and the
717
+ * result count is capped, so it answers "which handle did the user mean" rather than
718
+ * dumping the graph. Use it when someone typed "Tesla" and the rest of your code needs
719
+ * `TSLA`, or when you need the `urlSlug` an entity's metric series is addressed by.
720
+ *
721
+ * Returns a bare array, not a `PreviewResponse` envelope, and an empty array is the
722
+ * normal answer for a query that matches nothing.
723
+ *
724
+ * @param q What the user typed. At least 2 characters, or the API answers 400.
725
+ */
726
+ async searchEntities(q, options) {
727
+ return this.client.get("/api/v1/kb/entities/search", { q, ...options });
728
+ }
713
729
  };
714
730
 
715
731
  // src/resources/marketMood.ts
@@ -1226,7 +1242,7 @@ var Trackers = class {
1226
1242
  };
1227
1243
 
1228
1244
  // src/version.ts
1229
- var VERSION = "0.51.0";
1245
+ var VERSION = "0.52.0";
1230
1246
 
1231
1247
  // src/client.ts
1232
1248
  var DEFAULT_BASE_URL = "https://app.sentisense.ai";