sentisense 0.31.0 → 0.34.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
@@ -16,7 +16,7 @@ npm install sentisense
16
16
  ```typescript
17
17
  import SentiSense from "sentisense";
18
18
 
19
- const client = new SentiSense({ apiKey: "ss_live_YOUR_KEY" });
19
+ const client = new SentiSense({ apiKey: process.env.SENTISENSE_API_KEY });
20
20
 
21
21
  const price = await client.stocks.getPrice("AAPL");
22
22
  console.log(price.currentPrice);
@@ -40,8 +40,11 @@ Most methods resolve to the payload directly, but two families wrap it. The retu
40
40
  describe the wrapper, so `.data` / `.documents` type-check natively, no cast.
41
41
 
42
42
  **1. Tier-gated endpoints return a preview envelope.** The payload is in `data`, and
43
- `isPreview` tells you whether it was truncated for your tier. On a truncated response
44
- `totalCount` carries the untruncated size, so you can render "showing N of M".
43
+ `isPreview` tells you whether it was truncated for your tier. `totalCount` carries the
44
+ untruncated size whenever the server knows it: on a truncated response, so you can render
45
+ "showing N of M", and on a paged endpoint such as `politicians.getActivity`, where it is
46
+ the full match count on every tier including PRO. A missing `totalCount` means "count
47
+ `data` yourself", never "zero results".
45
48
 
46
49
  Affected: `institutional.getFlows` / `getHolders` / `getActivists`, and all five
47
50
  `insights` methods.
@@ -125,6 +128,98 @@ client.institutional.getHolders("AAPL", "2025-02-14")
125
128
  client.institutional.getActivists("2025-02-14")
126
129
  ```
127
130
 
