@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.
Files changed (80) hide show
  1. package/.claude/settings.local.json +13 -0
  2. package/CLAUDE.md +55 -0
  3. package/LICENSE +1 -1
  4. package/README.md +208 -39
  5. package/bin/cli.js +39 -0
  6. package/package.json +30 -17
  7. package/registry/registry.json +166 -1
  8. package/registry/schema.json +10 -0
  9. package/src/commands/skills/add.js +194 -0
  10. package/src/commands/skills/list.js +52 -0
  11. package/src/commands/skills/remove.js +27 -0
  12. package/src/commands/skills/update.js +74 -0
  13. package/src/config.js +5 -0
  14. package/src/skills/codex-cli/SKILL.md +265 -0
  15. package/src/skills/commafeed-api/SKILL.md +1012 -0
  16. package/src/skills/gemini-cli/SKILL.md +379 -0
  17. package/src/skills/gemini-cli/references/commands.md +145 -0
  18. package/src/skills/gemini-cli/references/configuration.md +182 -0
  19. package/src/skills/gemini-cli/references/headless-and-scripting.md +181 -0
  20. package/src/skills/gemini-cli/references/mcp-and-extensions.md +254 -0
  21. package/src/skills/n8n-api/SKILL.md +623 -0
  22. package/src/skills/notebook-lm/SKILL.md +217 -0
  23. package/src/skills/notebook-lm/references/artifact-options.md +168 -0
  24. package/src/skills/notebook-lm/references/auth.md +58 -0
  25. package/src/skills/notebook-lm/references/workflows.md +144 -0
  26. package/src/skills/screen-recording/SKILL.md +309 -0
  27. package/src/skills/screen-recording/references/approach1-programmatic.md +311 -0
  28. package/src/skills/screen-recording/references/approach2-xvfb.md +232 -0
  29. package/src/skills/screen-recording/references/design-patterns.md +168 -0
  30. package/src/skills/test-mobile-app/SKILL.md +212 -0
  31. package/src/skills/test-mobile-app/references/report-template.md +95 -0
  32. package/src/skills/test-mobile-app/references/setup-appium.md +154 -0
  33. package/src/skills/test-mobile-app/scripts/analyze_apk.py +164 -0
  34. package/src/skills/test-mobile-app/scripts/check_environment.py +116 -0
  35. package/src/skills/test-mobile-app/scripts/generate_report.py +250 -0
  36. package/src/skills/test-mobile-app/scripts/run_tests.py +326 -0
  37. package/src/skills/test-web-ui/SKILL.md +232 -0
  38. package/src/skills/test-web-ui/references/test_case_schema.md +102 -0
  39. package/src/skills/test-web-ui/scripts/discover.py +176 -0
  40. package/src/skills/test-web-ui/scripts/generate_report.py +237 -0
  41. package/src/skills/test-web-ui/scripts/run_tests.py +296 -0
  42. package/src/skills/text-to-speech/SKILL.md +236 -0
  43. package/src/skills/text-to-speech/references/espeak-cli.md +277 -0
  44. package/src/skills/text-to-speech/references/kokoro-onnx.md +124 -0
  45. package/src/skills/text-to-speech/references/online-engines.md +128 -0
  46. package/src/skills/text-to-speech/references/pyttsx3-espeak.md +143 -0
  47. package/src/skills/tm-search/SKILL.md +240 -0
  48. package/src/skills/tm-search/references/field-guide.md +79 -0
  49. package/src/skills/tm-search/references/scraping-fallback.md +140 -0
  50. package/src/skills/tm-search/scripts/tm_search.py +375 -0
  51. package/src/skills/wp-rest-api/SKILL.md +114 -0
  52. package/src/skills/wp-rest-api/references/authentication.md +18 -0
  53. package/src/skills/wp-rest-api/references/custom-content-types.md +20 -0
  54. package/src/skills/wp-rest-api/references/discovery-and-params.md +20 -0
  55. package/src/skills/wp-rest-api/references/responses-and-fields.md +30 -0
  56. package/src/skills/wp-rest-api/references/routes-and-endpoints.md +36 -0
  57. package/src/skills/wp-rest-api/references/schema.md +22 -0
  58. package/src/skills/youtube-search/SKILL.md +412 -0
  59. package/src/skills/youtube-search/references/parsing-examples.md +159 -0
  60. package/src/skills/youtube-search/references/youtube-api-quota.md +85 -0
  61. package/src/skills/youtube-thumbnail/SKILL.md +1060 -0
  62. package/tests/commands/info.test.js +49 -0
  63. package/tests/commands/install.test.js +36 -0
  64. package/tests/commands/list.test.js +66 -0
  65. package/tests/commands/publish.test.js +182 -0
  66. package/tests/commands/search.test.js +45 -0
  67. package/tests/commands/uninstall.test.js +29 -0
  68. package/tests/commands/update.test.js +59 -0
  69. package/tests/functional/skills-lifecycle.test.js +293 -0
  70. package/tests/helpers/fixtures.js +63 -0
  71. package/tests/integration/cli.test.js +83 -0
  72. package/tests/skills/add.test.js +138 -0
  73. package/tests/skills/list.test.js +63 -0
  74. package/tests/skills/remove.test.js +38 -0
  75. package/tests/skills/update.test.js +60 -0
  76. package/tests/unit/config.test.js +31 -0
  77. package/tests/unit/registry.test.js +79 -0
  78. package/tests/unit/utils.test.js +150 -0
  79. package/tests/validation/registry-schema.test.js +112 -0
  80. package/tests/validation/skills-validation.test.js +96 -0
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Web Tester - Site Discovery
4
+ Auto-crawls a website to discover pages, forms, links, and interactive elements.
5
+ """
6
+
7
+ import json
8
+ import sys
9
+ import os
10
+ import argparse
11
+ from urllib.parse import urljoin, urlparse
12
+
13
+ try:
14
+ from playwright.sync_api import sync_playwright
15
+ except ImportError:
16
+ print("ERROR: playwright not installed.")
17
+ sys.exit(1)
18
+
19
+
20
+ def discover_site(url, max_pages=10):
21
+ """Crawl site and return structured discovery data."""
22
+ parsed = urlparse(url)
23
+ base_domain = f"{parsed.scheme}://{parsed.netloc}"
24
+
25
+ visited = set()
26
+ to_visit = [url]
27
+ pages = []
28
+
29
+ with sync_playwright() as p:
30
+ browser = p.chromium.launch(args=['--no-sandbox', '--disable-dev-shm-usage'])
31
+ context = browser.new_context(viewport={'width': 1280, 'height': 800})
32
+ page = context.new_page()
33
+
34
+ while to_visit and len(visited) < max_pages:
35
+ current_url = to_visit.pop(0)
36
+ if current_url in visited:
37
+ continue
38
+ visited.add(current_url)
39
+
40
+ try:
41
+ page.goto(current_url, wait_until='domcontentloaded', timeout=15000)
42
+ page.wait_for_load_state('networkidle', timeout=8000)
43
+ except Exception as e:
44
+ print(f" SKIP {current_url}: {e}")
45
+ continue
46
+
47
+ page_data = {
48
+ 'url': current_url,
49
+ 'title': page.title(),
50
+ 'headings': [],
51
+ 'links': [],
52
+ 'forms': [],
53
+ 'buttons': [],
54
+ 'images': [],
55
+ 'nav_items': [],
56
+ 'console_errors': [],
57
+ }
58
+
59
+ # Collect console errors
60
+ console_errors = []
61
+ page.on('console', lambda msg: console_errors.append(msg.text) if msg.type == 'error' else None)
62
+
63
+ # Headings
64
+ for tag in ['h1', 'h2', 'h3']:
65
+ elements = page.locator(tag).all()
66
+ for el in elements[:5]:
67
+ try:
68
+ text = el.text_content().strip()
69
+ if text:
70
+ page_data['headings'].append({'tag': tag, 'text': text[:100]})
71
+ except Exception:
72
+ pass
73
+
74
+ # Navigation
75
+ nav_els = page.locator('nav a, header a, [role=navigation] a').all()
76
+ for el in nav_els[:20]:
77
+ try:
78
+ text = el.text_content().strip()
79
+ href = el.get_attribute('href') or ''
80
+ if text and href:
81
+ page_data['nav_items'].append({'text': text, 'href': href})
82
+ except Exception:
83
+ pass
84
+
85
+ # All links
86
+ links = page.locator('a[href]').all()
87
+ for link in links[:50]:
88
+ try:
89
+ href = link.get_attribute('href') or ''
90
+ text = link.text_content().strip()
91
+ if href and not href.startswith('#') and not href.startswith('javascript'):
92
+ full_url = urljoin(current_url, href)
93
+ page_data['links'].append({'text': text[:60], 'href': full_url})
94
+ if base_domain in full_url and full_url not in visited:
95
+ to_visit.append(full_url)
96
+ except Exception:
97
+ pass
98
+
99
+ # Forms
100
+ forms = page.locator('form').all()
101
+ for form in forms:
102
+ try:
103
+ inputs = form.locator('input, textarea, select').all()
104
+ buttons = form.locator('button, input[type=submit]').all()
105
+ form_data = {
106
+ 'action': form.get_attribute('action') or '',
107
+ 'method': form.get_attribute('method') or 'GET',
108
+ 'inputs': [],
109
+ 'submit_buttons': [],
110
+ }
111
+ for inp in inputs[:10]:
112
+ inp_type = inp.get_attribute('type') or 'text'
113
+ inp_name = inp.get_attribute('name') or inp.get_attribute('id') or ''
114
+ inp_placeholder = inp.get_attribute('placeholder') or ''
115
+ form_data['inputs'].append({
116
+ 'type': inp_type,
117
+ 'name': inp_name,
118
+ 'placeholder': inp_placeholder,
119
+ })
120
+ for btn in buttons[:3]:
121
+ btn_text = btn.text_content().strip() or btn.get_attribute('value') or ''
122
+ form_data['submit_buttons'].append(btn_text)
123
+ page_data['forms'].append(form_data)
124
+ except Exception:
125
+ pass
126
+
127
+ # Buttons
128
+ buttons = page.locator('button:visible, [role=button]:visible').all()
129
+ for btn in buttons[:15]:
130
+ try:
131
+ text = btn.text_content().strip()
132
+ if text:
133
+ page_data['buttons'].append(text[:60])
134
+ except Exception:
135
+ pass
136
+
137
+ # Images
138
+ images = page.locator('img').all()
139
+ broken = 0
140
+ for img in images:
141
+ try:
142
+ natural_w = img.evaluate('el => el.naturalWidth')
143
+ if natural_w == 0:
144
+ broken += 1
145
+ except Exception:
146
+ pass
147
+ page_data['images'] = {'total': len(images), 'broken': broken}
148
+
149
+ page_data['console_errors'] = console_errors[:5]
150
+ pages.append(page_data)
151
+ print(f" โœ“ Discovered: {current_url} ({page.title()})")
152
+
153
+ browser.close()
154
+
155
+ return {
156
+ 'base_url': url,
157
+ 'pages_found': len(pages),
158
+ 'pages': pages,
159
+ }
160
+
161
+
162
+ if __name__ == '__main__':
163
+ parser = argparse.ArgumentParser()
164
+ parser.add_argument('--url', required=True)
165
+ parser.add_argument('--max-pages', type=int, default=10)
166
+ parser.add_argument('--output', default='/home/claude/discovery.json')
167
+ args = parser.parse_args()
168
+
169
+ print(f"\nDiscovering: {args.url}")
170
+ result = discover_site(args.url, args.max_pages)
171
+
172
+ with open(args.output, 'w', encoding='utf-8') as f:
173
+ json.dump(result, f, ensure_ascii=False, indent=2)
174
+
175
+ print(f"\nDiscovery complete: {result['pages_found']} pages")
176
+ print(f"Saved to: {args.output}")
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Web Tester - HTML Report Generator
4
+ Produces a self-contained HTML QA report from test results.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ import base64
11
+ import argparse
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+
15
+
16
+ def load_screenshot_b64(path):
17
+ if not path or not os.path.exists(path):
18
+ return None
19
+ with open(path, 'rb') as f:
20
+ return base64.b64encode(f.read()).decode('utf-8')
21
+
22
+
23
+ def status_badge(status):
24
+ colors = {'PASS': '#22c55e', 'FAIL': '#ef4444', 'SKIP': '#f59e0b', 'WARN': '#f97316'}
25
+ color = colors.get(status, '#6b7280')
26
+ return f'<span style="background:{color};color:#fff;padding:2px 10px;border-radius:4px;font-size:12px;font-weight:700">{status}</span>'
27
+
28
+
29
+ def generate_report(results_path, screenshots_dir, output_path):
30
+ with open(results_path, 'r', encoding='utf-8') as f:
31
+ data = json.load(f)
32
+
33
+ meta = data['meta']
34
+ results = data['results']
35
+
36
+ total = meta.get('total', len(results))
37
+ passed = meta.get('passed', 0)
38
+ failed = meta.get('failed', 0)
39
+ skipped = meta.get('skipped', 0)
40
+ pass_rate = meta.get('pass_rate', 0)
41
+ url = meta.get('url', 'N/A')
42
+ run_at = meta.get('run_at', datetime.now().isoformat())
43
+ site_name = meta.get('site_name', url)
44
+
45
+ # Status color for header
46
+ rate_color = '#22c55e' if pass_rate >= 80 else '#f97316' if pass_rate >= 50 else '#ef4444'
47
+
48
+ # Build test rows
49
+ rows_html = ''
50
+ screenshots_html = ''
51
+
52
+ for r in results:
53
+ status = r.get('status', 'UNKNOWN')
54
+ errors = r.get('errors', [])
55
+ warnings = r.get('warnings', [])
56
+ console_errors = r.get('console_errors', [])
57
+ duration = r.get('duration_ms', 0)
58
+ assertions = r.get('assertions_total', 0)
59
+ assertions_passed = r.get('assertions_passed', 0)
60
+
61
+ error_text = '<br>'.join(f'โŒ {e}' for e in errors)
62
+ warning_text = '<br>'.join(f'โš ๏ธ {w}' for w in warnings)
63
+ detail_text = error_text + ('<br>' if error_text and warning_text else '') + warning_text
64
+ if not detail_text:
65
+ detail_text = 'โ€”'
66
+
67
+ rows_html += f"""
68
+ <tr>
69
+ <td style="font-family:monospace;font-size:12px;color:#64748b">{r.get('id','')}</td>
70
+ <td style="font-weight:500">{r.get('name','')}</td>
71
+ <td style="text-align:center">{status_badge(status)}</td>
72
+ <td style="text-align:center;color:#64748b;font-size:13px">{assertions_passed}/{assertions}</td>
73
+ <td style="text-align:center;color:#64748b;font-size:13px">{duration}ms</td>
74
+ <td style="font-size:12px;color:#64748b;max-width:300px">{detail_text}</td>
75
+ </tr>
76
+ """
77
+
78
+ # Screenshots section
79
+ before_b64 = load_screenshot_b64(r.get('screenshot_before'))
80
+ after_b64 = load_screenshot_b64(r.get('screenshot_after'))
81
+
82
+ if before_b64 or after_b64:
83
+ bg = '#fef2f2' if status == 'FAIL' else '#f0fdf4'
84
+ screenshots_html += f"""
85
+ <div style="border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:{bg}">
86
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
87
+ {status_badge(status)}
88
+ <strong style="font-size:14px">{r.get('id','')} โ€” {r.get('name','')}</strong>
89
+ </div>
90
+ """
91
+ if errors:
92
+ screenshots_html += f'<div style="color:#ef4444;font-size:12px;margin-bottom:10px">' + '<br>'.join(errors) + '</div>'
93
+
94
+ screenshots_html += '<div style="display:flex;gap:16px;flex-wrap:wrap">'
95
+ if before_b64:
96
+ screenshots_html += f"""
97
+ <div>
98
+ <div style="font-size:11px;color:#64748b;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.5px">Before</div>
99
+ <img src="data:image/png;base64,{before_b64}" style="max-width:580px;width:100%;border:1px solid #e2e8f0;border-radius:4px">
100
+ </div>"""
101
+ if after_b64:
102
+ screenshots_html += f"""
103
+ <div>
104
+ <div style="font-size:11px;color:#64748b;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.5px">After interaction</div>
105
+ <img src="data:image/png;base64,{after_b64}" style="max-width:580px;width:100%;border:1px solid #e2e8f0;border-radius:4px">
106
+ </div>"""
107
+ screenshots_html += '</div></div>'
108
+
109
+ # Recommendations
110
+ recs = []
111
+ if failed > 0:
112
+ failed_names = [r.get('name','') for r in results if r.get('status') == 'FAIL']
113
+ recs.append(f"๐Ÿ”ด Fix {failed} failing test(s): {', '.join(failed_names[:3])}{'...' if len(failed_names) > 3 else ''}")
114
+ console_err_total = sum(len(r.get('console_errors', [])) for r in results)
115
+ if console_err_total > 0:
116
+ recs.append(f"๐ŸŸก Resolve {console_err_total} JavaScript console error(s) to improve reliability")
117
+ broken_img_tests = [r for r in results if any('broken image' in w.lower() for w in r.get('warnings', []))]
118
+ if broken_img_tests:
119
+ recs.append("๐ŸŸก Fix broken/unloaded images detected on the page")
120
+ alt_warnings = [r for r in results if any('alt text' in w.lower() for w in r.get('warnings', []))]
121
+ if alt_warnings:
122
+ recs.append("๐ŸŸก Add missing alt text to images for accessibility compliance")
123
+ if not recs:
124
+ recs.append("โœ… All critical checks passed. Site appears to be functioning correctly.")
125
+
126
+ recs_html = ''.join(f'<li style="margin-bottom:8px">{r}</li>' for r in recs)
127
+
128
+ html = f"""<!DOCTYPE html>
129
+ <html lang="en">
130
+ <head>
131
+ <meta charset="UTF-8">
132
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
133
+ <title>QA Report โ€” {site_name}</title>
134
+ <style>
135
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
136
+ body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #1e293b; }}
137
+ .header {{ background: linear-gradient(135deg, #1e293b 0%, #334155 100%); color: white; padding: 40px; }}
138
+ .header h1 {{ font-size: 28px; font-weight: 700; margin-bottom: 4px; }}
139
+ .header p {{ color: #94a3b8; font-size: 14px; }}
140
+ .container {{ max-width: 1200px; margin: 0 auto; padding: 32px 24px; }}
141
+ .stats {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; margin-bottom: 32px; }}
142
+ .stat {{ background: white; border-radius: 12px; padding: 20px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }}
143
+ .stat .num {{ font-size: 36px; font-weight: 800; }}
144
+ .stat .label {{ font-size: 13px; color: #64748b; margin-top: 4px; }}
145
+ .section {{ background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }}
146
+ .section h2 {{ font-size: 18px; font-weight: 600; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #f1f5f9; }}
147
+ table {{ width: 100%; border-collapse: collapse; font-size: 14px; }}
148
+ th {{ background: #f8fafc; text-align: left; padding: 10px 12px; color: #64748b; font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }}
149
+ td {{ padding: 12px; border-top: 1px solid #f1f5f9; vertical-align: top; }}
150
+ tr:hover td {{ background: #f8fafc; }}
151
+ .progress-bar {{ height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden; margin-top: 8px; }}
152
+ .progress-fill {{ height: 100%; background: {rate_color}; border-radius: 4px; width: {pass_rate}%; }}
153
+ </style>
154
+ </head>
155
+ <body>
156
+
157
+ <div class="header">
158
+ <h1>๐Ÿงช QA Test Report</h1>
159
+ <p>{site_name} &nbsp;|&nbsp; {run_at[:19].replace('T', ' ')} &nbsp;|&nbsp; {url}</p>
160
+ </div>
161
+
162
+ <div class="container">
163
+
164
+ <div class="stats">
165
+ <div class="stat">
166
+ <div class="num" style="color:{rate_color}">{pass_rate}%</div>
167
+ <div class="label">Pass Rate</div>
168
+ <div class="progress-bar"><div class="progress-fill"></div></div>
169
+ </div>
170
+ <div class="stat">
171
+ <div class="num">{total}</div>
172
+ <div class="label">Total Tests</div>
173
+ </div>
174
+ <div class="stat">
175
+ <div class="num" style="color:#22c55e">{passed}</div>
176
+ <div class="label">Passed</div>
177
+ </div>
178
+ <div class="stat">
179
+ <div class="num" style="color:#ef4444">{failed}</div>
180
+ <div class="label">Failed</div>
181
+ </div>
182
+ <div class="stat">
183
+ <div class="num" style="color:#f59e0b">{skipped}</div>
184
+ <div class="label">Skipped</div>
185
+ </div>
186
+ </div>
187
+
188
+ <div class="section">
189
+ <h2>๐Ÿ“‹ Test Results</h2>
190
+ <table>
191
+ <thead>
192
+ <tr>
193
+ <th>ID</th>
194
+ <th>Test Case</th>
195
+ <th>Status</th>
196
+ <th>Assertions</th>
197
+ <th>Duration</th>
198
+ <th>Details</th>
199
+ </tr>
200
+ </thead>
201
+ <tbody>
202
+ {rows_html}
203
+ </tbody>
204
+ </table>
205
+ </div>
206
+
207
+ <div class="section">
208
+ <h2>๐Ÿ’ก Recommendations</h2>
209
+ <ul style="list-style:none;padding:0">
210
+ {recs_html}
211
+ </ul>
212
+ </div>
213
+
214
+ <div class="section">
215
+ <h2>๐Ÿ“ธ Screenshots</h2>
216
+ {screenshots_html if screenshots_html else '<p style="color:#64748b">No screenshots captured.</p>'}
217
+ </div>
218
+
219
+ </div>
220
+ </body>
221
+ </html>"""
222
+
223
+ os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else '.', exist_ok=True)
224
+ with open(output_path, 'w', encoding='utf-8') as f:
225
+ f.write(html)
226
+
227
+ print(f"โœ… Report saved: {output_path}")
228
+ return output_path
229
+
230
+
231
+ if __name__ == '__main__':
232
+ parser = argparse.ArgumentParser()
233
+ parser.add_argument('--results', required=True)
234
+ parser.add_argument('--screenshots', default='')
235
+ parser.add_argument('--output', required=True)
236
+ args = parser.parse_args()
237
+ generate_report(args.results, args.screenshots, args.output)