elizaos-plugin-github 2.0.0a4__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.
@@ -0,0 +1,68 @@
1
+ import os
2
+
3
+ from pydantic import BaseModel, ConfigDict, field_validator
4
+
5
+ from elizaos_plugin_github.error import ConfigError, MissingSettingError
6
+
7
+
8
+ class GitHubConfig(BaseModel):
9
+ model_config = ConfigDict(frozen=True)
10
+
11
+ # Required fields
12
+ api_token: str
13
+
14
+ # Optional fields with defaults
15
+ owner: str | None = None
16
+ repo: str | None = None
17
+ branch: str = "main"
18
+ webhook_secret: str | None = None
19
+ app_id: str | None = None
20
+ app_private_key: str | None = None
21
+ installation_id: str | None = None
22
+
23
+ @field_validator("api_token")
24
+ @classmethod
25
+ def validate_api_token(cls, v: str) -> str:
26
+ if not v or not v.strip():
27
+ raise ConfigError("API token cannot be empty")
28
+ return v
29
+
30
+ @classmethod
31
+ def from_env(cls) -> "GitHubConfig":
32
+ api_token = os.environ.get("GITHUB_API_TOKEN")
33
+ if not api_token:
34
+ raise MissingSettingError("GITHUB_API_TOKEN")
35
+
36
+ return cls(
37
+ api_token=api_token,
38
+ owner=os.environ.get("GITHUB_OWNER"),
39
+ repo=os.environ.get("GITHUB_REPO"),
40
+ branch=os.environ.get("GITHUB_BRANCH", "main"),
41
+ webhook_secret=os.environ.get("GITHUB_WEBHOOK_SECRET"),
42
+ app_id=os.environ.get("GITHUB_APP_ID"),
43
+ app_private_key=os.environ.get("GITHUB_APP_PRIVATE_KEY"),
44
+ installation_id=os.environ.get("GITHUB_INSTALLATION_ID"),
45
+ )
46
+
47
+ def get_repository_ref(
48
+ self, owner: str | None = None, repo: str | None = None
49
+ ) -> tuple[str, str]:
50
+ resolved_owner = owner or self.owner
51
+ resolved_repo = repo or self.repo
52
+
53
+ if not resolved_owner:
54
+ raise MissingSettingError("owner (GITHUB_OWNER)")
55
+
56
+ if not resolved_repo:
57
+ raise MissingSettingError("repo (GITHUB_REPO)")
58
+
59
+ return resolved_owner, resolved_repo
60
+
61
+ def has_app_auth(self) -> bool:
62
+ return bool(self.app_id and self.app_private_key)
63
+
64
+ def validate_all(self) -> None:
65
+ if self.has_app_auth() and not self.installation_id:
66
+ raise ConfigError(
67
+ "GITHUB_INSTALLATION_ID is required when using GitHub App authentication"
68
+ )
@@ -0,0 +1,165 @@
1
+ from datetime import datetime
2
+
3
+
4
+ class GitHubError(Exception):
5
+ def __init__(self, message: str) -> None:
6
+ self.message = message
7
+ super().__init__(message)
8
+
9
+ def is_retryable(self) -> bool:
10
+ return False
11
+
12
+ def retry_after_ms(self) -> int | None:
13
+ return None
14
+
15
+
16
+ class ClientNotInitializedError(GitHubError):
17
+ def __init__(self) -> None:
18
+ super().__init__("GitHub client not initialized - ensure GITHUB_API_TOKEN is configured")
19
+
20
+
21
+ class ConfigError(GitHubError):
22
+ def __init__(self, message: str) -> None:
23
+ super().__init__(f"Configuration error: {message}")
24
+
25
+
26
+ class MissingSettingError(GitHubError):
27
+ def __init__(self, setting_name: str) -> None:
28
+ self.setting_name = setting_name
29
+ super().__init__(f"Missing required setting: {setting_name}")
30
+
31
+
32
+ class InvalidArgumentError(GitHubError):
33
+ def __init__(self, message: str) -> None:
34
+ super().__init__(f"Invalid argument: {message}")
35
+
36
+
37
+ class RepositoryNotFoundError(GitHubError):
38
+ def __init__(self, owner: str, repo: str) -> None:
39
+ self.owner = owner
40
+ self.repo = repo
41
+ super().__init__(f"Repository not found: {owner}/{repo}")
42
+
43
+
44
+ class BranchNotFoundError(GitHubError):
45
+ def __init__(self, branch: str, owner: str, repo: str) -> None:
46
+ self.branch = branch
47
+ super().__init__(f"Branch not found: {branch} in {owner}/{repo}")
48
+
49
+
50
+ class FileNotFoundError(GitHubError):
51
+ def __init__(self, path: str, owner: str, repo: str) -> None:
52
+ self.path = path
53
+ super().__init__(f"File not found: {path} in {owner}/{repo}")
54
+
55
+
56
+ class IssueNotFoundError(GitHubError):
57
+ def __init__(self, issue_number: int, owner: str, repo: str) -> None:
58
+ self.issue_number = issue_number
59
+ super().__init__(f"Issue #{issue_number} not found in {owner}/{repo}")
60
+
61
+
62
+ class PullRequestNotFoundError(GitHubError):
63
+ def __init__(self, pull_number: int, owner: str, repo: str) -> None:
64
+ self.pull_number = pull_number
65
+ super().__init__(f"Pull request #{pull_number} not found in {owner}/{repo}")
66
+
67
+
68
+ class PermissionDeniedError(GitHubError):
69
+ def __init__(self, action: str) -> None:
70
+ super().__init__(f"Permission denied: {action}")
71
+
72
+
73
+ class RateLimitedError(GitHubError):
74
+ def __init__(self, retry_after_ms: int, remaining: int, reset_at: datetime) -> None:
75
+ self._retry_after_ms = retry_after_ms
76
+ self.remaining = remaining
77
+ self.reset_at = reset_at
78
+ super().__init__(f"Rate limited by GitHub API, retry after {retry_after_ms // 1000}s")
79
+
80
+ def is_retryable(self) -> bool:
81
+ return True
82
+
83
+ def retry_after_ms(self) -> int | None:
84
+ return self._retry_after_ms
85
+
86
+
87
+ class SecondaryRateLimitError(GitHubError):
88
+ def __init__(self, retry_after_ms: int) -> None:
89
+ self._retry_after_ms = retry_after_ms
90
+ super().__init__(f"Secondary rate limit hit, retry after {retry_after_ms // 1000}s")
91
+
92
+ def is_retryable(self) -> bool:
93
+ return True
94
+
95
+ def retry_after_ms(self) -> int | None:
96
+ return self._retry_after_ms
97
+
98
+
99
+ class TimeoutError(GitHubError):
100
+ def __init__(self, timeout_ms: int, operation: str) -> None:
101
+ self._timeout_ms = timeout_ms
102
+ super().__init__(f"Operation timed out after {timeout_ms}ms: {operation}")
103
+
104
+ def is_retryable(self) -> bool:
105
+ return True
106
+
107
+ def retry_after_ms(self) -> int | None:
108
+ return self._timeout_ms // 2
109
+
110
+
111
+ class MergeConflictError(GitHubError):
112
+ def __init__(self, pull_number: int, owner: str, repo: str) -> None:
113
+ self.pull_number = pull_number
114
+ super().__init__(f"Merge conflict in pull request #{pull_number} in {owner}/{repo}")
115
+
116
+
117
+ class BranchExistsError(GitHubError):
118
+ def __init__(self, branch: str, owner: str, repo: str) -> None:
119
+ self.branch = branch
120
+ super().__init__(f"Branch already exists: {branch} in {owner}/{repo}")
121
+
122
+
123
+ class ValidationError(GitHubError):
124
+ def __init__(self, field: str, reason: str) -> None:
125
+ self.field = field
126
+ super().__init__(f"Validation failed for {field}: {reason}")
127
+
128
+
129
+ class GitHubApiError(GitHubError):
130
+ def __init__(
131
+ self,
132
+ status: int,
133
+ message: str,
134
+ code: str | None = None,
135
+ documentation_url: str | None = None,
136
+ ) -> None:
137
+ self.status = status
138
+ self.code = code
139
+ self.documentation_url = documentation_url
140
+ super().__init__(f"GitHub API error ({status}): {message}")
141
+
142
+ def is_retryable(self) -> bool:
143
+ return self.status >= 500
144
+
145
+
146
+ class NetworkError(GitHubError):
147
+ def __init__(self, message: str) -> None:
148
+ super().__init__(f"Network error: {message}")
149
+
150
+ def is_retryable(self) -> bool:
151
+ return True
152
+
153
+ def retry_after_ms(self) -> int | None:
154
+ return 1000
155
+
156
+
157
+ class GitOperationError(GitHubError):
158
+ def __init__(self, operation: str, reason: str) -> None:
159
+ self.operation = operation
160
+ super().__init__(f"Git operation failed ({operation}): {reason}")
161
+
162
+
163
+ class WebhookVerificationError(GitHubError):
164
+ def __init__(self, reason: str) -> None:
165
+ super().__init__(f"Webhook verification failed: {reason}")
@@ -0,0 +1 @@
1
+ """Auto-generated module package."""
@@ -0,0 +1,171 @@
1
+ """
2
+ Auto-generated canonical action/provider/evaluator docs for plugin-github.
3
+ DO NOT EDIT - Generated from prompts/specs/**.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from typing import TypedDict
10
+
11
+
12
+ class ActionDoc(TypedDict, total=False):
13
+ name: str
14
+ description: str
15
+ similes: list[str]
16
+ parameters: list[object]
17
+ examples: list[list[object]]
18
+
19
+
20
+ class ProviderDoc(TypedDict, total=False):
21
+ name: str
22
+ description: str
23
+ position: int
24
+ dynamic: bool
25
+
26
+
27
+ class EvaluatorDoc(TypedDict, total=False):
28
+ name: str
29
+ description: str
30
+ similes: list[str]
31
+ alwaysRun: bool
32
+ examples: list[object]
33
+
34
+
35
+ _CORE_ACTION_DOCS_JSON = """{
36
+ "version": "1.0.0",
37
+ "actions": [
38
+ {
39
+ "name": "CREATE_GITHUB_BRANCH",
40
+ "description": "",
41
+ "parameters": []
42
+ },
43
+ {
44
+ "name": "CREATE_GITHUB_COMMENT",
45
+ "description": "",
46
+ "parameters": []
47
+ },
48
+ {
49
+ "name": "CREATE_GITHUB_ISSUE",
50
+ "description": "",
51
+ "parameters": []
52
+ },
53
+ {
54
+ "name": "CREATE_GITHUB_PULL_REQUEST",
55
+ "description": "",
56
+ "parameters": []
57
+ },
58
+ {
59
+ "name": "MERGE_GITHUB_PULL_REQUEST",
60
+ "description": "",
61
+ "parameters": []
62
+ },
63
+ {
64
+ "name": "PUSH_GITHUB_CODE",
65
+ "description": "",
66
+ "parameters": []
67
+ },
68
+ {
69
+ "name": "REVIEW_GITHUB_PULL_REQUEST",
70
+ "description": "",
71
+ "parameters": []
72
+ }
73
+ ]
74
+ }"""
75
+ _ALL_ACTION_DOCS_JSON = """{
76
+ "version": "1.0.0",
77
+ "actions": [
78
+ {
79
+ "name": "CREATE_GITHUB_BRANCH",
80
+ "description": "",
81
+ "parameters": []
82
+ },
83
+ {
84
+ "name": "CREATE_GITHUB_COMMENT",
85
+ "description": "",
86
+ "parameters": []
87
+ },
88
+ {
89
+ "name": "CREATE_GITHUB_ISSUE",
90
+ "description": "",
91
+ "parameters": []
92
+ },
93
+ {
94
+ "name": "CREATE_GITHUB_PULL_REQUEST",
95
+ "description": "",
96
+ "parameters": []
97
+ },
98
+ {
99
+ "name": "MERGE_GITHUB_PULL_REQUEST",
100
+ "description": "",
101
+ "parameters": []
102
+ },
103
+ {
104
+ "name": "PUSH_GITHUB_CODE",
105
+ "description": "",
106
+ "parameters": []
107
+ },
108
+ {
109
+ "name": "REVIEW_GITHUB_PULL_REQUEST",
110
+ "description": "",
111
+ "parameters": []
112
+ }
113
+ ]
114
+ }"""
115
+ _CORE_PROVIDER_DOCS_JSON = """{
116
+ "version": "1.0.0",
117
+ "providers": [
118
+ {
119
+ "name": "GITHUB_ISSUE_CONTEXT",
120
+ "description": "Provides detailed context about a specific GitHub issue or pull request when referenced",
121
+ "dynamic": true
122
+ },
123
+ {
124
+ "name": "GITHUB_REPOSITORY_STATE",
125
+ "description": "Provides context about the current GitHub repository including recent activity",
126
+ "dynamic": true
127
+ }
128
+ ]
129
+ }"""
130
+ _ALL_PROVIDER_DOCS_JSON = """{
131
+ "version": "1.0.0",
132
+ "providers": [
133
+ {
134
+ "name": "GITHUB_ISSUE_CONTEXT",
135
+ "description": "Provides detailed context about a specific GitHub issue or pull request when referenced",
136
+ "dynamic": true
137
+ },
138
+ {
139
+ "name": "GITHUB_REPOSITORY_STATE",
140
+ "description": "Provides context about the current GitHub repository including recent activity",
141
+ "dynamic": true
142
+ }
143
+ ]
144
+ }"""
145
+ _CORE_EVALUATOR_DOCS_JSON = """{
146
+ "version": "1.0.0",
147
+ "evaluators": []
148
+ }"""
149
+ _ALL_EVALUATOR_DOCS_JSON = """{
150
+ "version": "1.0.0",
151
+ "evaluators": []
152
+ }"""
153
+
154
+ core_action_docs: dict[str, object] = json.loads(_CORE_ACTION_DOCS_JSON)
155
+ all_action_docs: dict[str, object] = json.loads(_ALL_ACTION_DOCS_JSON)
156
+ core_provider_docs: dict[str, object] = json.loads(_CORE_PROVIDER_DOCS_JSON)
157
+ all_provider_docs: dict[str, object] = json.loads(_ALL_PROVIDER_DOCS_JSON)
158
+ core_evaluator_docs: dict[str, object] = json.loads(_CORE_EVALUATOR_DOCS_JSON)
159
+ all_evaluator_docs: dict[str, object] = json.loads(_ALL_EVALUATOR_DOCS_JSON)
160
+
161
+ __all__ = [
162
+ "ActionDoc",
163
+ "ProviderDoc",
164
+ "EvaluatorDoc",
165
+ "core_action_docs",
166
+ "all_action_docs",
167
+ "core_provider_docs",
168
+ "all_provider_docs",
169
+ "core_evaluator_docs",
170
+ "all_evaluator_docs",
171
+ ]
@@ -0,0 +1,12 @@
1
+ from elizaos_plugin_github.providers.issue_context import IssueContextProvider
2
+ from elizaos_plugin_github.providers.repository_state import RepositoryStateProvider
3
+
4
+ __all__ = [
5
+ "RepositoryStateProvider",
6
+ "IssueContextProvider",
7
+ ]
8
+
9
+ all_providers = [
10
+ RepositoryStateProvider(),
11
+ IssueContextProvider(),
12
+ ]
@@ -0,0 +1,136 @@
1
+ import re
2
+ from typing import Protocol
3
+
4
+
5
+ class ProviderContext(Protocol):
6
+ message: dict[str, object]
7
+
8
+
9
+ def extract_issue_number(text: str) -> int | None:
10
+ patterns = [
11
+ r"#(\d+)",
12
+ r"issue\s*#?(\d+)",
13
+ r"pr\s*#?(\d+)",
14
+ r"pull\s*request\s*#?(\d+)",
15
+ ]
16
+
17
+ for pattern in patterns:
18
+ match = re.search(pattern, text, re.IGNORECASE)
19
+ if match:
20
+ return int(match.group(1))
21
+
22
+ return None
23
+
24
+
25
+ class IssueContextProvider:
26
+ @property
27
+ def name(self) -> str:
28
+ return "GITHUB_ISSUE_CONTEXT"
29
+
30
+ @property
31
+ def description(self) -> str:
32
+ return "Provides detailed context about a specific GitHub issue or pull request when referenced"
33
+
34
+ async def get(
35
+ self,
36
+ context: ProviderContext,
37
+ service: object,
38
+ ) -> str | None:
39
+ from elizaos_plugin_github.service import GitHubService
40
+
41
+ if not isinstance(service, GitHubService):
42
+ return None
43
+
44
+ content = context.message.get("content", {})
45
+ text = ""
46
+ if isinstance(content, dict):
47
+ text = str(content.get("text", ""))
48
+
49
+ issue_number = extract_issue_number(text)
50
+ if not issue_number:
51
+ return None
52
+
53
+ try:
54
+ config = service.config
55
+
56
+ if not config.owner or not config.repo:
57
+ return None
58
+
59
+ try:
60
+ issue = await service.get_issue(config.owner, config.repo, issue_number)
61
+
62
+ if issue.is_pull_request:
63
+ pr = await service.get_pull_request(config.owner, config.repo, issue_number)
64
+
65
+ labels = ", ".join(label.name for label in pr.labels)
66
+ assignees = ", ".join(a.login for a in pr.assignees)
67
+ reviewers = ", ".join(r.login for r in pr.requested_reviewers)
68
+
69
+ parts = [
70
+ f"## Pull Request #{pr.number}: {pr.title}",
71
+ "",
72
+ f"**State:** {pr.state.value}{' (Draft)' if pr.draft else ''}{' (Merged)' if pr.merged else ''}",
73
+ f"**Author:** {pr.user.login}",
74
+ f"**Branch:** {pr.head.ref} → {pr.base.ref}",
75
+ f"**Created:** {pr.created_at}",
76
+ f"**Updated:** {pr.updated_at}",
77
+ ]
78
+
79
+ if labels:
80
+ parts.append(f"**Labels:** {labels}")
81
+ if assignees:
82
+ parts.append(f"**Assignees:** {assignees}")
83
+ if reviewers:
84
+ parts.append(f"**Reviewers Requested:** {reviewers}")
85
+
86
+ parts.extend(
87
+ [
88
+ "",
89
+ f"**Changes:** +{pr.additions} / -{pr.deletions} ({pr.changed_files} files)",
90
+ "",
91
+ "### Description",
92
+ pr.body or "_No description provided_",
93
+ "",
94
+ f"**URL:** {pr.html_url}",
95
+ ]
96
+ )
97
+
98
+ return "\n".join(parts)
99
+
100
+ labels = ", ".join(label.name for label in issue.labels)
101
+ assignees = ", ".join(a.login for a in issue.assignees)
102
+
103
+ parts = [
104
+ f"## Issue #{issue.number}: {issue.title}",
105
+ "",
106
+ f"**State:** {issue.state.value}{(' (' + issue.state_reason.value + ')') if issue.state_reason else ''}",
107
+ f"**Author:** {issue.user.login}",
108
+ f"**Created:** {issue.created_at}",
109
+ f"**Updated:** {issue.updated_at}",
110
+ f"**Comments:** {issue.comments}",
111
+ ]
112
+
113
+ if labels:
114
+ parts.append(f"**Labels:** {labels}")
115
+ if assignees:
116
+ parts.append(f"**Assignees:** {assignees}")
117
+ if issue.milestone:
118
+ parts.append(f"**Milestone:** {issue.milestone.title}")
119
+
120
+ parts.extend(
121
+ [
122
+ "",
123
+ "### Description",
124
+ issue.body or "_No description provided_",
125
+ "",
126
+ f"**URL:** {issue.html_url}",
127
+ ]
128
+ )
129
+
130
+ return "\n".join(parts)
131
+
132
+ except Exception:
133
+ return f"Issue/PR #{issue_number} not found in {config.owner}/{config.repo}"
134
+
135
+ except Exception as e:
136
+ return f"Unable to fetch issue context: {e}"
@@ -0,0 +1,87 @@
1
+ from typing import Protocol
2
+
3
+ from elizaos_plugin_github.types import ListIssuesParams, ListPullRequestsParams, RepositoryRef
4
+
5
+
6
+ class ProviderContext(Protocol):
7
+ pass
8
+
9
+
10
+ class RepositoryStateProvider:
11
+ @property
12
+ def name(self) -> str:
13
+ return "GITHUB_REPOSITORY_STATE"
14
+
15
+ @property
16
+ def description(self) -> str:
17
+ return "Provides context about the current GitHub repository including recent activity"
18
+
19
+ async def get(
20
+ self,
21
+ _context: ProviderContext,
22
+ service: object,
23
+ ) -> str | None:
24
+ from elizaos_plugin_github.service import GitHubService
25
+
26
+ if not isinstance(service, GitHubService):
27
+ return None
28
+
29
+ try:
30
+ config = service.config
31
+
32
+ if not config.owner or not config.repo:
33
+ return "GitHub repository not configured. Please set GITHUB_OWNER and GITHUB_REPO."
34
+
35
+ repo = await service.get_repository(RepositoryRef(owner=config.owner, repo=config.repo))
36
+
37
+ issues = await service.list_issues(
38
+ ListIssuesParams(
39
+ owner=config.owner,
40
+ repo=config.repo,
41
+ state="open",
42
+ per_page=5,
43
+ )
44
+ )
45
+
46
+ # Fetch recent open PRs (limit 5)
47
+ pull_requests = await service.list_pull_requests(
48
+ ListPullRequestsParams(
49
+ owner=config.owner,
50
+ repo=config.repo,
51
+ state="open",
52
+ per_page=5,
53
+ )
54
+ )
55
+
56
+ parts: list[str] = [
57
+ f"## GitHub Repository: {repo.full_name}",
58
+ "",
59
+ f"**Description:** {repo.description or 'No description'}",
60
+ f"**Default Branch:** {repo.default_branch}",
61
+ f"**Language:** {repo.language or 'Not specified'}",
62
+ f"**Stars:** {repo.stargazers_count} | **Forks:** {repo.forks_count}",
63
+ f"**Open Issues:** {repo.open_issues_count}",
64
+ "",
65
+ ]
66
+
67
+ if issues:
68
+ parts.append("### Recent Open Issues")
69
+ for issue in issues:
70
+ labels = ", ".join(label.name for label in issue.labels)
71
+ label_str = f" [{labels}]" if labels else ""
72
+ parts.append(f"- #{issue.number}: {issue.title}{label_str}")
73
+ parts.append("")
74
+
75
+ if pull_requests:
76
+ parts.append("### Recent Open Pull Requests")
77
+ for pr in pull_requests:
78
+ status = "[DRAFT] " if pr.draft else ""
79
+ parts.append(
80
+ f"- #{pr.number}: {status}{pr.title} ({pr.head.ref} → {pr.base.ref})"
81
+ )
82
+ parts.append("")
83
+
84
+ return "\n".join(parts)
85
+
86
+ except Exception as e:
87
+ return f"Unable to fetch GitHub repository state: {e}"
File without changes