131
+ #### Paging the holder list
132
+
133
+ A widely held ticker returns thousands of rows: a megacap quarter is roughly 6,000
134
+ holders and 1.5 MB on the wire. Pass `limit` unless you really want the whole list.
135
+ Omitting the options object sends the original unbounded request, so existing code
136
+ keeps working.
137
+
138
+ | Option | Values |
139
+ |--------|--------|
140
+ | `limit` | Maximum rows to return. Must be >= 1; values above 1000 are capped server-side. Omit for the full list. |
141
+ | `offset` | Row offset to start from. Server default is 0. Requires `limit`. |
142
+ | `sortBy` | `"shares"` (server default), `"valueUsd"`, or `"sharesChangePct"`. Requires `limit`. |
143
+ | `sortDir` | `"desc"` (server default) or `"asc"`. Requires `limit`. |
144
+
145
+ `limit` is the switch for the whole set. Send `offset`, `sortBy`, or `sortDir` without it
146
+ and the server ignores them, returning the full unsorted list with a 200 and no warning.
147
+
148
+ ```typescript
149
+ import SentiSense from "sentisense";
150
+
151
+ const client = new SentiSense({ apiKey: process.env.SENTISENSE_API_KEY });
152
+
153
+ // Top 10 holders by position value, largest first
154
+ const top = await client.institutional.getHolders("AAPL", "2026-03-31", {
155
+ limit: 10,
156
+ sortBy: "valueUsd",
157
+ sortDir: "desc",
158
+ });
159
+ for (const holder of top.data.holders) {
160
+ console.log(holder.filerName, holder.valueUsd);
161
+ }
162
+
163
+ // Walk the list a page at a time
164
+ const page = await client.institutional.getHolders("AAPL", "2026-03-31", {
165
+ limit: 100,
166
+ offset: 100,
167
+ });
168
+ console.log(`${page.data.holders.length} rows of ${page.data.holderCount}`);
169
+ ```
170
+
171
+ A response to a request carrying `limit` also has three fields the unbounded response does
172
+ not: `returnedCount` (rows on this page, smaller than your `limit` on the last one),
173
+ `offset` (echoed back), and `notableChanges`, a ticker-wide summary of the quarter's biggest
174
+ position moves so you do not have to scan every page to find them.
175
+
176
+ ```typescript
177
+ const page = await client.institutional.getHolders("AAPL", "2026-03-31", { limit: 100 });
178
+ console.log(`${page.data.returnedCount} of ${page.data.holderCount} holders`);
179
+ for (const mover of page.data.notableChanges?.top ?? []) {
180
+ console.log(mover.filerName, mover.changeType, mover.sharesChangePct);
181
+ }
182
+ ```
183
+
184
+ Each holder row also carries `entitySlug`, which you can hand straight to
185
+ `institutional.getInstitutionDetail()`, and `cikCount` when the row rolls up several SEC
186
+ filers under one manager. Both are null for filers we have not matched to an institution
187
+ page, so check before building a link.
188
+
189
+ ### Congressional Trading
190
+
191
+ ```typescript
192
+ client.politicians.getActivity({ lookbackDays: 90 }) // Market-wide STOCK Act feed
193
+ client.politicians.getFilings("NVDA") // Trades in one stock
194
+ client.politicians.getMembers() // Tracked members + trade stats
195
+ client.politicians.getMember("nancy-pelosi") // One member's profile and trades
196
+ ```
197
+
198
+ #### Paging the activity feed
199
+
200
+ A 90-day window is routinely well over a thousand disclosures, and without `limit` the
201
+ server returns the first 200 with nothing in the payload to say it stopped. `totalCount` on
202
+ the envelope is the real size on every tier, so size the walk from that rather than from
203
+ `data.length`.
204
+
205
+ | Option | Values |
206
+ |--------|--------|
207
+ | `lookbackDays` | Days to look back (1-365). Defaults to 90. |
208
+ | `limit` | Rows to return. Must be >= 1; anything above 500 is capped at 500. Omit for the default 200. |
209
+ | `offset` | Row offset to start from. Defaults to 0. Works with or without `limit`. |
210
+
211
+ ```typescript
212
+ const first = await client.politicians.getActivity({ limit: 100 });
213
+ console.log(`${first.data.length} of ${first.totalCount} disclosures`);
214
+
215
+ for (let offset = 100; offset < (first.totalCount ?? 0); offset += 100) {
216
+ const page = await client.politicians.getActivity({ limit: 100, offset });
217
+ for (const trade of page.data) {
218
+ console.log(trade.politicianName, trade.ticker, trade.transactionType);
219
+ }
220
+ }
221
+ ```
222
+
128
223
  ### Entity Metrics
129
224
 
130
225
  ```typescript
@@ -215,12 +310,16 @@ All errors extend `SentiSenseError` and include `status`, `code`, and `message`
215
310
 
216
311
  ```typescript
217
312
  const client = new SentiSense({
218
- apiKey: "ss_live_YOUR_KEY", // Get yours at app.sentisense.ai/settings/developer
219
- baseUrl: "https://...", // Default: https://app.sentisense.ai
220
- timeout: 30000, // Default: 30s (in milliseconds)
313
+ apiKey: process.env.SENTISENSE_API_KEY, // Get yours at app.sentisense.ai/settings/developer
314
+ baseUrl: "https://...", // Default: https://app.sentisense.ai
315
+ timeout: 30000, // Default: 30s (in milliseconds)
316
+ maxRetries: 3, // Default: 3
221
317
  });
