@biggora/claude-plugins 1.1.1 → 1.2.2
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/.claude/settings.local.json +3 -1
- package/README.md +24 -17
- package/package.json +1 -1
- package/registry/registry.json +319 -244
- package/specs/coding.md +24 -0
- package/specs/pod.md +2 -0
- package/src/skills/captcha/README.md +221 -0
- package/src/skills/captcha/SKILL.md +355 -0
- package/src/skills/captcha/references/captcha-types.md +254 -0
- package/src/skills/captcha/references/services.md +172 -0
- package/src/skills/captcha/references/stealth.md +238 -0
- package/src/skills/captcha/scripts/solve_captcha.py +323 -0
- package/src/skills/captcha/scripts/solve_image_grid.py +350 -0
- package/src/skills/codex-cli/SKILL.md +21 -11
- package/src/skills/gemini-cli/SKILL.md +27 -13
- package/src/skills/gemini-cli/references/commands.md +21 -14
- package/src/skills/gemini-cli/references/configuration.md +23 -18
- package/src/skills/gemini-cli/references/headless-and-scripting.md +7 -17
- package/src/skills/gemini-cli/references/mcp-and-extensions.md +12 -6
- package/src/skills/google-merchant-api/SKILL.md +581 -0
- package/src/skills/google-merchant-api/references/accounts.md +247 -0
- package/src/skills/google-merchant-api/references/content-api-legacy.md +216 -0
- package/src/skills/google-merchant-api/references/datasources.md +233 -0
- package/src/skills/google-merchant-api/references/inventories.md +201 -0
- package/src/skills/google-merchant-api/references/migration.md +267 -0
- package/src/skills/google-merchant-api/references/products.md +316 -0
- package/src/skills/google-merchant-api/references/promotions.md +201 -0
- package/src/skills/google-merchant-api/references/reports.md +240 -0
- package/src/skills/lv-aggregators-api/SKILL.md +113 -0
- package/src/skills/lv-aggregators-api/references/integration-guide.md +368 -0
- package/src/skills/lv-aggregators-api/references/kurpirkt.md +103 -0
- package/src/skills/lv-aggregators-api/references/salidzini.md +122 -0
- package/src/skills/notebook-lm/SKILL.md +1 -1
- package/src/skills/screen-recording/SKILL.md +243 -213
- package/src/skills/screen-recording/references/design-patterns.md +4 -2
- package/src/skills/screen-recording/references/ffmpeg-recording.md +473 -0
- package/src/skills/screen-recording/references/{approach1-programmatic.md → programmatic-generation.md} +45 -22
- package/src/skills/screen-recording/references/python-fallback.md +222 -0
- package/src/skills/tailwindcss-best-practices/SKILL.md +180 -0
- package/src/skills/tailwindcss-best-practices/references/best-practices-utility-patterns.md +87 -0
- package/src/skills/tailwindcss-best-practices/references/core-installation.md +109 -0
- package/src/skills/tailwindcss-best-practices/references/core-preflight.md +200 -0
- package/src/skills/tailwindcss-best-practices/references/core-responsive.md +163 -0
- package/src/skills/tailwindcss-best-practices/references/core-source-detection.md +114 -0
- package/src/skills/tailwindcss-best-practices/references/core-theme.md +108 -0
- package/src/skills/tailwindcss-best-practices/references/core-utility-classes.md +59 -0
- package/src/skills/tailwindcss-best-practices/references/core-variants.md +204 -0
- package/src/skills/tailwindcss-best-practices/references/effects-form-controls.md +76 -0
- package/src/skills/tailwindcss-best-practices/references/effects-mask.md +91 -0
- package/src/skills/tailwindcss-best-practices/references/effects-scroll-snap.md +59 -0
- package/src/skills/tailwindcss-best-practices/references/effects-text-shadow.md +78 -0
- package/src/skills/tailwindcss-best-practices/references/effects-transition-animation.md +80 -0
- package/src/skills/tailwindcss-best-practices/references/effects-visibility-interactivity.md +82 -0
- package/src/skills/tailwindcss-best-practices/references/features-content-detection.md +175 -0
- package/src/skills/tailwindcss-best-practices/references/features-custom-styles.md +203 -0
- package/src/skills/tailwindcss-best-practices/references/features-dark-mode.md +137 -0
- package/src/skills/tailwindcss-best-practices/references/features-functions-directives.md +241 -0
- package/src/skills/tailwindcss-best-practices/references/features-upgrade.md +160 -0
- package/src/skills/tailwindcss-best-practices/references/layout-aspect-ratio.md +39 -0
- package/src/skills/tailwindcss-best-practices/references/layout-columns.md +80 -0
- package/src/skills/tailwindcss-best-practices/references/layout-display.md +110 -0
- package/src/skills/tailwindcss-best-practices/references/layout-flexbox.md +112 -0
- package/src/skills/tailwindcss-best-practices/references/layout-grid.md +87 -0
- package/src/skills/tailwindcss-best-practices/references/layout-height.md +97 -0
- package/src/skills/tailwindcss-best-practices/references/layout-inset.md +103 -0
- package/src/skills/tailwindcss-best-practices/references/layout-logical-properties.md +92 -0
- package/src/skills/tailwindcss-best-practices/references/layout-margin.md +126 -0
- package/src/skills/tailwindcss-best-practices/references/layout-min-max-sizing.md +63 -0
- package/src/skills/tailwindcss-best-practices/references/layout-object-fit-position.md +64 -0
- package/src/skills/tailwindcss-best-practices/references/layout-overflow.md +57 -0
- package/src/skills/tailwindcss-best-practices/references/layout-padding.md +77 -0
- package/src/skills/tailwindcss-best-practices/references/layout-position.md +85 -0
- package/src/skills/tailwindcss-best-practices/references/layout-tables.md +67 -0
- package/src/skills/tailwindcss-best-practices/references/layout-width.md +102 -0
- package/src/skills/tailwindcss-best-practices/references/transform-base.md +68 -0
- package/src/skills/tailwindcss-best-practices/references/transform-rotate.md +70 -0
- package/src/skills/tailwindcss-best-practices/references/transform-scale.md +83 -0
- package/src/skills/tailwindcss-best-practices/references/transform-skew.md +62 -0
- package/src/skills/tailwindcss-best-practices/references/transform-translate.md +77 -0
- package/src/skills/tailwindcss-best-practices/references/typography-font-text.md +142 -0
- package/src/skills/tailwindcss-best-practices/references/typography-list-style.md +65 -0
- package/src/skills/tailwindcss-best-practices/references/typography-text-align.md +60 -0
- package/src/skills/tailwindcss-best-practices/references/visual-background.md +76 -0
- package/src/skills/tailwindcss-best-practices/references/visual-border.md +108 -0
- package/src/skills/tailwindcss-best-practices/references/visual-effects.md +111 -0
- package/src/skills/tailwindcss-best-practices/references/visual-svg.md +82 -0
- package/src/skills/test-mobile-app/SKILL.md +11 -6
- package/src/skills/test-mobile-app/scripts/analyze_apk.py +15 -4
- package/src/skills/test-mobile-app/scripts/check_environment.py +5 -5
- package/src/skills/test-mobile-app/scripts/run_tests.py +1 -1
- package/src/skills/test-web-ui/SKILL.md +264 -84
- package/src/skills/test-web-ui/scripts/discover.py +25 -12
- package/src/skills/test-web-ui/scripts/run_tests.py +3 -2
- package/src/skills/tm-search/SKILL.md +242 -106
- package/src/skills/tm-search/references/scraping-fallback.md +60 -95
- package/src/skills/tm-search/scripts/tm_search.py +453 -375
- package/src/skills/vite-best-practices/SKILL.md +115 -0
- package/src/skills/vite-best-practices/references/build-and-ssr.md +255 -0
- package/src/skills/vite-best-practices/references/core-config.md +231 -0
- package/src/skills/vite-best-practices/references/core-features.md +222 -0
- package/src/skills/vite-best-practices/references/core-plugin-api.md +294 -0
- package/src/skills/vite-best-practices/references/environment-api.md +108 -0
- package/src/skills/vite-best-practices/references/rolldown-migration.md +242 -0
- package/src/skills/screen-recording/references/approach2-xvfb.md +0 -232
|
@@ -1,375 +1,453 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
tm-search: US Trademark Search CLI Tool
|
|
4
|
-
Searches USPTO trademark database via tmsearch.uspto.gov and
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
python tm_search.py
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
DISCLAIMER
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
)
|
|
231
|
-
"""
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
tm-search: US Trademark Search CLI Tool
|
|
4
|
+
Searches USPTO trademark database via tmsearch.uspto.gov TSDR API and Playwright.
|
|
5
|
+
|
|
6
|
+
The keyword search uses Playwright browser automation because the tmsearch.uspto.gov
|
|
7
|
+
search backend is protected by AWS WAF and cannot be accessed via plain HTTP requests.
|
|
8
|
+
The TSDR details API (case lookup by serial number) works directly via HTTP.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python tm_search.py keyword <word> [--rows=25] [--json]
|
|
12
|
+
python tm_search.py available <word>
|
|
13
|
+
python tm_search.py status <serial_number> [--json]
|
|
14
|
+
python tm_search.py batch <word1,word2,...> [--csv]
|
|
15
|
+
python tm_search.py validate <file.txt> [--output=results.csv]
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
import json
|
|
20
|
+
import time
|
|
21
|
+
import csv
|
|
22
|
+
import argparse
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import requests
|
|
27
|
+
except ImportError:
|
|
28
|
+
print("Error: 'requests' library required. Run: pip install requests")
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
# ─── Constants ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
TSDR_DETAILS_URL = "https://tmsearch.uspto.gov/tsdr-api-v1-0-0/tsdr-api"
|
|
34
|
+
|
|
35
|
+
HEADERS = {
|
|
36
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (compatible; tm-search/1.0)",
|
|
37
|
+
"Accept": "application/json",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
DISCLAIMER = (
|
|
41
|
+
"\n[!] DISCLAIMER: This is a preliminary search only. Trademark availability depends on many\n"
|
|
42
|
+
" factors. Consult a licensed trademark attorney before filing.\n"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# ─── TSDR Details API (works without auth) ─────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
def get_status_by_serial(serial_number: str) -> dict:
|
|
48
|
+
"""
|
|
49
|
+
Get trademark case status by serial number via the TSDR Details API.
|
|
50
|
+
This endpoint works without authentication and returns JSON.
|
|
51
|
+
"""
|
|
52
|
+
sn = "".join(filter(str.isdigit, serial_number))
|
|
53
|
+
if len(sn) != 8:
|
|
54
|
+
return {"error": f"Serial number must be 8 digits, got {len(sn)}", "serialNumber": sn}
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
resp = requests.get(
|
|
58
|
+
TSDR_DETAILS_URL,
|
|
59
|
+
params={"serialNumber": sn},
|
|
60
|
+
headers=HEADERS,
|
|
61
|
+
timeout=30,
|
|
62
|
+
)
|
|
63
|
+
resp.raise_for_status()
|
|
64
|
+
data = resp.json()
|
|
65
|
+
|
|
66
|
+
metadata = data.get("metadata", {})
|
|
67
|
+
owners = metadata.get("owners", [])
|
|
68
|
+
owner_name = owners[0]["ipInfo"]["name"] if owners else None
|
|
69
|
+
tm5 = metadata.get("tm5Status", {})
|
|
70
|
+
classes = [c.get("classNumber") for c in metadata.get("classes", [])]
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"serialNumber": sn,
|
|
74
|
+
"status": metadata.get("caseStatus"),
|
|
75
|
+
"statusDate": metadata.get("statusDate"),
|
|
76
|
+
"liveDead": tm5.get("tm5LiveDead"),
|
|
77
|
+
"statusDescriptor": tm5.get("tm5StatusDescriptor"),
|
|
78
|
+
"owner": owner_name,
|
|
79
|
+
"attorney": (metadata.get("attorney", {}).get("ipInfo", {}).get("name")),
|
|
80
|
+
"classes": classes,
|
|
81
|
+
"isStandardChar": metadata.get("markDetails", {}).get("isStandardCharClaimed"),
|
|
82
|
+
}
|
|
83
|
+
except requests.exceptions.RequestException as e:
|
|
84
|
+
return {"error": str(e), "serialNumber": sn}
|
|
85
|
+
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
|
86
|
+
return {"error": f"Failed to parse response: {e}", "serialNumber": sn}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ─── Keyword Search via Playwright ─────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
def search_trademark_playwright(
|
|
92
|
+
keyword: str,
|
|
93
|
+
max_results: int = 25,
|
|
94
|
+
retries: int = 2,
|
|
95
|
+
) -> dict:
|
|
96
|
+
"""
|
|
97
|
+
Search USPTO trademark database by keyword using Playwright browser automation.
|
|
98
|
+
The tmsearch.uspto.gov Elasticsearch backend requires AWS WAF challenge tokens,
|
|
99
|
+
so we drive the actual web UI and intercept the API responses.
|
|
100
|
+
|
|
101
|
+
The AWS WAF bot detection is non-deterministic — sometimes it blocks headless
|
|
102
|
+
browsers, sometimes it doesn't. We use anti-detection settings and retries
|
|
103
|
+
to improve reliability.
|
|
104
|
+
|
|
105
|
+
Requires: pip install playwright && playwright install chromium
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
from playwright.sync_api import sync_playwright
|
|
109
|
+
except ImportError:
|
|
110
|
+
return {
|
|
111
|
+
"error": "Playwright required for keyword search. Run: pip install playwright && playwright install chromium",
|
|
112
|
+
"totalFound": 0,
|
|
113
|
+
"trademarks": [],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
keyword = keyword.upper().strip()
|
|
117
|
+
results = {"totalFound": 0, "trademarks": [], "keyword": keyword}
|
|
118
|
+
|
|
119
|
+
for attempt in range(1, retries + 1):
|
|
120
|
+
results = {"totalFound": 0, "trademarks": [], "keyword": keyword}
|
|
121
|
+
try:
|
|
122
|
+
with sync_playwright() as p:
|
|
123
|
+
# Use anti-detection settings to bypass AWS WAF
|
|
124
|
+
browser = p.chromium.launch(
|
|
125
|
+
headless=True,
|
|
126
|
+
args=["--disable-blink-features=AutomationControlled"],
|
|
127
|
+
)
|
|
128
|
+
context = browser.new_context(
|
|
129
|
+
user_agent=(
|
|
130
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
131
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
132
|
+
"Chrome/120.0.0.0 Safari/537.36"
|
|
133
|
+
),
|
|
134
|
+
viewport={"width": 1920, "height": 1080},
|
|
135
|
+
)
|
|
136
|
+
page = context.new_page()
|
|
137
|
+
page.add_init_script(
|
|
138
|
+
'Object.defineProperty(navigator, "webdriver", {get: () => undefined})'
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Intercept the Elasticsearch API responses
|
|
142
|
+
def handle_response(response):
|
|
143
|
+
if "prod-stage" in response.url and response.status == 200:
|
|
144
|
+
try:
|
|
145
|
+
data = response.json()
|
|
146
|
+
if isinstance(data, dict) and "hits" in data:
|
|
147
|
+
hits = data.get("hits", {})
|
|
148
|
+
total = hits.get("total", {})
|
|
149
|
+
if isinstance(total, dict):
|
|
150
|
+
results["totalFound"] = total.get("value", 0)
|
|
151
|
+
else:
|
|
152
|
+
results["totalFound"] = total
|
|
153
|
+
for hit in hits.get("hits", [])[:max_results]:
|
|
154
|
+
results["trademarks"].append(hit.get("_source", {}))
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
elif "prod-stage" in response.url and response.status == 403:
|
|
158
|
+
results["_waf_blocked"] = True
|
|
159
|
+
|
|
160
|
+
page.on("response", handle_response)
|
|
161
|
+
page.goto(
|
|
162
|
+
"https://tmsearch.uspto.gov/search/search-information",
|
|
163
|
+
timeout=30000,
|
|
164
|
+
)
|
|
165
|
+
page.wait_for_load_state("networkidle")
|
|
166
|
+
|
|
167
|
+
# Wait for WAF challenge to resolve
|
|
168
|
+
page.wait_for_timeout(2000)
|
|
169
|
+
|
|
170
|
+
# Fill in search and submit
|
|
171
|
+
search_input = page.locator('input[type="text"]').first
|
|
172
|
+
search_input.fill(keyword, timeout=10000)
|
|
173
|
+
page.wait_for_timeout(500)
|
|
174
|
+
search_input.press("Enter")
|
|
175
|
+
|
|
176
|
+
# Wait for results to load
|
|
177
|
+
page.wait_for_timeout(6000)
|
|
178
|
+
|
|
179
|
+
# Check if we landed on the error page
|
|
180
|
+
if "/errors" in page.url:
|
|
181
|
+
results["_waf_blocked"] = True
|
|
182
|
+
|
|
183
|
+
browser.close()
|
|
184
|
+
|
|
185
|
+
# If we got results or no WAF block, stop retrying
|
|
186
|
+
if results.get("totalFound", 0) > 0 or not results.get("_waf_blocked"):
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
if attempt < retries:
|
|
190
|
+
import time
|
|
191
|
+
time.sleep(2)
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
results["error"] = str(e)
|
|
195
|
+
if attempt < retries:
|
|
196
|
+
import time
|
|
197
|
+
time.sleep(2)
|
|
198
|
+
|
|
199
|
+
# Clean up internal flag
|
|
200
|
+
was_blocked = results.pop("_waf_blocked", False)
|
|
201
|
+
if was_blocked and results.get("totalFound", 0) == 0 and "error" not in results:
|
|
202
|
+
results["error"] = (
|
|
203
|
+
"AWS WAF blocked the search request. The USPTO site uses bot detection "
|
|
204
|
+
"that sometimes blocks automated searches. Try again, or use the RapidAPI "
|
|
205
|
+
"wrapper for more reliable results."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return results
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ─── Availability Check ────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
def check_availability(keyword: str) -> dict:
|
|
214
|
+
"""Check if a keyword is available (no active trademarks found)."""
|
|
215
|
+
result = search_trademark_playwright(keyword, max_results=5)
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"keyword": keyword.upper(),
|
|
219
|
+
"available": result.get("totalFound", 0) == 0,
|
|
220
|
+
"active_count": result.get("totalFound", 0),
|
|
221
|
+
"top_matches": result.get("trademarks", [])[:3],
|
|
222
|
+
"error": result.get("error"),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ─── Output Formatting ──────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
def format_trademark(tm: dict) -> str:
|
|
229
|
+
"""Format a single trademark result for display."""
|
|
230
|
+
word_mark = tm.get("wordMark") or tm.get("markIdentification") or "N/A"
|
|
231
|
+
owner = tm.get("ownerName") or tm.get("owner") or "N/A"
|
|
232
|
+
serial = tm.get("serialNumber") or "N/A"
|
|
233
|
+
status = tm.get("statusMark") or tm.get("status") or "N/A"
|
|
234
|
+
classes = tm.get("internationalClassification", [])
|
|
235
|
+
if isinstance(classes, list):
|
|
236
|
+
classes = ", ".join(str(c) for c in classes)
|
|
237
|
+
return (
|
|
238
|
+
f" • \"{word_mark}\" | "
|
|
239
|
+
f"Owner: {owner} | "
|
|
240
|
+
f"Classes: {classes or 'N/A'} | "
|
|
241
|
+
f"Status: {status} | "
|
|
242
|
+
f"Serial: {serial}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def print_search_results(keyword: str, results: dict, show_json: bool = False):
|
|
247
|
+
"""Print search results to stdout."""
|
|
248
|
+
if show_json:
|
|
249
|
+
print(json.dumps(results, indent=2, default=str))
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
if results.get("error"):
|
|
253
|
+
print(f"\n[!] Error: {results['error']}")
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
total = results.get("totalFound", 0)
|
|
257
|
+
trademarks = results.get("trademarks", [])
|
|
258
|
+
|
|
259
|
+
print(f"\n{'='*60}")
|
|
260
|
+
print(f"KEYWORD: \"{keyword.upper()}\"")
|
|
261
|
+
print(f"Total found: {total}")
|
|
262
|
+
|
|
263
|
+
if total == 0:
|
|
264
|
+
print("Status: [OK] LIKELY AVAILABLE (no matching active marks)")
|
|
265
|
+
else:
|
|
266
|
+
print(f"Status: [X] REGISTERED/PENDING MARKS FOUND")
|
|
267
|
+
print(f"\nTop results:")
|
|
268
|
+
for tm in trademarks[:10]:
|
|
269
|
+
print(format_trademark(tm))
|
|
270
|
+
if total > 10:
|
|
271
|
+
print(f" ... and {total - 10} more")
|
|
272
|
+
|
|
273
|
+
print(DISCLAIMER)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def print_availability(result: dict, show_json: bool = False):
|
|
277
|
+
"""Print availability check result."""
|
|
278
|
+
if show_json:
|
|
279
|
+
print(json.dumps(result, indent=2, default=str))
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
if result.get("error"):
|
|
283
|
+
print(f"\n[!] Error: {result['error']}")
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
keyword = result["keyword"]
|
|
287
|
+
print(f"\n{'='*60}")
|
|
288
|
+
print(f"AVAILABILITY CHECK: \"{keyword}\"")
|
|
289
|
+
|
|
290
|
+
if result["available"]:
|
|
291
|
+
print(f"Status: [OK] LIKELY AVAILABLE")
|
|
292
|
+
print(f"Active marks: 0")
|
|
293
|
+
else:
|
|
294
|
+
print(f"Status: [X] NOT AVAILABLE — {result['active_count']} active mark(s) found")
|
|
295
|
+
if result["top_matches"]:
|
|
296
|
+
print("\nConflicting marks:")
|
|
297
|
+
for tm in result["top_matches"]:
|
|
298
|
+
print(format_trademark(tm))
|
|
299
|
+
|
|
300
|
+
print(DISCLAIMER)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ─── Batch Validation ───────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
def batch_validate(
|
|
306
|
+
keywords: list[str],
|
|
307
|
+
delay: float = 3.0,
|
|
308
|
+
output_csv: Optional[str] = None,
|
|
309
|
+
) -> list[dict]:
|
|
310
|
+
"""
|
|
311
|
+
Validate a list of keywords against USPTO trademarks.
|
|
312
|
+
Uses Playwright for each search with a delay between requests.
|
|
313
|
+
"""
|
|
314
|
+
results = []
|
|
315
|
+
|
|
316
|
+
print(f"Checking {len(keywords)} keywords against USPTO trademarks...\n")
|
|
317
|
+
|
|
318
|
+
for i, word in enumerate(keywords, 1):
|
|
319
|
+
word = word.strip()
|
|
320
|
+
if not word:
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
print(f"[{i}/{len(keywords)}] Checking: {word.upper()}", end=" ... ", flush=True)
|
|
324
|
+
|
|
325
|
+
r = search_trademark_playwright(word, max_results=5)
|
|
326
|
+
count = r.get("totalFound", 0)
|
|
327
|
+
trademarks = r.get("trademarks", [])
|
|
328
|
+
|
|
329
|
+
result = {
|
|
330
|
+
"keyword": word.upper(),
|
|
331
|
+
"status": "AVAILABLE" if count == 0 else "TAKEN",
|
|
332
|
+
"count": count,
|
|
333
|
+
"top_owner": "",
|
|
334
|
+
"top_mark": "",
|
|
335
|
+
"top_serial": "",
|
|
336
|
+
}
|
|
337
|
+
if trademarks:
|
|
338
|
+
top = trademarks[0]
|
|
339
|
+
result["top_owner"] = top.get("ownerName", "")
|
|
340
|
+
result["top_mark"] = top.get("wordMark") or top.get("markIdentification", "")
|
|
341
|
+
result["top_serial"] = top.get("serialNumber", "")
|
|
342
|
+
|
|
343
|
+
results.append(result)
|
|
344
|
+
status_str = "[OK] AVAILABLE" if count == 0 else f"[X] TAKEN ({count} marks)"
|
|
345
|
+
if r.get("error"):
|
|
346
|
+
status_str = f"[!] ERROR: {r['error']}"
|
|
347
|
+
print(status_str)
|
|
348
|
+
|
|
349
|
+
if i < len(keywords):
|
|
350
|
+
time.sleep(delay)
|
|
351
|
+
|
|
352
|
+
if output_csv and results:
|
|
353
|
+
with open(output_csv, "w", newline="") as f:
|
|
354
|
+
writer = csv.DictWriter(f, fieldnames=results[0].keys())
|
|
355
|
+
writer.writeheader()
|
|
356
|
+
writer.writerows(results)
|
|
357
|
+
print(f"\n[OK] Results saved to: {output_csv}")
|
|
358
|
+
|
|
359
|
+
return results
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ─── CLI Entry Point ────────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
def main():
|
|
365
|
+
parser = argparse.ArgumentParser(
|
|
366
|
+
description="Search and validate US trademarks via USPTO",
|
|
367
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
368
|
+
epilog="""
|
|
369
|
+
Examples:
|
|
370
|
+
tm_search.py keyword "CLOUDPEAK"
|
|
371
|
+
tm_search.py keyword "APPLE" --rows=50
|
|
372
|
+
tm_search.py available "NEONPULSE"
|
|
373
|
+
tm_search.py status 78787878
|
|
374
|
+
tm_search.py batch "BRAND1,BRAND2,BRAND3" --csv
|
|
375
|
+
tm_search.py validate names.txt --output=results.csv
|
|
376
|
+
|
|
377
|
+
Note: keyword/available/batch/validate commands require Playwright:
|
|
378
|
+
pip install playwright && playwright install chromium
|
|
379
|
+
"""
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
383
|
+
|
|
384
|
+
# keyword command
|
|
385
|
+
p_kw = subparsers.add_parser("keyword", help="Search by keyword (requires Playwright)")
|
|
386
|
+
p_kw.add_argument("word", help="Keyword to search")
|
|
387
|
+
p_kw.add_argument("--rows", type=int, default=25, help="Max results (default 25)")
|
|
388
|
+
p_kw.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
389
|
+
|
|
390
|
+
# available command
|
|
391
|
+
p_av = subparsers.add_parser("available", help="Check if keyword is available")
|
|
392
|
+
p_av.add_argument("word", help="Keyword to check")
|
|
393
|
+
p_av.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
394
|
+
|
|
395
|
+
# status command
|
|
396
|
+
p_st = subparsers.add_parser("status", help="Get case status by serial number (no Playwright needed)")
|
|
397
|
+
p_st.add_argument("serial", help="Serial number (8 digits)")
|
|
398
|
+
p_st.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
399
|
+
|
|
400
|
+
# batch command
|
|
401
|
+
p_bt = subparsers.add_parser("batch", help="Check multiple comma-separated keywords")
|
|
402
|
+
p_bt.add_argument("words", help="Comma-separated keywords")
|
|
403
|
+
p_bt.add_argument("--csv", action="store_true", help="Output as CSV")
|
|
404
|
+
p_bt.add_argument("--delay", type=float, default=3.0, help="Delay between requests (seconds, default 3)")
|
|
405
|
+
|
|
406
|
+
# validate command
|
|
407
|
+
p_vl = subparsers.add_parser("validate", help="Validate keywords from a file")
|
|
408
|
+
p_vl.add_argument("file", help="Text file with one keyword per line")
|
|
409
|
+
p_vl.add_argument("--output", help="Output CSV file path")
|
|
410
|
+
p_vl.add_argument("--delay", type=float, default=3.0)
|
|
411
|
+
|
|
412
|
+
args = parser.parse_args()
|
|
413
|
+
|
|
414
|
+
if args.command == "keyword":
|
|
415
|
+
result = search_trademark_playwright(args.word, max_results=args.rows)
|
|
416
|
+
print_search_results(args.word, result, show_json=args.json)
|
|
417
|
+
|
|
418
|
+
elif args.command == "available":
|
|
419
|
+
result = check_availability(args.word)
|
|
420
|
+
print_availability(result, show_json=args.json)
|
|
421
|
+
|
|
422
|
+
elif args.command == "status":
|
|
423
|
+
result = get_status_by_serial(args.serial)
|
|
424
|
+
if args.json:
|
|
425
|
+
print(json.dumps(result, indent=2, default=str))
|
|
426
|
+
else:
|
|
427
|
+
if result.get("error"):
|
|
428
|
+
print(f"\n[!] Error: {result['error']}")
|
|
429
|
+
else:
|
|
430
|
+
print(f"\nSerial: {result.get('serialNumber')}")
|
|
431
|
+
for k, v in result.items():
|
|
432
|
+
if k != "serialNumber" and v is not None:
|
|
433
|
+
print(f" {k}: {v}")
|
|
434
|
+
|
|
435
|
+
elif args.command == "batch":
|
|
436
|
+
words = [w.strip() for w in args.words.split(",") if w.strip()]
|
|
437
|
+
results = batch_validate(words, delay=args.delay)
|
|
438
|
+
if args.csv and results:
|
|
439
|
+
import io
|
|
440
|
+
output = io.StringIO()
|
|
441
|
+
writer = csv.DictWriter(output, fieldnames=results[0].keys())
|
|
442
|
+
writer.writeheader()
|
|
443
|
+
writer.writerows(results)
|
|
444
|
+
print("\n" + output.getvalue())
|
|
445
|
+
|
|
446
|
+
elif args.command == "validate":
|
|
447
|
+
with open(args.file) as f:
|
|
448
|
+
words = [line.strip() for line in f if line.strip()]
|
|
449
|
+
batch_validate(words, delay=args.delay, output_csv=args.output)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
if __name__ == "__main__":
|
|
453
|
+
main()
|