emdash-core 0.1.33__py3-none-any.whl → 0.1.60__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 (67) hide show
  1. emdash_core/agent/agents.py +93 -23
  2. emdash_core/agent/background.py +481 -0
  3. emdash_core/agent/hooks.py +419 -0
  4. emdash_core/agent/inprocess_subagent.py +114 -10
  5. emdash_core/agent/mcp/config.py +78 -2
  6. emdash_core/agent/prompts/main_agent.py +88 -1
  7. emdash_core/agent/prompts/plan_mode.py +65 -44
  8. emdash_core/agent/prompts/subagents.py +96 -8
  9. emdash_core/agent/prompts/workflow.py +215 -50
  10. emdash_core/agent/providers/models.py +1 -1
  11. emdash_core/agent/providers/openai_provider.py +10 -0
  12. emdash_core/agent/research/researcher.py +154 -45
  13. emdash_core/agent/runner/agent_runner.py +157 -19
  14. emdash_core/agent/runner/context.py +28 -9
  15. emdash_core/agent/runner/sdk_runner.py +29 -2
  16. emdash_core/agent/skills.py +81 -1
  17. emdash_core/agent/toolkit.py +87 -11
  18. emdash_core/agent/toolkits/__init__.py +117 -18
  19. emdash_core/agent/toolkits/base.py +87 -2
  20. emdash_core/agent/toolkits/explore.py +18 -0
  21. emdash_core/agent/toolkits/plan.py +18 -0
  22. emdash_core/agent/tools/__init__.py +2 -0
  23. emdash_core/agent/tools/coding.py +344 -52
  24. emdash_core/agent/tools/lsp.py +361 -0
  25. emdash_core/agent/tools/skill.py +21 -1
  26. emdash_core/agent/tools/task.py +27 -23
  27. emdash_core/agent/tools/task_output.py +262 -32
  28. emdash_core/agent/verifier/__init__.py +11 -0
  29. emdash_core/agent/verifier/manager.py +295 -0
  30. emdash_core/agent/verifier/models.py +97 -0
  31. emdash_core/{swarm/worktree_manager.py → agent/worktree.py} +19 -1
  32. emdash_core/api/agent.py +451 -5
  33. emdash_core/api/research.py +3 -3
  34. emdash_core/api/router.py +0 -4
  35. emdash_core/context/longevity.py +197 -0
  36. emdash_core/context/providers/explored_areas.py +83 -39
  37. emdash_core/context/reranker.py +35 -144
  38. emdash_core/context/simple_reranker.py +500 -0
  39. emdash_core/context/tool_relevance.py +84 -0
  40. emdash_core/core/config.py +8 -0
  41. emdash_core/graph/__init__.py +8 -1
  42. emdash_core/graph/connection.py +24 -3
  43. emdash_core/graph/writer.py +7 -1
  44. emdash_core/ingestion/repository.py +17 -198
  45. emdash_core/models/agent.py +14 -0
  46. emdash_core/server.py +1 -6
  47. emdash_core/sse/stream.py +16 -1
  48. emdash_core/utils/__init__.py +0 -2
  49. emdash_core/utils/git.py +103 -0
  50. emdash_core/utils/image.py +147 -160
  51. {emdash_core-0.1.33.dist-info → emdash_core-0.1.60.dist-info}/METADATA +7 -5
  52. {emdash_core-0.1.33.dist-info → emdash_core-0.1.60.dist-info}/RECORD +54 -58
  53. emdash_core/api/swarm.py +0 -223
  54. emdash_core/db/__init__.py +0 -67
  55. emdash_core/db/auth.py +0 -134
  56. emdash_core/db/models.py +0 -91
  57. emdash_core/db/provider.py +0 -222
  58. emdash_core/db/providers/__init__.py +0 -5
  59. emdash_core/db/providers/supabase.py +0 -452
  60. emdash_core/swarm/__init__.py +0 -17
  61. emdash_core/swarm/merge_agent.py +0 -383
  62. emdash_core/swarm/session_manager.py +0 -274
  63. emdash_core/swarm/swarm_runner.py +0 -226
  64. emdash_core/swarm/task_definition.py +0 -137
  65. emdash_core/swarm/worker_spawner.py +0 -319
  66. {emdash_core-0.1.33.dist-info → emdash_core-0.1.60.dist-info}/WHEEL +0 -0
  67. {emdash_core-0.1.33.dist-info → emdash_core-0.1.60.dist-info}/entry_points.txt +0 -0
@@ -1,452 +0,0 @@
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]
@@ -1,17 +0,0 @@
1
- """Multi-agent swarm execution with git worktrees."""
2
-
3
- from .task_definition import SwarmTask, SwarmState, TaskStatus
4
- from .worktree_manager import WorktreeManager, WorktreeInfo, WorktreeError
5
- from .swarm_runner import SwarmRunner
6
- from .session_manager import SessionManager
7
-
8
- __all__ = [
9
- "SwarmTask",
10
- "SwarmState",
11
- "TaskStatus",
12
- "WorktreeManager",
13
- "WorktreeInfo",
14
- "WorktreeError",
15
- "SwarmRunner",
16
- "SessionManager",
17
- ]