trendsearch 0.0.1 → 0.1.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
@@ -11,17 +11,46 @@ Modern Google Trends SDK for Node.js and Bun, built with native `fetch`, strict
11
11
  - 🛡️ Built-in retry/backoff + rate limiting (`p-retry` + `p-queue`)
12
12
  - 🍪 Optional cookie persistence support
13
13
  - 🖥️ First-class `trendsearch` CLI for every endpoint
14
- - 🌐 Stable Google Trends API endpoints + experimental RPC/picker endpoints
14
+ - 🌐 Stable Google Trends API endpoints + experimental RPC/picker/CSV/top charts endpoints
15
15
  - 🧪 Deterministic fixture contracts + optional live endpoint tests
16
16
 
17
17
  ## 📦 Install
18
18
 
19
+ ### Install as a library dependency
20
+
19
21
  ```bash
20
22
  bun add trendsearch
21
- # or
22
23
  npm install trendsearch
24
+ pnpm add trendsearch
25
+ yarn add trendsearch
26
+ ```
27
+
28
+ ### Run the CLI without installing globally (always latest)
29
+
30
+ ```bash
31
+ npx trendsearch@latest --help
32
+ pnpm dlx trendsearch@latest --help
33
+ yarn dlx trendsearch@latest --help
34
+ bunx trendsearch@latest --help
35
+ ```
36
+
37
+ ### Install the CLI globally (`@latest`)
38
+
39
+ Use `@latest` so your global binary is always installed from the newest published version.
40
+
41
+ ```bash
42
+ npm install --global trendsearch@latest
43
+ pnpm add --global trendsearch@latest
44
+ bun add --global trendsearch@latest
45
+ yarn global add trendsearch@latest
23
46
  ```
24
47
 
48
+ For Yarn Berry/Modern, prefer `yarn dlx trendsearch@latest` (shown above) instead of global install.
49
+
50
+ ### Update an existing global install to latest
51
+
52
+ Re-run your package manager's global install command with `@latest`.
53
+
25
54
  ## ✅ Runtime Contract
26
55
 
27
56
  - 🟢 Node.js `>=20`
@@ -35,7 +64,9 @@ If you are in a CJS project, use dynamic import:
35
64
  const trendsearch = await import("trendsearch");
36
65
  ```
37
66
 
38
- ## 🚀 Quick Start
67
+ ## 🚀 Library Usage Guide
68
+
69
+ ### 1) Quick start with endpoint helpers
39
70
 
40
71
  ```ts
41
72
  import { interestOverTime } from "trendsearch";
