@tydung26/product-kit 1.3.2 → 1.5.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 (32) hide show
  1. package/README.md +4 -8
  2. package/dist/scripts/market-intel/search-app-store.d.ts +7 -0
  3. package/dist/scripts/market-intel/search-app-store.d.ts.map +1 -0
  4. package/dist/scripts/market-intel/search-app-store.js +91 -0
  5. package/dist/scripts/market-intel/search-app-store.js.map +1 -0
  6. package/dist/scripts/market-intel/search-google-play.d.ts +7 -0
  7. package/dist/scripts/market-intel/search-google-play.d.ts.map +1 -0
  8. package/dist/scripts/market-intel/search-google-play.js +195 -0
  9. package/dist/scripts/market-intel/search-google-play.js.map +1 -0
  10. package/dist/scripts/market-intel/search-product-hunt.d.ts +7 -0
  11. package/dist/scripts/market-intel/search-product-hunt.d.ts.map +1 -0
  12. package/dist/scripts/market-intel/search-product-hunt.js +236 -0
  13. package/dist/scripts/market-intel/search-product-hunt.js.map +1 -0
  14. package/dist/scripts/market-intel/search-yc-launch.d.ts +7 -0
  15. package/dist/scripts/market-intel/search-yc-launch.d.ts.map +1 -0
  16. package/dist/scripts/market-intel/search-yc-launch.js +229 -0
  17. package/dist/scripts/market-intel/search-yc-launch.js.map +1 -0
  18. package/dist/scripts/market-intel/shared-types.d.ts +44 -0
  19. package/dist/scripts/market-intel/shared-types.d.ts.map +1 -0
  20. package/dist/scripts/market-intel/shared-types.js +63 -0
  21. package/dist/scripts/market-intel/shared-types.js.map +1 -0
  22. package/package.json +8 -9
  23. package/skills/market-intel/SKILL.md +184 -61
  24. package/skills/market-intel/scripts/search-app-store.py +117 -0
  25. package/skills/market-intel/scripts/search-google-play.py +179 -0
  26. package/skills/market-intel/scripts/search-product-hunt.py +194 -0
  27. package/skills/market-intel/scripts/search-yc-launch.py +160 -0
  28. package/skills/naming/SKILL.md +66 -0
  29. package/dist/commands/config/index.d.ts +0 -3
  30. package/dist/commands/config/index.d.ts.map +0 -1
  31. package/dist/commands/config/index.js +0 -34
  32. package/dist/commands/config/index.js.map +0 -1
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python3
2
+ """Product Hunt crawler via HTML scraping + __NEXT_DATA__ extraction. Zero deps.
3
+ Usage: python3 search-product-hunt.py "<keywords>" [limit]
4
+ Output: JSON CrawlResult to stdout
5
+ """
6
+
7
+ import json
8
+ import re
9
+ import sys
10
+ import urllib.request
11
+ import urllib.parse
12
+ from datetime import datetime, timezone
13
+
14
+
15
+ def safe_fetch(url, timeout=10):
16
+ """Fetch URL with timeout and user-agent header."""
17
+ req = urllib.request.Request(url, headers={
18
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
19
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
20
+ "Chrome/120.0.0.0 Safari/537.36",
21
+ "Accept-Language": "en-US,en;q=0.9",
22
+ })
23
+ return urllib.request.urlopen(req, timeout=timeout).read().decode("utf-8", errors="replace")
24
+
25
+
26
+ def truncate(text, max_len=500):
27
+ return text[:max_len] + "..." if len(text) > max_len else text
28
+
29
+
30
+ def extract_next_data(html):
31
+ """Extract __NEXT_DATA__ JSON from a Next.js page."""
32
+ match = re.search(r'<script\s+id="__NEXT_DATA__"[^>]*>(.*?)</script>', html, re.DOTALL)
33
+ if not match:
34
+ return None
35
+ try:
36
+ return json.loads(match.group(1))
37
+ except (json.JSONDecodeError, ValueError):
38
+ return None
39
+
40
+
41
+ def deep_find_posts(obj, depth=0, max_depth=8):
42
+ """Recursively find post-like objects in nested data."""
43
+ if depth > max_depth or not isinstance(obj, (dict, list)):
44
+ return []
45
+
46
+ posts = []
47
+ if isinstance(obj, list):
48
+ for item in obj:
49
+ posts.extend(deep_find_posts(item, depth + 1, max_depth))
50
+ return posts
51
+
52
+ # Check if this dict looks like a post
53
+ if "slug" in obj and "name" in obj and isinstance(obj.get("name"), str):
54
+ posts.append(obj)
55
+
56
+ for value in obj.values():
57
+ if isinstance(value, (dict, list)):
58
+ posts.extend(deep_find_posts(value, depth + 1, max_depth))
59
+
60
+ return posts
61
+
62
+
63
+ def extract_post_urls(html):
64
+ """Extract product post URLs from search results."""
65
+ urls = []
66
+ seen = set()
67
+ for match in re.finditer(r'href="(/posts/[^"?#]+)"', html):
68
+ path = match.group(1)
69
+ if path not in seen:
70
+ seen.add(path)
71
+ urls.append(f"https://www.producthunt.com{path}")
72
+ return urls
73
+
74
+
75
+ def extract_post_from_page(html, url):
76
+ """Extract product details from a Product Hunt post page."""
77
+ next_data = extract_next_data(html)
78
+
79
+ if next_data:
80
+ # Search for post object in __NEXT_DATA__
81
+ posts = deep_find_posts(next_data)
82
+ for post in posts:
83
+ name = post.get("name", "")
84
+ if not name:
85
+ continue
86
+
87
+ tagline = post.get("tagline", "")
88
+ description = post.get("description", "") or tagline
89
+ votes = post.get("votesCount", 0)
90
+ rating = post.get("reviewsRating")
91
+ review_count = post.get("reviewsCount") or votes
92
+ topics = [t.get("name", "") for t in post.get("topics", []) if isinstance(t, dict)]
93
+
94
+ return {
95
+ "name": name,
96
+ "url": url,
97
+ "description": truncate(description),
98
+ "tagline": tagline or None,
99
+ "rating": round(float(rating), 1) if rating else None,
100
+ "reviewCount": review_count or None,
101
+ "pricing": {"free": True, "other": post.get("pricing")},
102
+ "features": [t for t in topics[:5] if t],
103
+ "reviews": [],
104
+ }
105
+
106
+ # Fallback: meta tags
107
+ name_match = re.search(r'<meta\s+property="og:title"\s+content="([^"]*)"', html)
108
+ desc_match = re.search(r'<meta\s+property="og:description"\s+content="([^"]*)"', html)
109
+ name = name_match.group(1).replace(" | Product Hunt", "") if name_match else ""
110
+ description = desc_match.group(1) if desc_match else ""
111
+
112
+ if not name:
113
+ return None
114
+
115
+ return {
116
+ "name": name,
117
+ "url": url,
118
+ "description": truncate(description),
119
+ "tagline": None,
120
+ "pricing": {"free": True},
121
+ "features": [],
122
+ "reviews": [],
123
+ }
124
+
125
+
126
+ def main():
127
+ if len(sys.argv) < 2:
128
+ print(json.dumps({"error": "Usage: python3 search-product-hunt.py <keywords> [limit]"}))
129
+ sys.exit(1)
130
+
131
+ query = sys.argv[1]
132
+ limit = 5
133
+ if len(sys.argv) >= 3:
134
+ try:
135
+ limit = max(1, min(int(sys.argv[2]), 10))
136
+ except ValueError:
137
+ limit = 5
138
+
139
+ errors = []
140
+ search_url = f"https://www.producthunt.com/search?q={urllib.parse.quote(query)}"
141
+
142
+ try:
143
+ search_html = safe_fetch(search_url)
144
+ except Exception as e:
145
+ print(json.dumps({
146
+ "platform": "product_hunt", "query": query,
147
+ "timestamp": datetime.now(timezone.utc).isoformat(),
148
+ "results": [], "errors": [f"Product Hunt search error: {e}"],
149
+ }, indent=2))
150
+ return
151
+
152
+ # Try to get post URLs from search page
153
+ post_urls = extract_post_urls(search_html)[:limit]
154
+
155
+ # Fallback: extract from __NEXT_DATA__ on search page
156
+ if not post_urls:
157
+ next_data = extract_next_data(search_html)
158
+ if next_data:
159
+ posts = deep_find_posts(next_data)
160
+ post_urls = [
161
+ f"https://www.producthunt.com/posts/{p['slug']}"
162
+ for p in posts[:limit] if p.get("slug")
163
+ ]
164
+
165
+ if not post_urls:
166
+ errors.append("No post URLs found — page may require JS rendering")
167
+ print(json.dumps({
168
+ "platform": "product_hunt", "query": query,
169
+ "timestamp": datetime.now(timezone.utc).isoformat(),
170
+ "results": [], "errors": errors,
171
+ }, indent=2))
172
+ return
173
+
174
+ results = []
175
+ for url in post_urls:
176
+ try:
177
+ page_html = safe_fetch(url)
178
+ entry = extract_post_from_page(page_html, url)
179
+ if entry:
180
+ results.append(entry)
181
+ except Exception as e:
182
+ errors.append(f"Error fetching {url}: {e}")
183
+
184
+ print(json.dumps({
185
+ "platform": "product_hunt",
186
+ "query": query,
187
+ "timestamp": datetime.now(timezone.utc).isoformat(),
188
+ "results": results,
189
+ "errors": errors,
190
+ }, indent=2))
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env python3
2
+ """YC Launch (Y Combinator) crawler via HTML scraping. Zero external dependencies.
3
+ Usage: python3 search-yc-launch.py "<keywords>" [limit]
4
+ Output: JSON CrawlResult to stdout
5
+ """
6
+
7
+ import json
8
+ import re
9
+ import sys
10
+ import urllib.request
11
+ import urllib.parse
12
+ from datetime import datetime, timezone
13
+
14
+
15
+ def safe_fetch(url, timeout=10):
16
+ """Fetch URL with timeout and user-agent header."""
17
+ req = urllib.request.Request(url, headers={
18
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
19
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
20
+ "Chrome/120.0.0.0 Safari/537.36",
21
+ "Accept-Language": "en-US,en;q=0.9",
22
+ })
23
+ return urllib.request.urlopen(req, timeout=timeout).read().decode("utf-8", errors="replace")
24
+
25
+
26
+ def truncate(text, max_len=500):
27
+ return text[:max_len] + "..." if len(text) > max_len else text
28
+
29
+
30
+ def extract_launch_urls(html):
31
+ """Extract launch URLs from YC launches page."""
32
+ urls = []
33
+ seen = set()
34
+ for match in re.finditer(r'href="(/launches/[^"?#]+)"', html):
35
+ path = match.group(1)
36
+ if path not in seen:
37
+ seen.add(path)
38
+ urls.append(f"https://www.ycombinator.com{path}")
39
+ return urls
40
+
41
+
42
+ def extract_launch_from_page(html, url):
43
+ """Extract launch details from a YC launch page."""
44
+ # Try JSON-LD
45
+ ld_match = re.search(
46
+ r'<script[^>]*type="application/ld\+json"[^>]*>(.*?)</script>',
47
+ html, re.DOTALL
48
+ )
49
+ name = ""
50
+ description = ""
51
+
52
+ if ld_match:
53
+ try:
54
+ data = json.loads(ld_match.group(1))
55
+ name = data.get("name", "")
56
+ description = data.get("description", "")
57
+ except (json.JSONDecodeError, ValueError):
58
+ pass
59
+
60
+ # Fallback: meta tags
61
+ if not name:
62
+ m = re.search(r'<meta\s+property="og:title"\s+content="([^"]*)"', html)
63
+ if m:
64
+ name = m.group(1).replace(" | Y Combinator", "").replace("Launch YC: ", "")
65
+
66
+ if not description:
67
+ m = re.search(r'<meta\s+property="og:description"\s+content="([^"]*)"', html)
68
+ if not m:
69
+ m = re.search(r'<meta\s+name="description"\s+content="([^"]*)"', html)
70
+ if m:
71
+ description = m.group(1)
72
+
73
+ if not name:
74
+ # Try h1 tag
75
+ m = re.search(r'<h1[^>]*>([^<]+)</h1>', html)
76
+ if m:
77
+ name = m.group(1).strip()
78
+
79
+ if not name:
80
+ return None
81
+
82
+ # Try to extract longer pitch from page body
83
+ pitch = ""
84
+ # Look for main content paragraphs
85
+ for m in re.finditer(r'<p[^>]*>([^<]{50,})</p>', html):
86
+ candidate = m.group(1).strip()
87
+ if len(candidate) > len(pitch):
88
+ pitch = candidate
89
+
90
+ return {
91
+ "name": name,
92
+ "url": url,
93
+ "description": truncate(pitch or description),
94
+ "tagline": description[:150] if len(description) < 150 else None,
95
+ "rating": None,
96
+ "reviewCount": None,
97
+ "pricing": {"free": True, "other": None},
98
+ "features": [],
99
+ "reviews": [],
100
+ }
101
+
102
+
103
+ def main():
104
+ if len(sys.argv) < 2:
105
+ print(json.dumps({"error": "Usage: python3 search-yc-launch.py <keywords> [limit]"}))
106
+ sys.exit(1)
107
+
108
+ query = sys.argv[1]
109
+ limit = 5
110
+ if len(sys.argv) >= 3:
111
+ try:
112
+ limit = max(1, min(int(sys.argv[2]), 10))
113
+ except ValueError:
114
+ limit = 5
115
+
116
+ errors = []
117
+ search_url = f"https://www.ycombinator.com/launches?q={urllib.parse.quote(query)}"
118
+
119
+ try:
120
+ search_html = safe_fetch(search_url)
121
+ except Exception as e:
122
+ print(json.dumps({
123
+ "platform": "yc_launch", "query": query,
124
+ "timestamp": datetime.now(timezone.utc).isoformat(),
125
+ "results": [], "errors": [f"YC launches error: {e}"],
126
+ }, indent=2))
127
+ return
128
+
129
+ launch_urls = extract_launch_urls(search_html)[:limit]
130
+
131
+ if not launch_urls:
132
+ errors.append("No launch URLs found — page may require JS rendering")
133
+ print(json.dumps({
134
+ "platform": "yc_launch", "query": query,
135
+ "timestamp": datetime.now(timezone.utc).isoformat(),
136
+ "results": [], "errors": errors,
137
+ }, indent=2))
138
+ return
139
+
140
+ results = []
141
+ for url in launch_urls:
142
+ try:
143
+ page_html = safe_fetch(url)
144
+ entry = extract_launch_from_page(page_html, url)
145
+ if entry:
146
+ results.append(entry)
147
+ except Exception as e:
148
+ errors.append(f"Error fetching {url}: {e}")
149
+
150
+ print(json.dumps({
151
+ "platform": "yc_launch",
152
+ "query": query,
153
+ "timestamp": datetime.now(timezone.utc).isoformat(),
154
+ "results": results,
155
+ "errors": errors,
156
+ }, indent=2))
157
+
158
+
159
+ if __name__ == "__main__":
160
+ main()
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: "pkit:naming"
3
+ description: >
4
+ Generate product/project name suggestions across multiple naming styles.
5
+ Use when user says "name my product", "name my app", "suggest a product name",
6
+ "I need a name for my project/tool/service". For software products only.
7
+ license: MIT
8
+ ---
9
+
10
+ # Naming
11
+
12
+ Interview the user and propose **product names** across 8 naming styles. This skill is for naming software products, apps, tools, APIs, and services — not general naming tasks.
13
+
14
+ **⚠️ ONLY for naming/renaming software products, apps, tools, APIs, or services.**
15
+ Do NOT use for: naming people/pets/companies/teams, branding exercises, or domain research.
16
+
17
+ ## Usage
18
+
19
+ ```
20
+ /pkit:naming <optional context>
21
+ ```
22
+
23
+ ## Workflow
24
+
25
+ ### Step 1 — Interview
26
+
27
+ Use `AskUserQuestion` with up to 4 questions at once:
28
+
29
+ 1. What does this project do? (1-sentence pitch)
30
+ 2. Who is the audience? (developers, consumers, enterprise, internal team)
31
+ 3. What personality/feel? (serious, playful, minimal, bold, technical, approachable)
32
+ 4. Any keywords, domain concepts, or technologies central to it?
33
+
34
+ If answers are thin, ask a follow-up batch:
35
+
36
+ 1. Any hard constraints? (must be pronounceable, short, avoid certain words)
37
+ 2. Any names you already like or dislike — and why?
38
+
39
+ ### Step 2 — Generate Names
40
+
41
+ Propose **exactly 3 names per style** across all 6 styles (18 total).
42
+
43
+ Per name format: `**Name** — one-line reason it fits (5–10 words)`
44
+
45
+ | Style | Approach | Example |
46
+ | ------------------ | ----------------------------------- | ------------------------------ |
47
+ | **Descriptive** | Says exactly what it does | `product-kit`, `file-sync` |
48
+ | **Portmanteau** | Blend 2 relevant words | `Snapchat`, `DevKit` |
49
+ | **Brandable** | Invented/abstract, memorable | `Notion`, `Vercel`, `Supabase` |
50
+ | **Metaphor** | Abstract concept mirroring essence | `Forge`, `Anchor`, `Tide` |
51
+ | **Greek / Latin** | Classical roots, timeless authority | `Hermes`, `Aether`, `Kairos` |
52
+ | **Simple / Plain** | Common everyday words, zero jargon | `Quick`, `Base`, `Core`, `Kit` |
53
+
54
+ ### Step 3 — Follow-up
55
+
56
+ End with:
57
+
58
+ > "Want more in a specific style, or variations on any of these?"
59
+
60
+ ## Rules
61
+
62
+ - **Product names only** — for software, apps, tools
63
+ - If user asks to name something non-product (person, pet, etc.), politely redirect: "This skill is for product naming — try `/pkit:naming` with a product concept."
64
+ - **Names only** — no domain checks, no trademark lookups, no availability
65
+ - Group output by style with a `##` heading per style
66
+ - If user provides very little context, state your assumptions briefly and proceed
@@ -1,3 +0,0 @@
1
- import type { CAC } from 'cac';
2
- export declare function registerConfig(cli: CAC): void;
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAI/B,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,QA2BtC"}
@@ -1,34 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerConfig = registerConfig;
4
- const config_1 = require("../../domains/config");
5
- const logger_1 = require("../../shared/logger");
6
- function registerConfig(cli) {
7
- // pkit config → show current config
8
- cli
9
- .command('config', 'View or set pkit configuration')
10
- .action(() => {
11
- const config = (0, config_1.getConfig)();
12
- logger_1.log.plain('\npkit configuration\n');
13
- logger_1.log.plain(` defaultScope ${config.defaultScope}`);
14
- logger_1.log.plain(` toolPaths.claude ${config.toolPaths.claude}`);
15
- logger_1.log.plain(` toolPaths.antigravity ${config.toolPaths.antigravity}`);
16
- logger_1.log.plain(` toolPaths.opencode ${config.toolPaths.opencode}`);
17
- logger_1.log.plain('\nChange with: pkit config set <key> <value>');
18
- logger_1.log.plain('Keys: defaultScope, toolPaths.claude, toolPaths.antigravity, toolPaths.opencode');
19
- });
20
- // pkit config set <key> <value>
21
- cli
22
- .command('config set <key> <value>', 'Set a configuration value')
23
- .action((key, value) => {
24
- try {
25
- (0, config_1.setConfigValue)(key, value);
26
- logger_1.log.success(`Set ${key} = ${value}`);
27
- }
28
- catch (err) {
29
- logger_1.log.error(err instanceof Error ? err.message : String(err));
30
- process.exit(1);
31
- }
32
- });
33
- }
34
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/config/index.ts"],"names":[],"mappings":";;AAIA,wCA2BC;AA9BD,iDAAiE;AACjE,gDAA0C;AAE1C,SAAgB,cAAc,CAAC,GAAQ;IACrC,oCAAoC;IACpC,GAAG;SACA,OAAO,CAAC,QAAQ,EAAE,gCAAgC,CAAC;SACnD,MAAM,CAAC,GAAG,EAAE;QACX,MAAM,MAAM,GAAG,IAAA,kBAAS,GAAE,CAAC;QAC3B,YAAG,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;QACpC,YAAG,CAAC,KAAK,CAAC,0BAA0B,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC;QAC3D,YAAG,CAAC,KAAK,CAAC,0BAA0B,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;QAC/D,YAAG,CAAC,KAAK,CAAC,4BAA4B,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC;QACtE,YAAG,CAAC,KAAK,CAAC,0BAA0B,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC;QACjE,YAAG,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAC1D,YAAG,CAAC,KAAK,CAAC,iFAAiF,CAAC,CAAC;IAC/F,CAAC,CAAC,CAAC;IAEL,gCAAgC;IAChC,GAAG;SACA,OAAO,CAAC,0BAA0B,EAAE,2BAA2B,CAAC;SAChE,MAAM,CAAC,CAAC,GAAW,EAAE,KAAa,EAAE,EAAE;QACrC,IAAI,CAAC;YACH,IAAA,uBAAc,EAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC3B,YAAG,CAAC,OAAO,CAAC,OAAO,GAAG,MAAM,KAAK,EAAE,CAAC,CAAC;QACvC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,YAAG,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}