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.
- emdash_core/__init__.py +3 -0
- emdash_core/agent/__init__.py +37 -0
- emdash_core/agent/agents.py +225 -0
- emdash_core/agent/code_reviewer.py +476 -0
- emdash_core/agent/compaction.py +143 -0
- emdash_core/agent/context_manager.py +140 -0
- emdash_core/agent/events.py +338 -0
- emdash_core/agent/handlers.py +224 -0
- emdash_core/agent/inprocess_subagent.py +377 -0
- emdash_core/agent/mcp/__init__.py +50 -0
- emdash_core/agent/mcp/client.py +346 -0
- emdash_core/agent/mcp/config.py +302 -0
- emdash_core/agent/mcp/manager.py +496 -0
- emdash_core/agent/mcp/tool_factory.py +213 -0
- emdash_core/agent/prompts/__init__.py +38 -0
- emdash_core/agent/prompts/main_agent.py +104 -0
- emdash_core/agent/prompts/subagents.py +131 -0
- emdash_core/agent/prompts/workflow.py +136 -0
- emdash_core/agent/providers/__init__.py +34 -0
- emdash_core/agent/providers/base.py +143 -0
- emdash_core/agent/providers/factory.py +80 -0
- emdash_core/agent/providers/models.py +220 -0
- emdash_core/agent/providers/openai_provider.py +463 -0
- emdash_core/agent/providers/transformers_provider.py +217 -0
- emdash_core/agent/research/__init__.py +81 -0
- emdash_core/agent/research/agent.py +143 -0
- emdash_core/agent/research/controller.py +254 -0
- emdash_core/agent/research/critic.py +428 -0
- emdash_core/agent/research/macros.py +469 -0
- emdash_core/agent/research/planner.py +449 -0
- emdash_core/agent/research/researcher.py +436 -0
- emdash_core/agent/research/state.py +523 -0
- emdash_core/agent/research/synthesizer.py +594 -0
- emdash_core/agent/reviewer_profile.py +475 -0
- emdash_core/agent/rules.py +123 -0
- emdash_core/agent/runner.py +601 -0
- emdash_core/agent/session.py +262 -0
- emdash_core/agent/spec_schema.py +66 -0
- emdash_core/agent/specification.py +479 -0
- emdash_core/agent/subagent.py +397 -0
- emdash_core/agent/subagent_prompts.py +13 -0
- emdash_core/agent/toolkit.py +482 -0
- emdash_core/agent/toolkits/__init__.py +64 -0
- emdash_core/agent/toolkits/base.py +96 -0
- emdash_core/agent/toolkits/explore.py +47 -0
- emdash_core/agent/toolkits/plan.py +55 -0
- emdash_core/agent/tools/__init__.py +141 -0
- emdash_core/agent/tools/analytics.py +436 -0
- emdash_core/agent/tools/base.py +131 -0
- emdash_core/agent/tools/coding.py +484 -0
- emdash_core/agent/tools/github_mcp.py +592 -0
- emdash_core/agent/tools/history.py +13 -0
- emdash_core/agent/tools/modes.py +153 -0
- emdash_core/agent/tools/plan.py +206 -0
- emdash_core/agent/tools/plan_write.py +135 -0
- emdash_core/agent/tools/search.py +412 -0
- emdash_core/agent/tools/spec.py +341 -0
- emdash_core/agent/tools/task.py +262 -0
- emdash_core/agent/tools/task_output.py +204 -0
- emdash_core/agent/tools/tasks.py +454 -0
- emdash_core/agent/tools/traversal.py +588 -0
- emdash_core/agent/tools/web.py +179 -0
- emdash_core/analytics/__init__.py +5 -0
- emdash_core/analytics/engine.py +1286 -0
- emdash_core/api/__init__.py +5 -0
- emdash_core/api/agent.py +308 -0
- emdash_core/api/agents.py +154 -0
- emdash_core/api/analyze.py +264 -0
- emdash_core/api/auth.py +173 -0
- emdash_core/api/context.py +77 -0
- emdash_core/api/db.py +121 -0
- emdash_core/api/embed.py +131 -0
- emdash_core/api/feature.py +143 -0
- emdash_core/api/health.py +93 -0
- emdash_core/api/index.py +162 -0
- emdash_core/api/plan.py +110 -0
- emdash_core/api/projectmd.py +210 -0
- emdash_core/api/query.py +320 -0
- emdash_core/api/research.py +122 -0
- emdash_core/api/review.py +161 -0
- emdash_core/api/router.py +76 -0
- emdash_core/api/rules.py +116 -0
- emdash_core/api/search.py +119 -0
- emdash_core/api/spec.py +99 -0
- emdash_core/api/swarm.py +223 -0
- emdash_core/api/tasks.py +109 -0
- emdash_core/api/team.py +120 -0
- emdash_core/auth/__init__.py +17 -0
- emdash_core/auth/github.py +389 -0
- emdash_core/config.py +74 -0
- emdash_core/context/__init__.py +52 -0
- emdash_core/context/models.py +50 -0
- emdash_core/context/providers/__init__.py +11 -0
- emdash_core/context/providers/base.py +74 -0
- emdash_core/context/providers/explored_areas.py +183 -0
- emdash_core/context/providers/touched_areas.py +360 -0
- emdash_core/context/registry.py +73 -0
- emdash_core/context/reranker.py +199 -0
- emdash_core/context/service.py +260 -0
- emdash_core/context/session.py +352 -0
- emdash_core/core/__init__.py +104 -0
- emdash_core/core/config.py +454 -0
- emdash_core/core/exceptions.py +55 -0
- emdash_core/core/models.py +265 -0
- emdash_core/core/review_config.py +57 -0
- emdash_core/db/__init__.py +67 -0
- emdash_core/db/auth.py +134 -0
- emdash_core/db/models.py +91 -0
- emdash_core/db/provider.py +222 -0
- emdash_core/db/providers/__init__.py +5 -0
- emdash_core/db/providers/supabase.py +452 -0
- emdash_core/embeddings/__init__.py +24 -0
- emdash_core/embeddings/indexer.py +534 -0
- emdash_core/embeddings/models.py +192 -0
- emdash_core/embeddings/providers/__init__.py +7 -0
- emdash_core/embeddings/providers/base.py +112 -0
- emdash_core/embeddings/providers/fireworks.py +141 -0
- emdash_core/embeddings/providers/openai.py +104 -0
- emdash_core/embeddings/registry.py +146 -0
- emdash_core/embeddings/service.py +215 -0
- emdash_core/graph/__init__.py +26 -0
- emdash_core/graph/builder.py +134 -0
- emdash_core/graph/connection.py +692 -0
- emdash_core/graph/schema.py +416 -0
- emdash_core/graph/writer.py +667 -0
- emdash_core/ingestion/__init__.py +7 -0
- emdash_core/ingestion/change_detector.py +150 -0
- emdash_core/ingestion/git/__init__.py +5 -0
- emdash_core/ingestion/git/commit_analyzer.py +196 -0
- emdash_core/ingestion/github/__init__.py +6 -0
- emdash_core/ingestion/github/pr_fetcher.py +296 -0
- emdash_core/ingestion/github/task_extractor.py +100 -0
- emdash_core/ingestion/orchestrator.py +540 -0
- emdash_core/ingestion/parsers/__init__.py +10 -0
- emdash_core/ingestion/parsers/base_parser.py +66 -0
- emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
- emdash_core/ingestion/parsers/class_extractor.py +154 -0
- emdash_core/ingestion/parsers/function_extractor.py +202 -0
- emdash_core/ingestion/parsers/import_analyzer.py +119 -0
- emdash_core/ingestion/parsers/python_parser.py +123 -0
- emdash_core/ingestion/parsers/registry.py +72 -0
- emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
- emdash_core/ingestion/parsers/typescript_parser.py +278 -0
- emdash_core/ingestion/repository.py +346 -0
- emdash_core/models/__init__.py +38 -0
- emdash_core/models/agent.py +68 -0
- emdash_core/models/index.py +77 -0
- emdash_core/models/query.py +113 -0
- emdash_core/planning/__init__.py +7 -0
- emdash_core/planning/agent_api.py +413 -0
- emdash_core/planning/context_builder.py +265 -0
- emdash_core/planning/feature_context.py +232 -0
- emdash_core/planning/feature_expander.py +646 -0
- emdash_core/planning/llm_explainer.py +198 -0
- emdash_core/planning/similarity.py +509 -0
- emdash_core/planning/team_focus.py +821 -0
- emdash_core/server.py +153 -0
- emdash_core/sse/__init__.py +5 -0
- emdash_core/sse/stream.py +196 -0
- emdash_core/swarm/__init__.py +17 -0
- emdash_core/swarm/merge_agent.py +383 -0
- emdash_core/swarm/session_manager.py +274 -0
- emdash_core/swarm/swarm_runner.py +226 -0
- emdash_core/swarm/task_definition.py +137 -0
- emdash_core/swarm/worker_spawner.py +319 -0
- emdash_core/swarm/worktree_manager.py +278 -0
- emdash_core/templates/__init__.py +10 -0
- emdash_core/templates/defaults/agent-builder.md.template +82 -0
- emdash_core/templates/defaults/focus.md.template +115 -0
- emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
- emdash_core/templates/defaults/pr-review.md.template +80 -0
- emdash_core/templates/defaults/project.md.template +85 -0
- emdash_core/templates/defaults/research_critic.md.template +112 -0
- emdash_core/templates/defaults/research_planner.md.template +85 -0
- emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
- emdash_core/templates/defaults/reviewer.md.template +81 -0
- emdash_core/templates/defaults/spec.md.template +41 -0
- emdash_core/templates/defaults/tasks.md.template +78 -0
- emdash_core/templates/loader.py +296 -0
- emdash_core/utils/__init__.py +45 -0
- emdash_core/utils/git.py +84 -0
- emdash_core/utils/image.py +502 -0
- emdash_core/utils/logger.py +51 -0
- emdash_core-0.1.7.dist-info/METADATA +35 -0
- emdash_core-0.1.7.dist-info/RECORD +187 -0
- emdash_core-0.1.7.dist-info/WHEEL +4 -0
- 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,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
|
+
]
|