@@ -49,17 +80,70 @@ const result = await interestOverTime({
49
80
  console.log(result.data.timeline.length);
50
81
  ```
51
82
 
52
- ## 🖥️ CLI
83
+ ### 2) Reuse shared defaults with `createClient`
84
+
85
+ ```ts
86
+ import { createClient } from "trendsearch";
87
+
88
+ const client = createClient({
89
+ hl: "en-US",
90
+ tz: 0,
91
+ timeoutMs: 15_000,
92
+ });
93
+
94
+ const result = await client.relatedQueries({
95
+ keywords: ["typescript"],
96
+ geo: "US",
97
+ });
98
+
99
+ console.log(result.data.top.length);
100
+ ```
101
+
102
+ ### 3) Use exported schemas/types when you need stricter boundaries
103
+
104
+ ```ts
105
+ import { schemas, type ExploreRequest } from "trendsearch";
106
+
107
+ const request: ExploreRequest = {
108
+ keywords: ["typescript", "javascript"],
109
+ geo: "US",
110
+ };
111
+
112
+ const validated = schemas.exploreRequestSchema.parse(request);
113
+ ```
114
+
115
+ ## 🖥️ CLI Usage Guide
53
116
 
54
117
  `trendsearch` ships with a production-ready CLI that wraps all stable and
55
118
  experimental endpoints.
56
119
 
120
+ ### 1) Start with help
121
+
122
+ ```bash
123
+ trendsearch --help
124
+ trendsearch explore --help
125
+ ```
126
+
127
+ ### 2) Run common endpoint commands
128
+
57
129
  ```bash
58
130
  trendsearch autocomplete typescript --output json
59
131
  trendsearch explore typescript --geo US --time "today 3-m" --output pretty
132
+ trendsearch interest-over-time typescript --geo US --time "today 3-m"
133
+ trendsearch related-queries typescript --geo US --time "today 12-m"
60
134
  trendsearch experimental trending-now --geo US --language en --hours 24
61
135
  ```
62
136
 
137
+ ### 3) Pass request payloads with `--input`
138
+
139
+ `--input` accepts inline JSON, a JSON file path, or `-` for stdin.
140
+
141
+ ```bash
142
+ trendsearch explore --input '{"keywords":["typescript"],"geo":"US"}' --output json
143
+ trendsearch explore --input ./requests/explore.json --output json
144
+ cat ./requests/explore.json | trendsearch explore --input - --output json
145
+ ```
146
+
63
147
  ### CLI Output Modes
64
148
 
65
149
  - `--output pretty` (human-friendly, default in TTY)
@@ -97,13 +181,17 @@ Error envelope (`json`/`jsonl`):
97
181
  }
98
182
  ```
99
183
 
100
- ### CLI Config / Wizard / Completion
184
+ ### 4) Persist defaults, use wizard, and generate completion
101
185
 
102
186
  ```bash
103
187
  trendsearch config set output json
188
+ trendsearch config set hl en-US
189
+ trendsearch config get output
104
190
  trendsearch config list
191
+ trendsearch config unset hl
192
+ trendsearch config reset
105
193
  trendsearch wizard
106
- trendsearch completion bash
194
+ trendsearch completion zsh
107
195
  ```
108
196
 
109
197
  Config precedence:
@@ -113,6 +201,7 @@ Config precedence:
113
201
  Supported env vars include:
114
202
 
115
203
  - `TRENDSEARCH_OUTPUT`
204
+ - `TRENDSEARCH_SPINNER`
116
205
  - `TRENDSEARCH_HL`
117
206
  - `TRENDSEARCH_TZ`
118
207
  - `TRENDSEARCH_BASE_URL`
@@ -123,6 +212,7 @@ Supported env vars include:
123
212
  - `TRENDSEARCH_MAX_CONCURRENT`
124
213
  - `TRENDSEARCH_MIN_DELAY_MS`
125
214
  - `TRENDSEARCH_USER_AGENT`
215
+ - `TRENDSEARCH_CONFIG_DIR` (override where persisted CLI config is stored)
126
216
 
127
217
  ## 🧭 API Surface
128
218
 
@@ -145,11 +235,54 @@ Supported env vars include:
145
235
  - `experimental.trendingArticles`
146
236
  - `experimental.geoPicker`
147
237
  - `experimental.categoryPicker`
238
+ - `experimental.topCharts`
239
+ - `experimental.interestOverTimeMultirange`
240
+ - `experimental.interestOverTimeCsv`
241
+ - `experimental.interestOverTimeMultirangeCsv`
242
+ - `experimental.interestByRegionCsv`
243
+ - `experimental.relatedQueriesCsv`
244
+ - `experimental.relatedTopicsCsv`
245
+ - `experimental.hotTrendsLegacy`
148
246
 
149
247
  ⚠️ Experimental endpoints are semver-minor unstable because Google can change internal RPC payloads.
150
248
 
151
249
  ℹ️ `dailyTrends` and `realTimeTrends` are kept for compatibility and may throw `EndpointUnavailableError` if Google retires those legacy routes.
152
250
 
251
+ ### Operational Caveats (Important)
252
+
253
+ - Internal/undocumented Google Trends routes can throttle aggressively (`HTTP 429`) even at low request volume.
254
+ - Some route families can intermittently fail with `HTTP 400`, `401`, `404`, or `410` depending on backend changes.
255
+ - For production use, keep concurrency low, cache aggressively, and use longer backoff windows.
256
+ - Prefer the official Google Trends API (alpha) when available for long-lived production integrations.
257
+ - Related data endpoints can validly return empty lists for some keywords/time windows.
258
+
259
+ ### Reducing `429` in Practice
260
+
261
+ `trendsearch` automatically retries on `429`, and when Google sends a `Retry-After` header,
262
+ the client respects that wait window before retrying.
263
+
264
+ Recommended profile for unstable/internal routes:
265
+
266
+ ```ts
267
+ import { MemoryCookieStore, createClient } from "trendsearch";
268
+
269
+ const client = createClient({
270
+ timeoutMs: 30_000,
271
+ retries: {
272
+ maxRetries: 5,
273
+ baseDelayMs: 2_500,
274
+ maxDelayMs: 45_000,
275
+ },
276
+ rateLimit: {
277
+ maxConcurrent: 1,
278
+ minDelayMs: 5_000,
279
+ },
280
+ userAgent:
281
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
282
+ cookieStore: new MemoryCookieStore(),
283
+ });
284
+ ```
285
+
153
286
  ## 🧰 Client Configuration
154
287
 
155
288
  Use `createClient` when you want shared runtime defaults and transport controls:
@@ -314,6 +447,7 @@ Output:
314
447
  - `data.top`
315
448
  - `data.rising`
316
449
  - `value` can be `number | string` (`"Breakout"`-style upstream values are preserved)
450
+ - `data.top` and `data.rising` may be empty arrays for some requests
317
451
 
318
452
  ### `relatedTopics`
319
453
 
@@ -333,6 +467,7 @@ Output:
333
467
 
334
468
  - `data.top`
335
469
  - `data.rising`
470
+ - `data.top` and `data.rising` may be empty arrays for some requests
336
471
 
337
472
  ### `dailyTrends`
338
473
 
@@ -429,6 +564,60 @@ const result = await experimental.categoryPicker({ hl: "en-US" });
429
564
  console.log(result.data.items);
430
565
  ```
431
566
 
567
+ ### `experimental.topCharts`
568
+
569
+ ```ts
570
+ import { experimental } from "trendsearch";
571
+
572
+ const result = await experimental.topCharts({
573
+ date: 2024,
574
+ geo: "GLOBAL",
575
+ });
576
+
577
+ console.log(result.data.charts);
578
+ console.log(result.data.items);
579
+ ```
580
+
581
+ ### `experimental.interestOverTimeMultirange`
582
+
583
+ ```ts
584
+ import { experimental } from "trendsearch";
585
+
586
+ const result = await experimental.interestOverTimeMultirange({
587
+ keywords: ["typescript"],
588
+ geo: "US",
589
+ time: "today 3-m",
590
+ });
591
+
592
+ console.log(result.data.timeline);
593
+ ```
594
+
595
+ ### CSV Variants (`experimental.*Csv`)
596
+
597
+ ```ts
598
+ import { experimental } from "trendsearch";
599
+
600
+ const result = await experimental.relatedQueriesCsv({
601
+ keywords: ["typescript"],
602
+ geo: "US",
603
+ });
604
+
605
+ console.log(result.data.csv);
606
+ ```
607
+
608
+ `experimental.interestOverTimeCsv`, `experimental.interestOverTimeMultirangeCsv`,
609
+ `experimental.interestByRegionCsv`, `experimental.relatedQueriesCsv`, and
610
+ `experimental.relatedTopicsCsv` currently return raw CSV text (`data.csv`) in v1.
611
+
612
+ ### `experimental.hotTrendsLegacy`
613
+
614
+ ```ts
615
+ import { experimental } from "trendsearch";
616
+
617
+ const result = await experimental.hotTrendsLegacy();
618
+ console.log(result.data.payload);
619
+ ```
620
+
432
621
  ## 🧾 Schemas and Types
433
622
 
434
623
  All request/response schemas and inferred types are exported:
@@ -474,6 +663,7 @@ try {
474
663
  } catch (error) {
475
664
  if (error instanceof RateLimitError) {
476
665
  console.error("Rate limited:", error.status);
666
+ console.error("Retry after ms:", error.retryAfterMs);
477
667
  }
478
668
 
479
669
  if (error instanceof SchemaValidationError) {
@@ -497,6 +687,11 @@ bun run test:all
497
687
  TRENDSEARCH_LIVE=1 bun run test:live
498
688
  ```
499
689
 
690
+ Live test notes:
691
+
692
+ - The live suite is intentionally slower (conservative pacing) to reduce `429`.
693
+ - Experimental routes are best-effort and can be unavailable depending on backend state.
694
+
500
695
  Package checks:
501
696
 
502
697
  ```bash
package/dist/cli.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as TrendSearchClient, n as CreateClientConfig } from "./public-types-B6pmbT6Z.mjs";
1
+ import { a as TrendSearchClient, n as CreateClientConfig } from "./public-types-CroPK1QN.mjs";
2
2
  import { Command } from "commander";
3
3
 
4
4
  //#region src/cli/output.d.ts
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as UnexpectedResponseError, c as RateLimitError, l as EndpointUnavailableError, n as schemas, o as TransportError, r as pickerRequestSchema, s as SchemaValidationError, t as createClient } from "./create-client-B1euQ5Of.mjs";
1
+ import { a as UnexpectedResponseError, c as RateLimitError, l as EndpointUnavailableError, n as schemas, o as TransportError, r as pickerRequestSchema, s as SchemaValidationError, t as createClient } from "./create-client-BQ8bZmFn.mjs";
2
2
  import { Command, CommanderError, Option } from "commander";
3
3
  import Conf from "conf";
4
4
  import { existsSync } from "node:fs";
@@ -177,7 +177,8 @@ const fromUnknownError = (error) => {
177
177
  message: error.message,
178
178
  details: {
179
179
  status: error.status,
180
- url: error.url
180
+ url: error.url,
181
+ retryAfterMs: error.retryAfterMs
181
182
  },
182
183
  exitCode: EXIT_CODES.rateLimited
183
184
  };
@@ -448,6 +449,17 @@ const readNumber = (value) => {
448
449
  const parsed = Number(value);
449
450
  return Number.isFinite(parsed) ? parsed : void 0;
450
451
  };
452
+ const readDateLike = (value) => {
453
+ if (typeof value === "number" && Number.isFinite(value)) return value;
454
+ if (typeof value !== "string") return;
455
+ const trimmed = value.trim();
456
+ if (trimmed.length === 0) return;
457
+ if (/^\d+$/.test(trimmed)) {
458
+ const parsed = Number(trimmed);
459
+ if (Number.isFinite(parsed)) return parsed;
460
+ }
461
+ return trimmed;
462
+ };
451
463
  const asRequest = (value) => value;
452
464
  const readString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
453
465
  const withGeo = (values) => {
@@ -735,6 +747,141 @@ const endpointManifest = [
735
747
  }),
736
748
  invoke: (client, request, debug) => client.trendingArticles(asRequest(request), debug)
737
749
  },
750
+ {
751
+ id: "experimental.topCharts",
752
+ path: ["experimental", "top-charts"],
753
+ summary: "Fetch experimental top charts data.",
754
+ requestSchema: schemas.topChartsRequestSchema,
755
+ options: [
756
+ {
757
+ key: "geo",
758
+ flags: "--geo <geo>",
759
+ description: "Geo code.",
760
+ type: "string"
761
+ },
762
+ {
763
+ key: "date",
764
+ flags: "--date <date>",
765
+ description: "Date or year.",
766
+ type: "string"
767
+ },
768
+ {
769
+ key: "isMobile",
770
+ flags: "--is-mobile",
771
+ description: "Enable mobile mode.",
772
+ type: "boolean"
773
+ }
774
+ ],
775
+ args: [],
776
+ buildRequest: (ctx) => ({
777
+ geo: readString(ctx.options.geo),
778
+ date: readDateLike(ctx.options.date),
779
+ isMobile: ctx.options.isMobile === true ? true : void 0
780
+ }),
781
+ invoke: (client, request, debug) => client.experimental.topCharts(asRequest(request), debug)
782
+ },
783
+ {
784
+ id: "experimental.interestOverTimeMultirange",
785
+ path: ["experimental", "interest-over-time-multirange"],
786
+ summary: "Fetch experimental interest over time (multirange).",
787
+ requestSchema: schemas.interestOverTimeMultirangeRequestSchema,
788
+ options: exploreLikeOptions,
789
+ args: [{
790
+ label: "[keywords...]",
791
+ description: "Keyword list."
792
+ }],
793
+ buildRequest: buildExploreLikeRequest,
794
+ invoke: (client, request, debug) => client.experimental.interestOverTimeMultirange(asRequest(request), debug)
795
+ },
796
+ {
797
+ id: "experimental.interestOverTimeCsv",
798
+ path: ["experimental", "interest-over-time-csv"],
799
+ summary: "Fetch experimental interest over time CSV.",
800
+ requestSchema: schemas.interestOverTimeRequestSchema,
801
+ options: exploreLikeOptions,
802
+ args: [{
803
+ label: "[keywords...]",
804
+ description: "Keyword list."
805
+ }],
806
+ buildRequest: buildExploreLikeRequest,
807
+ invoke: (client, request, debug) => client.experimental.interestOverTimeCsv(asRequest(request), debug)
808
+ },
809
+ {
810
+ id: "experimental.interestOverTimeMultirangeCsv",
811
+ path: ["experimental", "interest-over-time-multirange-csv"],
812
+ summary: "Fetch experimental interest over time multirange CSV.",
813
+ requestSchema: schemas.interestOverTimeMultirangeRequestSchema,
814
+ options: exploreLikeOptions,
815
+ args: [{
816
+ label: "[keywords...]",
817
+ description: "Keyword list."
818
+ }],
819
+ buildRequest: buildExploreLikeRequest,
820
+ invoke: (client, request, debug) => client.experimental.interestOverTimeMultirangeCsv(asRequest(request), debug)
821
+ },
822
+ {
823
+ id: "experimental.interestByRegionCsv",
824
+ path: ["experimental", "interest-by-region-csv"],
825
+ summary: "Fetch experimental interest by region CSV.",
826
+ requestSchema: schemas.interestByRegionRequestSchema,
827
+ options: [...exploreLikeOptions, {
828
+ key: "resolution",
829
+ flags: "--resolution <resolution>",
830
+ description: "Geo resolution (COUNTRY, REGION, CITY, DMA).",
831
+ type: "string",
832
+ choices: [
833
+ "COUNTRY",
834
+ "REGION",
835
+ "CITY",
836
+ "DMA"
837
+ ]
838
+ }],
839
+ args: [{
840
+ label: "[keywords...]",
841
+ description: "Keyword list."
842
+ }],
843
+ buildRequest: (ctx) => ({
844
+ ...buildExploreLikeRequest(ctx),
845
+ resolution: readString(ctx.options.resolution)
846
+ }),
847
+ invoke: (client, request, debug) => client.experimental.interestByRegionCsv(asRequest(request), debug)
848
+ },
849
+ {
850
+ id: "experimental.relatedQueriesCsv",
851
+ path: ["experimental", "related-queries-csv"],
852
+ summary: "Fetch experimental related queries CSV.",
853
+ requestSchema: schemas.relatedQueriesRequestSchema,
854
+ options: exploreLikeOptions,
855
+ args: [{
856
+ label: "[keywords...]",
857
+ description: "Keyword list."
858
+ }],
859
+ buildRequest: buildExploreLikeRequest,
860
+ invoke: (client, request, debug) => client.experimental.relatedQueriesCsv(asRequest(request), debug)
861
+ },
862
+ {
863
+ id: "experimental.relatedTopicsCsv",
864
+ path: ["experimental", "related-topics-csv"],
865
+ summary: "Fetch experimental related topics CSV.",
866
+ requestSchema: schemas.relatedTopicsRequestSchema,
867
+ options: exploreLikeOptions,
868
+ args: [{
869
+ label: "[keywords...]",
870
+ description: "Keyword list."
871
+ }],
872
+ buildRequest: buildExploreLikeRequest,
873
+ invoke: (client, request, debug) => client.experimental.relatedTopicsCsv(asRequest(request), debug)
874
+ },
875
+ {
876
+ id: "experimental.hotTrendsLegacy",
877
+ path: ["experimental", "hot-trends-legacy"],
878
+ summary: "Fetch legacy hot trends endpoint payload.",
879
+ requestSchema: schemas.hotTrendsLegacyRequestSchema,
880
+ options: [],
881
+ args: [],
882
+ buildRequest: () => ({}),
883
+ invoke: (client, request, debug) => client.experimental.hotTrendsLegacy(asRequest(request), debug)
884
+ },
738
885
  {
739
886
  id: "experimental.trendingNow",
740
887
  path: ["experimental", "trending-now"],