devrel-origin 0.2.14__py3-none-any.whl

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 (98) hide show
  1. devrel_origin/__init__.py +15 -0
  2. devrel_origin/cli/__init__.py +92 -0
  3. devrel_origin/cli/_common.py +243 -0
  4. devrel_origin/cli/analytics.py +28 -0
  5. devrel_origin/cli/argus.py +497 -0
  6. devrel_origin/cli/auth.py +227 -0
  7. devrel_origin/cli/config.py +108 -0
  8. devrel_origin/cli/content.py +259 -0
  9. devrel_origin/cli/cost.py +108 -0
  10. devrel_origin/cli/cro.py +298 -0
  11. devrel_origin/cli/deliverables.py +65 -0
  12. devrel_origin/cli/docs.py +91 -0
  13. devrel_origin/cli/doctor.py +178 -0
  14. devrel_origin/cli/experiment.py +29 -0
  15. devrel_origin/cli/growth.py +97 -0
  16. devrel_origin/cli/init.py +472 -0
  17. devrel_origin/cli/intel.py +27 -0
  18. devrel_origin/cli/kb.py +96 -0
  19. devrel_origin/cli/listen.py +31 -0
  20. devrel_origin/cli/marketing.py +66 -0
  21. devrel_origin/cli/migrate.py +45 -0
  22. devrel_origin/cli/run.py +46 -0
  23. devrel_origin/cli/sales.py +57 -0
  24. devrel_origin/cli/schedule.py +62 -0
  25. devrel_origin/cli/synthesize.py +28 -0
  26. devrel_origin/cli/triage.py +29 -0
  27. devrel_origin/cli/video.py +35 -0
  28. devrel_origin/core/__init__.py +58 -0
  29. devrel_origin/core/agent_config.py +75 -0
  30. devrel_origin/core/argus.py +964 -0
  31. devrel_origin/core/atlas.py +1450 -0
  32. devrel_origin/core/base.py +372 -0
  33. devrel_origin/core/cyra.py +563 -0
  34. devrel_origin/core/dex.py +708 -0
  35. devrel_origin/core/echo.py +614 -0
  36. devrel_origin/core/growth/__init__.py +27 -0
  37. devrel_origin/core/growth/recommendations.py +219 -0
  38. devrel_origin/core/growth/target_kinds.py +51 -0
  39. devrel_origin/core/iris.py +513 -0
  40. devrel_origin/core/kai.py +1367 -0
  41. devrel_origin/core/llm.py +542 -0
  42. devrel_origin/core/llm_backends.py +274 -0
  43. devrel_origin/core/mox.py +514 -0
  44. devrel_origin/core/nova.py +349 -0
  45. devrel_origin/core/pax.py +1205 -0
  46. devrel_origin/core/rex.py +532 -0
  47. devrel_origin/core/sage.py +486 -0
  48. devrel_origin/core/sentinel.py +385 -0
  49. devrel_origin/core/types.py +98 -0
  50. devrel_origin/core/video/__init__.py +22 -0
  51. devrel_origin/core/video/assembler.py +131 -0
  52. devrel_origin/core/video/browser_recorder.py +118 -0
  53. devrel_origin/core/video/desktop_recorder.py +254 -0
  54. devrel_origin/core/video/overlay_renderer.py +143 -0
  55. devrel_origin/core/video/script_parser.py +147 -0
  56. devrel_origin/core/video/tts_engine.py +82 -0
  57. devrel_origin/core/vox.py +268 -0
  58. devrel_origin/core/watchdog.py +321 -0
  59. devrel_origin/project/__init__.py +1 -0
  60. devrel_origin/project/config.py +75 -0
  61. devrel_origin/project/cost_sink.py +61 -0
  62. devrel_origin/project/init.py +104 -0
  63. devrel_origin/project/paths.py +75 -0
  64. devrel_origin/project/state.py +241 -0
  65. devrel_origin/project/templates/__init__.py +4 -0
  66. devrel_origin/project/templates/config.toml +24 -0
  67. devrel_origin/project/templates/devrel.gitignore +10 -0
  68. devrel_origin/project/templates/slop-blocklist.md +45 -0
  69. devrel_origin/project/templates/style.md +24 -0
  70. devrel_origin/project/templates/voice.md +29 -0
  71. devrel_origin/quality/__init__.py +66 -0
  72. devrel_origin/quality/editorial.py +357 -0
  73. devrel_origin/quality/persona.py +84 -0
  74. devrel_origin/quality/readability.py +148 -0
  75. devrel_origin/quality/slop.py +167 -0
  76. devrel_origin/quality/style.py +110 -0
  77. devrel_origin/quality/voice.py +15 -0
  78. devrel_origin/tools/__init__.py +9 -0
  79. devrel_origin/tools/analytics.py +304 -0
  80. devrel_origin/tools/api_client.py +393 -0
  81. devrel_origin/tools/apollo_client.py +305 -0
  82. devrel_origin/tools/code_validator.py +428 -0
  83. devrel_origin/tools/github_tools.py +297 -0
  84. devrel_origin/tools/instantly_client.py +412 -0
  85. devrel_origin/tools/kb_harvester.py +340 -0
  86. devrel_origin/tools/mcp_server.py +578 -0
  87. devrel_origin/tools/notifications.py +245 -0
  88. devrel_origin/tools/run_report.py +193 -0
  89. devrel_origin/tools/scheduler.py +231 -0
  90. devrel_origin/tools/search_tools.py +321 -0
  91. devrel_origin/tools/self_improve.py +168 -0
  92. devrel_origin/tools/sheets.py +236 -0
  93. devrel_origin-0.2.14.dist-info/METADATA +354 -0
  94. devrel_origin-0.2.14.dist-info/RECORD +98 -0
  95. devrel_origin-0.2.14.dist-info/WHEEL +5 -0
  96. devrel_origin-0.2.14.dist-info/entry_points.txt +2 -0
  97. devrel_origin-0.2.14.dist-info/licenses/LICENSE +21 -0
  98. devrel_origin-0.2.14.dist-info/top_level.txt +1 -0
