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,389 @@
1
+ """GitHub OAuth Device Flow authentication.
2
+
3
+ This module implements GitHub's device flow for CLI authentication,
4
+ similar to how `gh auth login` works.
5
+
6
+ Device Flow Steps:
7
+ 1. Request device and user codes from GitHub
8
+ 2. Display user code and open browser to github.com/login/device
9
+ 3. Poll GitHub until user completes authorization
10
+ 4. Store access token securely
11
+
12
+ Reference: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow
13
+ """
14
+
15
+ import json
16
+ import os
17
+ import time
18
+ import webbrowser
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import Optional
22
+ from urllib.parse import urlencode
23
+ from urllib.request import Request, urlopen
24
+ from urllib.error import HTTPError, URLError
25
+
26
+ from ..utils.logger import log
27
+
28
+
29
+ # Em Dash GitHub OAuth App Client ID
30
+ GITHUB_CLIENT_ID = "Ov23liMPlw6JMmzUainJ"
31
+
32
+ # GitHub OAuth endpoints
33
+ DEVICE_CODE_URL = "https://github.com/login/device/code"
34
+ ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
35
+ USER_API_URL = "https://api.github.com/user"
36
+
37
+ # Default scopes for Rove (repo access for code search, PRs, etc.)
38
+ DEFAULT_SCOPES = ["repo", "read:user"]
39
+
40
+
41
+ @dataclass
42
+ class AuthConfig:
43
+ """Stored authentication configuration."""
44
+
45
+ access_token: str
46
+ token_type: str = "bearer"
47
+ scope: str = ""
48
+ username: Optional[str] = None
49
+
50
+ def to_dict(self) -> dict:
51
+ return {
52
+ "access_token": self.access_token,
53
+ "token_type": self.token_type,
54
+ "scope": self.scope,
55
+ "username": self.username,
56
+ }
57
+
58
+ @classmethod
59
+ def from_dict(cls, data: dict) -> "AuthConfig":
60
+ return cls(
61
+ access_token=data.get("access_token", ""),
62
+ token_type=data.get("token_type", "bearer"),
63
+ scope=data.get("scope", ""),
64
+ username=data.get("username"),
65
+ )
66
+
67
+
68
+ @dataclass
69
+ class DeviceCodeResponse:
70
+ """Response from device code request."""
71
+
72
+ device_code: str
73
+ user_code: str
74
+ verification_uri: str
75
+ expires_in: int
76
+ interval: int
77
+
78
+
79
+ def get_config_dir() -> Path:
80
+ """Get the Rove config directory.
81
+
82
+ Returns:
83
+ Path to ~/.config/emdash/
84
+ """
85
+ # Follow XDG spec on Linux/Mac
86
+ xdg_config = os.environ.get("XDG_CONFIG_HOME")
87
+ if xdg_config:
88
+ config_dir = Path(xdg_config) / "emdash"
89
+ else:
90
+ config_dir = Path.home() / ".config" / "emdash"
91
+
92
+ config_dir.mkdir(parents=True, exist_ok=True)
93
+ return config_dir
94
+
95
+
96
+ def get_auth_file() -> Path:
97
+ """Get path to the auth config file."""
98
+ return get_config_dir() / "auth.json"
99
+
100
+
101
+ def _make_request(url: str, data: dict, headers: dict = None) -> dict:
102
+ """Make a POST request and return JSON response."""
103
+ headers = headers or {}
104
+ headers["Accept"] = "application/json"
105
+
106
+ encoded_data = urlencode(data).encode("utf-8")
107
+ request = Request(url, data=encoded_data, headers=headers, method="POST")
108
+
109
+ try:
110
+ with urlopen(request, timeout=30) as response:
111
+ return json.loads(response.read().decode("utf-8"))
112
+ except HTTPError as e:
113
+ error_body = e.read().decode("utf-8")
114
+ log.error(f"HTTP error {e.code}: {error_body}")
115
+ raise
116
+ except URLError as e:
117
+ log.error(f"URL error: {e.reason}")
118
+ raise
119
+
120
+
121
+ def _get_request(url: str, token: str) -> dict:
122
+ """Make a GET request with auth token."""
123
+ headers = {
124
+ "Accept": "application/json",
125
+ "Authorization": f"Bearer {token}",
126
+ }
127
+ request = Request(url, headers=headers)
128
+
129
+ with urlopen(request, timeout=30) as response:
130
+ return json.loads(response.read().decode("utf-8"))
131
+
132
+
133
+ class GitHubAuth:
134
+ """GitHub OAuth device flow authentication."""
135
+
136
+ def __init__(self, client_id: str = GITHUB_CLIENT_ID):
137
+ self.client_id = client_id
138
+
139
+ def request_device_code(self, scopes: list[str] = None) -> DeviceCodeResponse:
140
+ """Request a device code from GitHub.
141
+
142
+ Args:
143
+ scopes: OAuth scopes to request
144
+
145
+ Returns:
146
+ DeviceCodeResponse with codes and URIs
147
+ """
148
+ scopes = scopes or DEFAULT_SCOPES
149
+
150
+ data = {
151
+ "client_id": self.client_id,
152
+ "scope": " ".join(scopes),
153
+ }
154
+
155
+ response = _make_request(DEVICE_CODE_URL, data)
156
+
157
+ return DeviceCodeResponse(
158
+ device_code=response["device_code"],
159
+ user_code=response["user_code"],
160
+ verification_uri=response["verification_uri"],
161
+ expires_in=response["expires_in"],
162
+ interval=response["interval"],
163
+ )
164
+
165
+ def poll_for_token(
166
+ self,
167
+ device_code: str,
168
+ interval: int = 5,
169
+ timeout: int = 900,
170
+ ) -> Optional[AuthConfig]:
171
+ """Poll GitHub for access token after user authorizes.
172
+
173
+ Args:
174
+ device_code: Device code from request_device_code
175
+ interval: Polling interval in seconds
176
+ timeout: Maximum time to wait in seconds
177
+
178
+ Returns:
179
+ AuthConfig if successful, None if timeout/cancelled
180
+ """
181
+ data = {
182
+ "client_id": self.client_id,
183
+ "device_code": device_code,
184
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
185
+ }
186
+
187
+ start_time = time.time()
188
+
189
+ while time.time() - start_time < timeout:
190
+ try:
191
+ response = _make_request(ACCESS_TOKEN_URL, data)
192
+
193
+ if "access_token" in response:
194
+ # Success!
195
+ config = AuthConfig(
196
+ access_token=response["access_token"],
197
+ token_type=response.get("token_type", "bearer"),
198
+ scope=response.get("scope", ""),
199
+ )
200
+
201
+ # Fetch username
202
+ try:
203
+ user_info = _get_request(USER_API_URL, config.access_token)
204
+ config.username = user_info.get("login")
205
+ except Exception as e:
206
+ log.warning(f"Failed to fetch username: {e}")
207
+
208
+ return config
209
+
210
+ error = response.get("error")
211
+
212
+ if error == "authorization_pending":
213
+ # User hasn't authorized yet, keep polling
214
+ time.sleep(interval)
215
+ continue
216
+ elif error == "slow_down":
217
+ # We're polling too fast
218
+ interval += 5
219
+ time.sleep(interval)
220
+ continue
221
+ elif error == "expired_token":
222
+ log.error("Device code expired. Please try again.")
223
+ return None
224
+ elif error == "access_denied":
225
+ log.error("Authorization was denied.")
226
+ return None
227
+ else:
228
+ log.error(f"Unexpected error: {error}")
229
+ return None
230
+
231
+ except Exception as e:
232
+ log.error(f"Error polling for token: {e}")
233
+ time.sleep(interval)
234
+
235
+ log.error("Timeout waiting for authorization.")
236
+ return None
237
+
238
+ def login(self, scopes: list[str] = None, open_browser: bool = True) -> Optional[AuthConfig]:
239
+ """Perform full device flow login.
240
+
241
+ Args:
242
+ scopes: OAuth scopes to request
243
+ open_browser: Whether to open browser automatically
244
+
245
+ Returns:
246
+ AuthConfig if successful, None otherwise
247
+ """
248
+ # Request device code
249
+ device_response = self.request_device_code(scopes)
250
+
251
+ # Display instructions
252
+ print()
253
+ print(f"! First, copy your one-time code: {device_response.user_code}")
254
+ print(f"- Then visit: {device_response.verification_uri}")
255
+
256
+ if open_browser:
257
+ input("- Press Enter to open github.com in your browser...")
258
+ webbrowser.open(device_response.verification_uri)
259
+
260
+ print()
261
+ print("Waiting for authorization...")
262
+
263
+ # Poll for token
264
+ config = self.poll_for_token(
265
+ device_response.device_code,
266
+ interval=device_response.interval,
267
+ )
268
+
269
+ if config:
270
+ # Save to disk
271
+ save_auth_config(config)
272
+ print()
273
+ if config.username:
274
+ print(f"✓ Authentication complete. Logged in as @{config.username}")
275
+ else:
276
+ print("✓ Authentication complete.")
277
+
278
+ return config
279
+
280
+ def logout(self) -> bool:
281
+ """Remove stored authentication.
282
+
283
+ Returns:
284
+ True if logout successful
285
+ """
286
+ auth_file = get_auth_file()
287
+ if auth_file.exists():
288
+ auth_file.unlink()
289
+ return True
290
+ return False
291
+
292
+
293
+ def save_auth_config(config: AuthConfig) -> None:
294
+ """Save authentication config to disk.
295
+
296
+ Args:
297
+ config: AuthConfig to save
298
+ """
299
+ auth_file = get_auth_file()
300
+
301
+ # Set restrictive permissions (owner read/write only)
302
+ with open(auth_file, "w") as f:
303
+ json.dump(config.to_dict(), f, indent=2)
304
+
305
+ # chmod 600
306
+ os.chmod(auth_file, 0o600)
307
+
308
+ log.debug(f"Saved auth config to {auth_file}")
309
+
310
+
311
+ def load_auth_config() -> Optional[AuthConfig]:
312
+ """Load authentication config from disk.
313
+
314
+ Returns:
315
+ AuthConfig or None if not found
316
+ """
317
+ auth_file = get_auth_file()
318
+
319
+ if not auth_file.exists():
320
+ return None
321
+
322
+ try:
323
+ with open(auth_file) as f:
324
+ data = json.load(f)
325
+ return AuthConfig.from_dict(data)
326
+ except Exception as e:
327
+ log.warning(f"Failed to load auth config: {e}")
328
+ return None
329
+
330
+
331
+ def get_github_token() -> Optional[str]:
332
+ """Get GitHub token from auth config or environment.
333
+
334
+ Priority:
335
+ 1. Rove auth config (~/.config/emdash/auth.json)
336
+ 2. GITHUB_TOKEN environment variable
337
+ 3. GITHUB_PERSONAL_ACCESS_TOKEN environment variable
338
+
339
+ Returns:
340
+ GitHub access token or None
341
+ """
342
+ # Check Rove auth first
343
+ config = load_auth_config()
344
+ if config and config.access_token:
345
+ return config.access_token
346
+
347
+ # Fall back to environment variables
348
+ return os.environ.get("GITHUB_TOKEN") or os.environ.get("GITHUB_PERSONAL_ACCESS_TOKEN")
349
+
350
+
351
+ def is_authenticated() -> bool:
352
+ """Check if user is authenticated.
353
+
354
+ Returns:
355
+ True if authenticated (either via Rove auth or env var)
356
+ """
357
+ return get_github_token() is not None
358
+
359
+
360
+ def get_auth_status() -> dict:
361
+ """Get current authentication status.
362
+
363
+ Returns:
364
+ Dict with status info
365
+ """
366
+ config = load_auth_config()
367
+ env_token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GITHUB_PERSONAL_ACCESS_TOKEN")
368
+
369
+ if config and config.access_token:
370
+ return {
371
+ "authenticated": True,
372
+ "source": "emdash auth",
373
+ "username": config.username,
374
+ "scopes": config.scope.split() if config.scope else [],
375
+ }
376
+ elif env_token:
377
+ return {
378
+ "authenticated": True,
379
+ "source": "environment variable",
380
+ "username": None,
381
+ "scopes": [],
382
+ }
383
+ else:
384
+ return {
385
+ "authenticated": False,
386
+ "source": None,
387
+ "username": None,
388
+ "scopes": [],
389
+ }
emdash_core/config.py ADDED
@@ -0,0 +1,74 @@
1
+ """Server configuration."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from pydantic import Field
8
+ from pydantic_settings import BaseSettings
9
+
10
+
11
+ def _get_default_model() -> str:
12
+ """Get the default model from emdash_core or fallback."""
13
+ try:
14
+ from .agent.providers.factory import DEFAULT_MODEL
15
+ return DEFAULT_MODEL
16
+ except ImportError:
17
+ # Fallback if not available
18
+ return os.getenv("EMDASH_DEFAULT_MODEL", "fireworks:accounts/fireworks/models/minimax-m2p1")
19
+
20
+
21
+ class ServerConfig(BaseSettings):
22
+ """Configuration for the emdash-core server."""
23
+
24
+ # Server settings
25
+ host: str = Field(default="127.0.0.1", description="Host to bind to")
26
+ port: int = Field(default=8765, description="Port to bind to")
27
+
28
+ # Repository settings
29
+ repo_root: Optional[str] = Field(default=None, description="Repository root path")
30
+
31
+ # Database settings
32
+ database_path: str = Field(
33
+ default=".emdash/index/kuzu_db",
34
+ description="Path to Kuzu database relative to repo root"
35
+ )
36
+
37
+ # Agent settings
38
+ default_model: str = Field(default_factory=_get_default_model, description="Default LLM model")
39
+ max_iterations: int = Field(default=50, description="Max agent iterations")
40
+ context_threshold: float = Field(default=0.6, description="Context window threshold for summarization")
41
+
42
+ # SSE settings
43
+ sse_ping_interval: int = Field(default=15, description="SSE ping interval in seconds")
44
+
45
+ class Config:
46
+ env_prefix = "EMDASH_"
47
+ env_file = ".env"
48
+ extra = "ignore"
49
+
50
+ @property
51
+ def database_full_path(self) -> Path:
52
+ """Get full path to database."""
53
+ if self.repo_root:
54
+ return Path(self.repo_root) / self.database_path
55
+ return Path(self.database_path)
56
+
57
+
58
+ # Global config instance
59
+ _config: Optional[ServerConfig] = None
60
+
61
+
62
+ def get_config() -> ServerConfig:
63
+ """Get or create server configuration."""
64
+ global _config
65
+ if _config is None:
66
+ _config = ServerConfig()
67
+ return _config
68
+
69
+
70
+ def set_config(**kwargs) -> ServerConfig:
71
+ """Set server configuration with overrides."""
72
+ global _config
73
+ _config = ServerConfig(**kwargs)
74
+ return _config
@@ -0,0 +1,52 @@
1
+ """Session-based context provider system.
2
+
3
+ This module provides an extensible system for extracting and managing
4
+ contextual information during agent sessions.
5
+
6
+ Example usage:
7
+ from emdash_core.context import ContextService
8
+
9
+ # Create service
10
+ service = ContextService()
11
+
12
+ # Get terminal ID (creates one if not exists)
13
+ terminal_id = ContextService.get_terminal_id()
14
+
15
+ # Update context after changes
16
+ service.update_context(terminal_id)
17
+
18
+ # Get formatted context for LLM
19
+ context_prompt = service.get_context_prompt(terminal_id)
20
+
21
+ Adding new providers:
22
+ 1. Create a new provider class inheriting from ContextProvider
23
+ 2. Implement extract_context() method
24
+ 3. Register with ContextProviderRegistry.register("name", MyProvider)
25
+ 4. Add to CONTEXT_PROVIDERS env var
26
+ """
27
+
28
+ from .models import ContextItem, ContextProviderSpec, SessionContext
29
+ from .reranker import get_rerank_scores, rerank_context_items
30
+ from .registry import ContextProviderRegistry, get_provider
31
+ from .service import ContextService
32
+ from .session import SessionContextManager
33
+
34
+ # Import providers to trigger registration
35
+ from .providers import explored_areas, touched_areas # noqa: F401
36
+
37
+ __all__ = [
38
+ # Models
39
+ "ContextItem",
40
+ "ContextProviderSpec",
41
+ "SessionContext",
42
+ # Registry
43
+ "ContextProviderRegistry",
44
+ "get_provider",
45
+ # Reranker
46
+ "rerank_context_items",
47
+ "get_rerank_scores",
48
+ # Service
49
+ "ContextService",
50
+ # Session
51
+ "SessionContextManager",
52
+ ]
@@ -0,0 +1,50 @@
1
+ """Data models for the context provider system."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Optional
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class ContextProviderSpec:
10
+ """Specification for a context provider."""
11
+
12
+ name: str
13
+ description: str
14
+ requires_graph: bool = True
15
+
16
+
17
+ @dataclass
18
+ class ContextItem:
19
+ """A single context item extracted from code."""
20
+
21
+ qualified_name: str
22
+ entity_type: str # 'Function', 'Class', 'File'
23
+ description: Optional[str] = None
24
+ file_path: Optional[str] = None
25
+ score: float = 1.0
26
+ touch_count: int = 1
27
+ last_touched: Optional[datetime] = None
28
+ neighbors: list[str] = field(default_factory=list)
29
+
30
+ def __post_init__(self):
31
+ if self.last_touched is None:
32
+ object.__setattr__(self, "last_touched", datetime.now())
33
+
34
+
35
+ @dataclass
36
+ class SessionContext:
37
+ """Context for a terminal session."""
38
+
39
+ session_id: str
40
+ terminal_id: str
41
+ items: list[ContextItem] = field(default_factory=list)
42
+ created_at: Optional[datetime] = None
43
+ last_active: Optional[datetime] = None
44
+
45
+ def __post_init__(self):
46
+ now = datetime.now()
47
+ if self.created_at is None:
48
+ self.created_at = now
49
+ if self.last_active is None:
50
+ self.last_active = now
@@ -0,0 +1,11 @@
1
+ """Context providers package."""
2
+
3
+ from .base import ContextProvider
4
+ from .explored_areas import ExploredAreasProvider
5
+ from .touched_areas import TouchedAreasProvider
6
+
7
+ __all__ = [
8
+ "ContextProvider",
9
+ "ExploredAreasProvider",
10
+ "TouchedAreasProvider",
11
+ ]
@@ -0,0 +1,74 @@
1
+ """Abstract base class for context providers."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional
5
+
6
+ from ..models import ContextItem, ContextProviderSpec
7
+ from ...graph.connection import KuzuConnection
8
+
9
+
10
+ class ContextProvider(ABC):
11
+ """Base class for context providers.
12
+
13
+ Context providers extract relevant context from various sources
14
+ (AST, git history, semantic search, etc.) to help the LLM
15
+ understand the codebase during a session.
16
+ """
17
+
18
+ def __init__(self, connection: KuzuConnection, config: Optional[dict] = None):
19
+ """Initialize context provider.
20
+
21
+ Args:
22
+ connection: Kuzu database connection
23
+ config: Optional provider-specific configuration
24
+ """
25
+ self.connection = connection
26
+ self.config = config or {}
27
+
28
+ @property
29
+ @abstractmethod
30
+ def spec(self) -> ContextProviderSpec:
31
+ """Get provider specification."""
32
+ pass
33
+
34
+ @property
35
+ def name(self) -> str:
36
+ """Get provider name."""
37
+ return self.spec.name
38
+
39
+ @abstractmethod
40
+ def extract_context(self, modified_files: list[str]) -> list[ContextItem]:
41
+ """Extract context items from modified files.
42
+
43
+ Args:
44
+ modified_files: List of file paths that were modified
45
+
46
+ Returns:
47
+ List of context items extracted from those files
48
+ """
49
+ pass
50
+
51
+ def format_for_prompt(self, items: list[ContextItem]) -> str:
52
+ """Format context items for LLM system prompt.
53
+
54
+ Args:
55
+ items: Context items to format
56
+
57
+ Returns:
58
+ Formatted string for inclusion in system prompt
59
+ """
60
+ if not items:
61
+ return ""
62
+
63
+ lines = [f"## Context from {self.name}\n"]
64
+ for item in sorted(items, key=lambda x: -x.score):
65
+ lines.append(f"### {item.entity_type}: {item.qualified_name}")
66
+ if item.file_path:
67
+ lines.append(f"File: {item.file_path}")
68
+ if item.description:
69
+ lines.append(f"Description: {item.description}")
70
+ if item.neighbors:
71
+ lines.append(f"Related: {', '.join(item.neighbors[:5])}")
72
+ lines.append("")
73
+
74
+ return "\n".join(lines)