axion-code 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 (82) hide show
  1. axion/__init__.py +3 -0
  2. axion/api/__init__.py +0 -0
  3. axion/api/anthropic.py +460 -0
  4. axion/api/client.py +259 -0
  5. axion/api/error.py +161 -0
  6. axion/api/ollama.py +597 -0
  7. axion/api/openai_compat.py +805 -0
  8. axion/api/openai_responses.py +627 -0
  9. axion/api/prompt_cache.py +31 -0
  10. axion/api/sse.py +98 -0
  11. axion/api/types.py +451 -0
  12. axion/cli/__init__.py +0 -0
  13. axion/cli/init_cmd.py +50 -0
  14. axion/cli/input.py +290 -0
  15. axion/cli/main.py +2953 -0
  16. axion/cli/render.py +489 -0
  17. axion/cli/tui.py +766 -0
  18. axion/commands/__init__.py +0 -0
  19. axion/commands/handlers/__init__.py +0 -0
  20. axion/commands/handlers/agents.py +51 -0
  21. axion/commands/handlers/builtin_commands.py +367 -0
  22. axion/commands/handlers/mcp.py +59 -0
  23. axion/commands/handlers/models.py +75 -0
  24. axion/commands/handlers/plugins.py +55 -0
  25. axion/commands/handlers/skills.py +61 -0
  26. axion/commands/parsing.py +317 -0
  27. axion/commands/registry.py +166 -0
  28. axion/compat_harness/__init__.py +0 -0
  29. axion/compat_harness/extractor.py +145 -0
  30. axion/plugins/__init__.py +0 -0
  31. axion/plugins/hooks.py +22 -0
  32. axion/plugins/manager.py +391 -0
  33. axion/plugins/manifest.py +270 -0
  34. axion/runtime/__init__.py +0 -0
  35. axion/runtime/bash.py +388 -0
  36. axion/runtime/bootstrap.py +39 -0
  37. axion/runtime/claude_subscription.py +300 -0
  38. axion/runtime/compact.py +233 -0
  39. axion/runtime/config.py +397 -0
  40. axion/runtime/conversation.py +1073 -0
  41. axion/runtime/file_ops.py +613 -0
  42. axion/runtime/git.py +213 -0
  43. axion/runtime/hooks.py +235 -0
  44. axion/runtime/image.py +212 -0
  45. axion/runtime/lanes.py +282 -0
  46. axion/runtime/lsp.py +425 -0
  47. axion/runtime/mcp/__init__.py +0 -0
  48. axion/runtime/mcp/client.py +76 -0
  49. axion/runtime/mcp/lifecycle.py +96 -0
  50. axion/runtime/mcp/stdio.py +318 -0
  51. axion/runtime/mcp/tool_bridge.py +79 -0
  52. axion/runtime/memory.py +196 -0
  53. axion/runtime/oauth.py +329 -0
  54. axion/runtime/openai_subscription.py +346 -0
  55. axion/runtime/permissions.py +247 -0
  56. axion/runtime/plan_mode.py +96 -0
  57. axion/runtime/policy_engine.py +259 -0
  58. axion/runtime/prompt.py +586 -0
  59. axion/runtime/recovery.py +261 -0
  60. axion/runtime/remote.py +28 -0
  61. axion/runtime/sandbox.py +68 -0
  62. axion/runtime/scheduler.py +231 -0
  63. axion/runtime/session.py +365 -0
  64. axion/runtime/sharing.py +159 -0
  65. axion/runtime/skills.py +124 -0
  66. axion/runtime/tasks.py +258 -0
  67. axion/runtime/usage.py +241 -0
  68. axion/runtime/workers.py +186 -0
  69. axion/telemetry/__init__.py +0 -0
  70. axion/telemetry/events.py +67 -0
  71. axion/telemetry/profile.py +49 -0
  72. axion/telemetry/sink.py +60 -0
  73. axion/telemetry/tracer.py +95 -0
  74. axion/tools/__init__.py +0 -0
  75. axion/tools/lane_completion.py +33 -0
  76. axion/tools/registry.py +853 -0
  77. axion/tools/tool_search.py +226 -0
  78. axion_code-1.0.0.dist-info/METADATA +709 -0
  79. axion_code-1.0.0.dist-info/RECORD +82 -0
  80. axion_code-1.0.0.dist-info/WHEEL +4 -0
  81. axion_code-1.0.0.dist-info/entry_points.txt +2 -0
  82. axion_code-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,397 @@
