@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.
- package/.claude/skills/claude-md-generator/.env.example +7 -0
- package/.claude/skills/claude-md-generator/README.md +78 -0
- package/.claude/skills/claude-md-generator/SKILL.md +248 -0
- package/.claude/skills/claude-md-generator/evals/evals.json +35 -0
- package/.claude/skills/claude-md-generator/references/section-guide.md +175 -0
- package/dist/e2e.test.d.ts +1 -0
- package/dist/e2e.test.js +62 -0
- package/dist/fs-adapters.d.ts +4 -0
- package/dist/fs-adapters.js +101 -0
- package/dist/fs-adapters.test.d.ts +1 -0
- package/dist/fs-adapters.test.js +108 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +211 -0
- package/dist/transformers.d.ts +6 -0
- package/dist/transformers.js +2 -0
- package/package.json +25 -0
- package/registry.json +226 -0
- package/skills/blog-cover-image-cli/.github/workflows/publish.yml +19 -0
- package/skills/blog-cover-image-cli/LICENSE +15 -0
- package/skills/blog-cover-image-cli/README.md +126 -0
- package/skills/blog-cover-image-cli/SKILL.md +7 -0
- package/skills/blog-cover-image-cli/agent-skill/blog-cover-generator/README.md +30 -0
- package/skills/blog-cover-image-cli/agent-skill/blog-cover-generator/SKILL.md +72 -0
- package/skills/blog-cover-image-cli/bin/cli.js +226 -0
- package/skills/blog-cover-image-cli/examples/100x_UX_Research_AI_Agent.png +0 -0
- package/skills/blog-cover-image-cli/examples/Firecrawl-supabase-bolt.png +0 -0
- package/skills/blog-cover-image-cli/examples/Git-City_Case_study_Cover_Image.jpg +0 -0
- package/skills/blog-cover-image-cli/examples/THE DISTRIBUTION LAYER (2).png +0 -0
- package/skills/blog-cover-image-cli/examples/canva-perplexity-duolingo-cover-image.png +0 -0
- package/skills/blog-cover-image-cli/examples/gamma-mistral-veed.png +0 -0
- package/skills/blog-cover-image-cli/examples/server-survival-case-study-cover-image(1).png +0 -0
- package/skills/blog-cover-image-cli/examples/viral-meme-automation.png +0 -0
- package/skills/blog-cover-image-cli/index.js +2 -0
- package/skills/blog-cover-image-cli/package-lock.json +2238 -0
- package/skills/blog-cover-image-cli/package.json +37 -0
- package/skills/blog-cover-image-cli/src/geminiGenerator.js +126 -0
- package/skills/blog-cover-image-cli/src/imageValidator.js +54 -0
- package/skills/blog-cover-image-cli/src/logoFetcher.js +86 -0
- package/skills/claude-md-generator/.env.example +7 -0
- package/skills/claude-md-generator/README.md +78 -0
- package/skills/claude-md-generator/SKILL.md +254 -0
- package/skills/claude-md-generator/evals/evals.json +35 -0
- package/skills/claude-md-generator/references/section-guide.md +175 -0
- package/skills/cook-the-blog/README.md +86 -0
- package/skills/cook-the-blog/SKILL.md +130 -0
- package/skills/dependency-update-bot/.env.example +13 -0
- package/skills/dependency-update-bot/README.md +101 -0
- package/skills/dependency-update-bot/SKILL.md +376 -0
- package/skills/dependency-update-bot/evals/evals.json +45 -0
- package/skills/dependency-update-bot/references/changelog-patterns.md +201 -0
- package/skills/docs-from-code/.env.example +13 -0
- package/skills/docs-from-code/README.md +97 -0
- package/skills/docs-from-code/SKILL.md +160 -0
- package/skills/docs-from-code/evals/evals.json +29 -0
- package/skills/docs-from-code/references/extraction-guide.md +174 -0
- package/skills/docs-from-code/references/output-template.md +135 -0
- package/skills/docs-from-code/scripts/extract_py.py +238 -0
- package/skills/docs-from-code/scripts/extract_ts.ts +284 -0
- package/skills/docs-from-code/scripts/package.json +18 -0
- package/skills/explain-this-pr/README.md +74 -0
- package/skills/explain-this-pr/SKILL.md +130 -0
- package/skills/explain-this-pr/evals/evals.json +35 -0
- package/skills/google-trends-api-skills/README.md +78 -0
- package/skills/google-trends-api-skills/SKILL.md +7 -0
- package/skills/google-trends-api-skills/google-trends-api/SKILL.md +163 -0
- package/skills/google-trends-api-skills/google-trends-api/references/api-responses.md +188 -0
- package/skills/google-trends-api-skills/google-trends-api/scripts/discover_keywords.py +344 -0
- package/skills/google-trends-api-skills/seo-keyword-research/SKILL.md +205 -0
- package/skills/google-trends-api-skills/seo-keyword-research/references/keyword-placement-guide.md +89 -0
- package/skills/google-trends-api-skills/seo-keyword-research/references/tech-blog-examples.md +207 -0
- package/skills/google-trends-api-skills/seo-keyword-research/scripts/blog_seo_research.py +373 -0
- package/skills/hackernews-intel/.env.example +33 -0
- package/skills/hackernews-intel/README.md +161 -0
- package/skills/hackernews-intel/SKILL.md +156 -0
- package/skills/hackernews-intel/evals/evals.json +35 -0
- package/skills/hackernews-intel/package.json +15 -0
- package/skills/hackernews-intel/scripts/monitor-hn.js +258 -0
- package/skills/kill-the-standup/.env.example +22 -0
- package/skills/kill-the-standup/README.md +84 -0
- package/skills/kill-the-standup/SKILL.md +169 -0
- package/skills/kill-the-standup/evals/evals.json +35 -0
- package/skills/kill-the-standup/references/standup-format.md +102 -0
- package/skills/linkedin-post-generator/.env.example +14 -0
- package/skills/linkedin-post-generator/README.md +107 -0
- package/skills/linkedin-post-generator/SKILL.md +228 -0
- package/skills/linkedin-post-generator/evals/evals.json +35 -0
- package/skills/linkedin-post-generator/references/linkedin-format.md +216 -0
- package/skills/linkedin-post-generator/references/output-template.md +154 -0
- package/skills/llms-txt-generator/.env.example +18 -0
- package/skills/llms-txt-generator/README.md +142 -0
- package/skills/llms-txt-generator/SKILL.md +176 -0
- package/skills/llms-txt-generator/evals/evals.json +35 -0
- package/skills/llms-txt-generator/references/llms-txt-spec.md +88 -0
- package/skills/llms-txt-generator/references/output-template.md +76 -0
- package/skills/llms-txt-generator/test-output/genzcareer.in/llms.txt +31 -0
- package/skills/luma-attendees-scraper/README.md +170 -0
- package/skills/luma-attendees-scraper/SKILL.md +7 -0
- package/skills/luma-attendees-scraper/luma_attendees_export.js +223 -0
- package/skills/meeting-brief-generator/.env.example +21 -0
- package/skills/meeting-brief-generator/README.md +90 -0
- package/skills/meeting-brief-generator/SKILL.md +275 -0
- package/skills/meeting-brief-generator/evals/evals.json +35 -0
- package/skills/meeting-brief-generator/references/brief-format.md +114 -0
- package/skills/meeting-brief-generator/references/output-template.md +150 -0
- package/skills/meta-ads-skill/README.md +100 -0
- package/skills/meta-ads-skill/SKILL.md +7 -0
- package/skills/meta-ads-skill/meta-ads-skill/SKILL.md +41 -0
- package/skills/meta-ads-skill/meta-ads-skill/references/report_templates.md +47 -0
- package/skills/meta-ads-skill/meta-ads-skill/references/workflows.md +51 -0
- package/skills/meta-ads-skill/meta-ads-skill/scripts/auth_check.py +22 -0
- package/skills/meta-ads-skill/meta-ads-skill/scripts/formatters.py +46 -0
- package/skills/newsletter-digest/.env.example +20 -0
- package/skills/newsletter-digest/README.md +147 -0
- package/skills/newsletter-digest/SKILL.md +221 -0
- package/skills/newsletter-digest/evals/evals.json +35 -0
- package/skills/newsletter-digest/feeds.json +7 -0
- package/skills/newsletter-digest/package.json +15 -0
- package/skills/newsletter-digest/references/digest-format.md +123 -0
- package/skills/newsletter-digest/references/output-template.md +136 -0
- package/skills/newsletter-digest/scripts/fetch-feeds.js +141 -0
- package/skills/newsletter-digest/scripts/ghost-publish.js +147 -0
- package/skills/noise2blog/.env.example +16 -0
- package/skills/noise2blog/README.md +107 -0
- package/skills/noise2blog/SKILL.md +229 -0
- package/skills/noise2blog/evals/evals.json +35 -0
- package/skills/noise2blog/references/blog-format.md +188 -0
- package/skills/noise2blog/references/output-template.md +184 -0
- package/skills/outreach-sequence-builder/.env.example +12 -0
- package/skills/outreach-sequence-builder/README.md +108 -0
- package/skills/outreach-sequence-builder/SKILL.md +248 -0
- package/skills/outreach-sequence-builder/evals/evals.json +36 -0
- package/skills/outreach-sequence-builder/references/output-template.md +171 -0
- package/skills/outreach-sequence-builder/references/sequence-format.md +167 -0
- package/skills/outreach-sequence-builder/references/signal-playbook.md +117 -0
- package/skills/position-me/README.md +71 -0
- package/skills/position-me/SKILL.md +7 -0
- package/skills/position-me/position-me/SKILL.md +50 -0
- package/skills/position-me/position-me/references/EVALUATION_SOP.md +40 -0
- package/skills/position-me/position-me/references/REPORT_TEMPLATE.md +58 -0
- package/skills/position-me/position-me/scripts/extract_links.py +49 -0
- package/skills/pr-description-writer/README.md +81 -0
- package/skills/pr-description-writer/SKILL.md +141 -0
- package/skills/pr-description-writer/evals/evals.json +35 -0
- package/skills/pr-description-writer/references/pr-format-guide.md +145 -0
- package/skills/producthunt-launch-kit/.env.example +7 -0
- package/skills/producthunt-launch-kit/README.md +95 -0
- package/skills/producthunt-launch-kit/SKILL.md +380 -0
- package/skills/producthunt-launch-kit/evals/evals.json +35 -0
- package/skills/producthunt-launch-kit/references/copy-rules.md +124 -0
- package/skills/reddit-icp-monitor/.env.example +16 -0
- package/skills/reddit-icp-monitor/README.md +117 -0
- package/skills/reddit-icp-monitor/SKILL.md +271 -0
- package/skills/reddit-icp-monitor/evals/evals.json +40 -0
- package/skills/reddit-icp-monitor/references/icp-format.md +131 -0
- package/skills/reddit-icp-monitor/references/reply-rules.md +110 -0
- package/skills/reddit-post-engine/.env.example +13 -0
- package/skills/reddit-post-engine/README.md +103 -0
- package/skills/reddit-post-engine/SKILL.md +303 -0
- package/skills/reddit-post-engine/evals/evals.json +35 -0
- package/skills/reddit-post-engine/references/subreddit-playbook.md +156 -0
- package/skills/schema-markup-generator/.env.example +19 -0
- package/skills/schema-markup-generator/README.md +114 -0
- package/skills/schema-markup-generator/SKILL.md +192 -0
- package/skills/schema-markup-generator/evals/evals.json +35 -0
- package/skills/schema-markup-generator/references/json-ld-spec.md +263 -0
- package/skills/schema-markup-generator/references/output-template.md +556 -0
- package/skills/show-hn-writer/.env.example +14 -0
- package/skills/show-hn-writer/README.md +88 -0
- package/skills/show-hn-writer/SKILL.md +303 -0
- package/skills/show-hn-writer/evals/evals.json +35 -0
- package/skills/show-hn-writer/references/hn-rules.md +74 -0
- package/skills/show-hn-writer/references/title-formulas.md +93 -0
- package/skills/stargazer/README.md +79 -0
- package/skills/stargazer/SKILL.md +7 -0
- package/skills/stargazer/stargazer-skill/SKILL.md +58 -0
- package/skills/stargazer/stargazer-skill/assets/.env.example +18 -0
- package/skills/stargazer/stargazer-skill/scripts/convert_to_csv.py +63 -0
- package/skills/stargazer/stargazer-skill/scripts/count_emails.py +52 -0
- package/skills/stargazer/stargazer-skill/scripts/stargazer_deep_extractor.py +450 -0
- package/skills/tweet-thread-from-blog/.env.example +14 -0
- package/skills/tweet-thread-from-blog/README.md +109 -0
- package/skills/tweet-thread-from-blog/SKILL.md +177 -0
- package/skills/tweet-thread-from-blog/evals/evals.json +35 -0
- package/skills/tweet-thread-from-blog/references/output-template.md +193 -0
- package/skills/tweet-thread-from-blog/references/thread-format.md +107 -0
- package/skills/twitter-GTM-find-skill/README.md +43 -0
- package/skills/twitter-GTM-find-skill/SKILL.md +7 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/SKILL.md +37 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/references/icp-checklist.md +35 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/package.json +23 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/run_pipeline.sh +8 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/debug.ts +23 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/extractor.ts +79 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/icp-filter.ts +87 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/index.ts +94 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/scraper.ts +41 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/tsconfig.json +13 -0
- package/skills/yc-intent-radar-skill/README.md +39 -0
- package/skills/yc-intent-radar-skill/SKILL.md +7 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/SKILL.md +59 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/auth.js +29 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/db.js +62 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/export_radar_candidates.js +40 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/package-lock.json +1525 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/package.json +12 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/scraper.js +217 -0
- package/src/e2e.test.ts +35 -0
- package/src/fs-adapters.test.ts +91 -0
- package/src/fs-adapters.ts +65 -0
- package/src/index.ts +182 -0
- package/src/transformers.ts +6 -0
- 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
|