llmcode-cli 1.0.0__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 (212) hide show
  1. llm_code/__init__.py +2 -0
  2. llm_code/analysis/__init__.py +6 -0
  3. llm_code/analysis/cache.py +33 -0
  4. llm_code/analysis/engine.py +256 -0
  5. llm_code/analysis/go_rules.py +114 -0
  6. llm_code/analysis/js_rules.py +84 -0
  7. llm_code/analysis/python_rules.py +311 -0
  8. llm_code/analysis/rules.py +140 -0
  9. llm_code/analysis/rust_rules.py +108 -0
  10. llm_code/analysis/universal_rules.py +111 -0
  11. llm_code/api/__init__.py +0 -0
  12. llm_code/api/client.py +90 -0
  13. llm_code/api/errors.py +73 -0
  14. llm_code/api/openai_compat.py +390 -0
  15. llm_code/api/provider.py +35 -0
  16. llm_code/api/sse.py +52 -0
  17. llm_code/api/types.py +140 -0
  18. llm_code/cli/__init__.py +0 -0
  19. llm_code/cli/commands.py +70 -0
  20. llm_code/cli/image.py +122 -0
  21. llm_code/cli/render.py +214 -0
  22. llm_code/cli/status_line.py +79 -0
  23. llm_code/cli/streaming.py +92 -0
  24. llm_code/cli/tui_main.py +220 -0
  25. llm_code/computer_use/__init__.py +11 -0
  26. llm_code/computer_use/app_detect.py +49 -0
  27. llm_code/computer_use/app_tier.py +57 -0
  28. llm_code/computer_use/coordinator.py +99 -0
  29. llm_code/computer_use/input_control.py +71 -0
  30. llm_code/computer_use/screenshot.py +93 -0
  31. llm_code/cron/__init__.py +13 -0
  32. llm_code/cron/parser.py +145 -0
  33. llm_code/cron/scheduler.py +135 -0
  34. llm_code/cron/storage.py +126 -0
  35. llm_code/enterprise/__init__.py +1 -0
  36. llm_code/enterprise/audit.py +59 -0
  37. llm_code/enterprise/auth.py +26 -0
  38. llm_code/enterprise/oidc.py +95 -0
  39. llm_code/enterprise/rbac.py +65 -0
  40. llm_code/harness/__init__.py +5 -0
  41. llm_code/harness/config.py +33 -0
  42. llm_code/harness/engine.py +129 -0
  43. llm_code/harness/guides.py +41 -0
  44. llm_code/harness/sensors.py +68 -0
  45. llm_code/harness/templates.py +84 -0
  46. llm_code/hida/__init__.py +1 -0
  47. llm_code/hida/classifier.py +187 -0
  48. llm_code/hida/engine.py +49 -0
  49. llm_code/hida/profiles.py +95 -0
  50. llm_code/hida/types.py +28 -0
  51. llm_code/ide/__init__.py +1 -0
  52. llm_code/ide/bridge.py +80 -0
  53. llm_code/ide/detector.py +76 -0
  54. llm_code/ide/server.py +169 -0
  55. llm_code/logging.py +29 -0
  56. llm_code/lsp/__init__.py +0 -0
  57. llm_code/lsp/client.py +298 -0
  58. llm_code/lsp/detector.py +42 -0
  59. llm_code/lsp/manager.py +56 -0
  60. llm_code/lsp/tools.py +288 -0
  61. llm_code/marketplace/__init__.py +0 -0
  62. llm_code/marketplace/builtin_registry.py +102 -0
  63. llm_code/marketplace/installer.py +162 -0
  64. llm_code/marketplace/plugin.py +78 -0
  65. llm_code/marketplace/registry.py +360 -0
  66. llm_code/mcp/__init__.py +0 -0
  67. llm_code/mcp/bridge.py +87 -0
  68. llm_code/mcp/client.py +117 -0
  69. llm_code/mcp/health.py +120 -0
  70. llm_code/mcp/manager.py +214 -0
  71. llm_code/mcp/oauth.py +219 -0
  72. llm_code/mcp/transport.py +254 -0
  73. llm_code/mcp/types.py +53 -0
  74. llm_code/remote/__init__.py +0 -0
  75. llm_code/remote/client.py +136 -0
  76. llm_code/remote/protocol.py +22 -0
  77. llm_code/remote/server.py +275 -0
  78. llm_code/remote/ssh_proxy.py +56 -0
  79. llm_code/runtime/__init__.py +0 -0
  80. llm_code/runtime/auto_commit.py +56 -0
  81. llm_code/runtime/auto_diagnose.py +62 -0
  82. llm_code/runtime/checkpoint.py +70 -0
  83. llm_code/runtime/checkpoint_recovery.py +142 -0
  84. llm_code/runtime/compaction.py +35 -0
  85. llm_code/runtime/compressor.py +415 -0
  86. llm_code/runtime/config.py +533 -0
  87. llm_code/runtime/context.py +49 -0
  88. llm_code/runtime/conversation.py +921 -0
  89. llm_code/runtime/cost_tracker.py +126 -0
  90. llm_code/runtime/dream.py +127 -0
  91. llm_code/runtime/file_protection.py +150 -0
  92. llm_code/runtime/hardware.py +85 -0
  93. llm_code/runtime/hooks.py +223 -0
  94. llm_code/runtime/indexer.py +230 -0
  95. llm_code/runtime/knowledge_compiler.py +232 -0
  96. llm_code/runtime/memory.py +132 -0
  97. llm_code/runtime/memory_layers.py +467 -0
  98. llm_code/runtime/memory_lint.py +252 -0
  99. llm_code/runtime/model_aliases.py +37 -0
  100. llm_code/runtime/ollama.py +93 -0
  101. llm_code/runtime/overlay.py +124 -0
  102. llm_code/runtime/permissions.py +200 -0
  103. llm_code/runtime/plan.py +45 -0
  104. llm_code/runtime/prompt.py +238 -0
  105. llm_code/runtime/repo_map.py +174 -0
  106. llm_code/runtime/sandbox.py +116 -0
  107. llm_code/runtime/session.py +268 -0
  108. llm_code/runtime/skill_resolver.py +61 -0
  109. llm_code/runtime/skills.py +133 -0
  110. llm_code/runtime/speculative.py +75 -0
  111. llm_code/runtime/streaming_executor.py +216 -0
  112. llm_code/runtime/telemetry.py +196 -0
  113. llm_code/runtime/token_budget.py +26 -0
  114. llm_code/runtime/vcr.py +142 -0
  115. llm_code/runtime/vision.py +102 -0
  116. llm_code/swarm/__init__.py +1 -0
  117. llm_code/swarm/backend_subprocess.py +108 -0
  118. llm_code/swarm/backend_tmux.py +103 -0
  119. llm_code/swarm/backend_worktree.py +306 -0
  120. llm_code/swarm/checkpoint.py +74 -0
  121. llm_code/swarm/coordinator.py +236 -0
  122. llm_code/swarm/mailbox.py +88 -0
  123. llm_code/swarm/manager.py +202 -0
  124. llm_code/swarm/memory_sync.py +80 -0
  125. llm_code/swarm/recovery.py +21 -0
  126. llm_code/swarm/team.py +67 -0
  127. llm_code/swarm/types.py +31 -0
  128. llm_code/task/__init__.py +16 -0
  129. llm_code/task/diagnostics.py +93 -0
  130. llm_code/task/manager.py +162 -0
  131. llm_code/task/types.py +112 -0
  132. llm_code/task/verifier.py +104 -0
  133. llm_code/tools/__init__.py +0 -0
  134. llm_code/tools/agent.py +145 -0
  135. llm_code/tools/agent_roles.py +82 -0
  136. llm_code/tools/base.py +94 -0
  137. llm_code/tools/bash.py +565 -0
  138. llm_code/tools/computer_use_tools.py +278 -0
  139. llm_code/tools/coordinator_tool.py +75 -0
  140. llm_code/tools/cron_create.py +90 -0
  141. llm_code/tools/cron_delete.py +49 -0
  142. llm_code/tools/cron_list.py +51 -0
  143. llm_code/tools/deferred.py +92 -0
  144. llm_code/tools/dump.py +116 -0
  145. llm_code/tools/edit_file.py +282 -0
  146. llm_code/tools/git_tools.py +531 -0
  147. llm_code/tools/glob_search.py +112 -0
  148. llm_code/tools/grep_search.py +144 -0
  149. llm_code/tools/ide_diagnostics.py +59 -0
  150. llm_code/tools/ide_open.py +58 -0
  151. llm_code/tools/ide_selection.py +52 -0
  152. llm_code/tools/memory_tools.py +138 -0
  153. llm_code/tools/multi_edit.py +143 -0
  154. llm_code/tools/notebook_edit.py +107 -0
  155. llm_code/tools/notebook_read.py +81 -0
  156. llm_code/tools/parsing.py +63 -0
  157. llm_code/tools/read_file.py +154 -0
  158. llm_code/tools/registry.py +58 -0
  159. llm_code/tools/search_backends/__init__.py +56 -0
  160. llm_code/tools/search_backends/brave.py +56 -0
  161. llm_code/tools/search_backends/duckduckgo.py +129 -0
  162. llm_code/tools/search_backends/searxng.py +71 -0
  163. llm_code/tools/search_backends/tavily.py +73 -0
  164. llm_code/tools/swarm_create.py +109 -0
  165. llm_code/tools/swarm_delete.py +95 -0
  166. llm_code/tools/swarm_list.py +44 -0
  167. llm_code/tools/swarm_message.py +109 -0
  168. llm_code/tools/task_close.py +79 -0
  169. llm_code/tools/task_plan.py +79 -0
  170. llm_code/tools/task_verify.py +90 -0
  171. llm_code/tools/tool_search.py +65 -0
  172. llm_code/tools/web_common.py +258 -0
  173. llm_code/tools/web_fetch.py +223 -0
  174. llm_code/tools/web_search.py +280 -0
  175. llm_code/tools/write_file.py +118 -0
  176. llm_code/tui/__init__.py +1 -0
  177. llm_code/tui/app.py +2432 -0
  178. llm_code/tui/chat_view.py +82 -0
  179. llm_code/tui/chat_widgets.py +309 -0
  180. llm_code/tui/header_bar.py +46 -0
  181. llm_code/tui/input_bar.py +349 -0
  182. llm_code/tui/keybindings.py +142 -0
  183. llm_code/tui/marketplace.py +210 -0
  184. llm_code/tui/status_bar.py +72 -0
  185. llm_code/tui/theme.py +96 -0
  186. llm_code/utils/__init__.py +0 -0
  187. llm_code/utils/diff.py +111 -0
  188. llm_code/utils/errors.py +70 -0
  189. llm_code/utils/hyperlink.py +73 -0
  190. llm_code/utils/notebook.py +179 -0
  191. llm_code/utils/search.py +69 -0
  192. llm_code/utils/text_normalize.py +28 -0
  193. llm_code/utils/version_check.py +62 -0
  194. llm_code/vim/__init__.py +4 -0
  195. llm_code/vim/engine.py +51 -0
  196. llm_code/vim/motions.py +172 -0
  197. llm_code/vim/operators.py +183 -0
  198. llm_code/vim/text_objects.py +139 -0
  199. llm_code/vim/transitions.py +279 -0
  200. llm_code/vim/types.py +68 -0
  201. llm_code/voice/__init__.py +1 -0
  202. llm_code/voice/languages.py +43 -0
  203. llm_code/voice/recorder.py +136 -0
  204. llm_code/voice/stt.py +36 -0
  205. llm_code/voice/stt_anthropic.py +66 -0
  206. llm_code/voice/stt_google.py +32 -0
  207. llm_code/voice/stt_whisper.py +52 -0
  208. llmcode_cli-1.0.0.dist-info/METADATA +524 -0
  209. llmcode_cli-1.0.0.dist-info/RECORD +212 -0
  210. llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
  211. llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
  212. llmcode_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,533 @@
