@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.
- package/.claude/settings.local.json +3 -1
- package/README.md +13 -13
- package/codex-cli-workspace/iteration-1/benchmark.json +122 -0
- package/codex-cli-workspace/iteration-1/eval-1-ci-integration/eval_metadata.json +13 -0
- package/codex-cli-workspace/iteration-1/eval-1-ci-integration/with_skill/grading.json +52 -0
- package/codex-cli-workspace/iteration-1/eval-1-ci-integration/with_skill/outputs/response.md +163 -0
- package/codex-cli-workspace/iteration-1/eval-1-ci-integration/with_skill/timing.json +5 -0
- package/codex-cli-workspace/iteration-1/eval-1-ci-integration/without_skill/grading.json +58 -0
- package/codex-cli-workspace/iteration-1/eval-1-ci-integration/without_skill/outputs/response.md +151 -0
- package/codex-cli-workspace/iteration-1/eval-1-ci-integration/without_skill/timing.json +5 -0
- package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/eval_metadata.json +13 -0
- package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/grading.json +52 -0
- package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/outputs/response.md +86 -0
- package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/timing.json +5 -0
- package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/grading.json +58 -0
- package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/outputs/response.md +164 -0
- package/codex-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/timing.json +5 -0
- package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/eval_metadata.json +13 -0
- package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/with_skill/grading.json +52 -0
- package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/with_skill/outputs/response.md +130 -0
- package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/with_skill/timing.json +5 -0
- package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/without_skill/grading.json +64 -0
- package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/without_skill/outputs/response.md +209 -0
- package/codex-cli-workspace/iteration-1/eval-3-profiles-troubleshooting/without_skill/timing.json +5 -0
- package/codex-cli-workspace/iteration-1/review.html +1325 -0
- package/gemini-cli-workspace/iteration-1/benchmark.json +86 -0
- package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/eval_metadata.json +37 -0
- package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/with_skill/grading.json +37 -0
- package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/with_skill/outputs/response.md +401 -0
- package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/with_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/without_skill/grading.json +37 -0
- package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/without_skill/outputs/response.md +405 -0
- package/gemini-cli-workspace/iteration-1/eval-1-cicd-setup/without_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/eval_metadata.json +37 -0
- package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/grading.json +37 -0
- package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/outputs/response.md +212 -0
- package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/with_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/grading.json +37 -0
- package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/outputs/response.md +427 -0
- package/gemini-cli-workspace/iteration-1/eval-2-mcp-server-config/without_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/eval_metadata.json +32 -0
- package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/with_skill/grading.json +32 -0
- package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/with_skill/outputs/response.md +171 -0
- package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/with_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/without_skill/grading.json +32 -0
- package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/without_skill/outputs/response.md +199 -0
- package/gemini-cli-workspace/iteration-1/eval-3-custom-slash-command/without_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-1/review.html +1325 -0
- package/gemini-cli-workspace/iteration-2/benchmark.json +173 -0
- package/gemini-cli-workspace/iteration-2/benchmark.md +28 -0
- package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/eval_metadata.json +37 -0
- package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/with_skill/grading.json +37 -0
- package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/with_skill/outputs/response.md +195 -0
- package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/with_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/without_skill/grading.json +37 -0
- package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/without_skill/outputs/response.md +377 -0
- package/gemini-cli-workspace/iteration-2/eval-1-cicd-setup/without_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/eval_metadata.json +37 -0
- package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/with_skill/grading.json +37 -0
- package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/with_skill/outputs/response.md +127 -0
- package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/with_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/without_skill/grading.json +37 -0
- package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/without_skill/outputs/response.md +164 -0
- package/gemini-cli-workspace/iteration-2/eval-2-mcp-server-config/without_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/eval_metadata.json +32 -0
- package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/with_skill/grading.json +32 -0
- package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/with_skill/outputs/response.md +91 -0
- package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/with_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/without_skill/grading.json +32 -0
- package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/without_skill/outputs/response.md +112 -0
- package/gemini-cli-workspace/iteration-2/eval-3-custom-slash-command/without_skill/timing.json +5 -0
- package/gemini-cli-workspace/iteration-2/eval-viewer.html +1325 -0
- package/package.json +1 -1
- package/screen-recording-workspace/evals.json +41 -0
- package/screen-recording-workspace/iteration-1/benchmark.json +102 -0
- package/screen-recording-workspace/iteration-1/eval-0-fullscreen/eval_metadata.json +31 -0
- package/screen-recording-workspace/iteration-1/eval-0-fullscreen/with_skill/grading.json +11 -0
- package/screen-recording-workspace/iteration-1/eval-0-fullscreen/with_skill/outputs/demo.mp4 +0 -0
- package/screen-recording-workspace/iteration-1/eval-0-fullscreen/with_skill/timing.json +5 -0
- package/screen-recording-workspace/iteration-1/eval-0-fullscreen/without_skill/grading.json +11 -0
- package/screen-recording-workspace/iteration-1/eval-0-fullscreen/without_skill/outputs/demo.mp4 +0 -0
- package/screen-recording-workspace/iteration-1/eval-0-fullscreen/without_skill/timing.json +5 -0
- package/screen-recording-workspace/iteration-1/eval-1-region-audio/eval_metadata.json +31 -0
- package/screen-recording-workspace/iteration-1/eval-1-region-audio/with_skill/grading.json +11 -0
- package/screen-recording-workspace/iteration-1/eval-1-region-audio/with_skill/outputs/region_capture.mp4 +0 -0
- package/screen-recording-workspace/iteration-1/eval-1-region-audio/with_skill/timing.json +5 -0
- package/screen-recording-workspace/iteration-1/eval-1-region-audio/without_skill/grading.json +11 -0
- package/screen-recording-workspace/iteration-1/eval-1-region-audio/without_skill/outputs/region_capture.mp4 +0 -0
- package/screen-recording-workspace/iteration-1/eval-1-region-audio/without_skill/timing.json +5 -0
- package/screen-recording-workspace/iteration-1/eval-2-python-fallback/eval_metadata.json +31 -0
- package/screen-recording-workspace/iteration-1/eval-2-python-fallback/with_skill/grading.json +11 -0
- package/screen-recording-workspace/iteration-1/eval-2-python-fallback/with_skill/outputs/fallback_recording.mp4 +0 -0
- package/screen-recording-workspace/iteration-1/eval-2-python-fallback/with_skill/timing.json +5 -0
- package/screen-recording-workspace/iteration-1/eval-2-python-fallback/without_skill/grading.json +11 -0
- package/screen-recording-workspace/iteration-1/eval-2-python-fallback/without_skill/outputs/fallback_recording.mp4 +0 -0
- package/screen-recording-workspace/iteration-1/eval-2-python-fallback/without_skill/outputs/record_screen.py +67 -0
- package/screen-recording-workspace/iteration-1/eval-2-python-fallback/without_skill/timing.json +5 -0
- package/screen-recording-workspace/iteration-1/review.html +1325 -0
- package/src/skills/codex-cli/SKILL.md +21 -11
- package/src/skills/codex-cli/evals/evals.json +47 -0
- package/src/skills/gemini-cli/SKILL.md +27 -13
- package/src/skills/gemini-cli/evals/evals.json +46 -0
- 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/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/tm-search/SKILL.md +242 -106
- package/src/skills/tm-search/evals/evals.json +23 -0
- 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/screen-recording/references/approach2-xvfb.md +0 -232
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Python Fallback — Screen Recording without FFmpeg
|
|
2
|
+
|
|
3
|
+
Use this approach when FFmpeg is not installed. Captures screenshots in a loop using `mss` and assembles them into MP4 with OpenCV.
|
|
4
|
+
|
|
5
|
+
> **Limitations**: No audio capture, no cursor capture, lower FPS than FFmpeg, larger CPU usage.
|
|
6
|
+
|
|
7
|
+
## Dependencies
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install mss opencv-python numpy
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Full Working Template
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
#!/usr/bin/env python3
|
|
17
|
+
"""
|
|
18
|
+
Screen recorder using Python only (mss + OpenCV).
|
|
19
|
+
No FFmpeg required. Cross-platform: Windows, macOS, Linux.
|
|
20
|
+
|
|
21
|
+
Limitations:
|
|
22
|
+
- No audio capture
|
|
23
|
+
- No cursor capture
|
|
24
|
+
- Lower FPS (~15-25 depending on resolution and hardware)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import time, os, sys
|
|
28
|
+
import numpy as np
|
|
29
|
+
import cv2
|
|
30
|
+
import mss
|
|
31
|
+
|
|
32
|
+
def record_screen(output="recording.mp4", duration=30, fps=20,
|
|
33
|
+
region=None, monitor=1):
|
|
34
|
+
"""
|
|
35
|
+
Record the screen using mss + OpenCV.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
output: Output file path (.mp4)
|
|
39
|
+
duration: Recording duration in seconds
|
|
40
|
+
fps: Target frames per second (actual may be lower)
|
|
41
|
+
region: Tuple (x, y, w, h) for specific region, or None for full screen
|
|
42
|
+
monitor: Monitor number (1 = primary, 2 = secondary, etc.)
|
|
43
|
+
"""
|
|
44
|
+
with mss.mss() as sct:
|
|
45
|
+
# Define capture area
|
|
46
|
+
if region:
|
|
47
|
+
x, y, w, h = region
|
|
48
|
+
capture_area = {"left": x, "top": y, "width": w, "height": h}
|
|
49
|
+
else:
|
|
50
|
+
mon = sct.monitors[monitor] # monitors[0] is "all monitors"
|
|
51
|
+
capture_area = {
|
|
52
|
+
"left": mon["left"],
|
|
53
|
+
"top": mon["top"],
|
|
54
|
+
"width": mon["width"],
|
|
55
|
+
"height": mon["height"],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
width = capture_area["width"]
|
|
59
|
+
height = capture_area["height"]
|
|
60
|
+
|
|
61
|
+
# Setup video writer
|
|
62
|
+
# Try H264 first, fall back to mp4v
|
|
63
|
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
64
|
+
writer = cv2.VideoWriter(output, fourcc, fps, (width, height))
|
|
65
|
+
|
|
66
|
+
if not writer.isOpened():
|
|
67
|
+
raise RuntimeError(f"Failed to open VideoWriter for {output}")
|
|
68
|
+
|
|
69
|
+
print(f"Recording {width}x{height} @ {fps}fps → {output}")
|
|
70
|
+
print(f"Duration: {duration}s (Ctrl+C to stop early)")
|
|
71
|
+
|
|
72
|
+
frame_count = 0
|
|
73
|
+
frame_interval = 1.0 / fps
|
|
74
|
+
start_time = time.time()
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
while True:
|
|
78
|
+
frame_start = time.time()
|
|
79
|
+
elapsed = frame_start - start_time
|
|
80
|
+
|
|
81
|
+
if elapsed >= duration:
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
# Capture screenshot
|
|
85
|
+
screenshot = sct.grab(capture_area)
|
|
86
|
+
# Convert BGRA → BGR (OpenCV format)
|
|
87
|
+
frame = np.array(screenshot)[:, :, :3]
|
|
88
|
+
# mss returns BGRA, but numpy slice gives BGR which is what OpenCV expects
|
|
89
|
+
|
|
90
|
+
writer.write(frame)
|
|
91
|
+
frame_count += 1
|
|
92
|
+
|
|
93
|
+
# Frame rate control
|
|
94
|
+
frame_time = time.time() - frame_start
|
|
95
|
+
sleep_time = frame_interval - frame_time
|
|
96
|
+
if sleep_time > 0:
|
|
97
|
+
time.sleep(sleep_time)
|
|
98
|
+
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
print("\nStopped by user.")
|
|
101
|
+
|
|
102
|
+
writer.release()
|
|
103
|
+
actual_duration = time.time() - start_time
|
|
104
|
+
actual_fps = frame_count / actual_duration if actual_duration > 0 else 0
|
|
105
|
+
size = os.path.getsize(output) if os.path.exists(output) else 0
|
|
106
|
+
|
|
107
|
+
print(f"Saved: {output}")
|
|
108
|
+
print(f" Frames: {frame_count}")
|
|
109
|
+
print(f" Duration: {actual_duration:.1f}s")
|
|
110
|
+
print(f" Actual FPS: {actual_fps:.1f}")
|
|
111
|
+
print(f" Size: {size:,} bytes ({size // 1024} KB)")
|
|
112
|
+
|
|
113
|
+
return output
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def list_monitors():
|
|
117
|
+
"""List available monitors and their dimensions."""
|
|
118
|
+
with mss.mss() as sct:
|
|
119
|
+
for i, mon in enumerate(sct.monitors):
|
|
120
|
+
if i == 0:
|
|
121
|
+
print(f" [0] All monitors combined: {mon['width']}x{mon['height']}")
|
|
122
|
+
else:
|
|
123
|
+
print(f" [{i}] Monitor {i}: {mon['width']}x{mon['height']} at ({mon['left']}, {mon['top']})")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
import argparse
|
|
128
|
+
parser = argparse.ArgumentParser(description="Record screen (Python only)")
|
|
129
|
+
parser.add_argument("output", nargs="?", default="recording.mp4")
|
|
130
|
+
parser.add_argument("--duration", "-t", type=int, default=30)
|
|
131
|
+
parser.add_argument("--fps", type=int, default=20)
|
|
132
|
+
parser.add_argument("--region", "-r", type=str, default=None,
|
|
133
|
+
help="x,y,w,h (e.g. 100,200,800,600)")
|
|
134
|
+
parser.add_argument("--monitor", "-m", type=int, default=1,
|
|
135
|
+
help="Monitor number (default: 1 = primary)")
|
|
136
|
+
parser.add_argument("--list-monitors", action="store_true",
|
|
137
|
+
help="List available monitors and exit")
|
|
138
|
+
args = parser.parse_args()
|
|
139
|
+
|
|
140
|
+
if args.list_monitors:
|
|
141
|
+
list_monitors()
|
|
142
|
+
sys.exit(0)
|
|
143
|
+
|
|
144
|
+
region = tuple(map(int, args.region.split(","))) if args.region else None
|
|
145
|
+
record_screen(args.output, args.duration, args.fps, region, args.monitor)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Multi-Monitor Support
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
with mss.mss() as sct:
|
|
152
|
+
# sct.monitors[0] = virtual screen (all monitors combined)
|
|
153
|
+
# sct.monitors[1] = primary monitor
|
|
154
|
+
# sct.monitors[2] = secondary monitor, etc.
|
|
155
|
+
|
|
156
|
+
# Record specific monitor
|
|
157
|
+
mon = sct.monitors[2] # secondary monitor
|
|
158
|
+
screenshot = sct.grab(mon)
|
|
159
|
+
|
|
160
|
+
# Record all monitors as one image
|
|
161
|
+
screenshot = sct.grab(sct.monitors[0])
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Performance Tips
|
|
165
|
+
|
|
166
|
+
### Reduce resolution for higher FPS
|
|
167
|
+
```python
|
|
168
|
+
# Capture at half resolution, then resize for writing
|
|
169
|
+
frame = np.array(sct.grab(capture_area))[:, :, :3]
|
|
170
|
+
small = cv2.resize(frame, (width // 2, height // 2))
|
|
171
|
+
writer.write(small)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Use JPEG compression for intermediate frames
|
|
175
|
+
```python
|
|
176
|
+
# For very long recordings, write frames as JPEG to reduce memory/disk
|
|
177
|
+
_, buf = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
|
178
|
+
# Later decode for video assembly
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Codec alternatives
|
|
182
|
+
```python
|
|
183
|
+
# mp4v — most compatible, larger files
|
|
184
|
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
185
|
+
|
|
186
|
+
# XVID — better compression (requires ffmpeg/xvid codec)
|
|
187
|
+
fourcc = cv2.VideoWriter_fourcc(*"XVID")
|
|
188
|
+
# Output file should be .avi for XVID
|
|
189
|
+
|
|
190
|
+
# H264 — best compression (may not be available everywhere)
|
|
191
|
+
fourcc = cv2.VideoWriter_fourcc(*"avc1") # macOS
|
|
192
|
+
fourcc = cv2.VideoWriter_fourcc(*"H264") # Linux
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Adding Audio After Recording
|
|
196
|
+
|
|
197
|
+
If you need audio, record video with this script, then merge audio separately:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
import subprocess
|
|
201
|
+
|
|
202
|
+
def add_audio_to_video(video_path, audio_path, output_path):
|
|
203
|
+
"""Merge separately-recorded audio into the video."""
|
|
204
|
+
subprocess.run([
|
|
205
|
+
"ffmpeg", "-i", video_path, "-i", audio_path,
|
|
206
|
+
"-c:v", "copy", "-c:a", "aac", "-b:a", "128k",
|
|
207
|
+
"-shortest", output_path, "-y", "-loglevel", "quiet"
|
|
208
|
+
], check=True)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Comparison: Python Fallback vs FFmpeg
|
|
212
|
+
|
|
213
|
+
| Feature | Python (mss+cv2) | FFmpeg Native |
|
|
214
|
+
|---------|-------------------|---------------|
|
|
215
|
+
| **FPS** | 15-25 (resolution-dependent) | 30-60 |
|
|
216
|
+
| **Audio** | No | Yes |
|
|
217
|
+
| **Cursor** | No | Yes |
|
|
218
|
+
| **CPU Usage** | Higher | Lower |
|
|
219
|
+
| **Dependencies** | pip only | System FFmpeg |
|
|
220
|
+
| **File Size** | Larger (mp4v codec) | Smaller (H.264) |
|
|
221
|
+
| **Setup** | Easier | Requires FFmpeg install |
|
|
222
|
+
| **Reliability** | Good | Excellent |
|
|
@@ -16,123 +16,283 @@ description: >
|
|
|
16
16
|
|
|
17
17
|
This skill enables an agent to search, validate, and analyze US trademarks via USPTO APIs.
|
|
18
18
|
|
|
19
|
-
## API
|
|
19
|
+
## Important: API Access Reality
|
|
20
|
+
|
|
21
|
+
The USPTO trademark search system (`tmsearch.uspto.gov`) uses **AWS WAF bot protection** on its
|
|
22
|
+
Elasticsearch search backend. Direct keyword search via HTTP is **not possible** without browser
|
|
23
|
+
automation. However, the **TSDR details API** (case lookup by serial number) works without
|
|
24
|
+
authentication and returns JSON.
|
|
25
|
+
|
|
26
|
+
For **keyword search**, use one of these approaches:
|
|
27
|
+
1. **Playwright/browser automation** — drive the tmsearch.uspto.gov web UI (most reliable)
|
|
28
|
+
2. **Third-party API** — RapidAPI USPTO wrapper (requires API key, simpler to use)
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
For **case status lookup by serial number**, the built-in TSDR API works directly.
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
## API Overview
|
|
33
|
+
|
|
34
|
+
| Use Case | Approach |
|
|
24
35
|
|---|---|
|
|
25
|
-
| Keyword/name text search
|
|
26
|
-
|
|
|
27
|
-
| Batch
|
|
36
|
+
| Keyword/name text search | Browser automation on tmsearch.uspto.gov **or** RapidAPI |
|
|
37
|
+
| Case status by serial number | TSDR Details API (no auth, JSON) |
|
|
38
|
+
| Batch keyword validation | Browser automation + delays **or** RapidAPI |
|
|
39
|
+
| Case documents / images | TSDR API (requires API key since Oct 2024) |
|
|
28
40
|
|
|
29
41
|
---
|
|
30
42
|
|
|
31
|
-
## API 1:
|
|
43
|
+
## API 1: TSDR Details API (No auth required — JSON)
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
Look up trademark case details **by serial number**. This endpoint is hosted on the tmsearch
|
|
46
|
+
site and returns JSON without authentication.
|
|
35
47
|
|
|
36
|
-
###
|
|
48
|
+
### Endpoint
|
|
37
49
|
|
|
38
50
|
```
|
|
39
|
-
|
|
40
|
-
Content-Type: application/json
|
|
51
|
+
GET https://tmsearch.uspto.gov/tsdr-api-v1-0-0/tsdr-api?serialNumber={SERIAL_NUMBER}
|
|
41
52
|
```
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
```json
|
|
45
|
-
{
|
|
46
|
-
"keyword": "APPLE",
|
|
47
|
-
"searchType": "1",
|
|
48
|
-
"statusType": "A",
|
|
49
|
-
"pluralVariants": false,
|
|
50
|
-
"start": 0,
|
|
51
|
-
"rows": 25
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
**Parameters:**
|
|
56
|
-
- `keyword` — the word/phrase to search (required)
|
|
57
|
-
- `searchType` — `"1"` = basic keyword, `"2"` = design code, `"3"` = owner name
|
|
58
|
-
- `statusType` — `"A"` = active/live only, `"D"` = dead only, `""` = all
|
|
59
|
-
- `pluralVariants` — `true` to include plural variations
|
|
60
|
-
- `start` — pagination offset (0-indexed)
|
|
61
|
-
- `rows` — results per page (max 500)
|
|
62
|
-
|
|
63
|
-
### Alternative: GET search
|
|
54
|
+
Serial numbers are 8 digits (no dashes). Example:
|
|
64
55
|
|
|
65
|
-
```
|
|
66
|
-
|
|
56
|
+
```bash
|
|
57
|
+
curl -s "https://tmsearch.uspto.gov/tsdr-api-v1-0-0/tsdr-api?serialNumber=78787878" \
|
|
58
|
+
-H "Accept: application/json"
|
|
67
59
|
```
|
|
68
60
|
|
|
69
|
-
### Response
|
|
61
|
+
### Response Structure
|
|
70
62
|
|
|
71
63
|
```json
|
|
72
64
|
{
|
|
73
|
-
"
|
|
74
|
-
|
|
65
|
+
"metadata": {
|
|
66
|
+
"caseStatus": "Abandoned because the applicant failed to respond...",
|
|
67
|
+
"statusDate": "2007-09-25",
|
|
68
|
+
"owners": [
|
|
69
|
+
{
|
|
70
|
+
"ipInfo": {
|
|
71
|
+
"name": "OWNER NAME",
|
|
72
|
+
"legalEntity": "3",
|
|
73
|
+
"contactAddress": {
|
|
74
|
+
"mailingAddresses": [
|
|
75
|
+
{
|
|
76
|
+
"cityName": "City",
|
|
77
|
+
"geographicRegionName": "STATE",
|
|
78
|
+
"postalCode": "12345",
|
|
79
|
+
"countryName": "UNITED STATES OF AMERICA"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"citizenship": {
|
|
84
|
+
"countryName": "UNITED STATES OF AMERICA",
|
|
85
|
+
"geographicRegionName": "STATE"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"attorney": {
|
|
91
|
+
"ipInfo": { "name": "Attorney Name" }
|
|
92
|
+
},
|
|
93
|
+
"correspondent": {
|
|
94
|
+
"ipInfo": {
|
|
95
|
+
"name": "Correspondent Name",
|
|
96
|
+
"contactAddress": { "mailingAddresses": [...], "electronicAddresses": [...] }
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"markDetails": {
|
|
100
|
+
"isStandardCharClaimed": false
|
|
101
|
+
},
|
|
102
|
+
"tm5Status": {
|
|
103
|
+
"tm5StatusDescription": "...",
|
|
104
|
+
"tm5StatusCode": "10",
|
|
105
|
+
"tm5StatusDescriptor": "DEAD/APPLICATION/Refused/Dismissed or Invalidated",
|
|
106
|
+
"tm5LiveDead": "dead"
|
|
107
|
+
},
|
|
108
|
+
"classes": [
|
|
109
|
+
{ "classNumber": "009", "firstUseAnywhereDate": null, "firstUseInCommerceDate": null }
|
|
110
|
+
],
|
|
111
|
+
"docketNumber": "..."
|
|
112
|
+
},
|
|
113
|
+
"maintenance": {},
|
|
114
|
+
"prosecutionHistory": [
|
|
75
115
|
{
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"status": "Live/Registered",
|
|
80
|
-
"statusCode": "A",
|
|
81
|
-
"filingDate": "1997-03-15",
|
|
82
|
-
"registrationDate": "1999-08-17",
|
|
83
|
-
"owner": "Apple Inc.",
|
|
84
|
-
"ownerAddress": "Cupertino, CALIFORNIA, UNITED STATES",
|
|
85
|
-
"internationalClassification": ["009", "042"],
|
|
86
|
-
"goodsServices": "Computers, computer software...",
|
|
87
|
-
"attorney": "...",
|
|
88
|
-
"markDrawingCode": "4"
|
|
116
|
+
"historyDate": "2026-03-06",
|
|
117
|
+
"historyDescription": "Amended Drawing",
|
|
118
|
+
"documentId": "https://tsdr.uspto.gov/documentviewer?caseId=sn78787878&docId=..."
|
|
89
119
|
}
|
|
90
|
-
]
|
|
120
|
+
],
|
|
121
|
+
"assignments": [...],
|
|
122
|
+
"proceedings": [...],
|
|
123
|
+
"international": {}
|
|
91
124
|
}
|
|
92
125
|
```
|
|
93
126
|
|
|
94
|
-
|
|
95
|
-
> returns errors, fall back to scraping `https://tmsearch.uspto.gov/search/search-information` or
|
|
96
|
-
> use the TSDR API below. See `references/scraping-fallback.md` for the web scraping approach.
|
|
127
|
+
### Key Response Fields
|
|
97
128
|
|
|
98
|
-
|
|
129
|
+
| Path | Description |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `metadata.caseStatus` | Human-readable current status |
|
|
132
|
+
| `metadata.statusDate` | Date of last status change |
|
|
133
|
+
| `metadata.owners[].ipInfo.name` | Owner/applicant name |
|
|
134
|
+
| `metadata.owners[].ipInfo.contactAddress` | Owner address |
|
|
135
|
+
| `metadata.attorney.ipInfo.name` | Attorney of record |
|
|
136
|
+
| `metadata.tm5Status.tm5LiveDead` | `"live"` or `"dead"` |
|
|
137
|
+
| `metadata.tm5Status.tm5StatusDescriptor` | Structured status (e.g., `"LIVE/REGISTRATION/Registered"`) |
|
|
138
|
+
| `metadata.classes[].classNumber` | Nice Classification codes |
|
|
139
|
+
| `metadata.markDetails.isStandardCharClaimed` | Whether it's a standard character mark |
|
|
140
|
+
| `prosecutionHistory[]` | Timeline of case events with document links |
|
|
141
|
+
|
|
142
|
+
> **Note:** This API does NOT return the word mark text itself. To get the mark text, you need
|
|
143
|
+
> the keyword search approach (browser automation or RapidAPI).
|
|
99
144
|
|
|
100
|
-
|
|
145
|
+
---
|
|
101
146
|
|
|
102
|
-
|
|
147
|
+
## API 2: TSDR Bulk API (Requires API key — XML)
|
|
103
148
|
|
|
104
|
-
|
|
149
|
+
The original TSDR API at `tsdrapi.uspto.gov` now **requires an API key for all requests** (changed
|
|
150
|
+
October 2024). Register at https://account.uspto.gov/api-manager/.
|
|
105
151
|
|
|
106
|
-
###
|
|
152
|
+
### Endpoints
|
|
107
153
|
|
|
108
154
|
```bash
|
|
109
|
-
# Case status
|
|
110
|
-
GET https://tsdrapi.uspto.gov/ts/cd/casestatus/sn{SERIAL_NUMBER}/content.html
|
|
111
|
-
|
|
112
|
-
# Case status as XML (machine-readable)
|
|
155
|
+
# Case status as XML (requires API key)
|
|
113
156
|
GET https://tsdrapi.uspto.gov/ts/cd/casestatus/sn{SERIAL_NUMBER}/info.xml
|
|
157
|
+
Header: USPTO-API-KEY: YOUR_KEY
|
|
158
|
+
|
|
159
|
+
# Case status as HTML
|
|
160
|
+
GET https://tsdrapi.uspto.gov/ts/cd/casestatus/sn{SERIAL_NUMBER}/content.html
|
|
161
|
+
Header: USPTO-API-KEY: YOUR_KEY
|
|
114
162
|
|
|
115
|
-
#
|
|
163
|
+
# By registration number
|
|
116
164
|
GET https://tsdrapi.uspto.gov/ts/cd/casestatus/rn{REG_NUMBER}/info.xml
|
|
117
165
|
|
|
118
|
-
# Case documents
|
|
166
|
+
# Case documents PDF bundle
|
|
119
167
|
GET https://tsdrapi.uspto.gov/ts/cd/casedocs/bundle.pdf?sn={SERIAL_NUMBER}
|
|
120
168
|
|
|
121
169
|
# Raw trademark image
|
|
122
170
|
GET https://tsdrapi.uspto.gov/ts/cd/rawImage/{SERIAL_NUMBER}
|
|
123
171
|
```
|
|
124
172
|
|
|
125
|
-
|
|
173
|
+
Rate limit: **60 requests/minute per API key**.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Keyword Search via Browser Automation (Recommended)
|
|
178
|
+
|
|
179
|
+
Since the tmsearch.uspto.gov search backend is protected by AWS WAF, use Playwright to drive
|
|
180
|
+
the web UI and intercept API responses.
|
|
181
|
+
|
|
182
|
+
**Important:** The AWS WAF bot detection is non-deterministic — headless browsers are blocked
|
|
183
|
+
intermittently. Use anti-detection settings and retries. Even with these, keyword search may
|
|
184
|
+
fail occasionally. The RapidAPI wrapper (below) is more reliable if you need consistent results.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from playwright.sync_api import sync_playwright
|
|
188
|
+
|
|
189
|
+
def search_trademark(keyword: str, max_results: int = 25) -> dict:
|
|
190
|
+
"""Search USPTO trademarks by keyword using browser automation."""
|
|
191
|
+
results = {"totalFound": 0, "trademarks": []}
|
|
192
|
+
|
|
193
|
+
with sync_playwright() as p:
|
|
194
|
+
# Anti-detection settings to bypass AWS WAF
|
|
195
|
+
browser = p.chromium.launch(
|
|
196
|
+
headless=True,
|
|
197
|
+
args=["--disable-blink-features=AutomationControlled"],
|
|
198
|
+
)
|
|
199
|
+
context = browser.new_context(
|
|
200
|
+
user_agent=(
|
|
201
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
202
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
203
|
+
"Chrome/120.0.0.0 Safari/537.36"
|
|
204
|
+
),
|
|
205
|
+
viewport={"width": 1920, "height": 1080},
|
|
206
|
+
)
|
|
207
|
+
page = context.new_page()
|
|
208
|
+
page.add_init_script(
|
|
209
|
+
'Object.defineProperty(navigator, "webdriver", {get: () => undefined})'
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Intercept the Elasticsearch API responses
|
|
213
|
+
def handle_response(response):
|
|
214
|
+
if "prod-stage" in response.url and response.status == 200:
|
|
215
|
+
try:
|
|
216
|
+
data = response.json()
|
|
217
|
+
if isinstance(data, dict) and "hits" in data:
|
|
218
|
+
hits = data.get("hits", {})
|
|
219
|
+
total = hits.get("total", {})
|
|
220
|
+
results["totalFound"] = total.get("value", 0) if isinstance(total, dict) else total
|
|
221
|
+
for hit in hits.get("hits", [])[:max_results]:
|
|
222
|
+
results["trademarks"].append(hit.get("_source", {}))
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
page.on("response", handle_response)
|
|
227
|
+
page.goto("https://tmsearch.uspto.gov/search/search-information", timeout=30000)
|
|
228
|
+
page.wait_for_load_state("networkidle")
|
|
229
|
+
page.wait_for_timeout(2000) # Wait for WAF challenge to resolve
|
|
230
|
+
|
|
231
|
+
# Type keyword and submit
|
|
232
|
+
search_input = page.locator('input[type="text"]').first
|
|
233
|
+
search_input.fill(keyword.upper(), timeout=10000)
|
|
234
|
+
search_input.press("Enter")
|
|
235
|
+
|
|
236
|
+
# Wait for results (longer wait needed for WAF + async results)
|
|
237
|
+
page.wait_for_timeout(6000)
|
|
238
|
+
|
|
239
|
+
browser.close()
|
|
240
|
+
|
|
241
|
+
return results
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Install Playwright
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
pip install playwright
|
|
248
|
+
playwright install chromium
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Keyword Search via RapidAPI (Alternative — requires API key)
|
|
254
|
+
|
|
255
|
+
If browser automation is not practical, the RapidAPI unofficial USPTO wrapper provides reliable
|
|
256
|
+
keyword search.
|
|
126
257
|
|
|
127
|
-
|
|
128
|
-
- Add key as header: `USPTO-API-KEY: YOUR_KEY_HERE`
|
|
129
|
-
- Rate limit: **60 requests/minute per API key**
|
|
258
|
+
**Sign up:** https://rapidapi.com/pentium10/api/uspto-trademark
|
|
130
259
|
|
|
131
260
|
```bash
|
|
132
|
-
|
|
133
|
-
|
|
261
|
+
# Keyword search (active marks)
|
|
262
|
+
curl "https://uspto-trademark.p.rapidapi.com/v1/trademarkSearch/APPLE/active" \
|
|
263
|
+
-H "x-rapidapi-host: uspto-trademark.p.rapidapi.com" \
|
|
264
|
+
-H "x-rapidapi-key: YOUR_RAPIDAPI_KEY"
|
|
265
|
+
|
|
266
|
+
# Availability check
|
|
267
|
+
curl "https://uspto-trademark.p.rapidapi.com/v1/trademarkAvailable/CLOUDPEAK" \
|
|
268
|
+
-H "x-rapidapi-host: uspto-trademark.p.rapidapi.com" \
|
|
269
|
+
-H "x-rapidapi-key: YOUR_RAPIDAPI_KEY"
|
|
270
|
+
|
|
271
|
+
# Search by owner
|
|
272
|
+
curl "https://uspto-trademark.p.rapidapi.com/v1/ownerSearch/Apple%20Inc/" \
|
|
273
|
+
-H "x-rapidapi-host: uspto-trademark.p.rapidapi.com" \
|
|
274
|
+
-H "x-rapidapi-key: YOUR_RAPIDAPI_KEY"
|
|
275
|
+
|
|
276
|
+
# Lookup by serial number
|
|
277
|
+
curl "https://uspto-trademark.p.rapidapi.com/v1/serialSearch/78787878" \
|
|
278
|
+
-H "x-rapidapi-host: uspto-trademark.p.rapidapi.com" \
|
|
279
|
+
-H "x-rapidapi-key: YOUR_RAPIDAPI_KEY"
|
|
280
|
+
|
|
281
|
+
# Batch search (POST)
|
|
282
|
+
curl -X POST "https://uspto-trademark.p.rapidapi.com/v1/batchTrademarkSearch" \
|
|
283
|
+
-H "x-rapidapi-host: uspto-trademark.p.rapidapi.com" \
|
|
284
|
+
-H "x-rapidapi-key: YOUR_RAPIDAPI_KEY" \
|
|
285
|
+
-H "Content-Type: application/json" \
|
|
286
|
+
-d '{"keywords": ["CLOUDPEAK", "SKYBRIDGE", "NEONPULSE"]}'
|
|
134
287
|
```
|
|
135
288
|
|
|
289
|
+
RapidAPI endpoints:
|
|
290
|
+
- `/v1/trademarkSearch/{keyword}/{status}` — keyword search (`active`, `dead`, or `all`)
|
|
291
|
+
- `/v1/trademarkAvailable/{keyword}` — simple yes/no availability
|
|
292
|
+
- `/v1/ownerSearch/{owner_name}/{postcode}` — search by owner
|
|
293
|
+
- `/v1/serialSearch/{serial_number}` — lookup by serial number
|
|
294
|
+
- `/v1/batchTrademarkSearch` — multiple keywords (POST)
|
|
295
|
+
|
|
136
296
|
---
|
|
137
297
|
|
|
138
298
|
## CLI Tool Implementation (tm-search)
|
|
@@ -142,11 +302,11 @@ When building the `tm-search` command-line tool, follow this structure:
|
|
|
142
302
|
### Core Commands
|
|
143
303
|
|
|
144
304
|
```bash
|
|
145
|
-
tm-search keyword <word> # Search by keyword
|
|
305
|
+
tm-search keyword <word> # Search by keyword (uses Playwright)
|
|
146
306
|
tm-search keyword <word> --status=A # Active trademarks only
|
|
147
307
|
tm-search keyword <word> --status=D # Dead trademarks only
|
|
148
308
|
tm-search available <word> # Check if word is available (not registered live)
|
|
149
|
-
tm-search status <serial_number> # Lookup by serial number
|
|
309
|
+
tm-search status <serial_number> # Lookup by serial number (uses TSDR API)
|
|
150
310
|
tm-search batch <word1,word2,...> # Check multiple words
|
|
151
311
|
tm-search validate <file.txt> # Validate words from a file (one per line)
|
|
152
312
|
```
|
|
@@ -177,58 +337,34 @@ Top matches:
|
|
|
177
337
|
A keyword is considered **AVAILABLE** if there are zero live/active marks with exact OR confusingly
|
|
178
338
|
similar text. The agent should:
|
|
179
339
|
|
|
180
|
-
1. Search exact keyword
|
|
340
|
+
1. Search exact keyword (via Playwright or RapidAPI)
|
|
181
341
|
2. If results > 0 → **LIKELY REGISTERED** — show matches
|
|
182
342
|
3. If results == 0 → **LIKELY AVAILABLE** — note this is not legal advice
|
|
183
343
|
4. Always caveat: suggest professional trademark attorney review before filing
|
|
184
344
|
|
|
185
|
-
```python
|
|
186
|
-
def check_availability(keyword):
|
|
187
|
-
results = search_trademark(keyword, status="A")
|
|
188
|
-
if results["totalFound"] == 0:
|
|
189
|
-
return "LIKELY AVAILABLE"
|
|
190
|
-
else:
|
|
191
|
-
return f"LIKELY TAKEN ({results['totalFound']} active marks)"
|
|
192
|
-
```
|
|
193
|
-
|
|
194
345
|
---
|
|
195
346
|
|
|
196
347
|
## Batch Validation
|
|
197
348
|
|
|
198
|
-
For validating a list of keywords
|
|
199
|
-
|
|
200
|
-
```python
|
|
201
|
-
words = ["CloudPeak", "SkyBridge", "NeonPulse"]
|
|
202
|
-
results = []
|
|
203
|
-
for word in words:
|
|
204
|
-
r = search_trademark(word.upper(), status="A")
|
|
205
|
-
results.append({
|
|
206
|
-
"keyword": word,
|
|
207
|
-
"status": "AVAILABLE" if r["totalFound"] == 0 else "TAKEN",
|
|
208
|
-
"count": r["totalFound"]
|
|
209
|
-
})
|
|
210
|
-
# Output as table or CSV
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
Rate-limit: Add 0.5–1s delay between requests to avoid throttling.
|
|
349
|
+
For validating a list of keywords, add a 2–3 second delay between Playwright searches to avoid
|
|
350
|
+
triggering rate limits. With RapidAPI, 0.5–1s delay is sufficient.
|
|
214
351
|
|
|
215
352
|
---
|
|
216
353
|
|
|
217
354
|
## Implementation Notes
|
|
218
355
|
|
|
219
|
-
- The `tmsearch.uspto.gov`
|
|
356
|
+
- The `tmsearch.uspto.gov` search backend is protected by **AWS WAF** — direct HTTP keyword search does not work
|
|
357
|
+
- The TSDR Details API (`/tsdr-api-v1-0-0/tsdr-api?serialNumber=...`) works without auth and returns JSON
|
|
358
|
+
- The old TSDR XML API (`tsdrapi.uspto.gov`) requires an **API key for all requests** since October 2024
|
|
220
359
|
- Always **uppercase** keywords before searching (USPTO stores marks in uppercase)
|
|
221
|
-
- Include `User-Agent` header to avoid bot detection: `"Mozilla/5.0 (compatible; tm-search/1.0)"`
|
|
222
|
-
- For `pluralVariants=true`, USPTO auto-expands COFFEE → COFFEES, etc.
|
|
223
360
|
- International Classification (Nice Classification) codes define goods/services category
|
|
224
361
|
- Serial numbers are 8 digits; Registration numbers are 7 digits
|
|
225
362
|
|
|
226
363
|
## Reference Files
|
|
227
364
|
|
|
228
365
|
- `references/field-guide.md` — Full field descriptions and Nice Classification codes
|
|
229
|
-
- `references/scraping-fallback.md` —
|
|
366
|
+
- `references/scraping-fallback.md` — Browser automation fallback details
|
|
230
367
|
- `scripts/tm_search.py` — Ready-to-use Python implementation
|
|
231
|
-
- `scripts/tm_validate.py` — Batch validation script
|
|
232
368
|
|
|
233
369
|
---
|
|
234
370
|
|
|
@@ -237,4 +373,4 @@ Rate-limit: Add 0.5–1s delay between requests to avoid throttling.
|
|
|
237
373
|
Always include when providing availability results:
|
|
238
374
|
> "This is a preliminary search only. Trademark availability is complex and depends on many
|
|
239
375
|
> factors including similar marks, geographic use, and goods/services classification. Consult a
|
|
240
|
-
> licensed trademark attorney before filing."
|
|
376
|
+
> licensed trademark attorney before filing."
|