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,222 @@
1
+ """Abstract database provider interface."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional
5
+
6
+ from .models import (
7
+ Feature,
8
+ FeatureAssignee,
9
+ FeaturePR,
10
+ FeatureStatus,
11
+ PRStatus,
12
+ Project,
13
+ TeamMember,
14
+ )
15
+
16
+
17
+ class DatabaseProvider(ABC):
18
+ """Abstract base class for database providers.
19
+
20
+ Implementations must provide all CRUD operations for the data models.
21
+ """
22
+
23
+ # -------------------------------------------------------------------------
24
+ # Projects
25
+ # -------------------------------------------------------------------------
26
+
27
+ @abstractmethod
28
+ async def create_project(
29
+ self, name: str, repo_url: Optional[str] = None, owner_id: Optional[str] = None
30
+ ) -> Project:
31
+ """Create a new project.
32
+
33
+ Args:
34
+ name: Project name
35
+ repo_url: GitHub repository URL
36
+ owner_id: Auth user ID of the project owner
37
+ """
38
+ pass
39
+
40
+ @abstractmethod
41
+ async def get_project(self, project_id: str) -> Optional[Project]:
42
+ """Get a project by ID."""
43
+ pass
44
+
45
+ @abstractmethod
46
+ async def get_project_by_name(self, name: str) -> Optional[Project]:
47
+ """Get a project by name."""
48
+ pass
49
+
50
+ @abstractmethod
51
+ async def list_projects(self) -> list[Project]:
52
+ """List all projects."""
53
+ pass
54
+
55
+ @abstractmethod
56
+ async def update_project(
57
+ self, project_id: str, name: Optional[str] = None, repo_url: Optional[str] = None
58
+ ) -> Optional[Project]:
59
+ """Update a project."""
60
+ pass
61
+
62
+ @abstractmethod
63
+ async def delete_project(self, project_id: str) -> bool:
64
+ """Delete a project and all related data."""
65
+ pass
66
+
67
+ # -------------------------------------------------------------------------
68
+ # Team Members
69
+ # -------------------------------------------------------------------------
70
+
71
+ @abstractmethod
72
+ async def create_team_member(
73
+ self,
74
+ project_id: str,
75
+ name: str,
76
+ email: Optional[str] = None,
77
+ github_handle: Optional[str] = None,
78
+ role: Optional[str] = None,
79
+ user_id: Optional[str] = None,
80
+ ) -> TeamMember:
81
+ """Create a new team member.
82
+
83
+ Args:
84
+ project_id: Project this member belongs to
85
+ name: Display name
86
+ email: Email address
87
+ github_handle: GitHub username
88
+ role: Role in the project
89
+ user_id: Auth user ID (links to Supabase auth.users)
90
+ """
91
+ pass
92
+
93
+ @abstractmethod
94
+ async def get_team_member(self, member_id: str) -> Optional[TeamMember]:
95
+ """Get a team member by ID."""
96
+ pass
97
+
98
+ @abstractmethod
99
+ async def list_team_members(self, project_id: str) -> list[TeamMember]:
100
+ """List all team members for a project."""
101
+ pass
102
+
103
+ @abstractmethod
104
+ async def update_team_member(
105
+ self,
106
+ member_id: str,
107
+ name: Optional[str] = None,
108
+ email: Optional[str] = None,
109
+ github_handle: Optional[str] = None,
110
+ role: Optional[str] = None,
111
+ ) -> Optional[TeamMember]:
112
+ """Update a team member."""
113
+ pass
114
+
115
+ @abstractmethod
116
+ async def delete_team_member(self, member_id: str) -> bool:
117
+ """Delete a team member."""
118
+ pass
119
+
120
+ # -------------------------------------------------------------------------
121
+ # Features
122
+ # -------------------------------------------------------------------------
123
+
124
+ @abstractmethod
125
+ async def create_feature(
126
+ self,
127
+ project_id: str,
128
+ title: str,
129
+ description: Optional[str] = None,
130
+ status: FeatureStatus = FeatureStatus.TODO,
131
+ spec: Optional[str] = None,
132
+ plan: Optional[str] = None,
133
+ tasks: Optional[str] = None,
134
+ ) -> Feature:
135
+ """Create a new feature."""
136
+ pass
137
+
138
+ @abstractmethod
139
+ async def get_feature(self, feature_id: str, include_relations: bool = True) -> Optional[Feature]:
140
+ """Get a feature by ID, optionally including assignees and PRs."""
141
+ pass
142
+
143
+ @abstractmethod
144
+ async def list_features(
145
+ self, project_id: str, status: Optional[FeatureStatus] = None, include_relations: bool = False
146
+ ) -> list[Feature]:
147
+ """List features for a project, optionally filtered by status."""
148
+ pass
149
+
150
+ @abstractmethod
151
+ async def update_feature(
152
+ self,
153
+ feature_id: str,
154
+ title: Optional[str] = None,
155
+ description: Optional[str] = None,
156
+ status: Optional[FeatureStatus] = None,
157
+ spec: Optional[str] = None,
158
+ plan: Optional[str] = None,
159
+ tasks: Optional[str] = None,
160
+ ) -> Optional[Feature]:
161
+ """Update a feature."""
162
+ pass
163
+
164
+ @abstractmethod
165
+ async def delete_feature(self, feature_id: str) -> bool:
166
+ """Delete a feature and all related data."""
167
+ pass
168
+
169
+ # -------------------------------------------------------------------------
170
+ # Feature Assignees
171
+ # -------------------------------------------------------------------------
172
+
173
+ @abstractmethod
174
+ async def assign_feature(self, feature_id: str, team_member_id: str) -> FeatureAssignee:
175
+ """Assign a team member to a feature."""
176
+ pass
177
+
178
+ @abstractmethod
179
+ async def unassign_feature(self, feature_id: str, team_member_id: str) -> bool:
180
+ """Remove a team member from a feature."""
181
+ pass
182
+
183
+ @abstractmethod
184
+ async def get_feature_assignees(self, feature_id: str) -> list[TeamMember]:
185
+ """Get all assignees for a feature."""
186
+ pass
187
+
188
+ # -------------------------------------------------------------------------
189
+ # Feature PRs
190
+ # -------------------------------------------------------------------------
191
+
192
+ @abstractmethod
193
+ async def add_feature_pr(
194
+ self,
195
+ feature_id: str,
196
+ pr_url: str,
197
+ pr_number: int,
198
+ title: Optional[str] = None,
199
+ status: PRStatus = PRStatus.OPEN,
200
+ ) -> FeaturePR:
201
+ """Add a PR to a feature."""
202
+ pass
203
+
204
+ @abstractmethod
205
+ async def update_feature_pr(
206
+ self,
207
+ pr_id: str,
208
+ title: Optional[str] = None,
209
+ status: Optional[PRStatus] = None,
210
+ ) -> Optional[FeaturePR]:
211
+ """Update a feature PR."""
212
+ pass
213
+
214
+ @abstractmethod
215
+ async def remove_feature_pr(self, pr_id: str) -> bool:
216
+ """Remove a PR from a feature."""
217
+ pass
218
+
219
+ @abstractmethod
220
+ async def get_feature_prs(self, feature_id: str) -> list[FeaturePR]:
221
+ """Get all PRs for a feature."""
222
+ pass
@@ -0,0 +1,5 @@
1
+ """Database provider implementations."""
2
+
3
+ from .supabase import SupabaseProvider
4
+
5
+ __all__ = ["SupabaseProvider"]
@@ -0,0 +1,452 @@
1
+ """Supabase database provider implementation."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+ from typing import Optional
6
+
7
+ from supabase import create_client, Client
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
+
20
+
21
+ class SupabaseProvider(DatabaseProvider):
22
+ """Supabase implementation of the database provider."""
23
+
24
+ def __init__(
25
+ self,
26
+ url: Optional[str] = None,
27
+ key: Optional[str] = None,
28
+ access_token: Optional[str] = None,
29
+ ):
30
+ """Initialize Supabase client.
31
+
32
+ Args:
33
+ url: Supabase project URL. Defaults to SUPABASE_URL env var.
34
+ key: Supabase anon/service key. Defaults to SUPABASE_KEY env var.
35
+ access_token: User's JWT access token for authenticated requests.
36
+ Required for RLS policies to work correctly.
37
+ """
38
+ self.url = url or os.getenv("SUPABASE_URL")
39
+ self.key = key or os.getenv("SUPABASE_KEY")
40
+
41
+ if not self.url or not self.key:
42
+ raise ValueError("SUPABASE_URL and SUPABASE_KEY must be set")
43
+
44
+ self.client: Client = create_client(self.url, self.key)
45
+
46
+ # Set auth header for RLS if access token provided
47
+ if access_token:
48
+ self.client.postgrest.auth(access_token)
49
+
50
+ def _parse_datetime(self, value: Optional[str]) -> Optional[datetime]:
51
+ """Parse ISO datetime string from Supabase."""
52
+ if not value:
53
+ return None
54
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
55
+
56
+ def _row_to_project(self, row: dict) -> Project:
57
+ """Convert a database row to a Project model."""
58
+ return Project(
59
+ id=row["id"],
60
+ name=row["name"],
61
+ repo_url=row.get("repo_url"),
62
+ owner_id=row.get("owner_id"),
63
+ created_at=self._parse_datetime(row.get("created_at")),
64
+ updated_at=self._parse_datetime(row.get("updated_at")),
65
+ )
66
+
67
+ def _row_to_team_member(self, row: dict) -> TeamMember:
68
+ """Convert a database row to a TeamMember model."""
69
+ return TeamMember(
70
+ id=row["id"],
71
+ project_id=row["project_id"],
72
+ name=row["name"],
73
+ email=row.get("email"),
74
+ github_handle=row.get("github_handle"),
75
+ role=row.get("role"),
76
+ user_id=row.get("user_id"),
77
+ created_at=self._parse_datetime(row.get("created_at")),
78
+ )
79
+
80
+ def _row_to_feature(self, row: dict) -> Feature:
81
+ """Convert a database row to a Feature model."""
82
+ return Feature(
83
+ id=row["id"],
84
+ project_id=row["project_id"],
85
+ title=row["title"],
86
+ description=row.get("description"),
87
+ status=FeatureStatus(row.get("status", "todo")),
88
+ spec=row.get("spec"),
89
+ plan=row.get("plan"),
90
+ tasks=row.get("tasks"),
91
+ created_at=self._parse_datetime(row.get("created_at")),
92
+ updated_at=self._parse_datetime(row.get("updated_at")),
93
+ )
94
+
95
+ def _row_to_feature_pr(self, row: dict) -> FeaturePR:
96
+ """Convert a database row to a FeaturePR model."""
97
+ return FeaturePR(
98
+ id=row["id"],
99
+ feature_id=row["feature_id"],
100
+ pr_url=row["pr_url"],
101
+ pr_number=row["pr_number"],
102
+ title=row.get("title"),
103
+ status=PRStatus(row.get("status", "open")),
104
+ created_at=self._parse_datetime(row.get("created_at")),
105
+ )
106
+
107
+ # -------------------------------------------------------------------------
108
+ # Projects
109
+ # -------------------------------------------------------------------------
110
+
111
+ async def create_project(
112
+ self, name: str, repo_url: Optional[str] = None, owner_id: Optional[str] = None
113
+ ) -> Project:
114
+ data = {"name": name, "repo_url": repo_url}
115
+ if owner_id:
116
+ data["owner_id"] = owner_id
117
+ result = self.client.table("projects").insert(data).execute()
118
+ return self._row_to_project(result.data[0])
119
+
120
+ async def get_project(self, project_id: str) -> Optional[Project]:
121
+ result = self.client.table("projects").select("*").eq("id", project_id).execute()
122
+ if not result.data:
123
+ return None
124
+ return self._row_to_project(result.data[0])
125
+
126
+ async def get_project_by_name(self, name: str) -> Optional[Project]:
127
+ result = self.client.table("projects").select("*").eq("name", name).execute()
128
+ if not result.data:
129
+ return None
130
+ return self._row_to_project(result.data[0])
131
+
132
+ async def list_projects(self) -> list[Project]:
133
+ result = self.client.table("projects").select("*").order("created_at", desc=True).execute()
134
+ return [self._row_to_project(row) for row in result.data]
135
+
136
+ async def get_project_by_repo_url(self, repo_url: str) -> Optional[Project]:
137
+ """Find a project by matching repository URL.
138
+
139
+ Normalizes URLs before comparison to handle different formats
140
+ (git@, https://, with/without .git suffix).
141
+
142
+ Args:
143
+ repo_url: Repository URL to match (will be normalized)
144
+
145
+ Returns:
146
+ Project if found, None otherwise
147
+ """
148
+ from ...utils.git import normalize_repo_url
149
+
150
+ normalized_search = normalize_repo_url(repo_url)
151
+
152
+ # Get all projects and compare normalized URLs
153
+ result = self.client.table("projects").select("*").execute()
154
+ for row in result.data:
155
+ if row.get("repo_url"):
156
+ if normalize_repo_url(row["repo_url"]) == normalized_search:
157
+ return self._row_to_project(row)
158
+ return None
159
+
160
+ async def update_project(
161
+ self, project_id: str, name: Optional[str] = None, repo_url: Optional[str] = None
162
+ ) -> Optional[Project]:
163
+ updates = {}
164
+ if name is not None:
165
+ updates["name"] = name
166
+ if repo_url is not None:
167
+ updates["repo_url"] = repo_url
168
+
169
+ if not updates:
170
+ return await self.get_project(project_id)
171
+
172
+ result = self.client.table("projects").update(updates).eq("id", project_id).execute()
173
+ if not result.data:
174
+ return None
175
+ return self._row_to_project(result.data[0])
176
+
177
+ async def delete_project(self, project_id: str) -> bool:
178
+ result = self.client.table("projects").delete().eq("id", project_id).execute()
179
+ return len(result.data) > 0
180
+
181
+ # -------------------------------------------------------------------------
182
+ # Team Members
183
+ # -------------------------------------------------------------------------
184
+
185
+ async def create_team_member(
186
+ self,
187
+ project_id: str,
188
+ name: str,
189
+ email: Optional[str] = None,
190
+ github_handle: Optional[str] = None,
191
+ role: Optional[str] = None,
192
+ user_id: Optional[str] = None,
193
+ ) -> TeamMember:
194
+ result = (
195
+ self.client.table("team_members")
196
+ .insert(
197
+ {
198
+ "project_id": project_id,
199
+ "name": name,
200
+ "email": email,
201
+ "github_handle": github_handle,
202
+ "role": role,
203
+ "user_id": user_id,
204
+ }
205
+ )
206
+ .execute()
207
+ )
208
+ return self._row_to_team_member(result.data[0])
209
+
210
+ async def get_team_member(self, member_id: str) -> Optional[TeamMember]:
211
+ result = self.client.table("team_members").select("*").eq("id", member_id).execute()
212
+ if not result.data:
213
+ return None
214
+ return self._row_to_team_member(result.data[0])
215
+
216
+ async def list_team_members(self, project_id: str) -> list[TeamMember]:
217
+ result = (
218
+ self.client.table("team_members")
219
+ .select("*")
220
+ .eq("project_id", project_id)
221
+ .order("created_at")
222
+ .execute()
223
+ )
224
+ return [self._row_to_team_member(row) for row in result.data]
225
+
226
+ async def update_team_member(
227
+ self,
228
+ member_id: str,
229
+ name: Optional[str] = None,
230
+ email: Optional[str] = None,
231
+ github_handle: Optional[str] = None,
232
+ role: Optional[str] = None,
233
+ ) -> Optional[TeamMember]:
234
+ updates = {}
235
+ if name is not None:
236
+ updates["name"] = name
237
+ if email is not None:
238
+ updates["email"] = email
239
+ if github_handle is not None:
240
+ updates["github_handle"] = github_handle
241
+ if role is not None:
242
+ updates["role"] = role
243
+
244
+ if not updates:
245
+ return await self.get_team_member(member_id)
246
+
247
+ result = self.client.table("team_members").update(updates).eq("id", member_id).execute()
248
+ if not result.data:
249
+ return None
250
+ return self._row_to_team_member(result.data[0])
251
+
252
+ async def delete_team_member(self, member_id: str) -> bool:
253
+ result = self.client.table("team_members").delete().eq("id", member_id).execute()
254
+ return len(result.data) > 0
255
+
256
+ # -------------------------------------------------------------------------
257
+ # Features
258
+ # -------------------------------------------------------------------------
259
+
260
+ async def create_feature(
261
+ self,
262
+ project_id: str,
263
+ title: str,
264
+ description: Optional[str] = None,
265
+ status: FeatureStatus = FeatureStatus.TODO,
266
+ spec: Optional[str] = None,
267
+ plan: Optional[str] = None,
268
+ tasks: Optional[str] = None,
269
+ ) -> Feature:
270
+ result = (
271
+ self.client.table("features")
272
+ .insert(
273
+ {
274
+ "project_id": project_id,
275
+ "title": title,
276
+ "description": description,
277
+ "status": status.value,
278
+ "spec": spec,
279
+ "plan": plan,
280
+ "tasks": tasks,
281
+ }
282
+ )
283
+ .execute()
284
+ )
285
+ return self._row_to_feature(result.data[0])
286
+
287
+ async def get_feature(self, feature_id: str, include_relations: bool = True) -> Optional[Feature]:
288
+ result = self.client.table("features").select("*").eq("id", feature_id).execute()
289
+ if not result.data:
290
+ return None
291
+
292
+ feature = self._row_to_feature(result.data[0])
293
+
294
+ if include_relations:
295
+ feature.assignees = await self.get_feature_assignees(feature_id)
296
+ feature.prs = await self.get_feature_prs(feature_id)
297
+
298
+ return feature
299
+
300
+ async def list_features(
301
+ self, project_id: str, status: Optional[FeatureStatus] = None, include_relations: bool = False
302
+ ) -> list[Feature]:
303
+ query = self.client.table("features").select("*").eq("project_id", project_id)
304
+
305
+ if status:
306
+ query = query.eq("status", status.value)
307
+
308
+ result = query.order("created_at", desc=True).execute()
309
+ features = [self._row_to_feature(row) for row in result.data]
310
+
311
+ if include_relations:
312
+ for feature in features:
313
+ feature.assignees = await self.get_feature_assignees(feature.id)
314
+ feature.prs = await self.get_feature_prs(feature.id)
315
+
316
+ return features
317
+
318
+ async def update_feature(
319
+ self,
320
+ feature_id: str,
321
+ title: Optional[str] = None,
322
+ description: Optional[str] = None,
323
+ status: Optional[FeatureStatus] = None,
324
+ spec: Optional[str] = None,
325
+ plan: Optional[str] = None,
326
+ tasks: Optional[str] = None,
327
+ ) -> Optional[Feature]:
328
+ updates = {}
329
+ if title is not None:
330
+ updates["title"] = title
331
+ if description is not None:
332
+ updates["description"] = description
333
+ if status is not None:
334
+ updates["status"] = status.value
335
+ if spec is not None:
336
+ updates["spec"] = spec
337
+ if plan is not None:
338
+ updates["plan"] = plan
339
+ if tasks is not None:
340
+ updates["tasks"] = tasks
341
+
342
+ if not updates:
343
+ return await self.get_feature(feature_id)
344
+
345
+ result = self.client.table("features").update(updates).eq("id", feature_id).execute()
346
+ if not result.data:
347
+ return None
348
+ return self._row_to_feature(result.data[0])
349
+
350
+ async def delete_feature(self, feature_id: str) -> bool:
351
+ result = self.client.table("features").delete().eq("id", feature_id).execute()
352
+ return len(result.data) > 0
353
+
354
+ # -------------------------------------------------------------------------
355
+ # Feature Assignees
356
+ # -------------------------------------------------------------------------
357
+
358
+ async def assign_feature(self, feature_id: str, team_member_id: str) -> FeatureAssignee:
359
+ result = (
360
+ self.client.table("feature_assignees")
361
+ .insert({"feature_id": feature_id, "team_member_id": team_member_id})
362
+ .execute()
363
+ )
364
+ row = result.data[0]
365
+ return FeatureAssignee(
366
+ feature_id=row["feature_id"],
367
+ team_member_id=row["team_member_id"],
368
+ assigned_at=self._parse_datetime(row.get("assigned_at")),
369
+ )
370
+
371
+ async def unassign_feature(self, feature_id: str, team_member_id: str) -> bool:
372
+ result = (
373
+ self.client.table("feature_assignees")
374
+ .delete()
375
+ .eq("feature_id", feature_id)
376
+ .eq("team_member_id", team_member_id)
377
+ .execute()
378
+ )
379
+ return len(result.data) > 0
380
+
381
+ async def get_feature_assignees(self, feature_id: str) -> list[TeamMember]:
382
+ result = (
383
+ self.client.table("feature_assignees")
384
+ .select("team_member_id, team_members(*)")
385
+ .eq("feature_id", feature_id)
386
+ .execute()
387
+ )
388
+ return [self._row_to_team_member(row["team_members"]) for row in result.data if row.get("team_members")]
389
+
390
+ # -------------------------------------------------------------------------
391
+ # Feature PRs
392
+ # -------------------------------------------------------------------------
393
+
394
+ async def add_feature_pr(
395
+ self,
396
+ feature_id: str,
397
+ pr_url: str,
398
+ pr_number: int,
399
+ title: Optional[str] = None,
400
+ status: PRStatus = PRStatus.OPEN,
401
+ ) -> FeaturePR:
402
+ result = (
403
+ self.client.table("feature_prs")
404
+ .insert(
405
+ {
406
+ "feature_id": feature_id,
407
+ "pr_url": pr_url,
408
+ "pr_number": pr_number,
409
+ "title": title,
410
+ "status": status.value,
411
+ }
412
+ )
413
+ .execute()
414
+ )
415
+ return self._row_to_feature_pr(result.data[0])
416
+
417
+ async def update_feature_pr(
418
+ self,
419
+ pr_id: str,
420
+ title: Optional[str] = None,
421
+ status: Optional[PRStatus] = None,
422
+ ) -> Optional[FeaturePR]:
423
+ updates = {}
424
+ if title is not None:
425
+ updates["title"] = title
426
+ if status is not None:
427
+ updates["status"] = status.value
428
+
429
+ if not updates:
430
+ result = self.client.table("feature_prs").select("*").eq("id", pr_id).execute()
431
+ if not result.data:
432
+ return None
433
+ return self._row_to_feature_pr(result.data[0])
434
+
435
+ result = self.client.table("feature_prs").update(updates).eq("id", pr_id).execute()
436
+ if not result.data:
437
+ return None
438
+ return self._row_to_feature_pr(result.data[0])
439
+
440
+ async def remove_feature_pr(self, pr_id: str) -> bool:
441
+ result = self.client.table("feature_prs").delete().eq("id", pr_id).execute()
442
+ return len(result.data) > 0
443
+
444
+ async def get_feature_prs(self, feature_id: str) -> list[FeaturePR]:
445
+ result = (
446
+ self.client.table("feature_prs")
447
+ .select("*")
448
+ .eq("feature_id", feature_id)
449
+ .order("created_at")
450
+ .execute()
451
+ )
452
+ return [self._row_to_feature_pr(row) for row in result.data]
@@ -0,0 +1,24 @@
1
+ """Embedding generation and semantic search for EmDash."""
2
+
3
+ from .models import EmbeddingModel, ModelSpec
4
+ from .service import EmbeddingService
5
+ from .indexer import EmbeddingIndexer
6
+ from .registry import ProviderRegistry, get_provider, get_available_model
7
+ from .providers import EmbeddingProvider, OpenAIProvider, FireworksProvider
8
+
9
+ __all__ = [
10
+ # Models
11
+ "EmbeddingModel",
12
+ "ModelSpec",
13
+ # Service
14
+ "EmbeddingService",
15
+ "EmbeddingIndexer",
16
+ # Registry
17
+ "ProviderRegistry",
18
+ "get_provider",
19
+ "get_available_model",
20
+ # Providers
21
+ "EmbeddingProvider",
22
+ "OpenAIProvider",
23
+ "FireworksProvider",
24
+ ]