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,223 @@
1
+ """Swarm (multi-agent) endpoints with SSE streaming."""
2
+
3
+ import asyncio
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from typing import Optional
6
+
7
+ from fastapi import APIRouter, HTTPException
8
+ from fastapi.responses import StreamingResponse
9
+ from pydantic import BaseModel, Field
10
+
11
+ from ..sse.stream import SSEHandler, EventType
12
+
13
+ router = APIRouter(prefix="/swarm", tags=["swarm"])
14
+
15
+ _executor = ThreadPoolExecutor(max_workers=1)
16
+
17
+
18
+ class SwarmRequest(BaseModel):
19
+ """Request to run swarm."""
20
+ tasks: list[str] = Field(..., description="List of tasks to run in parallel")
21
+ model: Optional[str] = Field(default=None, description="LLM model")
22
+ workers: int = Field(default=3, description="Number of parallel workers")
23
+ timeout: int = Field(default=300, description="Timeout per task in seconds")
24
+ base_branch: Optional[str] = Field(default=None, description="Base branch")
25
+ auto_merge: bool = Field(default=True, description="Auto-merge completed branches")
26
+ llm_merge: bool = Field(default=False, description="Use LLM for merge conflicts")
27
+
28
+
29
+ class SwarmStatus(BaseModel):
30
+ """Status of swarm execution."""
31
+ is_running: bool
32
+ tasks_total: int
33
+ tasks_completed: int
34
+ tasks_failed: int
35
+ current_tasks: list[str]
36
+
37
+
38
+ class SwarmSession(BaseModel):
39
+ """An active swarm session."""
40
+ id: str
41
+ task: str
42
+ status: str # running, completed, failed
43
+ branch: Optional[str] = None
44
+
45
+
46
+ def _run_swarm_sync(
47
+ tasks: list[str],
48
+ model: Optional[str],
49
+ workers: int,
50
+ sse_handler: SSEHandler,
51
+ ):
52
+ """Run swarm synchronously."""
53
+ import sys
54
+ from pathlib import Path
55
+
56
+ repo_root = Path(__file__).parent.parent.parent.parent.parent
57
+ if str(repo_root) not in sys.path:
58
+ sys.path.insert(0, str(repo_root))
59
+
60
+ try:
61
+ from ..swarm.swarm_runner import SwarmRunner
62
+ from ..agent.events import AgentEventEmitter
63
+
64
+ class SSEBridge:
65
+ def __init__(self, handler):
66
+ self._handler = handler
67
+
68
+ def handle(self, event):
69
+ self._handler.handle(event)
70
+
71
+ emitter = AgentEventEmitter(agent_name="Swarm")
72
+ emitter.add_handler(SSEBridge(sse_handler))
73
+
74
+ runner = SwarmRunner(
75
+ repo_root=repo_root,
76
+ model=model or "gpt-4o-mini",
77
+ max_workers=workers,
78
+ )
79
+
80
+ sse_handler.emit(EventType.PROGRESS, {
81
+ "step": f"Starting {len(tasks)} tasks with {workers} workers",
82
+ "percent": 0,
83
+ })
84
+
85
+ state = runner.run(tasks)
86
+
87
+ # Count results from swarm state
88
+ from ..swarm.task_definition import TaskStatus
89
+ completed = sum(1 for t in state.tasks if t.status == TaskStatus.COMPLETED)
90
+ failed = sum(1 for t in state.tasks if t.status == TaskStatus.FAILED)
91
+
92
+ sse_handler.emit(EventType.RESPONSE, {
93
+ "completed": completed,
94
+ "failed": failed,
95
+ "results": [{"slug": t.slug, "status": t.status.value} for t in state.tasks],
96
+ })
97
+
98
+ except Exception as e:
99
+ sse_handler.emit(EventType.ERROR, {"message": str(e)})
100
+ finally:
101
+ sse_handler.close()
102
+
103
+
104
+ @router.post("/run")
105
+ async def run_swarm(request: SwarmRequest):
106
+ """Run multiple agents in parallel on separate tasks.
107
+
108
+ Each task runs in its own git worktree branch.
109
+ """
110
+ if not request.tasks:
111
+ raise HTTPException(status_code=400, detail="No tasks provided")
112
+
113
+ sse_handler = SSEHandler(agent_name="Swarm")
114
+
115
+ sse_handler.emit(EventType.SESSION_START, {
116
+ "agent_name": "Swarm",
117
+ "task_count": len(request.tasks),
118
+ "workers": request.workers,
119
+ })
120
+
121
+ async def run():
122
+ loop = asyncio.get_event_loop()
123
+ await loop.run_in_executor(
124
+ _executor,
125
+ _run_swarm_sync,
126
+ request.tasks,
127
+ request.model,
128
+ request.workers,
129
+ sse_handler,
130
+ )
131
+
132
+ asyncio.create_task(run())
133
+
134
+ return StreamingResponse(
135
+ sse_handler,
136
+ media_type="text/event-stream",
137
+ headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
138
+ )
139
+
140
+
141
+ @router.get("/status", response_model=SwarmStatus)
142
+ async def get_swarm_status():
143
+ """Get status of current swarm execution."""
144
+ # TODO: Implement actual status tracking
145
+ return SwarmStatus(
146
+ is_running=False,
147
+ tasks_total=0,
148
+ tasks_completed=0,
149
+ tasks_failed=0,
150
+ current_tasks=[],
151
+ )
152
+
153
+
154
+ @router.get("/sessions")
155
+ async def get_swarm_sessions():
156
+ """List active swarm sessions."""
157
+ # TODO: Implement session tracking
158
+ return {"sessions": []}
159
+
160
+
161
+ @router.post("/cleanup")
162
+ async def cleanup_swarm(force: bool = False):
163
+ """Clean up all swarm worktrees and branches."""
164
+ try:
165
+ from pathlib import Path
166
+ from ..swarm.swarm_runner import SwarmRunner
167
+
168
+ repo_root = Path(__file__).parent.parent.parent.parent.parent
169
+ runner = SwarmRunner.load(repo_root)
170
+
171
+ if runner:
172
+ cleaned = runner.cleanup()
173
+ else:
174
+ # No active swarm, just cleanup orphaned worktrees
175
+ from ..swarm.worktree_manager import WorktreeManager
176
+ manager = WorktreeManager(repo_root)
177
+ cleaned = manager.cleanup_all()
178
+
179
+ return {
180
+ "success": True,
181
+ "cleaned_worktrees": cleaned,
182
+ }
183
+ except Exception as e:
184
+ raise HTTPException(status_code=500, detail=str(e))
185
+
186
+
187
+ @router.post("/merge")
188
+ async def merge_swarm_branches(
189
+ llm_merge: bool = False,
190
+ target: Optional[str] = None,
191
+ ):
192
+ """Merge all completed task branches."""
193
+ try:
194
+ from pathlib import Path
195
+ from ..swarm.swarm_runner import SwarmRunner
196
+
197
+ repo_root = Path(__file__).parent.parent.parent.parent.parent
198
+ runner = SwarmRunner.load(repo_root)
199
+
200
+ if not runner:
201
+ return {
202
+ "success": False,
203
+ "error": "No active swarm found",
204
+ "merged_branches": [],
205
+ "conflicts": [],
206
+ }
207
+
208
+ results = runner.merge_completed(
209
+ use_llm=llm_merge,
210
+ target_branch=target,
211
+ )
212
+
213
+ merged = [r.task_id for r in results if r.success]
214
+ failed = [{"task_id": r.task_id, "conflicts": r.conflicts, "error": r.error_message}
215
+ for r in results if not r.success]
216
+
217
+ return {
218
+ "success": len(failed) == 0,
219
+ "merged_tasks": merged,
220
+ "failed": failed,
221
+ }
222
+ except Exception as e:
223
+ raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,109 @@
1
+ """Task generation endpoints with SSE streaming."""
2
+
3
+ import asyncio
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from typing import Optional
6
+
7
+ from fastapi import APIRouter
8
+ from fastapi.responses import StreamingResponse
9
+ from pydantic import BaseModel, Field
10
+
11
+ from ..sse.stream import SSEHandler, EventType
12
+
13
+ router = APIRouter(prefix="/tasks", tags=["tasks"])
14
+
15
+ _executor = ThreadPoolExecutor(max_workers=2)
16
+
17
+
18
+ class TasksRequest(BaseModel):
19
+ """Request to generate implementation tasks."""
20
+ spec_name: Optional[str] = Field(default=None, description="Specification name")
21
+ spec_content: Optional[str] = Field(default=None, description="Specification content")
22
+ project_md: Optional[str] = Field(default=None, description="PROJECT.md content")
23
+ model: Optional[str] = Field(default=None, description="LLM model to use")
24
+
25
+
26
+ class Task(BaseModel):
27
+ """A single implementation task."""
28
+ id: int
29
+ title: str
30
+ description: str
31
+ files: list[str] = Field(default_factory=list)
32
+ dependencies: list[int] = Field(default_factory=list)
33
+
34
+
35
+ class TasksResponse(BaseModel):
36
+ """Task generation response."""
37
+ tasks: list[Task]
38
+ total: int
39
+
40
+
41
+ def _run_tasks_sync(
42
+ spec_content: Optional[str],
43
+ model: Optional[str],
44
+ sse_handler: SSEHandler,
45
+ ):
46
+ """Run task generation synchronously."""
47
+ import sys
48
+ from pathlib import Path
49
+
50
+ try:
51
+ # Note: ImplementationAgent is actually ImplementationPlanAgent in the module
52
+ # The generate_tasks method needs to be added or this endpoint refactored
53
+ from ..agent.events import AgentEventEmitter
54
+
55
+ class SSEBridge:
56
+ def __init__(self, handler):
57
+ self._handler = handler
58
+
59
+ def handle(self, event):
60
+ self._handler.handle(event)
61
+
62
+ emitter = AgentEventEmitter(agent_name="Tasks")
63
+ emitter.add_handler(SSEBridge(sse_handler))
64
+
65
+ agent = ImplementationAgent(model=model, emitter=emitter)
66
+ result = agent.generate_tasks(spec_content or "")
67
+
68
+ tasks = result.get("tasks", [])
69
+ sse_handler.emit(EventType.RESPONSE, {
70
+ "tasks": tasks,
71
+ "total": len(tasks),
72
+ })
73
+
74
+ except Exception as e:
75
+ sse_handler.emit(EventType.ERROR, {"message": str(e)})
76
+ finally:
77
+ sse_handler.close()
78
+
79
+
80
+ @router.post("/generate")
81
+ async def generate_tasks(request: TasksRequest):
82
+ """Generate implementation tasks from a specification.
83
+
84
+ Returns a list of tasks with dependencies for implementing the spec.
85
+ """
86
+ sse_handler = SSEHandler(agent_name="Tasks")
87
+
88
+ sse_handler.emit(EventType.SESSION_START, {
89
+ "agent_name": "Tasks",
90
+ "spec_name": request.spec_name,
91
+ })
92
+
93
+ async def run():
94
+ loop = asyncio.get_event_loop()
95
+ await loop.run_in_executor(
96
+ _executor,
97
+ _run_tasks_sync,
98
+ request.spec_content,
99
+ request.model,
100
+ sse_handler,
101
+ )
102
+
103
+ asyncio.create_task(run())
104
+
105
+ return StreamingResponse(
106
+ sse_handler,
107
+ media_type="text/event-stream",
108
+ headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
109
+ )
@@ -0,0 +1,120 @@
1
+ """Team analytics endpoints with SSE streaming."""
2
+
3
+ import asyncio
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from typing import Optional
6
+
7
+ from fastapi import APIRouter
8
+ from fastapi.responses import StreamingResponse
9
+ from pydantic import BaseModel, Field
10
+
11
+ from ..sse.stream import SSEHandler, EventType
12
+
13
+ router = APIRouter(prefix="/team", tags=["team"])
14
+
15
+ _executor = ThreadPoolExecutor(max_workers=2)
16
+
17
+
18
+ class TeamFocusRequest(BaseModel):
19
+ """Request for team focus analysis."""
20
+ days: int = Field(default=7, description="Number of days to analyze")
21
+ model: Optional[str] = Field(default=None, description="LLM model for summaries")
22
+ include_graph: bool = Field(default=True, description="Include graph analysis")
23
+
24
+
25
+ class AuthorFocus(BaseModel):
26
+ """Focus area for an author."""
27
+ author: str
28
+ email: str
29
+ commit_count: int
30
+ files_touched: int
31
+ areas: list[str]
32
+ recent_work: str
33
+
34
+
35
+ class TeamFocusResponse(BaseModel):
36
+ """Team focus response."""
37
+ period_days: int
38
+ authors: list[AuthorFocus]
39
+ summary: Optional[str] = None
40
+
41
+
42
+ def _run_team_focus_sync(
43
+ days: int,
44
+ model: Optional[str],
45
+ include_graph: bool,
46
+ sse_handler: SSEHandler,
47
+ ):
48
+ """Run team focus analysis synchronously."""
49
+ try:
50
+ from ..planning.team_focus import TeamFocusAnalyzer
51
+
52
+ analyzer = TeamFocusAnalyzer(model=model)
53
+
54
+ sse_handler.emit(EventType.PROGRESS, {
55
+ "step": "Analyzing git history",
56
+ "percent": 20,
57
+ })
58
+
59
+ result = analyzer.analyze(days=days, include_graph=include_graph)
60
+
61
+ sse_handler.emit(EventType.PROGRESS, {
62
+ "step": "Generating summary",
63
+ "percent": 80,
64
+ })
65
+
66
+ authors = [
67
+ AuthorFocus(
68
+ author=a.get("name", ""),
69
+ email=a.get("email", ""),
70
+ commit_count=a.get("commit_count", 0),
71
+ files_touched=a.get("files_touched", 0),
72
+ areas=a.get("areas", []),
73
+ recent_work=a.get("summary", ""),
74
+ )
75
+ for a in result.get("authors", [])
76
+ ]
77
+
78
+ sse_handler.emit(EventType.RESPONSE, {
79
+ "period_days": days,
80
+ "authors": [a.model_dump() for a in authors],
81
+ "summary": result.get("summary"),
82
+ })
83
+
84
+ except Exception as e:
85
+ sse_handler.emit(EventType.ERROR, {"message": str(e)})
86
+ finally:
87
+ sse_handler.close()
88
+
89
+
90
+ @router.post("/focus")
91
+ async def get_team_focus(request: TeamFocusRequest):
92
+ """Get team's recent focus and work-in-progress.
93
+
94
+ Analyzes git history to understand what each team member is working on.
95
+ """
96
+ sse_handler = SSEHandler(agent_name="TeamFocus")
97
+
98
+ sse_handler.emit(EventType.SESSION_START, {
99
+ "agent_name": "TeamFocus",
100
+ "days": request.days,
101
+ })
102
+
103
+ async def run():
104
+ loop = asyncio.get_event_loop()
105
+ await loop.run_in_executor(
106
+ _executor,
107
+ _run_team_focus_sync,
108
+ request.days,
109
+ request.model,
110
+ request.include_graph,
111
+ sse_handler,
112
+ )
113
+
114
+ asyncio.create_task(run())
115
+
116
+ return StreamingResponse(
117
+ sse_handler,
118
+ media_type="text/event-stream",
119
+ headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
120
+ )
@@ -0,0 +1,17 @@
1
+ """GitHub OAuth authentication for EmDash."""
2
+
3
+ from .github import (
4
+ GitHubAuth,
5
+ AuthConfig,
6
+ get_github_token,
7
+ is_authenticated,
8
+ get_auth_status,
9
+ )
10
+
11
+ __all__ = [
12
+ "GitHubAuth",
13
+ "AuthConfig",
14
+ "get_github_token",
15
+ "is_authenticated",
16
+ "get_auth_status",
17
+ ]