1
+ """Runtime configuration dataclasses and loader."""
2
+ from __future__ import annotations
3
+
4
+ import copy
5
+ import json
6
+ import sys
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+
10
+ from pydantic import BaseModel, ValidationError, field_validator
11
+
12
+ from llm_code.harness.config import HarnessConfig, HarnessControl
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class HookConfig:
17
+ event: str # "pre_tool_use" | "post_tool_use" | "on_stop" | glob pattern
18
+ command: str
19
+ tool_pattern: str = "*"
20
+ timeout: float = 10.0
21
+ on_error: str = "warn" # "warn" | "deny" | "ignore"
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class VisionConfig:
26
+ fallback: str = ""
27
+ vision_model: str = ""
28
+ vision_api: str = ""
29
+ vision_api_key_env: str = ""
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ModelRoutingConfig:
34
+ sub_agent: str = ""
35
+ compaction: str = ""
36
+ fallback: str = ""
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class ThinkingConfig:
41
+ mode: str = "adaptive" # "adaptive" | "enabled" | "disabled"
42
+ budget_tokens: int = 10000
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class DreamConfig:
47
+ enabled: bool = True
48
+ min_turns: int = 3
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class KnowledgeConfig:
53
+ enabled: bool = True
54
+ compile_on_exit: bool = True
55
+ max_context_tokens: int = 3000
56
+ compile_model: str = ""
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class VoiceConfig:
61
+ enabled: bool = False
62
+ backend: str = "whisper" # "whisper" | "google" | "anthropic"
63
+ whisper_url: str = "http://localhost:8000/v1/audio/transcriptions"
64
+ google_language_code: str = ""
65
+ anthropic_ws_url: str = "wss://api.anthropic.com"
66
+ language: str = "en"
67
+ hotkey: str = "ctrl+space"
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class ComputerUseConfig:
72
+ enabled: bool = False
73
+ screenshot_delay: float = 0.5
74
+ app_tiers: tuple[dict, ...] = () # user-defined tier overrides
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class IDEConfig:
79
+ enabled: bool = False
80
+ port: int = 9876
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class WebFetchConfig:
85
+ default_renderer: str = "default"
86
+ browser_timeout: float = 30.0
87
+ cache_ttl: float = 900.0
88
+ cache_max_entries: int = 50
89
+ max_length: int = 50_000
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class WebSearchConfig:
94
+ default_backend: str = "duckduckgo"
95
+ brave_api_key_env: str = "BRAVE_API_KEY"
96
+ tavily_api_key_env: str = "TAVILY_API_KEY"
97
+ searxng_base_url: str = ""
98
+ max_results: int = 10
99
+ domain_allowlist: tuple[str, ...] = ()
100
+ domain_denylist: tuple[str, ...] = ()
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class WorktreeConfig:
105
+ on_complete: str = "diff" # "diff" | "merge" | "branch"
106
+ base_dir: str = ""
107
+ copy_gitignored: tuple[str, ...] = (".env", ".env.local")
108
+ cleanup_on_success: bool = True
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class SwarmConfig:
113
+ enabled: bool = False
114
+ backend: str = "auto" # "auto" | "tmux" | "subprocess" | "worktree"
115
+ max_members: int = 5
116
+ role_models: dict[str, str] = field(default_factory=dict)
117
+ worktree: WorktreeConfig = field(default_factory=WorktreeConfig)
118
+
119
+
120
+ @dataclass(frozen=True)
121
+ class VCRConfig:
122
+ enabled: bool = False
123
+ auto_record: bool = False
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class HidaConfig:
128
+ enabled: bool = False
129
+ confidence_threshold: float = 0.6
130
+ custom_profiles: tuple[dict, ...] = ()
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class TelemetryConfig:
135
+ enabled: bool = False
136
+ endpoint: str = "http://localhost:4318" # OTLP HTTP default
137
+ service_name: str = "llm-code"
138
+
139
+
140
+ @dataclass(frozen=True)
141
+ class CompressorConfig:
142
+ llm_summarize: bool = False
143
+ summarize_model: str = ""
144
+ max_summary_tokens: int = 1000
145
+
146
+
147
+ @dataclass(frozen=True)
148
+ class BashRule:
149
+ pattern: str = ""
150
+ action: str = "confirm" # "allow" | "confirm" | "block"
151
+ description: str = ""
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class BashRulesConfig:
156
+ rules: tuple[BashRule, ...] = ()
157
+
158
+
159
+ @dataclass(frozen=True)
160
+ class EnterpriseAuthConfig:
161
+ provider: str = "" # "" | "none" | "oidc"
162
+ oidc_issuer: str = ""
163
+ oidc_client_id: str = ""
164
+ oidc_client_secret: str = ""
165
+ oidc_scopes: tuple[str, ...] = ("openid", "email", "profile")
166
+ oidc_redirect_port: int = 9877
167
+
168
+
169
+ @dataclass(frozen=True)
170
+ class EnterpriseRBACConfig:
171
+ group_role_mapping: dict[str, str] = field(default_factory=dict)
172
+ custom_roles: dict = field(default_factory=dict)
173
+
174
+
175
+ @dataclass(frozen=True)
176
+ class EnterpriseAuditConfig:
177
+ retention_days: int = 90
178
+
179
+
180
+ @dataclass(frozen=True)
181
+ class EnterpriseConfig:
182
+ auth: EnterpriseAuthConfig = field(default_factory=EnterpriseAuthConfig)
183
+ rbac: EnterpriseRBACConfig = field(default_factory=EnterpriseRBACConfig)
184
+ audit: EnterpriseAuditConfig = field(default_factory=EnterpriseAuditConfig)
185
+
186
+
187
+ @dataclass(frozen=True)
188
+ class RuntimeConfig:
189
+ model: str = ""
190
+ provider_base_url: str | None = None
191
+ provider_api_key_env: str = "LLM_API_KEY"
192
+ permission_mode: str = "prompt"
193
+ max_turn_iterations: int = 10
194
+ max_tokens: int = 4096
195
+ temperature: float = 0.7
196
+ hooks: tuple[HookConfig, ...] = ()
197
+ allowed_tools: frozenset[str] = frozenset()
198
+ denied_tools: frozenset[str] = frozenset()
199
+ compact_after_tokens: int = 80000
200
+ timeout: float = 120.0
201
+ max_retries: int = 2
202
+ native_tools: bool = True
203
+ vision: VisionConfig = field(default_factory=VisionConfig)
204
+ model_routing: ModelRoutingConfig = field(default_factory=ModelRoutingConfig)
205
+ mcp_servers: dict = field(default_factory=dict)
206
+ registries: dict = field(default_factory=dict)
207
+ skills_dirs: tuple[str, ...] = ()
208
+ lsp_servers: dict = field(default_factory=dict)
209
+ lsp_auto_detect: bool = True
210
+ model_aliases: dict = field(default_factory=dict)
211
+ pricing: dict = field(default_factory=dict)
212
+ thinking: ThinkingConfig = field(default_factory=ThinkingConfig)
213
+ vim_mode: bool = False
214
+ voice: VoiceConfig = field(default_factory=VoiceConfig)
215
+ dream: DreamConfig = field(default_factory=DreamConfig)
216
+ computer_use: ComputerUseConfig = field(default_factory=ComputerUseConfig)
217
+ ide: IDEConfig = field(default_factory=IDEConfig)
218
+ swarm: SwarmConfig = field(default_factory=SwarmConfig)
219
+ vcr: VCRConfig = field(default_factory=VCRConfig)
220
+ hida: HidaConfig = field(default_factory=HidaConfig)
221
+ telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
222
+ web_fetch: WebFetchConfig = field(default_factory=WebFetchConfig)
223
+ web_search: WebSearchConfig = field(default_factory=WebSearchConfig)
224
+ max_budget_usd: float | None = None
225
+ compressor: CompressorConfig = field(default_factory=CompressorConfig)
226
+ bash_rules: BashRulesConfig = field(default_factory=BashRulesConfig)
227
+ enterprise: EnterpriseConfig = field(default_factory=EnterpriseConfig)
228
+ auto_commit: bool = False
229
+ lsp_auto_diagnose: bool = True
230
+ harness: HarnessConfig = field(default_factory=HarnessConfig)
231
+ knowledge: KnowledgeConfig = field(default_factory=KnowledgeConfig)
232
+
233
+
234
+ class ConfigSchema(BaseModel):
235
+ """Pydantic schema for validating the merged config dict before conversion."""
236
+
237
+ model: str = ""
238
+ provider: dict = {}
239
+ permissions: dict = {}
240
+ model_routing: dict = {}
241
+ vision: dict = {}
242
+ hooks: list = []
243
+ mcpServers: dict = {}
244
+ lspServers: dict = {}
245
+ registries: dict = {}
246
+ lsp_auto_detect: bool = True
247
+ max_turn_iterations: int = 10
248
+ thinking: dict = {}
249
+ max_tokens: int = 4096
250
+ temperature: float = 0.7
251
+ compact_after_tokens: int = 80000
252
+ native_tools: bool = True
253
+ compressor: dict = {}
254
+ bash_rules: list = []
255
+
256
+ @field_validator("temperature")
257
+ @classmethod
258
+ def temp_range(cls, v: float) -> float:
259
+ if not 0.0 <= v <= 2.0:
260
+ raise ValueError("temperature must be between 0.0 and 2.0")
261
+ return v
262
+
263
+ @field_validator("max_tokens")
264
+ @classmethod
265
+ def tokens_positive(cls, v: int) -> int:
266
+ if v <= 0:
267
+ raise ValueError("max_tokens must be positive")
268
+ return v
269
+
270
+ @field_validator("max_turn_iterations")
271
+ @classmethod
272
+ def iterations_positive(cls, v: int) -> int:
273
+ if v <= 0:
274
+ raise ValueError("max_turn_iterations must be positive")
275
+ return v
276
+
277
+ model_config = {"extra": "allow"}
278
+
279
+
280
+ def merge_configs(base: dict, override: dict) -> dict:
281
+ """Deep merge two dicts; override wins for leaf values."""
282
+ result = copy.deepcopy(base)
283
+ for key, value in override.items():
284
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
285
+ result[key] = merge_configs(result[key], value)
286
+ else:
287
+ result[key] = copy.deepcopy(value)
288
+ return result
289
+
290
+
291
+ def _load_json_file(path: Path) -> dict:
292
+ """Load a JSON file, returning empty dict if missing or invalid."""
293
+ try:
294
+ return json.loads(path.read_text())
295
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
296
+ return {}
297
+
298
+
299
+ def _dict_to_runtime_config(data: dict) -> RuntimeConfig:
300
+ """Convert a merged config dict to a RuntimeConfig instance."""
301
+ provider = data.get("provider", {})
302
+ permissions = data.get("permissions", {})
303
+ vision_raw = data.get("vision", {})
304
+
305
+ hooks_raw = data.get("hooks", [])
306
+ hooks = tuple(
307
+ HookConfig(
308
+ event=h["event"],
309
+ command=h["command"],
310
+ tool_pattern=h.get("tool_pattern", "*"),
311
+ timeout=float(h.get("timeout", 10.0)),
312
+ on_error=h.get("on_error", "warn"),
313
+ )
314
+ for h in hooks_raw
315
+ if isinstance(h, dict) and "event" in h and "command" in h
316
+ )
317
+
318
+ vision = VisionConfig(
319
+ fallback=vision_raw.get("fallback", ""),
320
+ vision_model=vision_raw.get("vision_model", ""),
321
+ vision_api=vision_raw.get("vision_api", ""),
322
+ vision_api_key_env=vision_raw.get("vision_api_key_env", ""),
323
+ )
324
+
325
+ routing_raw = data.get("model_routing", {})
326
+ model_routing = ModelRoutingConfig(
327
+ sub_agent=routing_raw.get("sub_agent", ""),
328
+ compaction=routing_raw.get("compaction", ""),
329
+ fallback=routing_raw.get("fallback", ""),
330
+ )
331
+
332
+ allow_tools = permissions.get("allow_tools", data.get("allowed_tools", []))
333
+ deny_tools = permissions.get("deny_tools", data.get("denied_tools", []))
334
+
335
+ voice_raw = data.get("voice", {})
336
+ voice = VoiceConfig(
337
+ enabled=voice_raw.get("enabled", False),
338
+ backend=voice_raw.get("backend", "whisper"),
339
+ whisper_url=voice_raw.get("whisper_url", "http://localhost:8000/v1/audio/transcriptions"),
340
+ google_language_code=voice_raw.get("google_language_code", ""),
341
+ anthropic_ws_url=voice_raw.get("anthropic_ws_url", "wss://api.anthropic.com"),
342
+ language=voice_raw.get("language", "en"),
343
+ hotkey=voice_raw.get("hotkey", "ctrl+space"),
344
+ )
345
+
346
+ thinking_raw = data.get("thinking", {})
347
+ thinking = ThinkingConfig(
348
+ mode=thinking_raw.get("mode", "adaptive"),
349
+ budget_tokens=thinking_raw.get("budget_tokens", 10000),
350
+ )
351
+
352
+ dream_raw = data.get("dream", {})
353
+ dream = DreamConfig(
354
+ enabled=dream_raw.get("enabled", True),
355
+ min_turns=dream_raw.get("min_turns", 3),
356
+ )
357
+
358
+ computer_use_raw = data.get("computer_use", {})
359
+ computer_use = ComputerUseConfig(
360
+ enabled=computer_use_raw.get("enabled", False),
361
+ screenshot_delay=computer_use_raw.get("screenshot_delay", 0.5),
362
+ app_tiers=tuple(computer_use_raw.get("app_tiers", [])),
363
+ )
364
+
365
+ ide_raw = data.get("ide", {})
366
+ ide = IDEConfig(
367
+ enabled=ide_raw.get("enabled", False),
368
+ port=ide_raw.get("port", 9876),
369
+ )
370
+
371
+ swarm_raw = data.get("swarm", {})
372
+ worktree_raw = swarm_raw.get("worktree", {})
373
+ worktree = WorktreeConfig(
374
+ on_complete=worktree_raw.get("on_complete", "diff"),
375
+ base_dir=worktree_raw.get("base_dir", ""),
376
+ copy_gitignored=tuple(worktree_raw.get("copy_gitignored", (".env", ".env.local"))),
377
+ cleanup_on_success=worktree_raw.get("cleanup_on_success", True),
378
+ )
379
+ swarm = SwarmConfig(
380
+ enabled=swarm_raw.get("enabled", False),
381
+ backend=swarm_raw.get("backend", "auto"),
382
+ max_members=swarm_raw.get("max_members", 5),
383
+ role_models=swarm_raw.get("role_models", {}),
384
+ worktree=worktree,
385
+ )
386
+
387
+ vcr_raw = data.get("vcr", {})
388
+ vcr = VCRConfig(
389
+ enabled=vcr_raw.get("enabled", False),
390
+ auto_record=vcr_raw.get("auto_record", False),
391
+ )
392
+
393
+ hida_raw = data.get("hida", {})
394
+ hida = HidaConfig(
395
+ enabled=hida_raw.get("enabled", False),
396
+ confidence_threshold=hida_raw.get("confidence_threshold", 0.6),
397
+ custom_profiles=tuple(hida_raw.get("custom_profiles", [])),
398
+ )
399
+
400
+ telemetry_raw = data.get("telemetry", {})
401
+ telemetry = TelemetryConfig(
402
+ enabled=telemetry_raw.get("enabled", False),
403
+ endpoint=telemetry_raw.get("endpoint", "http://localhost:4318"),
404
+ service_name=telemetry_raw.get("service_name", "llm-code"),
405
+ )
406
+
407
+ enterprise_raw = data.get("enterprise", {})
408
+ auth_raw = enterprise_raw.get("auth", {})
409
+ rbac_raw = enterprise_raw.get("rbac", {})
410
+ audit_raw = enterprise_raw.get("audit", {})
411
+ oidc_raw = auth_raw.get("oidc", {})
412
+ enterprise_auth = EnterpriseAuthConfig(
413
+ provider=auth_raw.get("provider", ""),
414
+ oidc_issuer=oidc_raw.get("issuer", ""),
415
+ oidc_client_id=oidc_raw.get("client_id", ""),
416
+ oidc_client_secret=oidc_raw.get("client_secret", ""),
417
+ oidc_scopes=tuple(oidc_raw.get("scopes", ("openid", "email", "profile"))),
418
+ oidc_redirect_port=oidc_raw.get("redirect_port", 9877),
419
+ )
420
+ enterprise_rbac = EnterpriseRBACConfig(
421
+ group_role_mapping=rbac_raw.get("group_role_mapping", {}),
422
+ custom_roles=rbac_raw.get("custom_roles", {}),
423
+ )
424
+ enterprise_audit = EnterpriseAuditConfig(
425
+ retention_days=audit_raw.get("retention_days", 90),
426
+ )
427
+ enterprise = EnterpriseConfig(
428
+ auth=enterprise_auth,
429
+ rbac=enterprise_rbac,
430
+ audit=enterprise_audit,
431
+ )
432
+
433
+ # Knowledge config
434
+ knowledge_raw = data.get("knowledge", {})
435
+ knowledge = KnowledgeConfig(
436
+ enabled=knowledge_raw.get("enabled", True),
437
+ compile_on_exit=knowledge_raw.get("compile_on_exit", True),
438
+ max_context_tokens=knowledge_raw.get("max_context_tokens", 3000),
439
+ compile_model=knowledge_raw.get("compile_model", ""),
440
+ )
441
+
442
+ # Harness config
443
+ harness_data = data.get("harness", {})
444
+ harness_controls: list[HarnessControl] = []
445
+ for name, overrides in harness_data.get("controls", {}).items():
446
+ harness_controls.append(HarnessControl(
447
+ name=name,
448
+ category=overrides.get("category", "sensor"),
449
+ kind=overrides.get("kind", "computational"),
450
+ enabled=overrides.get("enabled", True),
451
+ trigger=overrides.get("trigger", "post_tool"),
452
+ ))
453
+ harness = HarnessConfig(
454
+ template=harness_data.get("template", "auto"),
455
+ controls=tuple(harness_controls),
456
+ )
457
+
458
+ return RuntimeConfig(
459
+ model=data.get("model", ""),
460
+ provider_base_url=provider.get("base_url", None),
461
+ provider_api_key_env=provider.get("api_key_env", "LLM_API_KEY"),
462
+ permission_mode=permissions.get("mode", data.get("permission_mode", "prompt")),
463
+ max_turn_iterations=data.get("max_turn_iterations", 10),
464
+ max_tokens=data.get("max_tokens", 4096),
465
+ temperature=data.get("temperature", 0.7),
466
+ hooks=hooks,
467
+ allowed_tools=frozenset(allow_tools),
468
+ denied_tools=frozenset(deny_tools),
469
+ compact_after_tokens=data.get("compact_after_tokens", 80000),
470
+ timeout=data.get("timeout", 120.0),
471
+ max_retries=data.get("max_retries", 2),
472
+ native_tools=data.get("native_tools", True),
473
+ vision=vision,
474
+ model_routing=model_routing,
475
+ mcp_servers=data.get("mcpServers", {}),
476
+ registries=data.get("registries", {}),
477
+ skills_dirs=tuple(data.get("skills_dirs", [])),
478
+ lsp_servers=data.get("lspServers", {}),
479
+ lsp_auto_detect=data.get("lsp_auto_detect", True),
480
+ model_aliases=data.get("model_aliases", {}),
481
+ pricing=data.get("pricing", {}),
482
+ thinking=thinking,
483
+ vim_mode=data.get("vim_mode", False),
484
+ voice=voice,
485
+ dream=dream,
486
+ computer_use=computer_use,
487
+ ide=ide,
488
+ swarm=swarm,
489
+ vcr=vcr,
490
+ hida=hida,
491
+ telemetry=telemetry,
492
+ max_budget_usd=data.get("max_budget_usd", None),
493
+ enterprise=enterprise,
494
+ auto_commit=data.get("auto_commit", False),
495
+ lsp_auto_diagnose=data.get("lsp_auto_diagnose", True),
496
+ harness=harness,
497
+ knowledge=knowledge,
498
+ )
499
+
500
+
501
+ def load_config(
502
+ user_dir: Path,
503
+ project_dir: Path,
504
+ local_path: Path,
505
+ cli_overrides: dict,
506
+ ) -> RuntimeConfig:
507
+ """Load from JSON files in order, deep merge, convert to RuntimeConfig.
508
+
509
+ Precedence (lowest to highest):
510
+ user_dir/config.json -> project_dir/config.json -> local_path -> cli_overrides
511
+ """
512
+ merged: dict = {}
513
+
514
+ user_cfg = _load_json_file(Path(user_dir) / "config.json")
515
+ merged = merge_configs(merged, user_cfg)
516
+
517
+ project_cfg = _load_json_file(Path(project_dir) / "config.json")
518
+ merged = merge_configs(merged, project_cfg)
519
+
520
+ local_cfg = _load_json_file(Path(local_path))
521
+ merged = merge_configs(merged, local_cfg)
522
+
523
+ merged = merge_configs(merged, cli_overrides)
524
+
525
+ # Validate merged config; on error, log warning and continue with best-effort defaults
526
+ try:
527
+ ConfigSchema.model_validate(merged)
528
+ except ValidationError as exc:
529
+ import warnings
530
+ warnings.warn(f"Config validation error (continuing with defaults): {exc}", stacklevel=2)
531
+ print(f"[WARNING] Config validation error: {exc}", file=sys.stderr)
532
+
533
+ return _dict_to_runtime_config(merged)
@@ -0,0 +1,49 @@
1
+ """Project context discovery for the runtime layer."""
2
+ from __future__ import annotations
3
+
4
+ import dataclasses
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclasses.dataclass(frozen=True)
10
+ class ProjectContext:
11
+ cwd: Path
12
+ is_git_repo: bool
13
+ git_status: str
14
+ instructions: str
15
+
16
+ @classmethod
17
+ def discover(cls, cwd: Path) -> "ProjectContext":
18
+ """Discover project context from the given working directory."""
19
+ is_git = (cwd / ".git").exists()
20
+
21
+ git_status = ""
22
+ if is_git:
23
+ try:
24
+ result = subprocess.run(
25
+ ["git", "status", "--short"],
26
+ cwd=cwd,
27
+ capture_output=True,
28
+ text=True,
29
+ timeout=5,
30
+ )
31
+ if result.returncode == 0:
32
+ git_status = result.stdout.rstrip("\n")
33
+ except (subprocess.TimeoutExpired, FileNotFoundError):
34
+ pass
35
+
36
+ instructions = ""
37
+ instructions_path = cwd / ".llm-code" / "INSTRUCTIONS.md"
38
+ if instructions_path.exists():
39
+ try:
40
+ instructions = instructions_path.read_text(encoding="utf-8")
41
+ except OSError:
42
+ pass
43
+
44
+ return cls(
45
+ cwd=cwd,
46
+ is_git_repo=is_git,
47
+ git_status=git_status,
48
+ instructions=instructions,
49
+ )