emdash-core 0.1.7__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 (187) hide show
  1. emdash_core/__init__.py +3 -0
  2. emdash_core/agent/__init__.py +37 -0
  3. emdash_core/agent/agents.py +225 -0
  4. emdash_core/agent/code_reviewer.py +476 -0
  5. emdash_core/agent/compaction.py +143 -0
  6. emdash_core/agent/context_manager.py +140 -0
  7. emdash_core/agent/events.py +338 -0
  8. emdash_core/agent/handlers.py +224 -0
  9. emdash_core/agent/inprocess_subagent.py +377 -0
  10. emdash_core/agent/mcp/__init__.py +50 -0
  11. emdash_core/agent/mcp/client.py +346 -0
  12. emdash_core/agent/mcp/config.py +302 -0
  13. emdash_core/agent/mcp/manager.py +496 -0
  14. emdash_core/agent/mcp/tool_factory.py +213 -0
  15. emdash_core/agent/prompts/__init__.py +38 -0
  16. emdash_core/agent/prompts/main_agent.py +104 -0
  17. emdash_core/agent/prompts/subagents.py +131 -0
  18. emdash_core/agent/prompts/workflow.py +136 -0
  19. emdash_core/agent/providers/__init__.py +34 -0
  20. emdash_core/agent/providers/base.py +143 -0
  21. emdash_core/agent/providers/factory.py +80 -0
  22. emdash_core/agent/providers/models.py +220 -0
  23. emdash_core/agent/providers/openai_provider.py +463 -0
  24. emdash_core/agent/providers/transformers_provider.py +217 -0
  25. emdash_core/agent/research/__init__.py +81 -0
  26. emdash_core/agent/research/agent.py +143 -0
  27. emdash_core/agent/research/controller.py +254 -0
  28. emdash_core/agent/research/critic.py +428 -0
  29. emdash_core/agent/research/macros.py +469 -0
  30. emdash_core/agent/research/planner.py +449 -0
  31. emdash_core/agent/research/researcher.py +436 -0
  32. emdash_core/agent/research/state.py +523 -0
  33. emdash_core/agent/research/synthesizer.py +594 -0
  34. emdash_core/agent/reviewer_profile.py +475 -0
  35. emdash_core/agent/rules.py +123 -0
  36. emdash_core/agent/runner.py +601 -0
  37. emdash_core/agent/session.py +262 -0
  38. emdash_core/agent/spec_schema.py +66 -0
  39. emdash_core/agent/specification.py +479 -0
  40. emdash_core/agent/subagent.py +397 -0
  41. emdash_core/agent/subagent_prompts.py +13 -0
  42. emdash_core/agent/toolkit.py +482 -0
  43. emdash_core/agent/toolkits/__init__.py +64 -0
  44. emdash_core/agent/toolkits/base.py +96 -0
  45. emdash_core/agent/toolkits/explore.py +47 -0
  46. emdash_core/agent/toolkits/plan.py +55 -0
  47. emdash_core/agent/tools/__init__.py +141 -0
  48. emdash_core/agent/tools/analytics.py +436 -0
  49. emdash_core/agent/tools/base.py +131 -0
  50. emdash_core/agent/tools/coding.py +484 -0
  51. emdash_core/agent/tools/github_mcp.py +592 -0
  52. emdash_core/agent/tools/history.py +13 -0
  53. emdash_core/agent/tools/modes.py +153 -0
  54. emdash_core/agent/tools/plan.py +206 -0
  55. emdash_core/agent/tools/plan_write.py +135 -0
  56. emdash_core/agent/tools/search.py +412 -0
  57. emdash_core/agent/tools/spec.py +341 -0
  58. emdash_core/agent/tools/task.py +262 -0
  59. emdash_core/agent/tools/task_output.py +204 -0
  60. emdash_core/agent/tools/tasks.py +454 -0
  61. emdash_core/agent/tools/traversal.py +588 -0
  62. emdash_core/agent/tools/web.py +179 -0
  63. emdash_core/analytics/__init__.py +5 -0
  64. emdash_core/analytics/engine.py +1286 -0
  65. emdash_core/api/__init__.py +5 -0
  66. emdash_core/api/agent.py +308 -0
  67. emdash_core/api/agents.py +154 -0
  68. emdash_core/api/analyze.py +264 -0
  69. emdash_core/api/auth.py +173 -0
  70. emdash_core/api/context.py +77 -0
  71. emdash_core/api/db.py +121 -0
  72. emdash_core/api/embed.py +131 -0
  73. emdash_core/api/feature.py +143 -0
  74. emdash_core/api/health.py +93 -0
  75. emdash_core/api/index.py +162 -0
  76. emdash_core/api/plan.py +110 -0
  77. emdash_core/api/projectmd.py +210 -0
  78. emdash_core/api/query.py +320 -0
  79. emdash_core/api/research.py +122 -0
  80. emdash_core/api/review.py +161 -0
  81. emdash_core/api/router.py +76 -0
  82. emdash_core/api/rules.py +116 -0
  83. emdash_core/api/search.py +119 -0
  84. emdash_core/api/spec.py +99 -0
  85. emdash_core/api/swarm.py +223 -0
  86. emdash_core/api/tasks.py +109 -0
  87. emdash_core/api/team.py +120 -0
  88. emdash_core/auth/__init__.py +17 -0
  89. emdash_core/auth/github.py +389 -0
  90. emdash_core/config.py +74 -0
  91. emdash_core/context/__init__.py +52 -0
  92. emdash_core/context/models.py +50 -0
  93. emdash_core/context/providers/__init__.py +11 -0
  94. emdash_core/context/providers/base.py +74 -0
  95. emdash_core/context/providers/explored_areas.py +183 -0
  96. emdash_core/context/providers/touched_areas.py +360 -0
  97. emdash_core/context/registry.py +73 -0
  98. emdash_core/context/reranker.py +199 -0
  99. emdash_core/context/service.py +260 -0
  100. emdash_core/context/session.py +352 -0
  101. emdash_core/core/__init__.py +104 -0
  102. emdash_core/core/config.py +454 -0
  103. emdash_core/core/exceptions.py +55 -0
  104. emdash_core/core/models.py +265 -0
  105. emdash_core/core/review_config.py +57 -0
  106. emdash_core/db/__init__.py +67 -0
  107. emdash_core/db/auth.py +134 -0
  108. emdash_core/db/models.py +91 -0
  109. emdash_core/db/provider.py +222 -0
  110. emdash_core/db/providers/__init__.py +5 -0
  111. emdash_core/db/providers/supabase.py +452 -0
  112. emdash_core/embeddings/__init__.py +24 -0
  113. emdash_core/embeddings/indexer.py +534 -0
  114. emdash_core/embeddings/models.py +192 -0
  115. emdash_core/embeddings/providers/__init__.py +7 -0
  116. emdash_core/embeddings/providers/base.py +112 -0
  117. emdash_core/embeddings/providers/fireworks.py +141 -0
  118. emdash_core/embeddings/providers/openai.py +104 -0
  119. emdash_core/embeddings/registry.py +146 -0
  120. emdash_core/embeddings/service.py +215 -0
  121. emdash_core/graph/__init__.py +26 -0
  122. emdash_core/graph/builder.py +134 -0
  123. emdash_core/graph/connection.py +692 -0
  124. emdash_core/graph/schema.py +416 -0
  125. emdash_core/graph/writer.py +667 -0
  126. emdash_core/ingestion/__init__.py +7 -0
  127. emdash_core/ingestion/change_detector.py +150 -0
  128. emdash_core/ingestion/git/__init__.py +5 -0
  129. emdash_core/ingestion/git/commit_analyzer.py +196 -0
  130. emdash_core/ingestion/github/__init__.py +6 -0
  131. emdash_core/ingestion/github/pr_fetcher.py +296 -0
  132. emdash_core/ingestion/github/task_extractor.py +100 -0
  133. emdash_core/ingestion/orchestrator.py +540 -0
  134. emdash_core/ingestion/parsers/__init__.py +10 -0
  135. emdash_core/ingestion/parsers/base_parser.py +66 -0
  136. emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
  137. emdash_core/ingestion/parsers/class_extractor.py +154 -0
  138. emdash_core/ingestion/parsers/function_extractor.py +202 -0
  139. emdash_core/ingestion/parsers/import_analyzer.py +119 -0
  140. emdash_core/ingestion/parsers/python_parser.py +123 -0
  141. emdash_core/ingestion/parsers/registry.py +72 -0
  142. emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
  143. emdash_core/ingestion/parsers/typescript_parser.py +278 -0
  144. emdash_core/ingestion/repository.py +346 -0
  145. emdash_core/models/__init__.py +38 -0
  146. emdash_core/models/agent.py +68 -0
  147. emdash_core/models/index.py +77 -0
  148. emdash_core/models/query.py +113 -0
  149. emdash_core/planning/__init__.py +7 -0
  150. emdash_core/planning/agent_api.py +413 -0
  151. emdash_core/planning/context_builder.py +265 -0
  152. emdash_core/planning/feature_context.py +232 -0
  153. emdash_core/planning/feature_expander.py +646 -0
  154. emdash_core/planning/llm_explainer.py +198 -0
  155. emdash_core/planning/similarity.py +509 -0
  156. emdash_core/planning/team_focus.py +821 -0
  157. emdash_core/server.py +153 -0
  158. emdash_core/sse/__init__.py +5 -0
  159. emdash_core/sse/stream.py +196 -0
  160. emdash_core/swarm/__init__.py +17 -0
  161. emdash_core/swarm/merge_agent.py +383 -0
  162. emdash_core/swarm/session_manager.py +274 -0
  163. emdash_core/swarm/swarm_runner.py +226 -0
  164. emdash_core/swarm/task_definition.py +137 -0
  165. emdash_core/swarm/worker_spawner.py +319 -0
  166. emdash_core/swarm/worktree_manager.py +278 -0
  167. emdash_core/templates/__init__.py +10 -0
  168. emdash_core/templates/defaults/agent-builder.md.template +82 -0
  169. emdash_core/templates/defaults/focus.md.template +115 -0
  170. emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
  171. emdash_core/templates/defaults/pr-review.md.template +80 -0
  172. emdash_core/templates/defaults/project.md.template +85 -0
  173. emdash_core/templates/defaults/research_critic.md.template +112 -0
  174. emdash_core/templates/defaults/research_planner.md.template +85 -0
  175. emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
  176. emdash_core/templates/defaults/reviewer.md.template +81 -0
  177. emdash_core/templates/defaults/spec.md.template +41 -0
  178. emdash_core/templates/defaults/tasks.md.template +78 -0
  179. emdash_core/templates/loader.py +296 -0
  180. emdash_core/utils/__init__.py +45 -0
  181. emdash_core/utils/git.py +84 -0
  182. emdash_core/utils/image.py +502 -0
  183. emdash_core/utils/logger.py +51 -0
  184. emdash_core-0.1.7.dist-info/METADATA +35 -0
  185. emdash_core-0.1.7.dist-info/RECORD +187 -0
  186. emdash_core-0.1.7.dist-info/WHEEL +4 -0
  187. emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,265 @@