1
+ """Configuration management with 3-layer merge and full MCP server config.
2
+
3
+ Maps to: rust/crates/runtime/src/config.rs
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import enum
9
+ import json
10
+ import os
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+
16
+ class ConfigSource(enum.Enum):
17
+ USER = "user"
18
+ PROJECT = "project"
19
+ LOCAL = "local"
20
+ ENVIRONMENT = "environment"
21
+
22
+
23
+ class ConfigError(Exception):
24
+ """Configuration loading error."""
25
+
26
+
27
+ @dataclass
28
+ class ConfigEntry:
29
+ """A single loaded configuration entry."""
30
+
31
+ source: ConfigSource
32
+ path: Path
33
+ data: dict[str, Any]
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # MCP server configuration (6 transport types)
38
+ # ---------------------------------------------------------------------------
39
+
40
+ @dataclass
41
+ class McpStdioServerConfig:
42
+ command: str
43
+ args: list[str] = field(default_factory=list)
44
+ env: dict[str, str] = field(default_factory=dict)
45
+ tool_call_timeout_ms: int | None = None
46
+
47
+
48
+ @dataclass
49
+ class McpRemoteServerConfig:
50
+ url: str
51
+ headers: dict[str, str] = field(default_factory=dict)
52
+ headers_helper: str | None = None
53
+ auth: str | None = None # "oauth" or None
54
+
55
+
56
+ @dataclass
57
+ class McpWebSocketServerConfig:
58
+ url: str
59
+ headers: dict[str, str] = field(default_factory=dict)
60
+
61
+
62
+ @dataclass
63
+ class McpSdkServerConfig:
64
+ name: str
65
+
66
+
67
+ @dataclass
68
+ class McpManagedProxyServerConfig:
69
+ url: str
70
+ id: str
71
+
72
+
73
+ McpServerConfig = (
74
+ McpStdioServerConfig
75
+ | McpRemoteServerConfig
76
+ | McpWebSocketServerConfig
77
+ | McpSdkServerConfig
78
+ | McpManagedProxyServerConfig
79
+ )
80
+
81
+
82
+ def parse_mcp_server_config(name: str, data: dict[str, Any]) -> McpServerConfig | None:
83
+ """Parse a single MCP server config from JSON."""
84
+ server_type = data.get("type", "stdio")
85
+
86
+ if server_type == "stdio":
87
+ return McpStdioServerConfig(
88
+ command=data.get("command", ""),
89
+ args=data.get("args", []),
90
+ env=data.get("env", {}),
91
+ tool_call_timeout_ms=data.get("toolCallTimeoutMs"),
92
+ )
93
+ if server_type in ("sse", "http"):
94
+ return McpRemoteServerConfig(
95
+ url=data.get("url", ""),
96
+ headers=data.get("headers", {}),
97
+ headers_helper=data.get("headersHelper"),
98
+ auth=data.get("auth"),
99
+ )
100
+ if server_type == "ws":
101
+ return McpWebSocketServerConfig(
102
+ url=data.get("url", ""),
103
+ headers=data.get("headers", {}),
104
+ )
105
+ if server_type == "sdk":
106
+ return McpSdkServerConfig(name=data.get("name", name))
107
+ if server_type == "managed_proxy":
108
+ return McpManagedProxyServerConfig(
109
+ url=data.get("url", ""),
110
+ id=data.get("id", ""),
111
+ )
112
+ return None
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # OAuth configuration
117
+ # ---------------------------------------------------------------------------
118
+
119
+ @dataclass
120
+ class OAuthConfig:
121
+ client_id: str = ""
122
+ authorize_url: str = ""
123
+ token_url: str = ""
124
+ callback_port: int = 4545
125
+ scopes: list[str] = field(default_factory=list)
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Hook configuration
130
+ # ---------------------------------------------------------------------------
131
+
132
+ @dataclass
133
+ class HookMatcher:
134
+ """Matches specific tools for hook execution."""
135
+
136
+ tool_name: str | None = None
137
+ tool_prefix: str | None = None
138
+
139
+ def matches(self, tool_name: str) -> bool:
140
+ if self.tool_name and self.tool_name == tool_name:
141
+ return True
142
+ if self.tool_prefix and tool_name.startswith(self.tool_prefix):
143
+ return True
144
+ if self.tool_name is None and self.tool_prefix is None:
145
+ return True # Match all
146
+ return False
147
+
148
+
149
+ @dataclass
150
+ class HookEntry:
151
+ """A single hook configuration entry."""
152
+
153
+ command: str
154
+ timeout_ms: int = 10_000
155
+ matchers: list[HookMatcher] = field(default_factory=list)
156
+
157
+ def matches_tool(self, tool_name: str) -> bool:
158
+ if not self.matchers:
159
+ return True
160
+ return any(m.matches(tool_name) for m in self.matchers)
161
+
162
+
163
+ @dataclass
164
+ class RuntimeHookConfig:
165
+ pre_tool_use: list[HookEntry] = field(default_factory=list)
166
+ post_tool_use: list[HookEntry] = field(default_factory=list)
167
+ post_tool_use_failure: list[HookEntry] = field(default_factory=list)
168
+
169
+
170
+ def parse_hook_entries(data: list[Any]) -> list[HookEntry]:
171
+ """Parse hook entries from config."""
172
+ entries: list[HookEntry] = []
173
+ for item in data:
174
+ if isinstance(item, str):
175
+ entries.append(HookEntry(command=item))
176
+ elif isinstance(item, dict):
177
+ matchers = []
178
+ for m in item.get("matchers", []):
179
+ matchers.append(HookMatcher(
180
+ tool_name=m.get("tool_name"),
181
+ tool_prefix=m.get("tool_prefix"),
182
+ ))
183
+ entries.append(HookEntry(
184
+ command=item.get("command", ""),
185
+ timeout_ms=item.get("timeout_ms", 10_000),
186
+ matchers=matchers,
187
+ ))
188
+ return entries
189
+
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # Feature configuration
193
+ # ---------------------------------------------------------------------------
194
+
195
+ @dataclass
196
+ class RuntimeFeatureConfig:
197
+ """Feature configuration extracted from merged config."""
198
+
199
+ hooks: RuntimeHookConfig = field(default_factory=RuntimeHookConfig)
200
+ plugins: dict[str, Any] = field(default_factory=dict)
201
+ mcp_servers: dict[str, McpServerConfig] = field(default_factory=dict)
202
+ oauth: OAuthConfig | None = None
203
+ model: str | None = None
204
+ permission_mode: str | None = None
205
+ allowed_tools: list[str] | None = None
206
+ denied_tools: list[str] | None = None
207
+
208
+
209
+ @dataclass
210
+ class RuntimeConfig:
211
+ """Fully resolved configuration from all sources."""
212
+
213
+ merged: dict[str, Any] = field(default_factory=dict)
214
+ loaded_entries: list[ConfigEntry] = field(default_factory=list)
215
+ feature_config: RuntimeFeatureConfig = field(default_factory=RuntimeFeatureConfig)
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Config loader
220
+ # ---------------------------------------------------------------------------
221
+
222
+ class ConfigLoader:
223
+ """Loads and merges configuration from user, project, and local sources.
224
+
225
+ Maps to: rust/crates/runtime/src/config.rs::ConfigLoader
226
+
227
+ Resolution order (later overrides earlier):
228
+ 1. User: ~/.axion/settings.json, ~/.config/axion/settings.json, ~/.claude/settings.json
229
+ 2. Project: .axion.json, .claude.json
230
+ 3. Local: .axion/settings.json, .claude/settings.json
231
+ 4. Local: .axion/settings.local.json, .claude/settings.local.json
232
+ 5. Environment variables
233
+ """
234
+
235
+ def __init__(self, project_dir: Path | None = None) -> None:
236
+ self.project_dir = project_dir or Path.cwd()
237
+
238
+ def load(self) -> RuntimeConfig:
239
+ """Load and merge all configuration sources."""
240
+ entries: list[ConfigEntry] = []
241
+ merged: dict[str, Any] = {}
242
+
243
+ # 1. User config
244
+ for user_path in self._user_config_paths():
245
+ data = self._load_json(user_path)
246
+ if data is not None:
247
+ entries.append(ConfigEntry(source=ConfigSource.USER, path=user_path, data=data))
248
+ self._deep_merge(merged, data)
249
+
250
+ # 2. Project config (.axion.json first, .claude.json fallback)
251
+ for proj_path in [
252
+ self.project_dir / ".axion.json",
253
+ self.project_dir / ".claude.json",
254
+ ]:
255
+ data = self._load_json(proj_path)
256
+ if data is not None:
257
+ entries.append(ConfigEntry(source=ConfigSource.PROJECT, path=proj_path, data=data))
258
+ self._deep_merge(merged, data)
259
+
260
+ # 3-4. Local config (.axion/ first, .claude/ fallback)
261
+ for local_path in [
262
+ self.project_dir / ".axion" / "settings.json",
263
+ self.project_dir / ".axion" / "settings.local.json",
264
+ self.project_dir / ".claude" / "settings.json",
265
+ self.project_dir / ".claude" / "settings.local.json",
266
+ ]:
267
+ data = self._load_json(local_path)
268
+ if data is not None:
269
+ entries.append(ConfigEntry(source=ConfigSource.LOCAL, path=local_path, data=data))
270
+ self._deep_merge(merged, data)
271
+
272
+ # 5. Environment overrides
273
+ env_model = os.environ.get("AXION_MODEL") or os.environ.get("CLAUDE_MODEL")
274
+
275
+ # Build feature config
276
+ feature = self._extract_features(merged)
277
+ if env_model:
278
+ feature.model = env_model
279
+
280
+ return RuntimeConfig(merged=merged, loaded_entries=entries, feature_config=feature)
281
+
282
+ @classmethod
283
+ def default_for(cls, cwd: Path) -> ConfigLoader:
284
+ """Create a loader for the given working directory."""
285
+ return cls(project_dir=cwd)
286
+
287
+ def _extract_features(self, merged: dict[str, Any]) -> RuntimeFeatureConfig:
288
+ """Extract feature configuration from merged config."""
289
+ feature = RuntimeFeatureConfig()
290
+
291
+ # Permissions
292
+ perms = merged.get("permissions", {})
293
+ feature.permission_mode = perms.get("defaultMode")
294
+ feature.allowed_tools = perms.get("allowedTools")
295
+ feature.denied_tools = perms.get("deniedTools")
296
+
297
+ # Model
298
+ feature.model = merged.get("model")
299
+
300
+ # Hooks
301
+ hooks_data = merged.get("hooks", {})
302
+ if hooks_data:
303
+ feature.hooks = RuntimeHookConfig(
304
+ pre_tool_use=parse_hook_entries(hooks_data.get("preToolUse", [])),
305
+ post_tool_use=parse_hook_entries(hooks_data.get("postToolUse", [])),
306
+ post_tool_use_failure=parse_hook_entries(
307
+ hooks_data.get("postToolUseFailure", [])
308
+ ),
309
+ )
310
+
311
+ # MCP servers (full parsing)
312
+ mcp_data = merged.get("mcpServers", {})
313
+ for name, server_data in mcp_data.items():
314
+ if isinstance(server_data, dict):
315
+ config = parse_mcp_server_config(name, server_data)
316
+ if config:
317
+ feature.mcp_servers[name] = config
318
+
319
+ # OAuth
320
+ oauth_data = merged.get("oauth", {})
321
+ if oauth_data:
322
+ feature.oauth = OAuthConfig(
323
+ client_id=oauth_data.get("clientId", ""),
324
+ authorize_url=oauth_data.get("authorizeUrl", ""),
325
+ token_url=oauth_data.get("tokenUrl", ""),
326
+ callback_port=oauth_data.get("callbackPort", 4545),
327
+ scopes=oauth_data.get("scopes", []),
328
+ )
329
+
330
+ # Plugins
331
+ feature.plugins = merged.get("plugins", {})
332
+
333
+ return feature
334
+
335
+ @staticmethod
336
+ def _user_config_paths() -> list[Path]:
337
+ home = Path.home()
338
+ paths = [
339
+ home / ".axion" / "settings.json",
340
+ home / ".config" / "axion" / "settings.json",
341
+ home / ".claude" / "settings.json", # backwards compat
342
+ ]
343
+ # Check CLAUDE_CONFIG_DIR env
344
+ config_dir = os.environ.get("CLAUDE_CONFIG_DIR")
345
+ if config_dir:
346
+ paths.insert(0, Path(config_dir) / "settings.json")
347
+ return paths
348
+
349
+ @staticmethod
350
+ def _load_json(path: Path) -> dict[str, Any] | None:
351
+ if not path.exists():
352
+ return None
353
+ try:
354
+ text = path.read_text(encoding="utf-8")
355
+ data = json.loads(text)
356
+ return data if isinstance(data, dict) else None
357
+ except (json.JSONDecodeError, OSError):
358
+ return None
359
+
360
+ @staticmethod
361
+ def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> None:
362
+ for key, value in override.items():
363
+ if key in base and isinstance(base[key], dict) and isinstance(value, dict):
364
+ ConfigLoader._deep_merge(base[key], value)
365
+ else:
366
+ base[key] = value
367
+
368
+ def render_config_report(self) -> str:
369
+ """Render a human-readable configuration report."""
370
+ config = self.load()
371
+ lines = ["Configuration:"]
372
+ for entry in config.loaded_entries:
373
+ lines.append(f" [{entry.source.value}] {entry.path}")
374
+
375
+ fc = config.feature_config
376
+ lines.append(f"\n Model: {fc.model or '(default)'}")
377
+ lines.append(f" Permission mode: {fc.permission_mode or '(default)'}")
378
+
379
+ if fc.mcp_servers:
380
+ lines.append(f"\n MCP servers ({len(fc.mcp_servers)}):")
381
+ for name, srv in fc.mcp_servers.items():
382
+ if isinstance(srv, McpStdioServerConfig):
383
+ lines.append(f" {name}: stdio → {srv.command}")
384
+ elif isinstance(srv, McpRemoteServerConfig):
385
+ lines.append(f" {name}: remote → {srv.url}")
386
+ else:
387
+ lines.append(f" {name}: {type(srv).__name__}")
388
+
389
+ if fc.hooks.pre_tool_use or fc.hooks.post_tool_use:
390
+ hook_count = (
391
+ len(fc.hooks.pre_tool_use)
392
+ + len(fc.hooks.post_tool_use)
393
+ + len(fc.hooks.post_tool_use_failure)
394
+ )
395
+ lines.append(f"\n Hooks: {hook_count} configured")
396
+
397
+ return "\n".join(lines)