@opendirectory.dev/skills 0.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 (212) hide show
  1. package/.claude/skills/claude-md-generator/.env.example +7 -0
  2. package/.claude/skills/claude-md-generator/README.md +78 -0
  3. package/.claude/skills/claude-md-generator/SKILL.md +248 -0
  4. package/.claude/skills/claude-md-generator/evals/evals.json +35 -0
  5. package/.claude/skills/claude-md-generator/references/section-guide.md +175 -0
  6. package/dist/e2e.test.d.ts +1 -0
  7. package/dist/e2e.test.js +62 -0
  8. package/dist/fs-adapters.d.ts +4 -0
  9. package/dist/fs-adapters.js +101 -0
  10. package/dist/fs-adapters.test.d.ts +1 -0
  11. package/dist/fs-adapters.test.js +108 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +211 -0
  14. package/dist/transformers.d.ts +6 -0
  15. package/dist/transformers.js +2 -0
  16. package/package.json +25 -0
  17. package/registry.json +226 -0
  18. package/skills/blog-cover-image-cli/.github/workflows/publish.yml +19 -0
  19. package/skills/blog-cover-image-cli/LICENSE +15 -0
  20. package/skills/blog-cover-image-cli/README.md +126 -0
  21. package/skills/blog-cover-image-cli/SKILL.md +7 -0
  22. package/skills/blog-cover-image-cli/agent-skill/blog-cover-generator/README.md +30 -0
  23. package/skills/blog-cover-image-cli/agent-skill/blog-cover-generator/SKILL.md +72 -0
  24. package/skills/blog-cover-image-cli/bin/cli.js +226 -0
  25. package/skills/blog-cover-image-cli/examples/100x_UX_Research_AI_Agent.png +0 -0
  26. package/skills/blog-cover-image-cli/examples/Firecrawl-supabase-bolt.png +0 -0
  27. package/skills/blog-cover-image-cli/examples/Git-City_Case_study_Cover_Image.jpg +0 -0
  28. package/skills/blog-cover-image-cli/examples/THE DISTRIBUTION LAYER (2).png +0 -0
  29. package/skills/blog-cover-image-cli/examples/canva-perplexity-duolingo-cover-image.png +0 -0
  30. package/skills/blog-cover-image-cli/examples/gamma-mistral-veed.png +0 -0
  31. package/skills/blog-cover-image-cli/examples/server-survival-case-study-cover-image(1).png +0 -0
  32. package/skills/blog-cover-image-cli/examples/viral-meme-automation.png +0 -0
  33. package/skills/blog-cover-image-cli/index.js +2 -0
  34. package/skills/blog-cover-image-cli/package-lock.json +2238 -0
  35. package/skills/blog-cover-image-cli/package.json +37 -0
  36. package/skills/blog-cover-image-cli/src/geminiGenerator.js +126 -0
  37. package/skills/blog-cover-image-cli/src/imageValidator.js +54 -0
  38. package/skills/blog-cover-image-cli/src/logoFetcher.js +86 -0
  39. package/skills/claude-md-generator/.env.example +7 -0
  40. package/skills/claude-md-generator/README.md +78 -0
  41. package/skills/claude-md-generator/SKILL.md +254 -0
  42. package/skills/claude-md-generator/evals/evals.json +35 -0
  43. package/skills/claude-md-generator/references/section-guide.md +175 -0
  44. package/skills/cook-the-blog/README.md +86 -0
  45. package/skills/cook-the-blog/SKILL.md +130 -0
  46. package/skills/dependency-update-bot/.env.example +13 -0
  47. package/skills/dependency-update-bot/README.md +101 -0
  48. package/skills/dependency-update-bot/SKILL.md +376 -0
  49. package/skills/dependency-update-bot/evals/evals.json +45 -0
  50. package/skills/dependency-update-bot/references/changelog-patterns.md +201 -0
  51. package/skills/docs-from-code/.env.example +13 -0
  52. package/skills/docs-from-code/README.md +97 -0
  53. package/skills/docs-from-code/SKILL.md +160 -0
  54. package/skills/docs-from-code/evals/evals.json +29 -0
  55. package/skills/docs-from-code/references/extraction-guide.md +174 -0
  56. package/skills/docs-from-code/references/output-template.md +135 -0
  57. package/skills/docs-from-code/scripts/extract_py.py +238 -0
  58. package/skills/docs-from-code/scripts/extract_ts.ts +284 -0
  59. package/skills/docs-from-code/scripts/package.json +18 -0
  60. package/skills/explain-this-pr/README.md +74 -0
  61. package/skills/explain-this-pr/SKILL.md +130 -0
  62. package/skills/explain-this-pr/evals/evals.json +35 -0
  63. package/skills/google-trends-api-skills/README.md +78 -0
  64. package/skills/google-trends-api-skills/SKILL.md +7 -0
  65. package/skills/google-trends-api-skills/google-trends-api/SKILL.md +163 -0
  66. package/skills/google-trends-api-skills/google-trends-api/references/api-responses.md +188 -0
  67. package/skills/google-trends-api-skills/google-trends-api/scripts/discover_keywords.py +344 -0
  68. package/skills/google-trends-api-skills/seo-keyword-research/SKILL.md +205 -0
  69. package/skills/google-trends-api-skills/seo-keyword-research/references/keyword-placement-guide.md +89 -0
  70. package/skills/google-trends-api-skills/seo-keyword-research/references/tech-blog-examples.md +207 -0
  71. package/skills/google-trends-api-skills/seo-keyword-research/scripts/blog_seo_research.py +373 -0
  72. package/skills/hackernews-intel/.env.example +33 -0
  73. package/skills/hackernews-intel/README.md +161 -0
  74. package/skills/hackernews-intel/SKILL.md +156 -0
  75. package/skills/hackernews-intel/evals/evals.json +35 -0
  76. package/skills/hackernews-intel/package.json +15 -0
  77. package/skills/hackernews-intel/scripts/monitor-hn.js +258 -0
  78. package/skills/kill-the-standup/.env.example +22 -0
  79. package/skills/kill-the-standup/README.md +84 -0
  80. package/skills/kill-the-standup/SKILL.md +169 -0
  81. package/skills/kill-the-standup/evals/evals.json +35 -0
  82. package/skills/kill-the-standup/references/standup-format.md +102 -0
  83. package/skills/linkedin-post-generator/.env.example +14 -0
  84. package/skills/linkedin-post-generator/README.md +107 -0
  85. package/skills/linkedin-post-generator/SKILL.md +228 -0
  86. package/skills/linkedin-post-generator/evals/evals.json +35 -0
  87. package/skills/linkedin-post-generator/references/linkedin-format.md +216 -0
  88. package/skills/linkedin-post-generator/references/output-template.md +154 -0
  89. package/skills/llms-txt-generator/.env.example +18 -0
  90. package/skills/llms-txt-generator/README.md +142 -0
  91. package/skills/llms-txt-generator/SKILL.md +176 -0
  92. package/skills/llms-txt-generator/evals/evals.json +35 -0
  93. package/skills/llms-txt-generator/references/llms-txt-spec.md +88 -0
  94. package/skills/llms-txt-generator/references/output-template.md +76 -0
  95. package/skills/llms-txt-generator/test-output/genzcareer.in/llms.txt +31 -0
  96. package/skills/luma-attendees-scraper/README.md +170 -0
  97. package/skills/luma-attendees-scraper/SKILL.md +7 -0
  98. package/skills/luma-attendees-scraper/luma_attendees_export.js +223 -0
  99. package/skills/meeting-brief-generator/.env.example +21 -0
  100. package/skills/meeting-brief-generator/README.md +90 -0
  101. package/skills/meeting-brief-generator/SKILL.md +275 -0
  102. package/skills/meeting-brief-generator/evals/evals.json +35 -0
  103. package/skills/meeting-brief-generator/references/brief-format.md +114 -0
  104. package/skills/meeting-brief-generator/references/output-template.md +150 -0
  105. package/skills/meta-ads-skill/README.md +100 -0
  106. package/skills/meta-ads-skill/SKILL.md +7 -0
  107. package/skills/meta-ads-skill/meta-ads-skill/SKILL.md +41 -0
  108. package/skills/meta-ads-skill/meta-ads-skill/references/report_templates.md +47 -0
  109. package/skills/meta-ads-skill/meta-ads-skill/references/workflows.md +51 -0
  110. package/skills/meta-ads-skill/meta-ads-skill/scripts/auth_check.py +22 -0
  111. package/skills/meta-ads-skill/meta-ads-skill/scripts/formatters.py +46 -0
  112. package/skills/newsletter-digest/.env.example +20 -0
  113. package/skills/newsletter-digest/README.md +147 -0
  114. package/skills/newsletter-digest/SKILL.md +221 -0
  115. package/skills/newsletter-digest/evals/evals.json +35 -0
  116. package/skills/newsletter-digest/feeds.json +7 -0
  117. package/skills/newsletter-digest/package.json +15 -0
  118. package/skills/newsletter-digest/references/digest-format.md +123 -0
  119. package/skills/newsletter-digest/references/output-template.md +136 -0
  120. package/skills/newsletter-digest/scripts/fetch-feeds.js +141 -0
  121. package/skills/newsletter-digest/scripts/ghost-publish.js +147 -0
  122. package/skills/noise2blog/.env.example +16 -0
  123. package/skills/noise2blog/README.md +107 -0
  124. package/skills/noise2blog/SKILL.md +229 -0
  125. package/skills/noise2blog/evals/evals.json +35 -0
  126. package/skills/noise2blog/references/blog-format.md +188 -0
  127. package/skills/noise2blog/references/output-template.md +184 -0
  128. package/skills/outreach-sequence-builder/.env.example +12 -0
  129. package/skills/outreach-sequence-builder/README.md +108 -0
  130. package/skills/outreach-sequence-builder/SKILL.md +248 -0
  131. package/skills/outreach-sequence-builder/evals/evals.json +36 -0
  132. package/skills/outreach-sequence-builder/references/output-template.md +171 -0
  133. package/skills/outreach-sequence-builder/references/sequence-format.md +167 -0
  134. package/skills/outreach-sequence-builder/references/signal-playbook.md +117 -0
  135. package/skills/position-me/README.md +71 -0
  136. package/skills/position-me/SKILL.md +7 -0
  137. package/skills/position-me/position-me/SKILL.md +50 -0
  138. package/skills/position-me/position-me/references/EVALUATION_SOP.md +40 -0
  139. package/skills/position-me/position-me/references/REPORT_TEMPLATE.md +58 -0
  140. package/skills/position-me/position-me/scripts/extract_links.py +49 -0
  141. package/skills/pr-description-writer/README.md +81 -0
  142. package/skills/pr-description-writer/SKILL.md +141 -0
  143. package/skills/pr-description-writer/evals/evals.json +35 -0
  144. package/skills/pr-description-writer/references/pr-format-guide.md +145 -0
  145. package/skills/producthunt-launch-kit/.env.example +7 -0
  146. package/skills/producthunt-launch-kit/README.md +95 -0
  147. package/skills/producthunt-launch-kit/SKILL.md +380 -0
  148. package/skills/producthunt-launch-kit/evals/evals.json +35 -0
  149. package/skills/producthunt-launch-kit/references/copy-rules.md +124 -0
  150. package/skills/reddit-icp-monitor/.env.example +16 -0
  151. package/skills/reddit-icp-monitor/README.md +117 -0
  152. package/skills/reddit-icp-monitor/SKILL.md +271 -0
  153. package/skills/reddit-icp-monitor/evals/evals.json +40 -0
  154. package/skills/reddit-icp-monitor/references/icp-format.md +131 -0
  155. package/skills/reddit-icp-monitor/references/reply-rules.md +110 -0
  156. package/skills/reddit-post-engine/.env.example +13 -0
  157. package/skills/reddit-post-engine/README.md +103 -0
  158. package/skills/reddit-post-engine/SKILL.md +303 -0
  159. package/skills/reddit-post-engine/evals/evals.json +35 -0
  160. package/skills/reddit-post-engine/references/subreddit-playbook.md +156 -0
  161. package/skills/schema-markup-generator/.env.example +19 -0
  162. package/skills/schema-markup-generator/README.md +114 -0
  163. package/skills/schema-markup-generator/SKILL.md +192 -0
  164. package/skills/schema-markup-generator/evals/evals.json +35 -0
  165. package/skills/schema-markup-generator/references/json-ld-spec.md +263 -0
  166. package/skills/schema-markup-generator/references/output-template.md +556 -0
  167. package/skills/show-hn-writer/.env.example +14 -0
  168. package/skills/show-hn-writer/README.md +88 -0
  169. package/skills/show-hn-writer/SKILL.md +303 -0
  170. package/skills/show-hn-writer/evals/evals.json +35 -0
  171. package/skills/show-hn-writer/references/hn-rules.md +74 -0
  172. package/skills/show-hn-writer/references/title-formulas.md +93 -0
  173. package/skills/stargazer/README.md +79 -0
  174. package/skills/stargazer/SKILL.md +7 -0
  175. package/skills/stargazer/stargazer-skill/SKILL.md +58 -0
  176. package/skills/stargazer/stargazer-skill/assets/.env.example +18 -0
  177. package/skills/stargazer/stargazer-skill/scripts/convert_to_csv.py +63 -0
  178. package/skills/stargazer/stargazer-skill/scripts/count_emails.py +52 -0
  179. package/skills/stargazer/stargazer-skill/scripts/stargazer_deep_extractor.py +450 -0
  180. package/skills/tweet-thread-from-blog/.env.example +14 -0
  181. package/skills/tweet-thread-from-blog/README.md +109 -0
  182. package/skills/tweet-thread-from-blog/SKILL.md +177 -0
  183. package/skills/tweet-thread-from-blog/evals/evals.json +35 -0
  184. package/skills/tweet-thread-from-blog/references/output-template.md +193 -0
  185. package/skills/tweet-thread-from-blog/references/thread-format.md +107 -0
  186. package/skills/twitter-GTM-find-skill/README.md +43 -0
  187. package/skills/twitter-GTM-find-skill/SKILL.md +7 -0
  188. package/skills/twitter-GTM-find-skill/twitter-GTM-find/SKILL.md +37 -0
  189. package/skills/twitter-GTM-find-skill/twitter-GTM-find/references/icp-checklist.md +35 -0
  190. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/package.json +23 -0
  191. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/run_pipeline.sh +8 -0
  192. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/debug.ts +23 -0
  193. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/extractor.ts +79 -0
  194. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/icp-filter.ts +87 -0
  195. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/index.ts +94 -0
  196. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/scraper.ts +41 -0
  197. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/tsconfig.json +13 -0
  198. package/skills/yc-intent-radar-skill/README.md +39 -0
  199. package/skills/yc-intent-radar-skill/SKILL.md +7 -0
  200. package/skills/yc-intent-radar-skill/yc-jobs-scraper/SKILL.md +59 -0
  201. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/auth.js +29 -0
  202. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/db.js +62 -0
  203. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/export_radar_candidates.js +40 -0
  204. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/package-lock.json +1525 -0
  205. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/package.json +12 -0
  206. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/scraper.js +217 -0
  207. package/src/e2e.test.ts +35 -0
  208. package/src/fs-adapters.test.ts +91 -0
  209. package/src/fs-adapters.ts +65 -0
  210. package/src/index.ts +182 -0
  211. package/src/transformers.ts +6 -0
  212. package/tsconfig.json +8 -0
