@biggora/claude-plugins 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +13 -0
- package/CLAUDE.md +55 -0
- package/LICENSE +1 -1
- package/README.md +208 -39
- package/bin/cli.js +39 -0
- package/package.json +30 -17
- package/registry/registry.json +166 -1
- package/registry/schema.json +10 -0
- package/src/commands/skills/add.js +194 -0
- package/src/commands/skills/list.js +52 -0
- package/src/commands/skills/remove.js +27 -0
- package/src/commands/skills/update.js +74 -0
- package/src/config.js +5 -0
- package/src/skills/codex-cli/SKILL.md +265 -0
- package/src/skills/commafeed-api/SKILL.md +1012 -0
- package/src/skills/gemini-cli/SKILL.md +379 -0
- package/src/skills/gemini-cli/references/commands.md +145 -0
- package/src/skills/gemini-cli/references/configuration.md +182 -0
- package/src/skills/gemini-cli/references/headless-and-scripting.md +181 -0
- package/src/skills/gemini-cli/references/mcp-and-extensions.md +254 -0
- package/src/skills/n8n-api/SKILL.md +623 -0
- package/src/skills/notebook-lm/SKILL.md +217 -0
- package/src/skills/notebook-lm/references/artifact-options.md +168 -0
- package/src/skills/notebook-lm/references/auth.md +58 -0
- package/src/skills/notebook-lm/references/workflows.md +144 -0
- package/src/skills/screen-recording/SKILL.md +309 -0
- package/src/skills/screen-recording/references/approach1-programmatic.md +311 -0
- package/src/skills/screen-recording/references/approach2-xvfb.md +232 -0
- package/src/skills/screen-recording/references/design-patterns.md +168 -0
- package/src/skills/test-mobile-app/SKILL.md +212 -0
- package/src/skills/test-mobile-app/references/report-template.md +95 -0
- package/src/skills/test-mobile-app/references/setup-appium.md +154 -0
- package/src/skills/test-mobile-app/scripts/analyze_apk.py +164 -0
- package/src/skills/test-mobile-app/scripts/check_environment.py +116 -0
- package/src/skills/test-mobile-app/scripts/generate_report.py +250 -0
- package/src/skills/test-mobile-app/scripts/run_tests.py +326 -0
- package/src/skills/test-web-ui/SKILL.md +232 -0
- package/src/skills/test-web-ui/references/test_case_schema.md +102 -0
- package/src/skills/test-web-ui/scripts/discover.py +176 -0
- package/src/skills/test-web-ui/scripts/generate_report.py +237 -0
- package/src/skills/test-web-ui/scripts/run_tests.py +296 -0
- package/src/skills/text-to-speech/SKILL.md +236 -0
- package/src/skills/text-to-speech/references/espeak-cli.md +277 -0
- package/src/skills/text-to-speech/references/kokoro-onnx.md +124 -0
- package/src/skills/text-to-speech/references/online-engines.md +128 -0
- package/src/skills/text-to-speech/references/pyttsx3-espeak.md +143 -0
- package/src/skills/tm-search/SKILL.md +240 -0
- package/src/skills/tm-search/references/field-guide.md +79 -0
- package/src/skills/tm-search/references/scraping-fallback.md +140 -0
- package/src/skills/tm-search/scripts/tm_search.py +375 -0
- package/src/skills/wp-rest-api/SKILL.md +114 -0
- package/src/skills/wp-rest-api/references/authentication.md +18 -0
- package/src/skills/wp-rest-api/references/custom-content-types.md +20 -0
- package/src/skills/wp-rest-api/references/discovery-and-params.md +20 -0
- package/src/skills/wp-rest-api/references/responses-and-fields.md +30 -0
- package/src/skills/wp-rest-api/references/routes-and-endpoints.md +36 -0
- package/src/skills/wp-rest-api/references/schema.md +22 -0
- package/src/skills/youtube-search/SKILL.md +412 -0
- package/src/skills/youtube-search/references/parsing-examples.md +159 -0
- package/src/skills/youtube-search/references/youtube-api-quota.md +85 -0
- package/src/skills/youtube-thumbnail/SKILL.md +1060 -0
- package/tests/commands/info.test.js +49 -0
- package/tests/commands/install.test.js +36 -0
- package/tests/commands/list.test.js +66 -0
- package/tests/commands/publish.test.js +182 -0
- package/tests/commands/search.test.js +45 -0
- package/tests/commands/uninstall.test.js +29 -0
- package/tests/commands/update.test.js +59 -0
- package/tests/functional/skills-lifecycle.test.js +293 -0
- package/tests/helpers/fixtures.js +63 -0
- package/tests/integration/cli.test.js +83 -0
- package/tests/skills/add.test.js +138 -0
- package/tests/skills/list.test.js +63 -0
- package/tests/skills/remove.test.js +38 -0
- package/tests/skills/update.test.js +60 -0
- package/tests/unit/config.test.js +31 -0
- package/tests/unit/registry.test.js +79 -0
- package/tests/unit/utils.test.js +150 -0
- package/tests/validation/registry-schema.test.js +112 -0
- package/tests/validation/skills-validation.test.js +96 -0
|
@@ -0,0 +1,375 @@
|
|
|
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()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wp-rest-api
|
|
3
|
+
description: "Use when building, extending, or debugging WordPress REST API endpoints/routes: register_rest_route, WP_REST_Controller/controller classes, schema/argument validation, permission_callback/authentication, response shaping, register_rest_field/register_meta, or exposing CPTs/taxonomies via show_in_rest."
|
|
4
|
+
compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node. Some workflows require WP-CLI."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# WP REST API
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
Use this skill when you need to:
|
|
12
|
+
|
|
13
|
+
- create or update REST routes/endpoints
|
|
14
|
+
- debug 401/403/404 errors or permission/nonce issues
|
|
15
|
+
- add custom fields/meta to REST responses
|
|
16
|
+
- expose custom post types or taxonomies via REST
|
|
17
|
+
- implement schema + argument validation
|
|
18
|
+
- adjust response links/embedding/pagination
|
|
19
|
+
|
|
20
|
+
## Inputs required
|
|
21
|
+
|
|
22
|
+
- Repo root + target plugin/theme/mu-plugin (path to entrypoint).
|
|
23
|
+
- Desired namespace + version (e.g. `my-plugin/v1`) and routes.
|
|
24
|
+
- Authentication mode (cookie + nonce vs application passwords vs auth plugin).
|
|
25
|
+
- Target WordPress version constraints (if below 6.9, call out).
|
|
26
|
+
|
|
27
|
+
## Procedure
|
|
28
|
+
|
|
29
|
+
### 0) Triage and locate REST usage
|
|
30
|
+
|
|
31
|
+
1. Run triage:
|
|
32
|
+
- `node skills/wp-project-triage/scripts/detect_wp_project.mjs`
|
|
33
|
+
2. Search for existing REST usage:
|
|
34
|
+
- `register_rest_route`
|
|
35
|
+
- `WP_REST_Controller`
|
|
36
|
+
- `rest_api_init`
|
|
37
|
+
- `show_in_rest`, `rest_base`, `rest_controller_class`
|
|
38
|
+
|
|
39
|
+
If this is a full site repo, pick the specific plugin/theme before changing code.
|
|
40
|
+
|
|
41
|
+
### 1) Choose the right approach
|
|
42
|
+
|
|
43
|
+
- **Expose CPT/taxonomy in `wp/v2`:**
|
|
44
|
+
- Use `show_in_rest => true` + `rest_base` if needed.
|
|
45
|
+
- Optionally provide `rest_controller_class`.
|
|
46
|
+
- Read `references/custom-content-types.md`.
|
|
47
|
+
- **Custom endpoints:**
|
|
48
|
+
- Use `register_rest_route()` on `rest_api_init`.
|
|
49
|
+
- Prefer a controller class (`WP_REST_Controller` subclass) for anything non-trivial.
|
|
50
|
+
- Read `references/routes-and-endpoints.md` and `references/schema.md`.
|
|
51
|
+
|
|
52
|
+
### 2) Register routes safely (namespaces, methods, permissions)
|
|
53
|
+
|
|
54
|
+
- Use a unique namespace `vendor/v1`; avoid `wp/*` unless core.
|
|
55
|
+
- Always provide `permission_callback` (use `__return_true` for public endpoints).
|
|
56
|
+
- Use `WP_REST_Server::READABLE/CREATABLE/EDITABLE/DELETABLE` constants.
|
|
57
|
+
- Return data via `rest_ensure_response()` or `WP_REST_Response`.
|
|
58
|
+
- Return errors via `WP_Error` with an explicit `status`.
|
|
59
|
+
|
|
60
|
+
Read `references/routes-and-endpoints.md`.
|
|
61
|
+
|
|
62
|
+
### 3) Validate/sanitize request args
|
|
63
|
+
|
|
64
|
+
- Define `args` with `type`, `default`, `required`, `validate_callback`, `sanitize_callback`.
|
|
65
|
+
- Prefer JSON Schema validation with `rest_validate_value_from_schema` then `rest_sanitize_value_from_schema`.
|
|
66
|
+
- Never read `$_GET`/`$_POST` directly inside endpoints; use `WP_REST_Request`.
|
|
67
|
+
|
|
68
|
+
Read `references/schema.md`.
|
|
69
|
+
|
|
70
|
+
### 4) Responses, fields, and links
|
|
71
|
+
|
|
72
|
+
- Do **not** remove core fields from default endpoints; add fields instead.
|
|
73
|
+
- Use `register_rest_field` for computed fields; `register_meta` with `show_in_rest` for meta.
|
|
74
|
+
- For `object`/`array` meta, define schema in `show_in_rest.schema`.
|
|
75
|
+
- If you need unfiltered post content (e.g., ToC plugins injecting HTML), request `?context=edit` to access `content.raw` (auth required). Pair with `_fields=content.raw` to keep responses small.
|
|
76
|
+
- Add related resource links via `WP_REST_Response::add_link()`.
|
|
77
|
+
|
|
78
|
+
Read `references/responses-and-fields.md`.
|
|
79
|
+
|
|
80
|
+
### 5) Authentication and authorization
|
|
81
|
+
|
|
82
|
+
- For wp-admin/JS: cookie auth + `X-WP-Nonce` (action `wp_rest`).
|
|
83
|
+
- For external clients: application passwords (basic auth) or an auth plugin.
|
|
84
|
+
- Use capability checks in `permission_callback` (authorization), not just “logged in”.
|
|
85
|
+
|
|
86
|
+
Read `references/authentication.md`.
|
|
87
|
+
|
|
88
|
+
### 6) Client-facing behavior (discovery, pagination, embeds)
|
|
89
|
+
|
|
90
|
+
- Ensure discovery works (`Link` header or `<link rel="https://api.w.org/">`).
|
|
91
|
+
- Support `_fields`, `_embed`, `_method`, `_envelope`, pagination headers.
|
|
92
|
+
- Remember `per_page` is capped at 100.
|
|
93
|
+
|
|
94
|
+
Read `references/discovery-and-params.md`.
|
|
95
|
+
|
|
96
|
+
## Verification
|
|
97
|
+
|
|
98
|
+
- `/wp-json/` index includes your namespace.
|
|
99
|
+
- `OPTIONS` on your route returns schema (when provided).
|
|
100
|
+
- Endpoint returns expected data; permission failures return 401/403 as appropriate.
|
|
101
|
+
- CPT/taxonomy routes appear under `wp/v2` when `show_in_rest` is true.
|
|
102
|
+
- Run repo lint/tests and any PHP/JS build steps.
|
|
103
|
+
|
|
104
|
+
## Failure modes / debugging
|
|
105
|
+
|
|
106
|
+
- 404: `rest_api_init` not firing, route typo, or permalinks off (use `?rest_route=`).
|
|
107
|
+
- 401/403: missing nonce/auth, or `permission_callback` too strict.
|
|
108
|
+
- `_doing_it_wrong` for missing `permission_callback`: add it (use `__return_true` if public).
|
|
109
|
+
- Invalid params: missing/incorrect `args` schema or validation callbacks.
|
|
110
|
+
- Fields missing: `show_in_rest` false, meta not registered, or CPT lacks `custom-fields` support.
|
|
111
|
+
|
|
112
|
+
## Escalation
|
|
113
|
+
|
|
114
|
+
If version support or behavior is unclear, consult the REST API Handbook and core docs before inventing patterns.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Authentication (summary)
|
|
2
|
+
|
|
3
|
+
## Cookie authentication (in-dashboard / same-site)
|
|
4
|
+
|
|
5
|
+
- Standard for wp-admin and theme/plugin JS.
|
|
6
|
+
- Requires a REST nonce (`wp_rest`) sent as `X-WP-Nonce` header or `_wpnonce` param.
|
|
7
|
+
- If the nonce is missing, the request is treated as unauthenticated even if cookies exist.
|
|
8
|
+
|
|
9
|
+
## Application Passwords (external clients)
|
|
10
|
+
|
|
11
|
+
- Available in WordPress 5.6+.
|
|
12
|
+
- Use HTTPS + Basic Auth with the application password.
|
|
13
|
+
- Recommended over the legacy Basic Auth plugin.
|
|
14
|
+
|
|
15
|
+
## Auth plugins
|
|
16
|
+
|
|
17
|
+
- OAuth 1.0a or JWT plugins are common for external apps.
|
|
18
|
+
- Use only if required; follow plugin docs and security guidance.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Custom Content Types (summary)
|
|
2
|
+
|
|
3
|
+
## Custom post types
|
|
4
|
+
|
|
5
|
+
- Set `show_in_rest => true` in `register_post_type()` to expose in `wp/v2`.
|
|
6
|
+
- Use `rest_base` to change the route slug.
|
|
7
|
+
- Optionally set `rest_controller_class` (must extend `WP_REST_Controller`).
|
|
8
|
+
|
|
9
|
+
## Custom taxonomies
|
|
10
|
+
|
|
11
|
+
- Set `show_in_rest => true` in `register_taxonomy()`.
|
|
12
|
+
- Use `rest_base` and optional `rest_controller_class` (default `WP_REST_Terms_Controller`).
|
|
13
|
+
|
|
14
|
+
## Adding REST support to existing types
|
|
15
|
+
|
|
16
|
+
- Use `register_post_type_args` or `register_taxonomy_args` filters to enable `show_in_rest` for types you do not control.
|
|
17
|
+
|
|
18
|
+
## Discovery links for custom controllers
|
|
19
|
+
|
|
20
|
+
- If you use a custom controller class, use `rest_route_for_post` or `rest_route_for_term` filters to map objects to routes.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Discovery and Global Parameters (summary)
|
|
2
|
+
|
|
3
|
+
## API discovery
|
|
4
|
+
|
|
5
|
+
- REST API root is discovered via the `Link` header: `rel="https://api.w.org/"`.
|
|
6
|
+
- HTML pages also include a `<link rel="https://api.w.org/" href="...">` element.
|
|
7
|
+
- For non-pretty permalinks, use `?rest_route=/`.
|
|
8
|
+
|
|
9
|
+
## Global parameters
|
|
10
|
+
|
|
11
|
+
- `_fields` limits response fields (supports nested meta keys).
|
|
12
|
+
- `_embed` includes linked resources in `_embedded`.
|
|
13
|
+
- `_method` or `X-HTTP-Method-Override` allows POST to simulate PUT/DELETE.
|
|
14
|
+
- `_envelope` puts headers/status in the response body.
|
|
15
|
+
- `_jsonp` enables JSONP for legacy clients.
|
|
16
|
+
|
|
17
|
+
## Pagination
|
|
18
|
+
|
|
19
|
+
- Collections accept `page`, `per_page` (1-100), and `offset`.
|
|
20
|
+
- Pagination headers: `X-WP-Total` and `X-WP-TotalPages`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Responses and Fields (summary)
|
|
2
|
+
|
|
3
|
+
## Do not remove core fields
|
|
4
|
+
|
|
5
|
+
- Removing or changing core fields breaks clients (including wp-admin).
|
|
6
|
+
- Prefer adding new fields or using `_fields` to limit response size.
|
|
7
|
+
|
|
8
|
+
## register_rest_field
|
|
9
|
+
|
|
10
|
+
- Use for computed or custom fields.
|
|
11
|
+
- Provide `get_callback`, optional `update_callback`, and `schema`.
|
|
12
|
+
- Register on `rest_api_init`.
|
|
13
|
+
|
|
14
|
+
## Raw vs rendered content
|
|
15
|
+
|
|
16
|
+
- For posts, `content.rendered` reflects filters (plugins like ToC inject HTML).
|
|
17
|
+
- Use `?context=edit` (authenticated) to access `content.raw`.
|
|
18
|
+
- Combine with `_fields=content.raw` when you only need the editable body.
|
|
19
|
+
|
|
20
|
+
## register_meta / register_post_meta / register_term_meta
|
|
21
|
+
|
|
22
|
+
- Use when the data is stored as meta.
|
|
23
|
+
- Set `show_in_rest => true` to expose under `.meta`.
|
|
24
|
+
- For `object` or `array` types, provide a JSON schema in `show_in_rest.schema`.
|
|
25
|
+
|
|
26
|
+
## Links and embedding
|
|
27
|
+
|
|
28
|
+
- Add links with `WP_REST_Response::add_link( $rel, $href, $attrs )`.
|
|
29
|
+
- Use `embeddable => true` to allow `_embed`.
|
|
30
|
+
- Use IANA rels or a custom URI relation; CURIEs can be registered via `rest_response_link_curies`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Routes and Endpoints (summary)
|
|
2
|
+
|
|
3
|
+
## Registering routes
|
|
4
|
+
|
|
5
|
+
- Register routes on the `rest_api_init` hook with `register_rest_route( $namespace, $route, $args )`.
|
|
6
|
+
- A **route** is the URL pattern; an **endpoint** is the method + callback bound to that route.
|
|
7
|
+
- For non-pretty permalinks, the route is accessed via `?rest_route=/namespace/route`.
|
|
8
|
+
|
|
9
|
+
## Namespacing
|
|
10
|
+
|
|
11
|
+
- Always namespace routes (`vendor/v1`).
|
|
12
|
+
- **Do not** use the `wp/*` namespace unless you are targeting core.
|
|
13
|
+
|
|
14
|
+
## Methods
|
|
15
|
+
|
|
16
|
+
- Use `WP_REST_Server::READABLE` (GET), `CREATABLE` (POST), `EDITABLE` (PUT/PATCH), `DELETABLE` (DELETE).
|
|
17
|
+
- Multiple endpoints can share a route, one per method.
|
|
18
|
+
|
|
19
|
+
## permission_callback (required)
|
|
20
|
+
|
|
21
|
+
- Always provide `permission_callback`.
|
|
22
|
+
- Public endpoints should use `__return_true`.
|
|
23
|
+
- For restricted endpoints, use capability checks (`current_user_can`) or object-level authorization.
|
|
24
|
+
- Missing `permission_callback` emits a `_doing_it_wrong` notice in modern WP.
|
|
25
|
+
|
|
26
|
+
## Arguments
|
|
27
|
+
|
|
28
|
+
- Register `args` to validate and sanitize inputs.
|
|
29
|
+
- Use `type`, `required`, `default`, `validate_callback`, `sanitize_callback`.
|
|
30
|
+
- Access params via the `WP_REST_Request` object, not `$_GET`/`$_POST`.
|
|
31
|
+
|
|
32
|
+
## Return values
|
|
33
|
+
|
|
34
|
+
- Return data via `rest_ensure_response()` or a `WP_REST_Response`.
|
|
35
|
+
- Return `WP_Error` with a `status` in `data` for error responses.
|
|
36
|
+
- Do not call `wp_send_json()` in REST callbacks.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Schema and Argument Validation (summary)
|
|
2
|
+
|
|
3
|
+
## JSON Schema in WordPress
|
|
4
|
+
|
|
5
|
+
- REST API uses JSON Schema (draft 4 subset) for resource and argument definitions.
|
|
6
|
+
- Provide schema via `get_item_schema()` on controllers or `schema` callbacks on routes.
|
|
7
|
+
- Schema enables discovery (`OPTIONS`) and validation.
|
|
8
|
+
|
|
9
|
+
## Validation + sanitization
|
|
10
|
+
|
|
11
|
+
- Use `rest_validate_value_from_schema( $value, $schema )` then `rest_sanitize_value_from_schema( $value, $schema )`.
|
|
12
|
+
- If you override `sanitize_callback`, built-in schema validation will not run; use `rest_validate_request_arg` to keep it.
|
|
13
|
+
- `WP_REST_Controller::get_endpoint_args_for_item_schema()` wires validation automatically.
|
|
14
|
+
|
|
15
|
+
## Schema caching
|
|
16
|
+
|
|
17
|
+
- Cache the generated schema on the controller instance (`$this->schema`) to avoid recomputation.
|
|
18
|
+
|
|
19
|
+
## Formats and types
|
|
20
|
+
|
|
21
|
+
- Common formats: `date-time`, `uri`, `email`, `ip`, `uuid`, `hex-color`.
|
|
22
|
+
- For `array` and `object` types, you must define `items` or `properties` schemas.
|