@opendirectory.dev/skills 0.1.44 → 0.1.46

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 (30) hide show
  1. package/package.json +1 -1
  2. package/registry.json +16 -0
  3. package/skills/noise-to-linkedin-carousel/README.md +81 -0
  4. package/skills/noise-to-linkedin-carousel/SKILL.md +53 -0
  5. package/skills/noise-to-linkedin-carousel/cover.png +0 -0
  6. package/skills/noise-to-linkedin-carousel/references/hook-patterns.md +45 -0
  7. package/skills/noise-to-linkedin-carousel/references/output-format.md +59 -0
  8. package/skills/noise-to-linkedin-carousel/references/quality-checklist.md +12 -0
  9. package/skills/noise-to-linkedin-carousel/references/slide-types.md +57 -0
  10. package/skills/oss-launch-kit/.env.example +2 -0
  11. package/skills/oss-launch-kit/PRD.md +122 -0
  12. package/skills/oss-launch-kit/README.md +27 -0
  13. package/skills/oss-launch-kit/SKILL.md +33 -0
  14. package/skills/oss-launch-kit/TECHNICAL_DESIGN.md +187 -0
  15. package/skills/oss-launch-kit/evals/cli-cli.full.md +49 -0
  16. package/skills/oss-launch-kit/evals/evals.json +44 -0
  17. package/skills/oss-launch-kit/evals/octocat-hello-world.full.md +53 -0
  18. package/skills/oss-launch-kit/evals/pydantic-pydantic.full.md +152 -0
  19. package/skills/oss-launch-kit/evals/vercel-next-js.full.md +152 -0
  20. package/skills/oss-launch-kit/hero.png +0 -0
  21. package/skills/oss-launch-kit/references/channel_rules.md +9 -0
  22. package/skills/oss-launch-kit/references/launch_framework.md +10 -0
  23. package/skills/oss-launch-kit/references/output_template.md +3 -0
  24. package/skills/oss-launch-kit/scripts/__pycache__/build_product_brief.cpython-313.pyc +0 -0
  25. package/skills/oss-launch-kit/scripts/__pycache__/generate_assets.cpython-313.pyc +0 -0
  26. package/skills/oss-launch-kit/scripts/build_product_brief.py +335 -0
  27. package/skills/oss-launch-kit/scripts/fetch_repo_context.py +169 -0
  28. package/skills/oss-launch-kit/scripts/generate_assets.py +526 -0
  29. package/skills/oss-launch-kit/scripts/run.py +41 -0
  30. package/skills/oss-launch-kit/scripts/test_logic.py +99 -0