@@ -0,0 +1,297 @@
1
+ """
2
+ GitHub Tools — Issue, PR, and contributor analysis.
3
+
4
+ Provides typed async access to the GitHub REST API v3 for:
5
+ - Fetching and filtering issues and pull requests
6
+ - Analyzing contributor history and activity
7
+ - Detecting duplicate issues
8
+ - Extracting labels and milestones
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ from dataclasses import dataclass
14
+ from datetime import datetime, timezone
15
+ from typing import Any, Optional
16
+
17
+ import httpx
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ DEFAULT_REPO = os.getenv("GITHUB_REPO", "openclaw/openclaw")
22
+ GITHUB_API = "https://api.github.com"
23
+ API_TIMEOUT = 30.0
24
+
25
+
26
+ @dataclass
27
+ class GitHubIssue:
28
+ """Parsed GitHub issue."""
29
+
30
+ number: int
31
+ title: str
32
+ body: str
33
+ author: str
34
+ state: str
35
+ labels: list[str]
36
+ created_at: str
37
+ updated_at: str
38
+ comments_count: int
39
+ reactions_total: int = 0
40
+ is_pull_request: bool = False
41
+ url: str = ""
42
+
43
+
44
+ @dataclass
45
+ class ContributorProfile:
46
+ """Summary of a GitHub contributor's activity."""
47
+
48
+ username: str
49
+ total_issues: int
50
+ total_prs: int
51
+ total_comments: int
52
+ first_contribution: str
53
+ last_contribution: str
54
+ is_maintainer: bool = False
55
+
56
+
57
+ class GitHubTools:
58
+ """
59
+ Async GitHub API client focused on community health analysis.
60
+
61
+ Usage::
62
+
63
+ gh = GitHubTools(token="ghp_...")
64
+ issues = await gh.fetch_recent_issues(days=7)
65
+ profile = await gh.get_contributor_profile("some-user")
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ token: str = "",
71
+ repo: str = DEFAULT_REPO,
72
+ ):
73
+ self.repo = repo
74
+ headers: dict[str, str] = {
75
+ "Accept": "application/vnd.github.v3+json",
76
+ }
77
+ if token:
78
+ headers["Authorization"] = f"Bearer {token}"
79
+ self._client = httpx.AsyncClient(
80
+ base_url=GITHUB_API,
81
+ headers=headers,
82
+ timeout=API_TIMEOUT,
83
+ )
84
+
85
+ async def close(self) -> None:
86
+ await self._client.aclose()
87
+
88
+ @property
89
+ def repo_full_name(self) -> str:
90
+ """Compatibility alias used by analytics collectors."""
91
+ return self.repo
92
+
93
+ # -- Issues -----------------------------------------------------------
94
+
95
+ async def fetch_recent_issues(
96
+ self,
97
+ days: int = 7,
98
+ state: str = "open",
99
+ labels: Optional[list[str]] = None,
100
+ per_page: int = 100,
101
+ ) -> list[GitHubIssue]:
102
+ """Fetch issues created in the last N days."""
103
+ since = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0)
104
+ from datetime import timedelta
105
+
106
+ since -= timedelta(days=days)
107
+
108
+ params: dict[str, Any] = {
109
+ "state": state,
110
+ "since": since.isoformat() + "Z",
111
+ "per_page": per_page,
112
+ "sort": "created",
113
+ "direction": "desc",
114
+ }
115
+ if labels:
116
+ params["labels"] = ",".join(labels)
117
+
118
+ resp = await self._client.get(f"/repos/{self.repo}/issues", params=params)
119
+ resp.raise_for_status()
120
+
121
+ issues = []
122
+ for item in resp.json():
123
+ # Skip PRs returned via the issues endpoint
124
+ is_pr = "pull_request" in item
125
+ issues.append(
126
+ GitHubIssue(
127
+ number=item["number"],
128
+ title=item["title"],
129
+ body=item.get("body") or "",
130
+ author=item["user"]["login"],
131
+ state=item["state"],
132
+ labels=[lbl["name"] for lbl in item.get("labels", [])],
133
+ created_at=item["created_at"],
134
+ updated_at=item["updated_at"],
135
+ comments_count=item.get("comments", 0),
136
+ reactions_total=item.get("reactions", {}).get("total_count", 0),
137
+ is_pull_request=is_pr,
138
+ url=item["html_url"],
139
+ )
140
+ )
141
+
142
+ logger.info(f"Fetched {len(issues)} issues from {self.repo} (last {days} days)")
143
+ return issues
144
+
145
+ async def get_issue(self, issue_number: int) -> GitHubIssue:
146
+ """Fetch a single issue by number."""
147
+ resp = await self._client.get(f"/repos/{self.repo}/issues/{issue_number}")
148
+ resp.raise_for_status()
149
+ item = resp.json()
150
+ return GitHubIssue(
151
+ number=item["number"],
152
+ title=item["title"],
153
+ body=item.get("body") or "",
154
+ author=item["user"]["login"],
155
+ state=item["state"],
156
+ labels=[lbl["name"] for lbl in item.get("labels", [])],
157
+ created_at=item["created_at"],
158
+ updated_at=item["updated_at"],
159
+ comments_count=item.get("comments", 0),
160
+ reactions_total=item.get("reactions", {}).get("total_count", 0),
161
+ is_pull_request="pull_request" in item,
162
+ url=item["html_url"],
163
+ )
164
+
165
+ async def get_issue_comments(
166
+ self, issue_number: int, per_page: int = 100
167
+ ) -> list[dict[str, Any]]:
168
+ """Fetch all comments on an issue."""
169
+ resp = await self._client.get(
170
+ f"/repos/{self.repo}/issues/{issue_number}/comments",
171
+ params={"per_page": per_page},
172
+ )
173
+ resp.raise_for_status()
174
+ return [
175
+ {
176
+ "author": c["user"]["login"],
177
+ "body": c["body"],
178
+ "created_at": c["created_at"],
179
+ "reactions": c.get("reactions", {}).get("total_count", 0),
180
+ }
181
+ for c in resp.json()
182
+ ]
183
+
184
+ # -- Contributors -----------------------------------------------------
185
+
186
+ async def get_contributor_profile(self, username: str) -> ContributorProfile:
187
+ """Build a contributor profile from issue/PR/comment activity."""
188
+ # Issues authored
189
+ issues_resp = await self._client.get(
190
+ "/search/issues",
191
+ params={
192
+ "q": f"repo:{self.repo} author:{username} is:issue",
193
+ "per_page": 1,
194
+ },
195
+ )
196
+ issues_resp.raise_for_status()
197
+ total_issues = issues_resp.json().get("total_count", 0)
198
+
199
+ # PRs authored
200
+ prs_resp = await self._client.get(
201
+ "/search/issues",
202
+ params={
203
+ "q": f"repo:{self.repo} author:{username} is:pr",
204
+ "per_page": 1,
205
+ },
206
+ )
207
+ prs_resp.raise_for_status()
208
+ total_prs = prs_resp.json().get("total_count", 0)
209
+
210
+ # Comments (approximation via commenter search)
211
+ comments_resp = await self._client.get(
212
+ "/search/issues",
213
+ params={
214
+ "q": f"repo:{self.repo} commenter:{username}",
215
+ "per_page": 1,
216
+ },
217
+ )
218
+ comments_resp.raise_for_status()
219
+ total_comments = comments_resp.json().get("total_count", 0)
220
+
221
+ return ContributorProfile(
222
+ username=username,
223
+ total_issues=total_issues,
224
+ total_prs=total_prs,
225
+ total_comments=total_comments,
226
+ first_contribution="", # Would require deeper pagination
227
+ last_contribution="",
228
+ )
229
+
230
+ # -- Search / Duplicates ---------------------------------------------
231
+
232
+ async def search_similar_issues(self, query: str, limit: int = 5) -> list[GitHubIssue]:
233
+ """Search for issues matching a query (for duplicate detection)."""
234
+ resp = await self._client.get(
235
+ "/search/issues",
236
+ params={
237
+ "q": f"repo:{self.repo} is:issue {query}",
238
+ "per_page": limit,
239
+ "sort": "relevance",
240
+ },
241
+ )
242
+ resp.raise_for_status()
243
+ return [
244
+ GitHubIssue(
245
+ number=item["number"],
246
+ title=item["title"],
247
+ body=item.get("body") or "",
248
+ author=item["user"]["login"],
249
+ state=item["state"],
250
+ labels=[lbl["name"] for lbl in item.get("labels", [])],
251
+ created_at=item["created_at"],
252
+ updated_at=item["updated_at"],
253
+ comments_count=item.get("comments", 0),
254
+ url=item["html_url"],
255
+ )
256
+ for item in resp.json().get("items", [])
257
+ ]
258
+
259
+ # -- Labels & Milestones ---------------------------------------------
260
+
261
+ async def list_labels(self) -> list[dict[str, str]]:
262
+ """List all repo labels."""
263
+ resp = await self._client.get(f"/repos/{self.repo}/labels", params={"per_page": 100})
264
+ resp.raise_for_status()
265
+ return [
266
+ {
267
+ "name": lbl["name"],
268
+ "color": lbl["color"],
269
+ "description": lbl.get("description") or "",
270
+ }
271
+ for lbl in resp.json()
272
+ ]
273
+
274
+ async def add_labels(self, issue_number: int, labels: list[str]) -> list[dict[str, str]]:
275
+ """Add labels to an issue."""
276
+ resp = await self._client.post(
277
+ f"/repos/{self.repo}/issues/{issue_number}/labels",
278
+ json={"labels": labels},
279
+ )
280
+ resp.raise_for_status()
281
+ return resp.json()
282
+
283
+ # -- Repo Stats -------------------------------------------------------
284
+
285
+ async def get_repo_stats(self) -> dict[str, Any]:
286
+ """Get basic repository statistics."""
287
+ resp = await self._client.get(f"/repos/{self.repo}")
288
+ resp.raise_for_status()
289
+ data = resp.json()
290
+ return {
291
+ "stars": data["stargazers_count"],
292
+ "forks": data["forks_count"],
293
+ "open_issues": data["open_issues_count"],
294
+ "watchers": data["subscribers_count"],
295
+ "language": data["language"],
296
+ "updated_at": data["updated_at"],
297
+ }