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,454 @@
1
+ """Configuration management for EmDash."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from pydantic import BaseModel, Field
8
+ from dotenv import load_dotenv
9
+
10
+ from .exceptions import ConfigurationError
11
+
12
+
13
+ def get_config_dir() -> Path:
14
+ """Get the emdash config directory (~/.config/emdash)."""
15
+ return Path.home() / ".config" / "emdash"
16
+
17
+
18
+ def get_user_config_path() -> Path:
19
+ """Get the user config file path (~/.config/emdash/config)."""
20
+ return get_config_dir() / "config"
21
+
22
+
23
+ def get_local_env_path() -> Path:
24
+ """Get the .env file path in the current working directory."""
25
+ return Path.cwd() / ".env"
26
+
27
+
28
+ def load_env_files() -> None:
29
+ """Load environment variables from config files.
30
+
31
+ Load order (later files override earlier):
32
+ 1. ~/.config/emdash/config (user-level defaults)
33
+ 2. .env in current working directory (project-level overrides)
34
+ """
35
+ # Load user-level config first (lower priority)
36
+ user_config = get_user_config_path()
37
+ if user_config.exists():
38
+ load_dotenv(user_config, override=False)
39
+
40
+ # Load local .env from current working directory (higher priority)
41
+ local_env = get_local_env_path()
42
+ if local_env.exists():
43
+ load_dotenv(local_env, override=True)
44
+
45
+
46
+ # Load environment variables from config files
47
+ load_env_files()
48
+
49
+
50
+ class KuzuConfig(BaseModel):
51
+ """Kuzu embedded database configuration."""
52
+
53
+ database_path: str = Field(default=".emdash/index/kuzu_db")
54
+ read_only: bool = Field(default=False)
55
+
56
+ @classmethod
57
+ def from_env(cls) -> "KuzuConfig":
58
+ """Load configuration from environment variables."""
59
+ return cls(
60
+ database_path=os.getenv("KUZU_DATABASE_PATH", ".emdash/index/kuzu_db"),
61
+ read_only=os.getenv("KUZU_READ_ONLY", "false").lower() == "true",
62
+ )
63
+
64
+
65
+ class IngestionConfig(BaseModel):
66
+ """Configuration for repository ingestion."""
67
+
68
+ max_workers: int = Field(default=4, ge=1, le=16)
69
+ batch_size: int = Field(default=1000, ge=100, le=10000)
70
+ git_depth: Optional[int] = Field(default=None)
71
+ ast_only: bool = Field(default=False, description="Skip Layer B (git) and Layer C (analytics)")
72
+ ignore_patterns: list[str] = Field(
73
+ default_factory=lambda: [
74
+ "__pycache__",
75
+ "*.pyc",
76
+ "*.pyo",
77
+ ".git",
78
+ ".venv",
79
+ "venv",
80
+ "env",
81
+ "node_modules",
82
+ ".tox",
83
+ ".pytest_cache",
84
+ "*.egg-info",
85
+ # Build outputs - minified files slow down indexing significantly
86
+ "dist",
87
+ "build",
88
+ ".next",
89
+ ".nuxt",
90
+ ".output",
91
+ "_astro",
92
+ "*.min.js",
93
+ "*.min.css",
94
+ "*.bundle.js",
95
+ "*.chunk.js",
96
+ # Other common excludes
97
+ "coverage",
98
+ ".nyc_output",
99
+ "*.map",
100
+ ]
101
+ )
102
+
103
+ @classmethod
104
+ def from_env(cls) -> "IngestionConfig":
105
+ """Load configuration from environment variables."""
106
+ git_depth = os.getenv("GIT_DEPTH")
107
+ return cls(
108
+ max_workers=int(os.getenv("MAX_WORKERS", "4")),
109
+ batch_size=int(os.getenv("BATCH_SIZE", "1000")),
110
+ git_depth=int(git_depth) if git_depth else None,
111
+ ast_only=os.getenv("AST_ONLY", "false").lower() == "true",
112
+ )
113
+
114
+
115
+ class AnalyticsConfig(BaseModel):
116
+ """Configuration for graph analytics."""
117
+
118
+ pagerank_iterations: int = Field(default=20, ge=5, le=100)
119
+ pagerank_damping: float = Field(default=0.85, ge=0.0, le=1.0)
120
+ clustering_algorithm: str = Field(default="louvain")
121
+
122
+ @classmethod
123
+ def from_env(cls) -> "AnalyticsConfig":
124
+ """Load configuration from environment variables."""
125
+ return cls(
126
+ pagerank_iterations=int(os.getenv("PAGERANK_ITERATIONS", "20")),
127
+ pagerank_damping=float(os.getenv("PAGERANK_DAMPING", "0.85")),
128
+ clustering_algorithm=os.getenv("CLUSTERING_ALGORITHM", "louvain"),
129
+ )
130
+
131
+
132
+ class GitHubConfig(BaseModel):
133
+ """Configuration for GitHub API access."""
134
+
135
+ token: Optional[str] = Field(default=None)
136
+
137
+ @classmethod
138
+ def from_env(cls) -> "GitHubConfig":
139
+ """Load configuration from environment variables."""
140
+ return cls(token=os.getenv("GITHUB_TOKEN"))
141
+
142
+ @property
143
+ def is_available(self) -> bool:
144
+ """Check if GitHub token is configured."""
145
+ return self.token is not None and len(self.token) > 0
146
+
147
+
148
+ class OpenAIConfig(BaseModel):
149
+ """Configuration for OpenAI API (embeddings and chat)."""
150
+
151
+ api_key: Optional[str] = Field(default=None)
152
+ model: str = Field(default="text-embedding-3-small")
153
+ dimensions: int = Field(default=1536)
154
+ batch_size: int = Field(default=100, ge=1, le=2000)
155
+
156
+ @classmethod
157
+ def from_env(cls) -> "OpenAIConfig":
158
+ """Load configuration from environment variables."""
159
+ return cls(
160
+ api_key=os.getenv("OPENAI_API_KEY"),
161
+ model=os.getenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small"),
162
+ dimensions=int(os.getenv("OPENAI_EMBEDDING_DIMENSIONS", "1536")),
163
+ batch_size=int(os.getenv("OPENAI_BATCH_SIZE", "100")),
164
+ )
165
+
166
+ @property
167
+ def is_available(self) -> bool:
168
+ """Check if OpenAI API key is configured."""
169
+ return self.api_key is not None and len(self.api_key) > 0
170
+
171
+
172
+ class AnthropicConfig(BaseModel):
173
+ """Configuration for Anthropic API (chat)."""
174
+
175
+ api_key: Optional[str] = Field(default=None)
176
+
177
+ @classmethod
178
+ def from_env(cls) -> "AnthropicConfig":
179
+ """Load configuration from environment variables."""
180
+ return cls(
181
+ api_key=os.getenv("ANTHROPIC_API_KEY"),
182
+ )
183
+
184
+ @property
185
+ def is_available(self) -> bool:
186
+ """Check if Anthropic API key is configured."""
187
+ return self.api_key is not None and len(self.api_key) > 0
188
+
189
+
190
+ class FireworksConfig(BaseModel):
191
+ """Configuration for Fireworks AI API (embeddings)."""
192
+
193
+ api_key: Optional[str] = Field(default=None)
194
+ batch_size: int = Field(default=100, ge=1, le=500)
195
+
196
+ @classmethod
197
+ def from_env(cls) -> "FireworksConfig":
198
+ """Load configuration from environment variables."""
199
+ return cls(
200
+ api_key=os.getenv("FIREWORKS_API_KEY"),
201
+ batch_size=int(os.getenv("FIREWORKS_BATCH_SIZE", "100")),
202
+ )
203
+
204
+ @property
205
+ def is_available(self) -> bool:
206
+ """Check if Fireworks API key is configured."""
207
+ return self.api_key is not None and len(self.api_key) > 0
208
+
209
+
210
+ class ContextConfig(BaseModel):
211
+ """Configuration for session context providers."""
212
+
213
+ providers: list[str] = Field(
214
+ default_factory=lambda: ["touched_areas"],
215
+ description="Comma-separated list of context provider names",
216
+ )
217
+ min_score: float = Field(
218
+ default=0.3,
219
+ ge=0.0,
220
+ le=1.0,
221
+ description="Minimum score threshold to include context items",
222
+ )
223
+ decay_factor: float = Field(
224
+ default=0.8,
225
+ ge=0.0,
226
+ le=1.0,
227
+ description="Factor to multiply existing scores on new touch",
228
+ )
229
+ neighbor_depth: int = Field(
230
+ default=2,
231
+ ge=1,
232
+ le=5,
233
+ description="Number of hops for AST neighbor traversal",
234
+ )
235
+ max_items: int = Field(
236
+ default=50,
237
+ ge=1,
238
+ le=200,
239
+ description="Maximum number of context items to include",
240
+ )
241
+ enabled: bool = Field(
242
+ default=True,
243
+ description="Whether session context is enabled",
244
+ )
245
+
246
+ @classmethod
247
+ def from_env(cls) -> "ContextConfig":
248
+ """Load configuration from environment variables."""
249
+ providers_str = os.getenv("CONTEXT_PROVIDERS", "touched_areas,explored_areas")
250
+ return cls(
251
+ providers=[p.strip() for p in providers_str.split(",") if p.strip()],
252
+ min_score=float(os.getenv("CONTEXT_MIN_SCORE", "0.5")),
253
+ decay_factor=float(os.getenv("CONTEXT_DECAY_FACTOR", "0.8")),
254
+ neighbor_depth=int(os.getenv("CONTEXT_NEIGHBOR_DEPTH", "2")),
255
+ max_items=int(os.getenv("CONTEXT_MAX_ITEMS", "50")),
256
+ enabled=os.getenv("CONTEXT_ENABLED", "true").lower() == "true",
257
+ )
258
+
259
+
260
+ class AgentConfig(BaseModel):
261
+ """Configuration for agent behavior."""
262
+
263
+ max_context_messages: int = Field(
264
+ default=25,
265
+ ge=5,
266
+ le=100,
267
+ description="Maximum number of previous messages to send to LLM (excludes system prompt)",
268
+ )
269
+
270
+ max_iterations: int = Field(
271
+ default=50,
272
+ ge=10,
273
+ le=200,
274
+ description="Maximum tool call iterations before stopping",
275
+ )
276
+
277
+ tool_max_output_tokens: int = Field(
278
+ default=25000,
279
+ ge=1000,
280
+ le=100000,
281
+ description="Maximum tokens for tool output (estimated at ~4 chars/token)",
282
+ )
283
+
284
+ @classmethod
285
+ def from_env(cls) -> "AgentConfig":
286
+ """Load configuration from environment variables."""
287
+ return cls(
288
+ max_context_messages=int(os.getenv("EMDASH_MAX_CONTEXT_MESSAGES", "25")),
289
+ max_iterations=int(os.getenv("EMDASH_MAX_ITERATIONS", "50")),
290
+ tool_max_output_tokens=int(os.getenv("EMDASH_TOOL_MAX_OUTPUT", "25000")),
291
+ )
292
+
293
+
294
+ class MCPConfig(BaseModel):
295
+ """Configuration for MCP (Model Context Protocol) servers."""
296
+
297
+ github_token: Optional[str] = Field(default=None)
298
+ github_repo: Optional[str] = Field(default=None) # Format: "owner/repo"
299
+ server_mode: str = Field(default="local") # "local" | "docker"
300
+ toolsets: list[str] = Field(
301
+ default_factory=lambda: ["repos", "pull_requests", "issues", "code_security"]
302
+ )
303
+ binary_path: str = Field(default="github-mcp-server")
304
+ read_only: bool = Field(default=True)
305
+ timeout: int = Field(default=30)
306
+
307
+ @staticmethod
308
+ def _get_gh_cli_token() -> Optional[str]:
309
+ """Get GitHub token from gh CLI if available (supports browser auth)."""
310
+ import subprocess
311
+ try:
312
+ result = subprocess.run(
313
+ ["gh", "auth", "token"],
314
+ capture_output=True,
315
+ text=True,
316
+ timeout=5,
317
+ )
318
+ if result.returncode == 0 and result.stdout.strip():
319
+ return result.stdout.strip()
320
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
321
+ pass
322
+ return None
323
+
324
+ @staticmethod
325
+ def _get_repo_from_git() -> Optional[str]:
326
+ """Auto-detect owner/repo from git remote origin URL."""
327
+ import subprocess
328
+ import re
329
+ try:
330
+ result = subprocess.run(
331
+ ["git", "remote", "get-url", "origin"],
332
+ capture_output=True,
333
+ text=True,
334
+ timeout=5,
335
+ )
336
+ if result.returncode == 0 and result.stdout.strip():
337
+ url = result.stdout.strip()
338
+ # Handle SSH: git@github.com:owner/repo.git
339
+ ssh_match = re.search(r"git@github\.com[:/](.+?)/(.+?)(?:\.git)?$", url)
340
+ if ssh_match:
341
+ return f"{ssh_match.group(1)}/{ssh_match.group(2)}"
342
+ # Handle HTTPS: https://github.com/owner/repo.git
343
+ https_match = re.search(r"github\.com/(.+?)/(.+?)(?:\.git)?$", url)
344
+ if https_match:
345
+ return f"{https_match.group(1)}/{https_match.group(2)}"
346
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
347
+ pass
348
+ return None
349
+
350
+ @classmethod
351
+ def from_env(cls) -> "MCPConfig":
352
+ """Load configuration from environment variables.
353
+
354
+ Token priority:
355
+ 1. GITHUB_TOKEN env var
356
+ 2. GITHUB_PERSONAL_ACCESS_TOKEN env var
357
+ 3. gh CLI auth token (supports browser auth for private repos)
358
+ """
359
+ toolsets_str = os.getenv("MCP_TOOLSETS", "repos,pull_requests,issues,code_security")
360
+
361
+ # Try env vars first, then fall back to gh CLI
362
+ github_token = (
363
+ os.getenv("GITHUB_TOKEN") or
364
+ os.getenv("GITHUB_PERSONAL_ACCESS_TOKEN") or
365
+ MCPConfig._get_gh_cli_token()
366
+ )
367
+
368
+ # Auto-detect repo from git remote if not set in env
369
+ github_repo = os.getenv("GITHUB_REPO") or MCPConfig._get_repo_from_git()
370
+
371
+ return cls(
372
+ github_token=github_token,
373
+ github_repo=github_repo,
374
+ server_mode=os.getenv("MCP_SERVER_MODE", "local"),
375
+ toolsets=toolsets_str.split(",") if toolsets_str else [],
376
+ binary_path=os.getenv("MCP_BINARY_PATH", "github-mcp-server"),
377
+ read_only=os.getenv("MCP_READ_ONLY", "true").lower() == "true",
378
+ timeout=int(os.getenv("MCP_TIMEOUT", "30")),
379
+ )
380
+
381
+ @property
382
+ def is_available(self) -> bool:
383
+ """Check if MCP is properly configured."""
384
+ return self.github_token is not None and len(self.github_token) > 0
385
+
386
+ @property
387
+ def repo_owner(self) -> Optional[str]:
388
+ """Get the owner from github_repo (e.g., 'wix-private' from 'wix-private/picasso')."""
389
+ if self.github_repo and "/" in self.github_repo:
390
+ return self.github_repo.split("/")[0]
391
+ return None
392
+
393
+ @property
394
+ def repo_name(self) -> Optional[str]:
395
+ """Get the repo name from github_repo (e.g., 'picasso' from 'wix-private/picasso')."""
396
+ if self.github_repo and "/" in self.github_repo:
397
+ return self.github_repo.split("/")[1]
398
+ return None
399
+
400
+
401
+ class EmDashConfig(BaseModel):
402
+ """Main configuration for EmDash."""
403
+
404
+ kuzu: KuzuConfig = Field(default_factory=KuzuConfig)
405
+ ingestion: IngestionConfig = Field(default_factory=IngestionConfig)
406
+ analytics: AnalyticsConfig = Field(default_factory=AnalyticsConfig)
407
+ github: GitHubConfig = Field(default_factory=GitHubConfig)
408
+ openai: OpenAIConfig = Field(default_factory=OpenAIConfig)
409
+ anthropic: AnthropicConfig = Field(default_factory=AnthropicConfig)
410
+ fireworks: FireworksConfig = Field(default_factory=FireworksConfig)
411
+ mcp: MCPConfig = Field(default_factory=MCPConfig)
412
+ context: ContextConfig = Field(default_factory=ContextConfig)
413
+ agent: AgentConfig = Field(default_factory=AgentConfig)
414
+ log_level: str = Field(default="WARNING")
415
+
416
+ @classmethod
417
+ def from_env(cls) -> "EmDashConfig":
418
+ """Load full configuration from environment variables."""
419
+ return cls(
420
+ kuzu=KuzuConfig.from_env(),
421
+ ingestion=IngestionConfig.from_env(),
422
+ analytics=AnalyticsConfig.from_env(),
423
+ github=GitHubConfig.from_env(),
424
+ openai=OpenAIConfig.from_env(),
425
+ anthropic=AnthropicConfig.from_env(),
426
+ fireworks=FireworksConfig.from_env(),
427
+ mcp=MCPConfig.from_env(),
428
+ context=ContextConfig.from_env(),
429
+ agent=AgentConfig.from_env(),
430
+ log_level=os.getenv("LOG_LEVEL", "WARNING"),
431
+ )
432
+
433
+ def validate_kuzu(self) -> None:
434
+ """Validate Kuzu configuration."""
435
+ if not self.kuzu.database_path:
436
+ raise ConfigurationError("Kuzu database path is required")
437
+
438
+
439
+ # Global configuration instance
440
+ _config: Optional[EmDashConfig] = None
441
+
442
+
443
+ def get_config() -> EmDashConfig:
444
+ """Get the global configuration instance."""
445
+ global _config
446
+ if _config is None:
447
+ _config = EmDashConfig.from_env()
448
+ return _config
449
+
450
+
451
+ def set_config(config: EmDashConfig) -> None:
452
+ """Set the global configuration instance."""
453
+ global _config
454
+ _config = config
@@ -0,0 +1,55 @@
1
+ """Custom exceptions for EmDash."""
2
+
3
+
4
+ class EmDashException(Exception):
5
+ """Base exception for all EmDash errors."""
6
+ pass
7
+
8
+
9
+ class ConfigurationError(EmDashException):
10
+ """Raised when configuration is invalid or missing."""
11
+ pass
12
+
13
+
14
+ class DatabaseConnectionError(EmDashException):
15
+ """Raised when database connection fails."""
16
+ pass
17
+
18
+
19
+ class RepositoryError(EmDashException):
20
+ """Raised when repository operations fail."""
21
+ pass
22
+
23
+
24
+ class ParsingError(EmDashException):
25
+ """Raised when code parsing fails."""
26
+ pass
27
+
28
+
29
+ class GraphBuildError(EmDashException):
30
+ """Raised when graph construction fails."""
31
+ pass
32
+
33
+
34
+ class QueryError(EmDashException):
35
+ """Raised when graph queries fail."""
36
+ pass
37
+
38
+
39
+ class AnalyticsError(EmDashException):
40
+ """Raised when analytics computation fails."""
41
+ pass
42
+
43
+
44
+ class ContextLengthError(EmDashException):
45
+ """Raised when context length exceeds model's limit."""
46
+
47
+ def __init__(
48
+ self,
49
+ message: str,
50
+ tokens_requested: int = 0,
51
+ tokens_limit: int = 0,
52
+ ):
53
+ super().__init__(message)
54
+ self.tokens_requested = tokens_requested
55
+ self.tokens_limit = tokens_limit