@@ -0,0 +1,335 @@
1
+ """Build a grounded product brief from GitHub repo context.
2
+
3
+ This is a deterministic Stage A transformer: it extracts factual signals from
4
+ repo metadata and README text, then assembles a concise launch brief that the
5
+ asset generator can safely use.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import sys
13
+ from typing import Any
14
+
15
+
16
+ def _clean(text: str | None) -> str:
17
+ return re.sub(r"\s+", " ", (text or "").strip())
18
+
19
+
20
+ def _strip_markdown(text: str | None) -> str:
21
+ cleaned = text or ""
22
+ cleaned = re.sub(r"\[([^\]]+)\]\([^\)]+\)", r"\1", cleaned)
23
+ cleaned = re.sub(r"\[([^\]]+)\]\[\]", r"\1", cleaned)
24
+ cleaned = re.sub(r"[`*_>#]", "", cleaned)
25
+ return _clean(cleaned)
26
+
27
+
28
+ def _first_sentence(text: str) -> str:
29
+ cleaned = _strip_markdown(text)
30
+ if not cleaned:
31
+ return ""
32
+ match = re.search(r"^(.+?[.!?])(\s|$)", cleaned)
33
+ return match.group(1).strip() if match else cleaned
34
+
35
+
36
+ def _readme_paragraphs(readme_text: str) -> list[str]:
37
+ blocks = [block.strip() for block in re.split(r"\n\s*\n", readme_text or "")]
38
+ return [block for block in blocks if block]
39
+
40
+
41
+ def _readme_bullets(readme_text: str) -> list[str]:
42
+ bullets = []
43
+ for line in (readme_text or "").splitlines():
44
+ if re.match(r"^\s*[-*+]\s+", line):
45
+ bullets.append(_strip_markdown(re.sub(r"^\s*[-*+]\s+", "", line)))
46
+ return bullets
47
+
48
+
49
+ def _infer_audience(repo_context: dict[str, Any], readme_text: str) -> tuple[str, str]:
50
+ text = f"{repo_context.get('description') or ''}\n{readme_text}".lower()
51
+ candidates = [
52
+ ("developers", ["developer", "developers", "devs", "engineers", "coding"]),
53
+ ("teams", ["team", "teams", "workspace", "collaboration"]),
54
+ ("authors and maintainers", ["maintainer", "maintainers", "author", "open source"]),
55
+ ("data/infra users", ["api", "cli", "server", "deploy", "infra", "pipeline"]),
56
+ ("end users", ["app", "users", "personal", "workflow"]),
57
+ ]
58
+ for label, keywords in candidates:
59
+ if any(keyword in text for keyword in keywords):
60
+ return label, "medium"
61
+ return "unknown", "low"
62
+
63
+
64
+ def _infer_project_type(repo_context: dict[str, Any], readme_text: str) -> str:
65
+ text = f"{repo_context.get('description') or ''}\n{readme_text}".lower()
66
+ if any(term in text for term in ["framework", "platform"]):
67
+ return "framework"
68
+ if any(term in text for term in ["library", "package", "module", "sdk"]):
69
+ return "library"
70
+ if any(term in text for term in ["cli", "tool", "automation", "workflow"]):
71
+ return "tool"
72
+ if any(term in text for term in ["app", "saas", "dashboard", "interface"]):
73
+ return "app"
74
+ if any(term in text for term in ["template", "boilerplate", "starter"]):
75
+ return "template"
76
+ return "project"
77
+
78
+
79
+ def _evaluate_channel_fitness(project_type: str, readiness: str) -> dict[str, str]:
80
+ """Evaluate fitness for launch channels based on project type and readiness."""
81
+ fitness = {
82
+ "show_hn": "medium",
83
+ "product_hunt": "medium",
84
+ "reddit": "medium",
85
+ "twitter_x": "high",
86
+ }
87
+
88
+ # Weak repo / Unready project logic
89
+ if readiness == "low":
90
+ return {
91
+ "show_hn": "low",
92
+ "product_hunt": "low",
93
+ "reddit": "low",
94
+ "twitter_x": "medium"
95
+ }
96
+
97
+ # Project type nuances for ready projects
98
+ if project_type in ["library", "tool", "framework"]:
99
+ fitness["product_hunt"] = "low"
100
+ fitness["show_hn"] = "high"
101
+ elif project_type == "app":
102
+ fitness["product_hunt"] = "high"
103
+ fitness["show_hn"] = "medium"
104
+ elif project_type == "template":
105
+ fitness["show_hn"] = "low"
106
+ fitness["product_hunt"] = "low"
107
+ fitness["reddit"] = "high"
108
+
109
+ return fitness
110
+
111
+
112
+ def _infer_problem(repo_context: dict[str, Any], readme_text: str) -> tuple[str, str]:
113
+ description = _strip_markdown(repo_context.get("description"))
114
+ if description:
115
+ return description, "medium"
116
+
117
+ paras = _readme_paragraphs(readme_text)
118
+ if paras:
119
+ return _first_sentence(paras[0]), "low"
120
+
121
+ return "Unknown from available repo metadata.", "low"
122
+
123
+
124
+ def _infer_value_prop(problem: str, key_features: list[str]) -> str:
125
+ if key_features:
126
+ first = key_features[0]
127
+ return f"Solves {problem.lower()} with {first.lower()}."
128
+ return f"Grounds launch copy in the repository's own README and metadata around {problem.lower()}."
129
+
130
+
131
+ def _extract_key_features(readme_text: str, repo_context: dict[str, Any]) -> list[str]:
132
+ bullets = _readme_bullets(readme_text)
133
+ features: list[str] = []
134
+ for bullet in bullets:
135
+ if len(features) >= 5:
136
+ break
137
+ if len(bullet) < 3:
138
+ continue
139
+ features.append(bullet)
140
+
141
+ if not features:
142
+ description = _strip_markdown(repo_context.get("description"))
143
+ if description:
144
+ features.append(description)
145
+
146
+ return features[:5]
147
+
148
+
149
+ def _build_links(repo_context: dict[str, Any]) -> dict[str, str | None]:
150
+ return {
151
+ "repo": repo_context.get("repo_url"),
152
+ "homepage": repo_context.get("homepage") or None,
153
+ "readme_source": repo_context.get("fetched_from", {}).get("readme_api"),
154
+ }
155
+
156
+
157
+ def _check_readiness_signals(readme_text: str, repo_context: dict[str, Any]) -> dict[str, bool]:
158
+ text = readme_text.lower()
159
+ desc = _clean(repo_context.get("description")).lower()
160
+ return {
161
+ "has_license": bool(repo_context.get("license")),
162
+ "has_install": any(term in text for term in ["# install", "npm install", "pip install", "pip3 install", "go get", "setup", "quickstart"]),
163
+ "has_usage_example": any(term in text for term in ["example", "usage", "how to use", "code snippet"]),
164
+ "has_contributing": any(term in text for term in ["contributing", "pull request", "license path", "issue tracker"]),
165
+ "has_quickstart": any(term in text for term in ["# quickstart", "tl;dr", "getting started", "one-command", "fast start"]),
166
+ "has_clear_positioning": len(desc) > 30 and any(term in desc for term in ["built for", "helps", "allows", "solves", "for devs", "for users"]),
167
+ "has_demo_or_proof": any(term in text for term in ["demo", "screenshot", "gif", "preview", "live link", "http", "view it"]),
168
+ "has_description": len(desc) > 20,
169
+ }
170
+
171
+
172
+ def _generate_fix_plan(signals: dict[str, bool]) -> list[dict[str, Any]]:
173
+ """Generate a prioritized list of fixes for the repository."""
174
+ plan = []
175
+
176
+ if not signals["has_quickstart"]:
177
+ plan.append({
178
+ "id": "add_quickstart_example",
179
+ "severity": "high",
180
+ "reason": "New users need a copy-paste example to reach 'first success' quickly.",
181
+ "suggested_fix": "Add a 'Quickstart' section at the top of README with a single command or minimal code snippet.",
182
+ "likely_files": ["README.md"]
183
+ })
184
+
185
+ if not signals["has_usage_example"]:
186
+ plan.append({
187
+ "id": "add_usage_example",
188
+ "severity": "high",
189
+ "reason": "Without a concrete usage example, users cannot see how to apply the project.",
190
+ "suggested_fix": "Add a minimal 'Usage' section with one realistic example and expected output.",
191
+ "likely_files": ["README.md", "docs/usage.md"]
192
+ })
193
+
194
+ if not signals["has_clear_positioning"]:
195
+ plan.append({
196
+ "id": "improve_positioning",
197
+ "severity": "high",
198
+ "reason": "The project description is too short or lacks clear target audience context.",
199
+ "suggested_fix": "Update the repository description to clearly state 'Who it is for' and 'What problem it solves'.",
200
+ "likely_files": ["GitHub Metadata", "README.md"]
201
+ })
202
+
203
+ if not signals["has_demo_or_proof"]:
204
+ plan.append({
205
+ "id": "add_demo_or_proof",
206
+ "severity": "medium",
207
+ "reason": "Visual proof or live demos significantly increase conversion and trust.",
208
+ "suggested_fix": "Add a screenshot, GIF, or a link to a live demo/sample output in the README.",
209
+ "likely_files": ["README.md"]
210
+ })
211
+
212
+ if not signals["has_license"]:
213
+ plan.append({
214
+ "id": "add_license",
215
+ "severity": "medium",
216
+ "reason": "OSS users need a clear license to feel safe adopting the code.",
217
+ "suggested_fix": "Add an MIT or Apache-2.0 LICENSE file to the repository root.",
218
+ "likely_files": ["LICENSE"]
219
+ })
220
+
221
+ if not signals["has_contributing"]:
222
+ plan.append({
223
+ "id": "add_contribution_guide",
224
+ "severity": "low",
225
+ "reason": "Contributors need clear expectations for how to propose changes.",
226
+ "suggested_fix": "Create a CONTRIBUTING.md with basic rules (how to open PRs, coding style, etc.).",
227
+ "likely_files": ["CONTRIBUTING.md"]
228
+ })
229
+
230
+ return plan
231
+
232
+
233
+ def build_product_brief(repo_context: dict[str, Any]) -> dict[str, Any]:
234
+ """Convert fetched repo context into a grounded launch brief."""
235
+
236
+ readme_text = repo_context.get("readme_text") or ""
237
+ repo_name = repo_context.get("full_name") or repo_context.get("name") or "unknown-repo"
238
+
239
+ problem, problem_confidence = _infer_problem(repo_context, readme_text)
240
+ audience, audience_confidence = _infer_audience(repo_context, readme_text)
241
+ key_features = _extract_key_features(readme_text, repo_context)
242
+ value_prop = _infer_value_prop(problem, key_features)
243
+
244
+ summary = _clean(repo_context.get("description"))
245
+ if not summary:
246
+ summary = _first_sentence(readme_text) or f"Open-source project in {repo_context.get('language') or 'unknown language'}."
247
+
248
+ readiness_signals = _check_readiness_signals(readme_text, repo_context)
249
+ signal_count = sum(1 for v in readiness_signals.values() if v)
250
+
251
+ assumptions: list[str] = []
252
+ if repo_context.get("readme_error"):
253
+ assumptions.append(f"README issue: {repo_context['readme_error']}")
254
+ if audience == "unknown":
255
+ assumptions.append("Audience inferred from repo metadata only.")
256
+ if not key_features:
257
+ assumptions.append("No strong feature bullets extracted from README.")
258
+ if not readiness_signals["has_install"]:
259
+ assumptions.append("No clear installation instructions found in README.")
260
+
261
+ confidence = "high"
262
+ if repo_context.get("confidence") == "low" or audience_confidence == "low" or problem_confidence == "low":
263
+ confidence = "low"
264
+ elif assumptions:
265
+ confidence = "medium"
266
+
267
+ proof_points = []
268
+ if repo_context.get("stars") is not None:
269
+ proof_points.append(f"{repo_context['stars']} GitHub stars")
270
+ if repo_context.get("forks") is not None:
271
+ proof_points.append(f"{repo_context['forks']} forks")
272
+ if repo_context.get("topics"):
273
+ proof_points.append("topics: " + ", ".join(repo_context.get("topics", [])[:5]))
274
+ if repo_context.get("language"):
275
+ proof_points.append(f"primary language: {repo_context['language']}")
276
+ if repo_context.get("license"):
277
+ proof_points.append(f"license: {repo_context['license']}")
278
+
279
+ project_type = _infer_project_type(repo_context, readme_text)
280
+
281
+ # Launch Readiness assessment
282
+ stars = repo_context.get("stars", 0)
283
+ readme_len = len(readme_text)
284
+
285
+ # Heuristic scoring
286
+ if stars >= 50 and readme_len > 1500 and signal_count >= 5:
287
+ score = "high"
288
+ elif stars < 10 or readme_len < 500 or signal_count <= 2:
289
+ score = "low"
290
+ else:
291
+ score = "medium"
292
+
293
+ readiness = {
294
+ "score": score,
295
+ "signals": readiness_signals,
296
+ "fix_plan": _generate_fix_plan(readiness_signals)
297
+ }
298
+
299
+ channel_fitness = _evaluate_channel_fitness(project_type, score)
300
+
301
+ return {
302
+ "repo_name": repo_name,
303
+ "one_line_summary": summary,
304
+ "project_type": project_type,
305
+ "launch_readiness": readiness,
306
+ "audience": audience,
307
+ "problem_solved": problem,
308
+ "value_proposition": value_prop,
309
+ "key_proof_points": proof_points,
310
+ "key_features": key_features,
311
+ "links": _build_links(repo_context),
312
+ "confidence": confidence,
313
+ "assumptions": assumptions,
314
+ "channel_fitness": channel_fitness,
315
+ "source_confidence": {
316
+ "problem": problem_confidence,
317
+ "audience": audience_confidence,
318
+ "repo_context": repo_context.get("confidence", "low"),
319
+ },
320
+ }
321
+
322
+
323
+ def main() -> None:
324
+ if len(sys.argv) != 2:
325
+ raise SystemExit("Usage: python build_product_brief.py <repo-context-json>")
326
+
327
+ with open(sys.argv[1], "r", encoding="utf-8") as handle:
328
+ repo_context = json.load(handle)
329
+
330
+ brief = build_product_brief(repo_context)
331
+ print(json.dumps(brief, indent=2, ensure_ascii=True))
332
+
333
+
334
+ if __name__ == "__main__":
335
+ main()
@@ -0,0 +1,169 @@
1
+ """Fetch GitHub repo context for oss-launch-kit.
2
+
3
+ This module resolves a public GitHub repo URL into structured context that
4
+ downstream stages can use without re-hitting GitHub.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import os
11
+ import os
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+ from urllib.parse import urlparse
15
+
16
+ try:
17
+ import requests
18
+ except ImportError as exc:
19
+ # Use the class name directly since RepoContextError is defined below
20
+ raise ValueError(
21
+ "This skill requires the 'requests' library. Install it with: pip install requests"
22
+ ) from exc
23
+
24
+ GITHUB_API_BASE = "https://api.github.com"
25
+ DEFAULT_TIMEOUT = 30
26
+
27
+
28
+ class RepoContextError(ValueError):
29
+ """Raised when repo context cannot be fetched or validated."""
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class RepoRef:
34
+ owner: str
35
+ name: str
36
+
37
+
38
+ def _parse_repo_url(repo_url: str) -> RepoRef:
39
+ parsed = urlparse(repo_url.strip())
40
+ if parsed.scheme not in {"http", "https"}:
41
+ raise RepoContextError("repo-url must start with http:// or https://")
42
+ if parsed.netloc.lower() != "github.com":
43
+ raise RepoContextError("repo-url must point to github.com")
44
+
45
+ parts = [part for part in parsed.path.split("/") if part]
46
+ if len(parts) < 2:
47
+ raise RepoContextError("repo-url must look like https://github.com/owner/repo")
48
+
49
+ owner, name = parts[0], parts[1]
50
+ if name.endswith(".git"):
51
+ name = name[:-4]
52
+ if not owner or not name:
53
+ raise RepoContextError("repo-url is missing owner or repository name")
54
+
55
+ return RepoRef(owner=owner, name=name)
56
+
57
+
58
+ def _headers() -> dict[str, str]:
59
+ headers = {
60
+ "Accept": "application/vnd.github+json",
61
+ "X-GitHub-Api-Version": "2022-11-28",
62
+ "User-Agent": "opendirectory-oss-launch-kit",
63
+ }
64
+ token = os.getenv("GITHUB_TOKEN")
65
+ if token:
66
+ headers["Authorization"] = f"Bearer {token}"
67
+ return headers
68
+
69
+
70
+ def _request_json(url: str) -> requests.Response:
71
+ return requests.get(url, headers=_headers(), timeout=DEFAULT_TIMEOUT)
72
+
73
+
74
+ def _raise_for_github_error(response: requests.Response, repo_url: str) -> None:
75
+ if response.status_code == 401:
76
+ raise RepoContextError("GitHub authentication failed. Check GITHUB_TOKEN.")
77
+ if response.status_code == 403:
78
+ remaining = response.headers.get("X-RateLimit-Remaining")
79
+ if remaining == "0":
80
+ raise RepoContextError("GitHub API rate limit exceeded. Set GITHUB_TOKEN and retry.")
81
+ raise RepoContextError("GitHub access forbidden. The repo may be private or blocked.")
82
+ if response.status_code == 404:
83
+ raise RepoContextError(f"Repository not found or inaccessible: {repo_url}")
84
+ raise RepoContextError(f"GitHub API error {response.status_code} for {repo_url}")
85
+
86
+
87
+ def _decode_readme(content: str, encoding: str | None) -> str:
88
+ if not content:
89
+ return ""
90
+ if encoding == "base64":
91
+ return base64.b64decode(content).decode("utf-8", errors="replace")
92
+ return content
93
+
94
+
95
+ def _pick_readme_path(repo_ref: RepoRef) -> str | None:
96
+ # GitHub's /readme endpoint already resolves the default README.
97
+ return f"{GITHUB_API_BASE}/repos/{repo_ref.owner}/{repo_ref.name}/readme"
98
+
99
+
100
+ def fetch_repo_context(repo_url: str) -> dict[str, Any]:
101
+ """Fetch structured GitHub repo context for the launch-kit pipeline.
102
+
103
+ Returns a dict with repo metadata, README text, and a confidence flag.
104
+ """
105
+
106
+ repo_ref = _parse_repo_url(repo_url)
107
+ repo_api_url = f"{GITHUB_API_BASE}/repos/{repo_ref.owner}/{repo_ref.name}"
108
+
109
+ repo_response = _request_json(repo_api_url)
110
+ if not repo_response.ok:
111
+ _raise_for_github_error(repo_response, repo_url)
112
+ repo = repo_response.json()
113
+
114
+ readme_text = ""
115
+ readme_url = _pick_readme_path(repo_ref)
116
+ readme_error = None
117
+ readme_response = _request_json(readme_url)
118
+ if readme_response.status_code == 200:
119
+ readme_payload = readme_response.json()
120
+ readme_text = _decode_readme(readme_payload.get("content", ""), readme_payload.get("encoding"))
121
+ elif readme_response.status_code == 404:
122
+ readme_error = "README not found"
123
+ else:
124
+ readme_error = f"README fetch failed with status {readme_response.status_code}"
125
+ if readme_response.status_code in {401, 403}:
126
+ _raise_for_github_error(readme_response, repo_url)
127
+
128
+ return {
129
+ "repo_url": repo_url,
130
+ "owner": repo.get("owner", {}).get("login"),
131
+ "name": repo.get("name"),
132
+ "full_name": repo.get("full_name"),
133
+ "description": repo.get("description"),
134
+ "homepage": repo.get("homepage"),
135
+ "topics": repo.get("topics", []),
136
+ "language": repo.get("language"),
137
+ "license": (repo.get("license") or {}).get("spdx_id") or (repo.get("license") or {}).get("name"),
138
+ "stars": repo.get("stargazers_count", 0),
139
+ "forks": repo.get("forks_count", 0),
140
+ "open_issues": repo.get("open_issues_count", 0),
141
+ "created_at": repo.get("created_at"),
142
+ "updated_at": repo.get("updated_at"),
143
+ "archived": repo.get("archived", False),
144
+ "fork": repo.get("fork", False),
145
+ "default_branch": repo.get("default_branch"),
146
+ "readme_text": readme_text,
147
+ "readme_found": bool(readme_text),
148
+ "readme_error": readme_error,
149
+ "fetched_from": {
150
+ "repo_api": repo_api_url,
151
+ "readme_api": readme_url,
152
+ },
153
+ "confidence": "low" if not readme_text else "medium",
154
+ }
155
+
156
+
157
+ def main() -> None:
158
+ import json
159
+ import sys
160
+
161
+ if len(sys.argv) != 2:
162
+ raise SystemExit("Usage: python fetch_repo_context.py <github-repo-url>")
163
+
164
+ context = fetch_repo_context(sys.argv[1])
165
+ print(json.dumps(context, indent=2, ensure_ascii=True))
166
+
167
+
168
+ if __name__ == "__main__":
169
+ main()