1
+ """Data models for EmDash entities."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+
9
+ # ============================================================================
10
+ # Layer A: Code Structure Entities
11
+ # ============================================================================
12
+
13
+
14
+ @dataclass
15
+ class FileEntity:
16
+ """Represents a source code file."""
17
+
18
+ path: str
19
+ name: str
20
+ extension: str
21
+ size_bytes: int
22
+ lines_of_code: int
23
+ hash: str
24
+ last_modified: Optional[datetime] = None
25
+
26
+ @classmethod
27
+ def from_path(cls, file_path: Path, content_hash: str) -> "FileEntity":
28
+ """Create a FileEntity from a file path."""
29
+ stat = file_path.stat()
30
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
31
+ lines = len(f.readlines())
32
+
33
+ return cls(
34
+ path=str(file_path),
35
+ name=file_path.name,
36
+ extension=file_path.suffix,
37
+ size_bytes=stat.st_size,
38
+ lines_of_code=lines,
39
+ hash=content_hash,
40
+ last_modified=datetime.fromtimestamp(stat.st_mtime),
41
+ )
42
+
43
+
44
+ @dataclass
45
+ class ClassEntity:
46
+ """Represents a class definition."""
47
+
48
+ name: str
49
+ qualified_name: str # e.g., "module.ClassName"
50
+ file_path: str
51
+ line_start: int
52
+ line_end: int
53
+ docstring: Optional[str] = None
54
+ is_abstract: bool = False
55
+ decorators: list[str] = field(default_factory=list)
56
+ base_classes: list[str] = field(default_factory=list) # For inheritance
57
+ attributes: list[str] = field(default_factory=list)
58
+ methods: list[str] = field(default_factory=list) # Qualified names of methods
59
+
60
+
61
+ @dataclass
62
+ class FunctionEntity:
63
+ """Represents a function or method definition."""
64
+
65
+ name: str
66
+ qualified_name: str # e.g., "module.function" or "module.Class.method"
67
+ file_path: str
68
+ line_start: int
69
+ line_end: int
70
+ docstring: Optional[str] = None
71
+ parameters: list[str] = field(default_factory=list)
72
+ return_annotation: Optional[str] = None
73
+ is_async: bool = False
74
+ is_method: bool = False
75
+ is_static: bool = False
76
+ is_classmethod: bool = False
77
+ decorators: list[str] = field(default_factory=list)
78
+ cyclomatic_complexity: int = 1
79
+ calls: list[str] = field(default_factory=list) # Qualified names of called functions
80
+
81
+
82
+ @dataclass
83
+ class ModuleEntity:
84
+ """Represents a Python module (imported)."""
85
+
86
+ name: str
87
+ import_path: str
88
+ is_external: bool = False # stdlib or third-party
89
+ package: Optional[str] = None
90
+
91
+
92
+ @dataclass
93
+ class ImportStatement:
94
+ """Represents an import statement."""
95
+
96
+ file_path: str
97
+ line_number: int
98
+ module: str
99
+ imported_names: list[str] = field(default_factory=list)
100
+ alias: Optional[str] = None
101
+ import_type: str = "import" # "import" or "from_import"
102
+
103
+
104
+ # ============================================================================
105
+ # Layer B: Git History Entities
106
+ # ============================================================================
107
+
108
+
109
+ @dataclass
110
+ class CommitEntity:
111
+ """Represents a git commit."""
112
+
113
+ sha: str
114
+ message: str
115
+ timestamp: datetime
116
+ author_name: str
117
+ author_email: str
118
+ committer_name: str
119
+ committer_email: str
120
+ insertions: int = 0
121
+ deletions: int = 0
122
+ files_changed: int = 0
123
+ is_merge: bool = False
124
+ parent_shas: list[str] = field(default_factory=list)
125
+
126
+
127
+ @dataclass
128
+ class FileModification:
129
+ """Represents a file modification in a commit."""
130
+
131
+ commit_sha: str
132
+ file_path: str
133
+ change_type: str # "added", "modified", "deleted", "renamed"
134
+ insertions: int = 0
135
+ deletions: int = 0
136
+ old_path: Optional[str] = None # For renames
137
+
138
+
139
+ @dataclass
140
+ class AuthorEntity:
141
+ """Represents a code contributor."""
142
+
143
+ email: str
144
+ name: str
145
+ first_commit: Optional[datetime] = None
146
+ last_commit: Optional[datetime] = None
147
+ total_commits: int = 0
148
+ total_lines_added: int = 0
149
+ total_lines_deleted: int = 0
150
+
151
+
152
+ @dataclass
153
+ class PullRequestEntity:
154
+ """Represents a GitHub pull request."""
155
+
156
+ number: int
157
+ title: str
158
+ state: str # "open", "closed", "merged"
159
+ created_at: datetime
160
+ author: str
161
+ description: Optional[str] = None # Full PR body text
162
+ merged_at: Optional[datetime] = None
163
+ reviewers: list[str] = field(default_factory=list)
164
+ labels: list[str] = field(default_factory=list)
165
+ additions: int = 0
166
+ deletions: int = 0
167
+ files_changed: list[str] = field(default_factory=list) # File paths modified
168
+ commit_shas: list[str] = field(default_factory=list)
169
+ base_branch: str = "main"
170
+ head_branch: str = ""
171
+ embedding: Optional[list[float]] = None # Vector embedding for similarity
172
+
173
+
174
+ @dataclass
175
+ class TaskEntity:
176
+ """Represents a subtask extracted from a PR description."""
177
+
178
+ id: str # Format: "pr_{number}_task_{index}"
179
+ pr_number: int
180
+ description: str
181
+ is_completed: bool
182
+ order: int # Position in the task list
183
+
184
+
185
+ @dataclass
186
+ class RepositoryEntity:
187
+ """Represents a repository."""
188
+
189
+ url: str
190
+ name: str
191
+ owner: Optional[str] = None
192
+ default_branch: str = "main"
193
+ last_ingested: Optional[datetime] = None
194
+ ingestion_status: str = "pending"
195
+ commit_count: int = 0
196
+ file_count: int = 0
197
+ primary_language: str = "Python"
198
+
199
+
200
+ # ============================================================================
201
+ # Layer C: Analytics Entities
202
+ # ============================================================================
203
+
204
+
205
+ @dataclass
206
+ class ClusterEntity:
207
+ """Represents a detected code cluster/community."""
208
+
209
+ cluster_id: int
210
+ algorithm: str # "louvain", "label_propagation"
211
+ modularity_score: float
212
+ size: int # Number of nodes in cluster
213
+ members: list[str] = field(default_factory=list) # Qualified names
214
+
215
+
216
+ # ============================================================================
217
+ # Aggregate Models
218
+ # ============================================================================
219
+
220
+
221
+ @dataclass
222
+ class CodebaseEntities:
223
+ """Aggregate of all entities extracted from a codebase."""
224
+
225
+ files: list[FileEntity] = field(default_factory=list)
226
+ classes: list[ClassEntity] = field(default_factory=list)
227
+ functions: list[FunctionEntity] = field(default_factory=list)
228
+ modules: list[ModuleEntity] = field(default_factory=list)
229
+ imports: list[ImportStatement] = field(default_factory=list)
230
+
231
+ @classmethod
232
+ def merge(cls, entities_list: list["FileEntities"]) -> "CodebaseEntities":
233
+ """Merge multiple FileEntities into a single CodebaseEntities."""
234
+ result = cls()
235
+ for file_entities in entities_list:
236
+ if file_entities:
237
+ if file_entities.file:
238
+ result.files.append(file_entities.file)
239
+ result.classes.extend(file_entities.classes)
240
+ result.functions.extend(file_entities.functions)
241
+ result.modules.extend(file_entities.modules)
242
+ result.imports.extend(file_entities.imports)
243
+ return result
244
+
245
+
246
+ @dataclass
247
+ class FileEntities:
248
+ """Entities extracted from a single file."""
249
+
250
+ file: Optional[FileEntity] = None
251
+ classes: list[ClassEntity] = field(default_factory=list)
252
+ functions: list[FunctionEntity] = field(default_factory=list)
253
+ modules: list[ModuleEntity] = field(default_factory=list)
254
+ imports: list[ImportStatement] = field(default_factory=list)
255
+
256
+
257
+ @dataclass
258
+ class GitData:
259
+ """Aggregate of all Git history data."""
260
+
261
+ repository: RepositoryEntity
262
+ commits: list[CommitEntity] = field(default_factory=list)
263
+ modifications: list[FileModification] = field(default_factory=list)
264
+ authors: list[AuthorEntity] = field(default_factory=list)
265
+ pull_requests: list[PullRequestEntity] = field(default_factory=list)
@@ -0,0 +1,57 @@
1
+ """Configuration loader for PR review settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class ReviewConfig(BaseModel):
14
+ """Configuration for PR review command."""
15
+
16
+ max_files: int = Field(default=50, ge=1, le=500)
17
+ max_tokens: int = Field(default=50000, ge=1000, le=200000)
18
+
19
+
20
+ def _find_config_path() -> Optional[Path]:
21
+ """Find the review config file path."""
22
+ env_path = os.getenv("EMDASH_CONFIG_PATH")
23
+ if env_path:
24
+ return Path(env_path)
25
+
26
+ cwd = Path.cwd()
27
+ candidates = [
28
+ cwd / "emdash.config.json",
29
+ cwd / ".emdash-rules" / "config.json",
30
+ ]
31
+ for path in candidates:
32
+ if path.is_file():
33
+ return path
34
+
35
+ return None
36
+
37
+
38
+ def load_review_config() -> ReviewConfig:
39
+ """Load review configuration from file if present."""
40
+ path = _find_config_path()
41
+ if not path or not path.is_file():
42
+ return ReviewConfig()
43
+
44
+ try:
45
+ with open(path, "r", encoding="utf-8") as handle:
46
+ raw = json.load(handle)
47
+ except (json.JSONDecodeError, OSError):
48
+ return ReviewConfig()
49
+
50
+ review = raw.get("review", {}) if isinstance(raw, dict) else {}
51
+ if not isinstance(review, dict):
52
+ return ReviewConfig()
53
+
54
+ return ReviewConfig(
55
+ max_files=review.get("max_files", 50),
56
+ max_tokens=review.get("max_tokens", 50000),
57
+ )
@@ -0,0 +1,67 @@
1
+ """Database layer for EmDash.
2
+
3
+ Provides an abstraction over database backends with Supabase as the default implementation.
4
+ """
5
+
6
+ import os
7
+ from typing import Optional
8
+
9
+ from .models import (
10
+ Feature,
11
+ FeatureAssignee,
12
+ FeaturePR,
13
+ FeatureStatus,
14
+ PRStatus,
15
+ Project,
16
+ TeamMember,
17
+ )
18
+ from .provider import DatabaseProvider
19
+ from .providers.supabase import SupabaseProvider
20
+ from .auth import AuthProvider, User, get_auth
21
+
22
+ __all__ = [
23
+ # Models
24
+ "Feature",
25
+ "FeatureAssignee",
26
+ "FeaturePR",
27
+ "FeatureStatus",
28
+ "PRStatus",
29
+ "Project",
30
+ "TeamMember",
31
+ "User",
32
+ # Provider
33
+ "DatabaseProvider",
34
+ "SupabaseProvider",
35
+ # Auth
36
+ "AuthProvider",
37
+ "get_auth",
38
+ # Factory
39
+ "get_provider",
40
+ ]
41
+
42
+ _provider_instance: Optional[DatabaseProvider] = None
43
+
44
+
45
+ def get_provider() -> DatabaseProvider:
46
+ """Get the configured database provider instance.
47
+
48
+ Returns a singleton instance of the database provider.
49
+ Currently only supports Supabase.
50
+
51
+ Returns:
52
+ DatabaseProvider instance
53
+
54
+ Raises:
55
+ ValueError: If required environment variables are not set
56
+ """
57
+ global _provider_instance
58
+
59
+ if _provider_instance is None:
60
+ provider_type = os.getenv("EMDASH_DB_PROVIDER", "supabase")
61
+
62
+ if provider_type == "supabase":
63
+ _provider_instance = SupabaseProvider()
64
+ else:
65
+ raise ValueError(f"Unknown database provider: {provider_type}")
66
+
67
+ return _provider_instance
emdash_core/db/auth.py ADDED
@@ -0,0 +1,134 @@
1
+ """Supabase authentication with GitHub OAuth."""
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import Optional
6
+
7
+ from supabase import create_client, Client
8
+
9
+
10
+ @dataclass
11
+ class User:
12
+ """Authenticated user from Supabase Auth."""
13
+
14
+ id: str
15
+ email: Optional[str]
16
+ github_handle: Optional[str]
17
+ avatar_url: Optional[str]
18
+ access_token: Optional[str] = None
19
+
20
+
21
+ class AuthProvider:
22
+ """Handle Supabase authentication with GitHub OAuth."""
23
+
24
+ def __init__(self, url: Optional[str] = None, key: Optional[str] = None):
25
+ self.url = url or os.getenv("SUPABASE_URL")
26
+ self.key = key or os.getenv("SUPABASE_KEY")
27
+
28
+ if not self.url or not self.key:
29
+ raise ValueError("SUPABASE_URL and SUPABASE_KEY must be set")
30
+
31
+ self.client: Client = create_client(self.url, self.key)
32
+
33
+ def get_github_login_url(self, redirect_to: Optional[str] = None) -> str:
34
+ """Get the GitHub OAuth login URL.
35
+
36
+ Args:
37
+ redirect_to: URL to redirect after login (for web apps)
38
+
39
+ Returns:
40
+ GitHub OAuth URL to redirect user to
41
+ """
42
+ options = {}
43
+ if redirect_to:
44
+ options["redirect_to"] = redirect_to
45
+
46
+ response = self.client.auth.sign_in_with_oauth(
47
+ {"provider": "github", "options": options}
48
+ )
49
+ return response.url
50
+
51
+ def sign_in_with_github_token(self, access_token: str) -> Optional[User]:
52
+ """Sign in with an existing GitHub access token.
53
+
54
+ Args:
55
+ access_token: GitHub OAuth access token
56
+
57
+ Returns:
58
+ User if successful, None otherwise
59
+ """
60
+ try:
61
+ response = self.client.auth.sign_in_with_id_token(
62
+ {"provider": "github", "token": access_token}
63
+ )
64
+ return self._session_to_user(response)
65
+ except Exception:
66
+ return None
67
+
68
+ def get_session(self) -> Optional[User]:
69
+ """Get the current authenticated user from session.
70
+
71
+ Returns:
72
+ User if authenticated, None otherwise
73
+ """
74
+ try:
75
+ response = self.client.auth.get_session()
76
+ if response and response.user:
77
+ return self._response_to_user(response)
78
+ return None
79
+ except Exception:
80
+ return None
81
+
82
+ def sign_out(self) -> bool:
83
+ """Sign out the current user.
84
+
85
+ Returns:
86
+ True if successful
87
+ """
88
+ try:
89
+ self.client.auth.sign_out()
90
+ return True
91
+ except Exception:
92
+ return False
93
+
94
+ def _session_to_user(self, session) -> Optional[User]:
95
+ """Convert Supabase session to User model."""
96
+ if not session or not session.user:
97
+ return None
98
+
99
+ user_meta = session.user.user_metadata or {}
100
+
101
+ return User(
102
+ id=session.user.id,
103
+ email=session.user.email,
104
+ github_handle=user_meta.get("user_name") or user_meta.get("preferred_username"),
105
+ avatar_url=user_meta.get("avatar_url"),
106
+ access_token=session.access_token if hasattr(session, "access_token") else None,
107
+ )
108
+
109
+ def _response_to_user(self, response) -> Optional[User]:
110
+ """Convert Supabase auth response to User model."""
111
+ if not response or not response.user:
112
+ return None
113
+
114
+ user_meta = response.user.user_metadata or {}
115
+
116
+ return User(
117
+ id=response.user.id,
118
+ email=response.user.email,
119
+ github_handle=user_meta.get("user_name") or user_meta.get("preferred_username"),
120
+ avatar_url=user_meta.get("avatar_url"),
121
+ access_token=response.session.access_token if response.session else None,
122
+ )
123
+
124
+
125
+ # Singleton instance
126
+ _auth_instance: Optional[AuthProvider] = None
127
+
128
+
129
+ def get_auth() -> AuthProvider:
130
+ """Get the auth provider instance."""
131
+ global _auth_instance
132
+ if _auth_instance is None:
133
+ _auth_instance = AuthProvider()
134
+ return _auth_instance
@@ -0,0 +1,91 @@
1
+ """Data models for the database layer."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from typing import Optional
7
+
8
+
9
+ class FeatureStatus(str, Enum):
10
+ """Status of a feature in the workflow."""
11
+
12
+ TODO = "todo"
13
+ IN_DESIGN_REVIEW = "in_design_review"
14
+ IN_PROGRESS = "in_progress"
15
+ IN_PR = "in_pr"
16
+ DONE = "done"
17
+
18
+
19
+ class PRStatus(str, Enum):
20
+ """Status of a pull request."""
21
+
22
+ OPEN = "open"
23
+ MERGED = "merged"
24
+ CLOSED = "closed"
25
+
26
+
27
+ @dataclass
28
+ class Project:
29
+ """A project in the system."""
30
+
31
+ id: str
32
+ name: str
33
+ repo_url: Optional[str] = None
34
+ owner_id: Optional[str] = None
35
+ created_at: Optional[datetime] = None
36
+ updated_at: Optional[datetime] = None
37
+
38
+
39
+ @dataclass
40
+ class TeamMember:
41
+ """A team member belonging to a project."""
42
+
43
+ id: str
44
+ project_id: str
45
+ name: str
46
+ email: Optional[str] = None
47
+ github_handle: Optional[str] = None
48
+ role: Optional[str] = None
49
+ user_id: Optional[str] = None # Links to auth.users
50
+ created_at: Optional[datetime] = None
51
+
52
+
53
+ @dataclass
54
+ class Feature:
55
+ """A feature belonging to a project."""
56
+
57
+ id: str
58
+ project_id: str
59
+ title: str
60
+ description: Optional[str] = None
61
+ status: FeatureStatus = FeatureStatus.TODO
62
+ spec: Optional[str] = None
63
+ plan: Optional[str] = None
64
+ tasks: Optional[str] = None
65
+ created_at: Optional[datetime] = None
66
+ updated_at: Optional[datetime] = None
67
+ # Populated by joins
68
+ assignees: list["TeamMember"] = field(default_factory=list)
69
+ prs: list["FeaturePR"] = field(default_factory=list)
70
+
71
+
72
+ @dataclass
73
+ class FeatureAssignee:
74
+ """Junction table for feature-assignee many-to-many relationship."""
75
+
76
+ feature_id: str
77
+ team_member_id: str
78
+ assigned_at: Optional[datetime] = None
79
+
80
+
81
+ @dataclass
82
+ class FeaturePR:
83
+ """A pull request linked to a feature."""
84
+
85
+ id: str
86
+ feature_id: str
87
+ pr_url: str
88
+ pr_number: int
89
+ title: Optional[str] = None
90
+ status: PRStatus = PRStatus.OPEN
91
+ created_at: Optional[datetime] = None