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/README.md +46 -1
- package/dist/cli.cjs +450 -52
- package/dist/index.cjs +25 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +78 -3
- package/dist/index.d.ts +78 -3
- package/dist/index.mjs +25 -2
- 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.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(
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
1252
|
+
var VERSION = "0.53.0";
|
|
1230
1253
|
|
|
1231
1254
|
// src/client.ts
|
|
1232
1255
|
var DEFAULT_BASE_URL = "https://app.sentisense.ai";
|