222
318
  ```
223
319
 
320
+ Keep the key in the environment rather than in source. Committing a literal key leaks it
321
+ into git history and into every registry security scan that reads your repo.
322
+
224
323
  ## Get an API Key
225
324
 
226
325
  Generate your API key from the [Developer Console](https://app.sentisense.ai/settings/developer).
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  APIError: () => APIError,
24
24
  AuthenticationError: () => AuthenticationError,
25
+ DeepHistoryUnavailableError: () => DeepHistoryUnavailableError,
25
26
  NotFoundError: () => NotFoundError,
26
27
  RateLimitError: () => RateLimitError,
27
28
  SentiSense: () => SentiSense,
@@ -52,6 +53,13 @@ var NotFoundError = class extends SentiSenseError {
52
53
  this.name = "NotFoundError";
53
54
  }
54
55
  };
56
+ var DeepHistoryUnavailableError = class extends SentiSenseError {
57
+ constructor(message, retryAfter) {
58
+ super(message, 202);
59
+ this.name = "DeepHistoryUnavailableError";
60
+ this.retryAfter = retryAfter;
61
+ }
62
+ };
55
63
  var RateLimitError = class extends SentiSenseError {
56
64
  constructor(message, code, retryAfter) {
57
65
  super(message, 429, code);
@@ -185,7 +193,8 @@ var EntityMetrics = class {
185
193
  /**
186
194
  * Get time-series metric data for an entity using the v2 Serving Metrics API.
187
195
  *
188
- * @param symbol Ticker symbol (e.g. "AAPL") the backend resolves the entity.
196
+ * @param symbol Ticker symbol (e.g. "AAPL") or entity urlSlug (e.g. "Nancy-Pelosi",
197
+ * case-insensitive; discover slugs via stocks.getEntities()).
189
198
  * @param options Metric type and optional time range / resolution.
190
199
  */
191
200
  async getMetrics(symbol, options = {}) {
@@ -202,7 +211,7 @@ var EntityMetrics = class {
202
211
  /**
203
212
  * Get distribution data for a metric, broken down by a dimension (default: source).
204
213
  *
205
- * @param symbol Ticker symbol (e.g. "AAPL").
214
+ * @param symbol Ticker symbol (e.g. "AAPL") or entity urlSlug.
206
215
  * @param metricType The metric to break down (e.g. "mentions", "sentiment").
207
216
  * @param options Optional dimension parameter.
208
217
  */
@@ -313,6 +322,19 @@ var Politicians = class {
313
322
  *
314
323
  * PRO-gated. Free/unauthenticated users receive a preview (top 5 trades)
315
324
  * with `isPreview: true` in the response.
325
+ *
326
+ * The feed is longer than one response: a default 90-day window is routinely well over a
327
+ * thousand disclosures, and without `limit` the server sends the first 200 with no marker
328
+ * that it stopped. `totalCount` on the envelope is the real size on every tier, so page
329
+ * with `limit` and `offset` rather than reading `data.length` as the total.
330
+ *
331
+ * ```typescript
332
+ * const first = await client.politicians.getActivity({ limit: 100 });
333
+ * for (let offset = 100; offset < first.totalCount!; offset += 100) {
334
+ * const page = await client.politicians.getActivity({ limit: 100, offset });
335
+ * // ... page.data
336
+ * }
337
+ * ```
316
338
  */
317
339
  async getActivity(options) {
318
340
  return this.client.get("/api/v1/politicians/activity", options);
@@ -453,11 +475,20 @@ var Institutional = class {
453
475
  * are two levels down: `(await getHolders(t, d)).data.holders`, alongside ticker-level
454
476
  * totals like `holderCount`. Free callers get a truncated `holders` array with
455
477
  * `isPreview: true`.
478
+ *
479
+ * A widely held ticker returns thousands of rows: a megacap quarter is about
480
+ * 6,000 holders and 1.5 MB. Pass `limit` unless you really want all of them.
481
+ * Omitting `options` sends the original unbounded request.
482
+ *
483
+ * `limit` is the switch for the whole option set. With it, the response also carries
484
+ * `returnedCount`, `offset`, and a `notableChanges` summary, so you can walk the list
485
+ * without re-counting it. Without it, `offset` / `sortBy` / `sortDir` are ignored by the
486
+ * server and you get the full unsorted list back with a 200.
456
487
  */
457
- async getHolders(ticker, reportDate) {
488
+ async getHolders(ticker, reportDate, options) {
458
489
  return this.client.get(
459
490
  `/api/v1/institutional/holders/${encodeURIComponent(ticker)}`,
460
- { reportDate }
491
+ { reportDate, ...options }
461
492
  );
462
493
  }
463
494
  /**
@@ -597,6 +628,22 @@ var Stocks = class {
597
628
  async getProfile(ticker, options) {
598
629
  return this.client.get(`/api/v1/stocks/${encodeURIComponent(ticker)}/profile`, options);
599
630
  }
631
+ /**
632
+ * Get the headline sentiment picture for a stock in one call.
633
+ *
634
+ * Returns the SentiSense Score with its 30-day regime, mention volume and social
635
+ * dominance, per-source tone in `bySource`, plus related tickers, story drivers, a
636
+ * narrative and an FAQ. Available in full on every API-key tier.
637
+ *
638
+ * Use `entityMetrics.getMetrics(ticker, "sentiment", ...)` instead when you need a time
639
+ * series over a specific window rather than the headline read. Returns 404 for tickers
640
+ * with no sentiment coverage.
641
+ */
642
+ async getSentiment(ticker) {
643
+ return this.client.get(
644
+ `/api/v1/stocks/${encodeURIComponent(ticker)}/sentiment`
645
+ );
646
+ }
600
647
  /** Get related KB entities (people, products, partners). */
601
648
  async getEntities(ticker) {
602
649
  return this.client.get(`/api/v1/stocks/${encodeURIComponent(ticker)}/entities`);
@@ -630,7 +677,13 @@ var Stocks = class {
630
677
  async getMarketStatus() {
631
678
  return this.client.get("/api/v1/stocks/market-status");
632
679
  }
633
- /** Get financial statement data. */
680
+ /**
681
+ * Get financial statement data for one reporting period: income statement, balance sheet,
682
+ * and cash flow, including `capitalExpenditure` and `freeCashFlow`.
683
+ *
684
+ * Capital expenditure is signed as filed, so normally negative. See {@link Fundamentals}
685
+ * for the free-cash-flow relationship and when it is `null`.
686
+ */
634
687
  async getFundamentals(ticker, options) {
635
688
  return this.client.get("/api/v1/stocks/fundamentals", { ticker, ...options });
636
689
  }
@@ -689,8 +742,8 @@ var Stocks = class {
689
742
  return this.client.get("/api/v1/stocks/with-kpis");
690
743
  }
691
744
  /**
692
- * List the KPI metadata tuples available for a ticker `id, name, category,
693
- * chartType` without paying the cost of the full series payload. Mirrors
745
+ * List the KPI metadata tuples available for a ticker (`id, name, category,
746
+ * chartType`) without paying the cost of the full series payload. Mirrors
694
747
  * the `/api/v1/insights/stock/{ticker}/types` precedent.
695
748
  *
696
749
  * Auth: API key required, no quota cost. 404 if the ticker has no curated KPIs.
@@ -708,7 +761,7 @@ var Trackers = class {
708
761
  this.client = client;
709
762
  }
710
763
  /**
711
- * List every publicly-visible tracker id, display name, category,
764
+ * List every publicly-visible tracker: id, display name, category,
712
765
  * one-line description, and the methodology anchor to link out to.
713
766
  */
714
767
  async list() {
@@ -722,8 +775,8 @@ var Trackers = class {
722
775
  * `"choropleth"` they live at `data.geo[]`; etc. Dispatch on `viewType`
723
776
  * in your renderer.
724
777
  *
725
- * @param trackerId slug from {@link list}, e.g. `"institution-concentration"`.
726
- * @param params provider-specific query params (e.g. `{ scope: "us" }` for
778
+ * @param trackerId slug from {@link list}, e.g. `"institution-concentration"`.
779
+ * @param params provider-specific query params (e.g. `{ scope: "us" }` for
727
780
  * geographically-scoped trackers like hantavirus). Unknown keys are ignored.
728
781
  */
729
782
  async get(trackerId, params) {
@@ -732,7 +785,7 @@ var Trackers = class {
732
785
  };
733
786
 
734
787
  // src/version.ts
735
- var VERSION = "0.31.0";
788
+ var VERSION = "0.34.0";
736
789
 
737
790
  // src/client.ts
738
791
  var DEFAULT_BASE_URL = "https://app.sentisense.ai";
@@ -740,6 +793,19 @@ var DEFAULT_TIMEOUT = 3e4;
740
793
  var DEFAULT_MAX_RETRIES = 3;
741
794
  var BASE_DELAY_MS = 1e3;
742
795
  var MAX_DELAY_MS = 6e4;
796
+ var DEEP_HISTORY_FALLBACK_WAIT_S = 3;
797
+ var MAX_DEEP_HISTORY_WAIT_S = 30;
798
+ var MAX_RATE_LIMIT_WAIT_S = 120;
799
+ var RATE_LIMIT_FALLBACK_WAIT_S = 60;
800
+ function clampRetryAfter(raw, maxWaitS) {
801
+ if (!raw) return void 0;
802
+ const parsed = Number(raw);
803
+ if (!Number.isFinite(parsed)) return void 0;
804
+ return Math.min(Math.max(0.5, parsed), maxWaitS);
805
+ }
806
+ function retryAfterSeconds(raw, defaultS, maxWaitS) {
807
+ return clampRetryAfter(raw, maxWaitS) ?? defaultS;
808
+ }
743
809
  function sleep(ms) {
744
810
  return new Promise((resolve) => setTimeout(resolve, ms));
745
811
  }
@@ -790,12 +856,34 @@ var SentiSense = class {
790
856
  headers,
791
857
  signal: controller.signal
792
858
  });
859
+ if (response.status === 202) {
860
+ const waitSeconds = retryAfterSeconds(
861
+ response.headers.get("Retry-After"),
862
+ DEEP_HISTORY_FALLBACK_WAIT_S,
863
+ MAX_DEEP_HISTORY_WAIT_S
864
+ );
865
+ try {
866
+ await response.body?.cancel();
867
+ } catch {
868
+ }
869
+ if (attempt < this.maxRetries) {
870
+ delayMs = waitSeconds * 1e3;
871
+ continue;
872
+ }
873
+ throw new DeepHistoryUnavailableError(
874
+ "Deep history is still being assembled. Retry in a few seconds.",
875
+ waitSeconds
876
+ );
877
+ }
793
878
  if (!response.ok) {
794
879
  const isRetryable = response.status === 429 || response.status >= 500;
795
880
  if (isRetryable && attempt < this.maxRetries) {
796
881
  if (response.status === 429) {
797
- const ra = response.headers.get("Retry-After");
798
- delayMs = (ra ? parseInt(ra, 10) : 60) * 1e3;
882
+ delayMs = retryAfterSeconds(
883
+ response.headers.get("Retry-After"),
884
+ RATE_LIMIT_FALLBACK_WAIT_S,
885
+ MAX_RATE_LIMIT_WAIT_S
886
+ ) * 1e3;
799
887
  } else {
800
888
  delayMs = Math.min(BASE_DELAY_MS * Math.pow(2, attempt), MAX_DELAY_MS) + Math.random() * 1e3;
801
889
  }
@@ -886,8 +974,11 @@ var SentiSense = class {
886
974
  case 404:
887
975
  throw new NotFoundError(message, code);
888
976
  case 429: {
889
- const ra = response.headers.get("Retry-After");
890
- throw new RateLimitError(message, code, ra ? parseInt(ra, 10) : void 0);
977
+ const retryAfter = clampRetryAfter(
978
+ response.headers.get("Retry-After"),
979
+ MAX_RATE_LIMIT_WAIT_S
980
+ );
981
+ throw new RateLimitError(message, code, retryAfter);
891
982
  }
892
983
  default:
893
984
  throw new APIError(message, response.status, code);
@@ -898,6 +989,7 @@ var SentiSense = class {
898
989
  0 && (module.exports = {
899
990
  APIError,
900
991
  AuthenticationError,
992
+ DeepHistoryUnavailableError,
901
993
  NotFoundError,
902
994
  RateLimitError,
903
995
  SentiSense,