sentisense 0.50.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 +36 -7
- package/dist/cli.cjs +461 -59
- package/dist/index.cjs +37 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +126 -19
- package/dist/index.d.ts +126 -19
- package/dist/index.mjs +37 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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(
|
|
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
|
|
@@ -2079,10 +2382,10 @@ var Stocks = class {
|
|
|
2079
2382
|
* security. `disclaimer` carries the wording to display alongside a grade. Methodology:
|
|
2080
2383
|
* https://sentisense.ai/methodology/#sentisense-rating
|
|
2081
2384
|
*
|
|
2082
|
-
* **A discriminated union on `rated`.** `if (rating.rated)` narrows to `
|
|
2083
|
-
* `percentile`, `composite`, `ratedCount` and `methodologyVersion`; the
|
|
2084
|
-
* narrows to `reason`, `dimensionsPresent` and `presentDimensions`.
|
|
2085
|
-
* rather than testing a field for `undefined`.
|
|
2385
|
+
* **A discriminated union on `rated`.** `if (rating.rated)` narrows to `score`,
|
|
2386
|
+
* `letter`, `percentile`, `composite`, `ratedCount` and `methodologyVersion`; the
|
|
2387
|
+
* `else` branch narrows to `reason`, `dimensionsPresent` and `presentDimensions`.
|
|
2388
|
+
* Branch on the flag rather than testing a field for `undefined`.
|
|
2086
2389
|
*
|
|
2087
2390
|
* **Having no grade is a normal 200, not a 404.** ETFs and tickers outside the swept
|
|
2088
2391
|
* universe answer with `rated` false, and the composition still arrives so a card can
|
|
@@ -2092,11 +2395,22 @@ var Stocks = class {
|
|
|
2092
2395
|
*
|
|
2093
2396
|
* `dimensions` always holds all six rows in a fixed order, including the ones with no
|
|
2094
2397
|
* data, which arrive with `present` false and a `null` percentile. Read `present` first
|
|
2095
|
-
* and never read a missing percentile as zero.
|
|
2096
|
-
* derived from `percentile`, so read it instead of computing your own bucket edges.
|
|
2398
|
+
* and never read a missing percentile as zero.
|
|
2097
2399
|
*
|
|
2098
|
-
*
|
|
2099
|
-
*
|
|
2400
|
+
* **`score` and `percentile` are different numbers.** `percentile` is the rank of the
|
|
2401
|
+
* blended signals against the day's rated set, and
|
|
2402
|
+
* `score = percentile - sum(riskAdjustments.map((a) => a.points))`, floored at 10 when
|
|
2403
|
+
* fewer than five dimensions are available and at 0 otherwise. `letter` is the band
|
|
2404
|
+
* `score` falls in, at edges 90, 70, 30 and 10, while `bucketLetter` is the band the
|
|
2405
|
+
* percentile alone would fall in, so a difference between the two letters is exactly
|
|
2406
|
+
* what the conditions cost. `riskConditions` names the active ones, `riskAdjustments`
|
|
2407
|
+
* gives the points each cost (graded, up to 12 apiece), and `penaltyPoints` is their
|
|
2408
|
+
* sum. `letter` is served as stored, so read it instead of computing your own bucket
|
|
2409
|
+
* edges. The five fields arrive from the next API deploy onward and are optional, so a
|
|
2410
|
+
* response served before then still parses.
|
|
2411
|
+
*
|
|
2412
|
+
* For the daily history of a stock's score, ask `client.entityMetrics.getMetrics` for
|
|
2413
|
+
* the `sentisense_rating` metric.
|
|
2100
2414
|
*/
|
|
2101
2415
|
async getRating(ticker) {
|
|
2102
2416
|
return this.client.get(`/api/v1/rating/${encodeURIComponent(ticker.toUpperCase())}`);
|
|
@@ -3611,6 +3925,90 @@ var screenCommand = {
|
|
|
3611
3925
|
}
|
|
3612
3926
|
};
|
|
3613
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
|
+
|
|
3614
4012
|
// src/cli/commands/sentiment.ts
|
|
3615
4013
|
var DEFAULT_DAYS = 30;
|
|
3616
4014
|
function toneForDirection(value) {
|
|
@@ -3773,6 +4171,7 @@ var COMMANDS = [
|
|
|
3773
4171
|
sentimentCommand,
|
|
3774
4172
|
moodCommand,
|
|
3775
4173
|
analystsCommand,
|
|
4174
|
+
analystCommand,
|
|
3776
4175
|
earningsCommand,
|
|
3777
4176
|
insidersCommand,
|
|
3778
4177
|
insightsCommand,
|
|
@@ -3780,7 +4179,8 @@ var COMMANDS = [
|
|
|
3780
4179
|
newsCommand,
|
|
3781
4180
|
flowsCommand,
|
|
3782
4181
|
optionsCommand,
|
|
3783
|
-
screenCommand
|
|
4182
|
+
screenCommand,
|
|
4183
|
+
searchCommand
|
|
3784
4184
|
];
|
|
3785
4185
|
var COMMAND_NAMES = COMMANDS.map((command) => command.name);
|
|
3786
4186
|
function findCommand(name) {
|
|
@@ -3795,7 +4195,8 @@ var EXAMPLE = {
|
|
|
3795
4195
|
quote: "sentisense quote NVDA AAPL",
|
|
3796
4196
|
sentiment: "sentisense sentiment NVDA --days 30",
|
|
3797
4197
|
mood: "sentisense mood",
|
|
3798
|
-
analysts: "sentisense analysts NVDA",
|
|
4198
|
+
analysts: "sentisense analysts NVDA --coverage",
|
|
4199
|
+
analyst: "sentisense analyst dan-ives --calls",
|
|
3799
4200
|
earnings: "sentisense earnings --week next",
|
|
3800
4201
|
insiders: "sentisense insiders NVDA",
|
|
3801
4202
|
insights: "sentisense insights NVDA",
|
|
@@ -3803,7 +4204,8 @@ var EXAMPLE = {
|
|
|
3803
4204
|
news: "sentisense news NVDA",
|
|
3804
4205
|
flows: "sentisense flows NVDA",
|
|
3805
4206
|
options: "sentisense options NVDA",
|
|
3806
|
-
screen: "sentisense screen --filter SENTI_SCORE_7D:GTE:13"
|
|
4207
|
+
screen: "sentisense screen --filter SENTI_SCORE_7D:GTE:13",
|
|
4208
|
+
search: "sentisense search Tesla"
|
|
3807
4209
|
};
|
|
3808
4210
|
function pad(text, width) {
|
|
3809
4211
|
return text.length >= width ? text : text + " ".repeat(width - text.length);
|