@@ -0,0 +1,238 @@
1
+ """
2
+ docs-from-code — Python Code Extractor
3
+ Uses Python's built-in `ast` module to extract structured metadata.
4
+ Works for Flask, FastAPI, Django REST, and plain Python projects.
5
+
6
+ Usage: python extract_py.py <project-root> [output-file]
7
+ """
8
+
9
+ import ast
10
+ import json
11
+ import os
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ def get_docstring(node: ast.AST) -> str:
18
+ """Extract docstring from a function, class, or module node."""
19
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Module)):
20
+ if (node.body and isinstance(node.body[0], ast.Expr)
21
+ and isinstance(node.body[0].value, ast.Constant)
22
+ and isinstance(node.body[0].value.value, str)):
23
+ return node.body[0].value.value.strip()
24
+ return ""
25
+
26
+
27
+ def get_annotation(node: Any) -> str:
28
+ """Convert AST annotation to string."""
29
+ if node is None:
30
+ return ""
31
+ if isinstance(node, ast.Name):
32
+ return node.id
33
+ if isinstance(node, ast.Constant):
34
+ return str(node.value)
35
+ if isinstance(node, ast.Attribute):
36
+ return f"{get_annotation(node.value)}.{node.attr}"
37
+ if isinstance(node, ast.Subscript):
38
+ return f"{get_annotation(node.value)}[{get_annotation(node.slice)}]"
39
+ if isinstance(node, ast.Tuple):
40
+ return ", ".join(get_annotation(e) for e in node.elts)
41
+ if isinstance(node, ast.BinOp):
42
+ return f"{get_annotation(node.left)} | {get_annotation(node.right)}"
43
+ try:
44
+ return ast.unparse(node)
45
+ except Exception:
46
+ return ""
47
+
48
+
49
+ def extract_functions(tree: ast.Module, file_path: str, is_exported_only: bool = False):
50
+ """Extract top-level functions and their signatures."""
51
+ functions = []
52
+ for node in ast.walk(tree):
53
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
54
+ continue
55
+ if node.col_offset != 0: # skip nested functions / methods
56
+ continue
57
+ if is_exported_only and node.name.startswith("_"):
58
+ continue
59
+
60
+ args = node.args
61
+ params = []
62
+ all_args = args.posonlyargs + args.args + args.kwonlyargs
63
+ for i, arg in enumerate(all_args):
64
+ params.append({
65
+ "name": arg.arg,
66
+ "type": get_annotation(arg.annotation),
67
+ "description": ""
68
+ })
69
+
70
+ returns = get_annotation(node.returns)
71
+ is_async = isinstance(node, ast.AsyncFunctionDef)
72
+
73
+ functions.append({
74
+ "name": node.name,
75
+ "signature": f"{'async ' if is_async else ''}def {node.name}({', '.join(p['name'] + (': ' + p['type'] if p['type'] else '') for p in params)}) -> {returns}",
76
+ "description": get_docstring(node),
77
+ "params": params,
78
+ "returns": returns,
79
+ "file": file_path,
80
+ "isExported": not node.name.startswith("_")
81
+ })
82
+ return functions
83
+
84
+
85
+ def extract_routes(tree: ast.Module, file_path: str, framework: str):
86
+ """Extract API routes from FastAPI, Flask, or Django decorators."""
87
+ routes = []
88
+
89
+ for node in ast.walk(tree):
90
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
91
+ continue
92
+
93
+ for decorator in node.decorator_list:
94
+ # FastAPI / Flask style: @app.get("/path") or @router.post("/path")
95
+ if isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Attribute):
96
+ method = decorator.func.attr.upper()
97
+ if method in ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]:
98
+ route_path = ""
99
+ if decorator.args and isinstance(decorator.args[0], ast.Constant):
100
+ route_path = decorator.args[0].value
101
+ routes.append({
102
+ "method": method,
103
+ "path": route_path,
104
+ "handler": node.name,
105
+ "description": get_docstring(node),
106
+ "file": file_path
107
+ })
108
+
109
+ # Django REST: @api_view(['GET', 'POST'])
110
+ if isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Name):
111
+ if decorator.func.id == "api_view":
112
+ for arg in decorator.args:
113
+ if isinstance(arg, ast.List):
114
+ for elt in arg.elts:
115
+ if isinstance(elt, ast.Constant):
116
+ routes.append({
117
+ "method": elt.value,
118
+ "path": "",
119
+ "handler": node.name,
120
+ "description": get_docstring(node),
121
+ "file": file_path
122
+ })
123
+
124
+ return routes
125
+
126
+
127
+ def extract_classes(tree: ast.Module, file_path: str):
128
+ """Extract class definitions with their public methods."""
129
+ classes = []
130
+ for node in ast.iter_child_nodes(tree):
131
+ if not isinstance(node, ast.ClassDef):
132
+ continue
133
+ methods = []
134
+ for item in ast.iter_child_nodes(node):
135
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
136
+ if not item.name.startswith("__"):
137
+ methods.append(item.name)
138
+ classes.append({
139
+ "name": node.name,
140
+ "kind": "class",
141
+ "definition": f"class {node.name}",
142
+ "description": get_docstring(node),
143
+ "methods": methods,
144
+ "file": file_path
145
+ })
146
+ return classes
147
+
148
+
149
+ def detect_framework(project_root: str) -> str:
150
+ """Detect Python framework from requirements.txt or pyproject.toml."""
151
+ req_files = [
152
+ os.path.join(project_root, "requirements.txt"),
153
+ os.path.join(project_root, "requirements/base.txt"),
154
+ ]
155
+ for req_file in req_files:
156
+ if os.path.exists(req_file):
157
+ content = open(req_file).read().lower()
158
+ if "fastapi" in content:
159
+ return "fastapi"
160
+ if "flask" in content:
161
+ return "flask"
162
+ if "django" in content:
163
+ return "django"
164
+ if "tornado" in content:
165
+ return "tornado"
166
+
167
+ pyproject = os.path.join(project_root, "pyproject.toml")
168
+ if os.path.exists(pyproject):
169
+ content = open(pyproject).read().lower()
170
+ if "fastapi" in content:
171
+ return "fastapi"
172
+ if "flask" in content:
173
+ return "flask"
174
+ if "django" in content:
175
+ return "django"
176
+
177
+ return "python"
178
+
179
+
180
+ def main():
181
+ project_root = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
182
+ output_file = sys.argv[2] if len(sys.argv) > 2 else os.path.join(project_root, ".docs-extract.json")
183
+
184
+ framework = detect_framework(project_root)
185
+ skip_dirs = {"__pycache__", ".git", "node_modules", "venv", ".venv", "env",
186
+ "dist", "build", ".tox", "migrations", "static", "media"}
187
+
188
+ metadata = {
189
+ "projectName": os.path.basename(project_root),
190
+ "language": "python",
191
+ "framework": framework,
192
+ "entryPoints": [],
193
+ "functions": [],
194
+ "routes": [],
195
+ "types": [],
196
+ "dependencies": {},
197
+ "scripts": {}
198
+ }
199
+
200
+ for root, dirs, files in os.walk(project_root):
201
+ dirs[:] = [d for d in dirs if d not in skip_dirs and not d.startswith(".")]
202
+ for file in files:
203
+ if not file.endswith(".py"):
204
+ continue
205
+ file_path = os.path.join(root, file)
206
+ try:
207
+ source = open(file_path, encoding="utf-8").read()
208
+ tree = ast.parse(source, filename=file_path)
209
+ except (SyntaxError, UnicodeDecodeError):
210
+ continue
211
+
212
+ metadata["functions"].extend(extract_functions(tree, file_path, is_exported_only=True))
213
+ metadata["routes"].extend(extract_routes(tree, file_path, framework))
214
+ metadata["types"].extend(extract_classes(tree, file_path))
215
+
216
+ # Detect entry points
217
+ for candidate in ["main.py", "app.py", "server.py", "manage.py", "run.py", "wsgi.py"]:
218
+ if os.path.exists(os.path.join(project_root, candidate)):
219
+ metadata["entryPoints"].append(candidate)
220
+
221
+ # Read requirements
222
+ req_file = os.path.join(project_root, "requirements.txt")
223
+ if os.path.exists(req_file):
224
+ for line in open(req_file):
225
+ line = line.strip()
226
+ if line and not line.startswith("#"):
227
+ parts = line.split("==")
228
+ metadata["dependencies"][parts[0]] = parts[1] if len(parts) > 1 else "*"
229
+
230
+ with open(output_file, "w") as f:
231
+ json.dump(metadata, f, indent=2)
232
+
233
+ print(f"Extracted {len(metadata['functions'])} functions, {len(metadata['routes'])} routes, {len(metadata['types'])} classes")
234
+ print(f"Output: {output_file}")
235
+
236
+
237
+ if __name__ == "__main__":
238
+ main()
@@ -0,0 +1,284 @@
1
+ /**
2
+ * docs-from-code — TypeScript/JavaScript Code Extractor
3
+ * Uses ts-morph to extract structured metadata from a TS/JS codebase.
4
+ * Outputs a JSON file the agent feeds to Gemini for doc generation.
5
+ *
6
+ * Usage: npx ts-node extract_ts.ts <project-root> [output-file]
7
+ */
8
+
9
+ import { Project, SyntaxKind, SourceFile } from "ts-morph";
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+
13
+ interface FunctionDoc {
14
+ name: string;
15
+ signature: string;
16
+ description: string;
17
+ params: { name: string; type: string; description: string }[];
18
+ returns: string;
19
+ file: string;
20
+ isExported: boolean;
21
+ }
22
+
23
+ interface RouteDoc {
24
+ method: string;
25
+ path: string;
26
+ handler: string;
27
+ description: string;
28
+ file: string;
29
+ }
30
+
31
+ interface TypeDoc {
32
+ name: string;
33
+ kind: "interface" | "type" | "enum" | "class";
34
+ definition: string;
35
+ description: string;
36
+ file: string;
37
+ }
38
+
39
+ interface CodeMetadata {
40
+ projectName: string;
41
+ language: "typescript" | "javascript";
42
+ framework: string | null;
43
+ entryPoints: string[];
44
+ functions: FunctionDoc[];
45
+ routes: RouteDoc[];
46
+ types: TypeDoc[];
47
+ dependencies: Record<string, string>;
48
+ scripts: Record<string, string>;
49
+ }
50
+
51
+ function detectFramework(pkgJson: any): string | null {
52
+ const deps = {
53
+ ...pkgJson.dependencies,
54
+ ...pkgJson.devDependencies,
55
+ };
56
+ if (deps["next"]) return "nextjs";
57
+ if (deps["express"]) return "express";
58
+ if (deps["hono"]) return "hono";
59
+ if (deps["fastify"]) return "fastify";
60
+ if (deps["@nestjs/core"]) return "nestjs";
61
+ if (deps["koa"]) return "koa";
62
+ if (deps["@sveltejs/kit"]) return "sveltekit";
63
+ if (deps["astro"]) return "astro";
64
+ return null;
65
+ }
66
+
67
+ function extractJsDocComment(node: any): string {
68
+ try {
69
+ const docs = node.getJsDocs?.();
70
+ if (docs && docs.length > 0) {
71
+ return docs[0].getDescription().trim();
72
+ }
73
+ } catch {}
74
+ return "";
75
+ }
76
+
77
+ function extractRoutes(sourceFile: SourceFile, framework: string | null): RouteDoc[] {
78
+ const routes: RouteDoc[] = [];
79
+ const filePath = sourceFile.getFilePath();
80
+
81
+ // Express / Hono / Fastify / Koa style: app.get('/path', handler)
82
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
83
+ for (const call of callExpressions) {
84
+ const expr = call.getExpression().getText();
85
+ const httpMethods = ["get", "post", "put", "patch", "delete", "options", "head"];
86
+ const methodMatch = httpMethods.find((m) => expr.endsWith(`.${m}`));
87
+ if (methodMatch) {
88
+ const args = call.getArguments();
89
+ if (args.length >= 1) {
90
+ const routePath = args[0].getText().replace(/['"]/g, "");
91
+ routes.push({
92
+ method: methodMatch.toUpperCase(),
93
+ path: routePath,
94
+ handler: args[1]?.getText()?.slice(0, 80) || "",
95
+ description: "",
96
+ file: filePath,
97
+ });
98
+ }
99
+ }
100
+ }
101
+
102
+ // Next.js app router: export async function GET/POST etc
103
+ if (framework === "nextjs" && filePath.includes("/api/")) {
104
+ const functions = sourceFile.getFunctions();
105
+ for (const fn of functions) {
106
+ const name = fn.getName() || "";
107
+ const httpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
108
+ if (httpMethods.includes(name) && fn.isExported()) {
109
+ const routePath = filePath
110
+ .replace(/.*\/app/, "")
111
+ .replace(/\/route\.(ts|tsx|js|jsx)$/, "")
112
+ || "/";
113
+ routes.push({
114
+ method: name,
115
+ path: routePath,
116
+ handler: fn.getName() || "",
117
+ description: extractJsDocComment(fn),
118
+ file: filePath,
119
+ });
120
+ }
121
+ }
122
+ }
123
+
124
+ return routes;
125
+ }
126
+
127
+ function extractFunctions(sourceFile: SourceFile): FunctionDoc[] {
128
+ const functions: FunctionDoc[] = [];
129
+ const filePath = sourceFile.getFilePath();
130
+
131
+ // Regular functions
132
+ for (const fn of sourceFile.getFunctions()) {
133
+ if (!fn.isExported()) continue;
134
+ const name = fn.getName() || "anonymous";
135
+ const params = fn.getParameters().map((p) => ({
136
+ name: p.getName(),
137
+ type: p.getType().getText(),
138
+ description: "",
139
+ }));
140
+ functions.push({
141
+ name,
142
+ signature: `${name}(${fn.getParameters().map((p) => p.getText()).join(", ")}): ${fn.getReturnType().getText()}`,
143
+ description: extractJsDocComment(fn),
144
+ params,
145
+ returns: fn.getReturnType().getText(),
146
+ file: filePath,
147
+ isExported: true,
148
+ });
149
+ }
150
+
151
+ // Arrow functions assigned to exported const
152
+ for (const varDecl of sourceFile.getVariableDeclarations()) {
153
+ const init = varDecl.getInitializer();
154
+ if (!init) continue;
155
+ const isArrow = init.getKind() === SyntaxKind.ArrowFunction;
156
+ if (!isArrow) continue;
157
+ const varStatement = varDecl.getVariableStatement();
158
+ if (!varStatement?.isExported()) continue;
159
+ const name = varDecl.getName();
160
+ functions.push({
161
+ name,
162
+ signature: `${name} = ${init.getText().slice(0, 120)}`,
163
+ description: extractJsDocComment(varStatement),
164
+ params: [],
165
+ returns: "",
166
+ file: filePath,
167
+ isExported: true,
168
+ });
169
+ }
170
+
171
+ return functions;
172
+ }
173
+
174
+ function extractTypes(sourceFile: SourceFile): TypeDoc[] {
175
+ const types: TypeDoc[] = [];
176
+ const filePath = sourceFile.getFilePath();
177
+
178
+ for (const iface of sourceFile.getInterfaces()) {
179
+ if (!iface.isExported()) continue;
180
+ types.push({
181
+ name: iface.getName(),
182
+ kind: "interface",
183
+ definition: iface.getText().slice(0, 400),
184
+ description: extractJsDocComment(iface),
185
+ file: filePath,
186
+ });
187
+ }
188
+
189
+ for (const typeAlias of sourceFile.getTypeAliases()) {
190
+ if (!typeAlias.isExported()) continue;
191
+ types.push({
192
+ name: typeAlias.getName(),
193
+ kind: "type",
194
+ definition: typeAlias.getText().slice(0, 400),
195
+ description: extractJsDocComment(typeAlias),
196
+ file: filePath,
197
+ });
198
+ }
199
+
200
+ for (const cls of sourceFile.getClasses()) {
201
+ if (!cls.isExported()) continue;
202
+ types.push({
203
+ name: cls.getName() || "AnonymousClass",
204
+ kind: "class",
205
+ definition: cls.getText().slice(0, 600),
206
+ description: extractJsDocComment(cls),
207
+ file: filePath,
208
+ });
209
+ }
210
+
211
+ return types;
212
+ }
213
+
214
+ async function main() {
215
+ const projectRoot = process.argv[2] || process.cwd();
216
+ const outputFile = process.argv[3] || path.join(projectRoot, ".docs-extract.json");
217
+
218
+ // Load package.json
219
+ const pkgPath = path.join(projectRoot, "package.json");
220
+ let pkgJson: any = {};
221
+ if (fs.existsSync(pkgPath)) {
222
+ pkgJson = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
223
+ }
224
+
225
+ const framework = detectFramework(pkgJson);
226
+
227
+ // Init ts-morph project
228
+ const project = new Project({
229
+ tsConfigFilePath: fs.existsSync(path.join(projectRoot, "tsconfig.json"))
230
+ ? path.join(projectRoot, "tsconfig.json")
231
+ : undefined,
232
+ addFilesFromTsConfig: false,
233
+ });
234
+
235
+ // Add source files — skip node_modules, .next, dist, build
236
+ const extensions = [".ts", ".tsx"];
237
+ const skipDirs = ["node_modules", ".next", "dist", "build", ".turbo", "coverage", ".cache"];
238
+
239
+ function addFiles(dir: string) {
240
+ if (!fs.existsSync(dir)) return;
241
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
242
+ if (entry.isDirectory()) {
243
+ if (!skipDirs.includes(entry.name)) {
244
+ addFiles(path.join(dir, entry.name));
245
+ }
246
+ } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
247
+ project.addSourceFileAtPath(path.join(dir, entry.name));
248
+ }
249
+ }
250
+ }
251
+
252
+ addFiles(projectRoot);
253
+
254
+ const metadata: CodeMetadata = {
255
+ projectName: pkgJson.name || path.basename(projectRoot),
256
+ language: "typescript",
257
+ framework,
258
+ entryPoints: [],
259
+ functions: [],
260
+ routes: [],
261
+ types: [],
262
+ dependencies: pkgJson.dependencies || {},
263
+ scripts: pkgJson.scripts || {},
264
+ };
265
+
266
+ for (const sourceFile of project.getSourceFiles()) {
267
+ metadata.functions.push(...extractFunctions(sourceFile));
268
+ metadata.routes.push(...extractRoutes(sourceFile, framework));
269
+ metadata.types.push(...extractTypes(sourceFile));
270
+ }
271
+
272
+ // Detect entry points
273
+ const commonEntries = ["src/index.ts", "index.ts", "src/main.ts", "server.ts", "app.ts"];
274
+ metadata.entryPoints = commonEntries
275
+ .filter((e) => fs.existsSync(path.join(projectRoot, e)))
276
+ .map((e) => e);
277
+
278
+ fs.writeFileSync(outputFile, JSON.stringify(metadata, null, 2));
279
+
280
+ console.log(`Extracted ${metadata.functions.length} functions, ${metadata.routes.length} routes, ${metadata.types.length} types`);
281
+ console.log(`Output: ${outputFile}`);
282
+ }
283
+
284
+ main().catch(console.error);
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "docs-from-code-scripts",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Extraction scripts for docs-from-code skill",
6
+ "scripts": {
7
+ "extract:ts": "ts-node extract_ts.ts",
8
+ "extract:py": "python3 extract_py.py"
9
+ },
10
+ "dependencies": {
11
+ "ts-morph": "^21.0.1"
12
+ },
13
+ "devDependencies": {
14
+ "@types/node": "^20.0.0",
15
+ "ts-node": "^10.9.2",
16
+ "typescript": "^5.4.0"
17
+ }
18
+ }
@@ -0,0 +1,74 @@
1
+ # explain-this-pr
2
+
3
+ <img width="1280" height="640" alt="explain-this-pr" src="https://github.com/user-attachments/assets/9e76fc64-f982-4c4c-9b58-4398541aab97" />
4
+
5
+
6
+ Point this skill at any GitHub PR and it writes a plain-English explanation of what changed and why, then posts it as a PR comment.
7
+
8
+ ## What It Does
9
+
10
+ - Fetches the PR diff and metadata via `gh`
11
+ - Writes two paragraphs: what changed (technical) and why it matters (impact)
12
+ - Posts the explanation as a PR comment with `gh pr comment`
13
+ - Works with any public or private repo you have access to via `gh`
14
+
15
+ ## Requirements
16
+
17
+ | Requirement | Purpose | How to Set Up |
18
+ |------------|---------|--------------|
19
+ | `gh` CLI | Fetching PR data and posting comments | https://cli.github.com, then run `gh auth login` |
20
+
21
+ No API keys needed.
22
+
23
+ ## How to Use
24
+
25
+ Explain a PR by URL:
26
+
27
+ ```
28
+ "Explain this PR: https://github.com/owner/repo/pull/123"
29
+ "What does this PR do? https://github.com/owner/repo/pull/456"
30
+ "Summarize this pull request: [URL]"
31
+ ```
32
+
33
+ Explain by PR number (in the current repo):
34
+
35
+ ```
36
+ "Explain PR #42"
37
+ "Add a summary comment to PR #99"
38
+ ```
39
+
40
+ Explain the current branch PR:
41
+
42
+ ```
43
+ "Explain the current branch PR"
44
+ "Add a plain-English comment to my PR"
45
+ ```
46
+
47
+ Output without posting:
48
+
49
+ ```
50
+ "Explain this PR but don't post the comment: [URL]"
51
+ "What does this PR change? Just give me the text."
52
+ ```
53
+
54
+ ## Output Format
55
+
56
+ Two paragraphs, under 150 words total.
57
+
58
+ Paragraph 1: What it does (technical). Names the specific files, functions, or systems that changed. States the before/after if visible in the diff.
59
+
60
+ Paragraph 2: Why it matters (impact). Explains who benefits and what problem is solved. Omitted if there is no clear "why" in the diff or commits. The skill never guesses at impact.
61
+
62
+ ## Project Structure
63
+
64
+ ```
65
+ explain-this-pr/
66
+ ├── SKILL.md
67
+ ├── README.md
68
+ └── evals/
69
+ └── evals.json
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT