@biggora/claude-plugins 1.1.1 → 1.2.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.
Files changed (117) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/README.md +13 -13
  3. package/codex-cli-workspace/iteration-1/benchmark.json +122 -0
  4. package/codex-cli-workspace/iteration-1/eval-1-ci-integration/eval_metadata.json +13 -0
  5. package/codex-cli-workspace/iteration-1/eval-1-ci-integration/with_skill/grading.json +52 -0
  6. package/codex-cli-workspace/iteration-1/eval-1-ci-integration/with_skill/outputs/response.md +163 -0
  7. package/codex-cli-workspace/iteration-1/eval-1-ci-integration/with_skill/timing.json +5 -0
  8. package/codex-cli-workspace/iteration-1/eval-1-ci-integration/without_skill/grading.json +58 -0
  9. package/codex-cli-workspace/iteration-1/eval-1-ci-integration/without_skill/outputs/response.md +151 -0
  10. package/codex-cli-workspace/iteration-1/eval-1-ci-integration/without_skill/timing.json +5 -0
  11. package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/eval_metadata.json +13 -0
  12. package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/grading.json +52 -0
  13. package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/outputs/response.md +86 -0
  14. package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/timing.json +5 -0
  15. package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/grading.json +58 -0
  16. package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/outputs/response.md +164 -0
  17. package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/timing.json +5 -0
  18. package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/eval_metadata.json +13 -0
  19. package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/with_skill/grading.json +52 -0
  20. package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/with_skill/outputs/response.md +130 -0
  21. package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/with_skill/timing.json +5 -0
  22. package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/without_skill/grading.json +64 -0
  23. package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/without_skill/outputs/response.md +209 -0
  24. package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/without_skill/timing.json +5 -0
  25. package/codex-cli-workspace/iteration-1/review.html +1325 -0
  26. package/gemini-cli-workspace/iteration-1/benchmark.json +86 -0
  27. package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/eval_metadata.json +37 -0
  28. package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/with_skill/grading.json +37 -0
  29. package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/with_skill/outputs/response.md +401 -0
  30. package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/with_skill/timing.json +5 -0
  31. package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/without_skill/grading.json +37 -0
  32. package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/without_skill/outputs/response.md +405 -0
  33. package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/without_skill/timing.json +5 -0
  34. package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/eval_metadata.json +37 -0
  35. package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/grading.json +37 -0
  36. package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/outputs/response.md +212 -0
  37. package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/timing.json +5 -0
  38. package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/grading.json +37 -0
  39. package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/outputs/response.md +427 -0
  40. package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/timing.json +5 -0
  41. package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/eval_metadata.json +32 -0
  42. package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/with_skill/grading.json +32 -0
  43. package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/with_skill/outputs/response.md +171 -0
  44. package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/with_skill/timing.json +5 -0
  45. package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/without_skill/grading.json +32 -0
  46. package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/without_skill/outputs/response.md +199 -0
  47. package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/without_skill/timing.json +5 -0
  48. package/gemini-cli-workspace/iteration-1/review.html +1325 -0
  49. package/gemini-cli-workspace/iteration-2/benchmark.json +173 -0
  50. package/gemini-cli-workspace/iteration-2/benchmark.md +28 -0
  51. package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/eval_metadata.json +37 -0
  52. package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/with_skill/grading.json +37 -0
  53. package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/with_skill/outputs/response.md +195 -0
  54. package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/with_skill/timing.json +5 -0
  55. package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/without_skill/grading.json +37 -0
  56. package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/without_skill/outputs/response.md +377 -0
  57. package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/without_skill/timing.json +5 -0
  58. package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/eval_metadata.json +37 -0
  59. package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/with_skill/grading.json +37 -0
  60. package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/with_skill/outputs/response.md +127 -0
  61. package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/with_skill/timing.json +5 -0
  62. package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/without_skill/grading.json +37 -0
  63. package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/without_skill/outputs/response.md +164 -0
  64. package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/without_skill/timing.json +5 -0
  65. package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/eval_metadata.json +32 -0
  66. package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/with_skill/grading.json +32 -0
  67. package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/with_skill/outputs/response.md +91 -0
  68. package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/with_skill/timing.json +5 -0
  69. package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/without_skill/grading.json +32 -0
  70. package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/without_skill/outputs/response.md +112 -0
  71. package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/without_skill/timing.json +5 -0
  72. package/gemini-cli-workspace/iteration-2/eval-viewer.html +1325 -0
  73. package/package.json +1 -1
  74. package/screen-recording-workspace/evals.json +41 -0
  75. package/screen-recording-workspace/iteration-1/benchmark.json +102 -0
  76. package/screen-recording-workspace/iteration-1/eval-0-fullscreen/eval_metadata.json +31 -0
  77. package/screen-recording-workspace/iteration-1/eval-0-fullscreen/with_skill/grading.json +11 -0
  78. package/screen-recording-workspace/iteration-1/eval-0-fullscreen/with_skill/outputs/demo.mp4 +0 -0
  79. package/screen-recording-workspace/iteration-1/eval-0-fullscreen/with_skill/timing.json +5 -0
  80. package/screen-recording-workspace/iteration-1/eval-0-fullscreen/without_skill/grading.json +11 -0
  81. package/screen-recording-workspace/iteration-1/eval-0-fullscreen/without_skill/outputs/demo.mp4 +0 -0
  82. package/screen-recording-workspace/iteration-1/eval-0-fullscreen/without_skill/timing.json +5 -0
  83. package/screen-recording-workspace/iteration-1/eval-1-region-audio/eval_metadata.json +31 -0
  84. package/screen-recording-workspace/iteration-1/eval-1-region-audio/with_skill/grading.json +11 -0
  85. package/screen-recording-workspace/iteration-1/eval-1-region-audio/with_skill/outputs/region_capture.mp4 +0 -0
  86. package/screen-recording-workspace/iteration-1/eval-1-region-audio/with_skill/timing.json +5 -0
  87. package/screen-recording-workspace/iteration-1/eval-1-region-audio/without_skill/grading.json +11 -0
  88. package/screen-recording-workspace/iteration-1/eval-1-region-audio/without_skill/outputs/region_capture.mp4 +0 -0
  89. package/screen-recording-workspace/iteration-1/eval-1-region-audio/without_skill/timing.json +5 -0
  90. package/screen-recording-workspace/iteration-1/eval-2-python-fallback/eval_metadata.json +31 -0
  91. package/screen-recording-workspace/iteration-1/eval-2-python-fallback/with_skill/grading.json +11 -0
  92. package/screen-recording-workspace/iteration-1/eval-2-python-fallback/with_skill/outputs/fallback_recording.mp4 +0 -0
  93. package/screen-recording-workspace/iteration-1/eval-2-python-fallback/with_skill/timing.json +5 -0
  94. package/screen-recording-workspace/iteration-1/eval-2-python-fallback/without_skill/grading.json +11 -0
  95. package/screen-recording-workspace/iteration-1/eval-2-python-fallback/without_skill/outputs/fallback_recording.mp4 +0 -0
  96. package/screen-recording-workspace/iteration-1/eval-2-python-fallback/without_skill/outputs/record_screen.py +67 -0
  97. package/screen-recording-workspace/iteration-1/eval-2-python-fallback/without_skill/timing.json +5 -0
  98. package/screen-recording-workspace/iteration-1/review.html +1325 -0
  99. package/src/skills/codex-cli/SKILL.md +21 -11
  100. package/src/skills/codex-cli/evals/evals.json +47 -0
  101. package/src/skills/gemini-cli/SKILL.md +27 -13
  102. package/src/skills/gemini-cli/evals/evals.json +46 -0
  103. package/src/skills/gemini-cli/references/commands.md +21 -14
  104. package/src/skills/gemini-cli/references/configuration.md +23 -18
  105. package/src/skills/gemini-cli/references/headless-and-scripting.md +7 -17
  106. package/src/skills/gemini-cli/references/mcp-and-extensions.md +12 -6
  107. package/src/skills/notebook-lm/SKILL.md +1 -1
  108. package/src/skills/screen-recording/SKILL.md +243 -213
  109. package/src/skills/screen-recording/references/design-patterns.md +4 -2
  110. package/src/skills/screen-recording/references/ffmpeg-recording.md +473 -0
  111. package/src/skills/screen-recording/references/{approach1-programmatic.md → programmatic-generation.md} +45 -22
  112. package/src/skills/screen-recording/references/python-fallback.md +222 -0
  113. package/src/skills/tm-search/SKILL.md +242 -106
  114. package/src/skills/tm-search/evals/evals.json +23 -0
  115. package/src/skills/tm-search/references/scraping-fallback.md +60 -95
  116. package/src/skills/tm-search/scripts/tm_search.py +453 -375
  117. 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 tsdrapi.uspto.gov
5
-
6
- Usage:
7
- python tm_search.py keyword <word> [--status=A] [--rows=25] [--json]
8
- python tm_search.py available <word>
9
- python tm_search.py status <serial_number> [--api-key=KEY]
10
- python tm_search.py batch <word1,word2,...> [--status=A] [--csv]
11
- python tm_search.py validate <file.txt> [--status=A] [--output=results.csv]
12
- """
13
-
14
- import sys
15
- import json
16
- import time
17
- import csv
18
- import argparse
19
- import xml.etree.ElementTree as ET
20
- from typing import Optional
21
-
22
- try:
23
- import requests
24
- except ImportError:
25
- print("Error: 'requests' library required. Run: pip install requests")
26
- sys.exit(1)
27
-
28
- # ─── Constants ─────────────────────────────────────────────────────────────────
29
-
30
- SEARCH_BASE = "https://tmsearch.uspto.gov"
31
- TSDR_BASE = "https://tsdrapi.uspto.gov/ts/cd"
32
-
33
- HEADERS = {
34
- "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (compatible; tm-search/1.0)",
35
- "Accept": "application/json, text/plain, */*",
36
- "Accept-Language": "en-US,en;q=0.9",
37
- "Referer": f"{SEARCH_BASE}/search/search-information",
38
- "Origin": SEARCH_BASE,
39
- }
40
-
41
- DISCLAIMER = (
42
- "\n⚠️ DISCLAIMER: This is a preliminary search only. Trademark availability depends on many\n"
43
- " factors. Consult a licensed trademark attorney before filing.\n"
44
- )
45
-
46
- # ─── Search Functions ───────────────────────────────────────────────────────────
47
-
48
- def search_trademark(
49
- keyword: str,
50
- status: str = "A",
51
- rows: int = 25,
52
- start: int = 0,
53
- plural_variants: bool = False,
54
- session: Optional[requests.Session] = None
55
- ) -> dict:
56
- """
57
- Search USPTO trademark database by keyword.
58
-
59
- Args:
60
- keyword: Word/phrase to search (auto-uppercased)
61
- status: "A"=active, "D"=dead, ""=all
62
- rows: Number of results (max 500)
63
- start: Pagination offset
64
- plural_variants: Include plural forms
65
-
66
- Returns:
67
- dict with "totalFound" and "trademarks" list
68
- """
69
- s = session or requests.Session()
70
- keyword = keyword.upper().strip()
71
-
72
- payload = {
73
- "keyword": keyword,
74
- "searchType": "1",
75
- "statusType": status,
76
- "pluralVariants": plural_variants,
77
- "start": start,
78
- "rows": rows,
79
- }
80
-
81
- try:
82
- # Try POST first
83
- resp = s.post(
84
- f"{SEARCH_BASE}/search/keyword",
85
- json=payload,
86
- headers=HEADERS,
87
- timeout=30
88
- )
89
- if resp.status_code == 200:
90
- return resp.json()
91
-
92
- # Fallback: GET with query params
93
- params = {k: str(v).lower() if isinstance(v, bool) else v for k, v in payload.items()}
94
- resp = s.get(
95
- f"{SEARCH_BASE}/search/keyword",
96
- params=params,
97
- headers=HEADERS,
98
- timeout=30
99
- )
100
- resp.raise_for_status()
101
- return resp.json()
102
-
103
- except requests.exceptions.RequestException as e:
104
- return {"error": str(e), "totalFound": 0, "trademarks": []}
105
-
106
-
107
- def check_availability(keyword: str) -> dict:
108
- """Check if a keyword is available (no active trademarks)."""
109
- result = search_trademark(keyword, status="A", rows=5)
110
- dead_result = search_trademark(keyword, status="D", rows=5)
111
-
112
- return {
113
- "keyword": keyword.upper(),
114
- "available": result.get("totalFound", 0) == 0,
115
- "active_count": result.get("totalFound", 0),
116
- "dead_count": dead_result.get("totalFound", 0),
117
- "top_matches": result.get("trademarks", [])[:3],
118
- }
119
-
120
-
121
- def get_status_by_serial(serial_number: str, api_key: Optional[str] = None) -> dict:
122
- """Get trademark case status by serial number via TSDR API."""
123
- headers = {"Accept": "application/xml"}
124
- if api_key:
125
- headers["USPTO-API-KEY"] = api_key
126
-
127
- # Remove non-digits
128
- sn = "".join(filter(str.isdigit, serial_number))
129
- url = f"{TSDR_BASE}/casestatus/sn{sn}/info.xml"
130
-
131
- try:
132
- resp = requests.get(url, headers=headers, timeout=30)
133
- resp.raise_for_status()
134
-
135
- # Parse XML response
136
- root = ET.fromstring(resp.content)
137
- ns = {"ns1": "urn:us:gov:doc:uspto:trademark:status"}
138
-
139
- def find_text(path):
140
- el = root.find(path, ns)
141
- return el.text if el is not None else None
142
-
143
- return {
144
- "serialNumber": sn,
145
- "status": find_text(".//ns1:MarkCurrentStatusExternalDescriptionText"),
146
- "statusDate": find_text(".//ns1:MarkCurrentStatusDate"),
147
- "wordMark": find_text(".//ns1:MarkVerbalElementText"),
148
- "owner": find_text(".//ns1:EntityName"),
149
- "filingDate": find_text(".//ns1:ApplicationDate"),
150
- "registrationDate": find_text(".//ns1:RegistrationDate"),
151
- "registrationNumber": find_text(".//ns1:RegistrationNumber"),
152
- }
153
- except requests.exceptions.RequestException as e:
154
- return {"error": str(e), "serialNumber": sn}
155
- except ET.ParseError:
156
- return {"error": "Failed to parse XML response", "serialNumber": sn}
157
-
158
-
159
- # ─── Output Formatting ──────────────────────────────────────────────────────────
160
-
161
- def format_trademark(tm: dict) -> str:
162
- """Format a single trademark result for display."""
163
- classes = ", ".join(tm.get("internationalClassification", []))
164
- return (
165
- f" • \"{tm.get('wordMark', 'N/A')}\" | "
166
- f"Owner: {tm.get('owner', 'N/A')} | "
167
- f"Classes: {classes or 'N/A'} | "
168
- f"Status: {tm.get('status', 'N/A')} | "
169
- f"Serial: {tm.get('serialNumber', 'N/A')}"
170
- )
171
-
172
-
173
- def print_search_results(keyword: str, results: dict, show_json: bool = False):
174
- """Print search results to stdout."""
175
- if show_json:
176
- print(json.dumps(results, indent=2))
177
- return
178
-
179
- total = results.get("totalFound", 0)
180
- trademarks = results.get("trademarks", [])
181
-
182
- print(f"\n{'='*60}")
183
- print(f"KEYWORD: \"{keyword.upper()}\"")
184
- print(f"Total found: {total}")
185
-
186
- if total == 0:
187
- print("Status: ✅ LIKELY AVAILABLE (no matching active marks)")
188
- else:
189
- print(f"Status: REGISTERED/PENDING MARKS FOUND")
190
- print(f"\nTop results:")
191
- for tm in trademarks[:10]:
192
- print(format_trademark(tm))
193
- if total > 10:
194
- print(f" ... and {total - 10} more")
195
-
196
- print(DISCLAIMER)
197
-
198
-
199
- def print_availability(result: dict, show_json: bool = False):
200
- """Print availability check result."""
201
- if show_json:
202
- print(json.dumps(result, indent=2))
203
- return
204
-
205
- keyword = result["keyword"]
206
- print(f"\n{'='*60}")
207
- print(f"AVAILABILITY CHECK: \"{keyword}\"")
208
-
209
- if result["available"]:
210
- print(f"Status: ✅ LIKELY AVAILABLE")
211
- print(f"Active marks: 0")
212
- print(f"Dead/cancelled marks: {result['dead_count']} (historical, may not block registration)")
213
- else:
214
- print(f"Status: NOT AVAILABLE {result['active_count']} active mark(s) found")
215
- if result["top_matches"]:
216
- print("\nConflicting marks:")
217
- for tm in result["top_matches"]:
218
- print(format_trademark(tm))
219
-
220
- print(DISCLAIMER)
221
-
222
-
223
- # ─── Batch Validation ───────────────────────────────────────────────────────────
224
-
225
- def batch_validate(
226
- keywords: list[str],
227
- status: str = "A",
228
- delay: float = 0.75,
229
- output_csv: Optional[str] = None
230
- ) -> list[dict]:
231
- """
232
- Validate a list of keywords against USPTO trademarks.
233
-
234
- Args:
235
- keywords: List of words to check
236
- status: Filter status
237
- delay: Seconds between requests (avoid rate limiting)
238
- output_csv: If set, write results to this CSV file
239
-
240
- Returns:
241
- List of result dicts
242
- """
243
- results = []
244
- session = requests.Session()
245
-
246
- print(f"Checking {len(keywords)} keywords against USPTO trademarks...")
247
- print(f"Status filter: {'ACTIVE' if status == 'A' else 'DEAD' if status == 'D' else 'ALL'}\n")
248
-
249
- for i, word in enumerate(keywords, 1):
250
- word = word.strip()
251
- if not word:
252
- continue
253
-
254
- print(f"[{i}/{len(keywords)}] Checking: {word.upper()}", end=" ... ", flush=True)
255
-
256
- r = search_trademark(word, status=status, rows=5, session=session)
257
- count = r.get("totalFound", 0)
258
- top = r.get("trademarks", [{}])
259
-
260
- result = {
261
- "keyword": word.upper(),
262
- "status": "AVAILABLE" if count == 0 else "TAKEN",
263
- "count": count,
264
- "top_owner": top[0].get("owner", "") if top else "",
265
- "top_mark": top[0].get("wordMark", "") if top else "",
266
- "top_serial": top[0].get("serialNumber", "") if top else "",
267
- }
268
- results.append(result)
269
-
270
- print(f"{'✅ AVAILABLE' if count == 0 else f'❌ TAKEN ({count} marks)'}")
271
-
272
- if i < len(keywords):
273
- time.sleep(delay)
274
-
275
- if output_csv:
276
- with open(output_csv, "w", newline="") as f:
277
- writer = csv.DictWriter(f, fieldnames=results[0].keys())
278
- writer.writeheader()
279
- writer.writerows(results)
280
- print(f"\n✅ Results saved to: {output_csv}")
281
-
282
- return results
283
-
284
-
285
- # ─── CLI Entry Point ────────────────────────────────────────────────────────────
286
-
287
- def main():
288
- parser = argparse.ArgumentParser(
289
- description="Search and validate US trademarks via USPTO",
290
- formatter_class=argparse.RawDescriptionHelpFormatter,
291
- epilog="""
292
- Examples:
293
- tm_search.py keyword "CLOUDPEAK"
294
- tm_search.py keyword "APPLE" --status=A --rows=50
295
- tm_search.py available "NEONPULSE"
296
- tm_search.py status 78787878 --api-key=YOUR_KEY
297
- tm_search.py batch "BRAND1,BRAND2,BRAND3" --csv
298
- tm_search.py validate names.txt --output=results.csv
299
- """
300
- )
301
-
302
- subparsers = parser.add_subparsers(dest="command", required=True)
303
-
304
- # keyword command
305
- p_kw = subparsers.add_parser("keyword", help="Search by keyword")
306
- p_kw.add_argument("word", help="Keyword to search")
307
- p_kw.add_argument("--status", default="A", choices=["A", "D", ""], help="A=active, D=dead")
308
- p_kw.add_argument("--rows", type=int, default=25, help="Results per page (max 500)")
309
- p_kw.add_argument("--plural", action="store_true", help="Include plural variants")
310
- p_kw.add_argument("--json", action="store_true", help="Output raw JSON")
311
-
312
- # available command
313
- p_av = subparsers.add_parser("available", help="Check if keyword is available")
314
- p_av.add_argument("word", help="Keyword to check")
315
- p_av.add_argument("--json", action="store_true", help="Output raw JSON")
316
-
317
- # status command
318
- p_st = subparsers.add_parser("status", help="Get case status by serial number")
319
- p_st.add_argument("serial", help="Serial number (8 digits)")
320
- p_st.add_argument("--api-key", help="USPTO API key for bulk access")
321
- p_st.add_argument("--json", action="store_true", help="Output raw JSON")
322
-
323
- # batch command
324
- p_bt = subparsers.add_parser("batch", help="Check multiple comma-separated keywords")
325
- p_bt.add_argument("words", help="Comma-separated keywords")
326
- p_bt.add_argument("--status", default="A", choices=["A", "D", ""])
327
- p_bt.add_argument("--csv", action="store_true", help="Output as CSV")
328
- p_bt.add_argument("--delay", type=float, default=0.75, help="Delay between requests (seconds)")
329
-
330
- # validate command
331
- p_vl = subparsers.add_parser("validate", help="Validate keywords from a file")
332
- p_vl.add_argument("file", help="Text file with one keyword per line")
333
- p_vl.add_argument("--status", default="A", choices=["A", "D", ""])
334
- p_vl.add_argument("--output", help="Output CSV file path")
335
- p_vl.add_argument("--delay", type=float, default=0.75)
336
-
337
- args = parser.parse_args()
338
-
339
- if args.command == "keyword":
340
- result = search_trademark(args.word, status=args.status, rows=args.rows, plural_variants=args.plural)
341
- print_search_results(args.word, result, show_json=args.json)
342
-
343
- elif args.command == "available":
344
- result = check_availability(args.word)
345
- print_availability(result, show_json=args.json)
346
-
347
- elif args.command == "status":
348
- result = get_status_by_serial(args.serial, api_key=args.api_key)
349
- if args.json:
350
- print(json.dumps(result, indent=2))
351
- else:
352
- print(f"\nSerial: {result.get('serialNumber')}")
353
- for k, v in result.items():
354
- if k != "serialNumber" and v:
355
- print(f" {k}: {v}")
356
-
357
- elif args.command == "batch":
358
- words = [w.strip() for w in args.words.split(",") if w.strip()]
359
- results = batch_validate(words, status=args.status, delay=args.delay)
360
- if args.csv:
361
- import io
362
- output = io.StringIO()
363
- writer = csv.DictWriter(output, fieldnames=results[0].keys())
364
- writer.writeheader()
365
- writer.writerows(results)
366
- print("\n" + output.getvalue())
367
-
368
- elif args.command == "validate":
369
- with open(args.file) as f:
370
- words = [line.strip() for line in f if line.strip()]
371
- batch_validate(words, status=args.status, delay=args.delay, output_csv=args.output)
372
-
373
-
374
- if __name__ == "__main__":
375
- main()
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()