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,76 @@
1
+ """MCP transport configuration types.
2
+
3
+ Maps to: rust/crates/runtime/src/mcp_client.rs
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import enum
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+
13
+ class McpTransportType(enum.Enum):
14
+ STDIO = "stdio"
15
+ SSE = "sse"
16
+ HTTP = "http"
17
+ WEBSOCKET = "websocket"
18
+ SDK = "sdk"
19
+ MANAGED_PROXY = "managed_proxy"
20
+
21
+
22
+ @dataclass
23
+ class McpStdioTransport:
24
+ command: str
25
+ args: list[str] = field(default_factory=list)
26
+ env: dict[str, str] = field(default_factory=dict)
27
+ tool_call_timeout_ms: int | None = None
28
+
29
+
30
+ @dataclass
31
+ class McpRemoteTransport:
32
+ url: str
33
+ headers: dict[str, str] = field(default_factory=dict)
34
+ auth: McpClientAuth | None = None
35
+
36
+
37
+ @dataclass
38
+ class McpSdkTransport:
39
+ name: str
40
+
41
+
42
+ @dataclass
43
+ class McpManagedProxyTransport:
44
+ url: str
45
+ id: str
46
+
47
+
48
+ # Union type for all transport configurations
49
+ McpClientTransport = (
50
+ McpStdioTransport
51
+ | McpRemoteTransport
52
+ | McpSdkTransport
53
+ | McpManagedProxyTransport
54
+ )
55
+
56
+
57
+ class McpClientAuthType(enum.Enum):
58
+ NONE = "none"
59
+ OAUTH = "oauth"
60
+
61
+
62
+ @dataclass
63
+ class McpClientAuth:
64
+ auth_type: McpClientAuthType = McpClientAuthType.NONE
65
+ oauth_config: dict[str, Any] | None = None
66
+
67
+
68
+ @dataclass
69
+ class McpClientBootstrap:
70
+ """Bootstrap configuration for an MCP client connection."""
71
+
72
+ server_name: str
73
+ normalized_name: str
74
+ tool_prefix: str
75
+ transport: McpClientTransport
76
+ signature: str = ""
@@ -0,0 +1,96 @@
1
+ """Hardened MCP lifecycle management.
2
+
3
+ Maps to: rust/crates/runtime/src/mcp_lifecycle_hardened.rs
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ from axion.runtime.mcp.tool_bridge import McpConnectionStatus, McpServerState, McpToolRegistry
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class McpLifecycleEvent:
19
+ server_name: str
20
+ event_type: str
21
+ message: str = ""
22
+ error: str | None = None
23
+
24
+
25
+ class McpLifecycleManager:
26
+ """Manages MCP server lifecycle with error handling and degradation.
27
+
28
+ Maps to: rust/crates/runtime/src/mcp_lifecycle_hardened.rs
29
+ """
30
+
31
+ def __init__(self, registry: McpToolRegistry | None = None) -> None:
32
+ self.registry = registry or McpToolRegistry()
33
+ self._events: list[McpLifecycleEvent] = []
34
+
35
+ def record_event(self, event: McpLifecycleEvent) -> None:
36
+ self._events.append(event)
37
+ if event.error:
38
+ logger.warning(
39
+ "MCP lifecycle event [%s] %s: %s",
40
+ event.server_name,
41
+ event.event_type,
42
+ event.error,
43
+ )
44
+
45
+ def mark_server_connected(
46
+ self, server_name: str, tools: list[Any] | None = None
47
+ ) -> None:
48
+ from axion.runtime.mcp.tool_bridge import McpToolInfo
49
+
50
+ tool_infos = []
51
+ if tools:
52
+ for t in tools:
53
+ if isinstance(t, McpToolInfo):
54
+ tool_infos.append(t)
55
+ elif isinstance(t, dict):
56
+ tool_infos.append(McpToolInfo(
57
+ name=t.get("name", ""),
58
+ description=t.get("description", ""),
59
+ input_schema=t.get("inputSchema", {}),
60
+ ))
61
+
62
+ state = McpServerState(
63
+ server_name=server_name,
64
+ status=McpConnectionStatus.CONNECTED,
65
+ tools=tool_infos,
66
+ )
67
+ self.registry.register_server(state)
68
+ self.record_event(McpLifecycleEvent(
69
+ server_name=server_name,
70
+ event_type="connected",
71
+ message=f"Connected with {len(tool_infos)} tools",
72
+ ))
73
+
74
+ def mark_server_error(self, server_name: str, error: str) -> None:
75
+ state = McpServerState(
76
+ server_name=server_name,
77
+ status=McpConnectionStatus.ERROR,
78
+ error_message=error,
79
+ )
80
+ self.registry.register_server(state)
81
+ self.record_event(McpLifecycleEvent(
82
+ server_name=server_name,
83
+ event_type="error",
84
+ error=error,
85
+ ))
86
+
87
+ def mark_server_disconnected(self, server_name: str) -> None:
88
+ state = McpServerState(
89
+ server_name=server_name,
90
+ status=McpConnectionStatus.DISCONNECTED,
91
+ )
92
+ self.registry.register_server(state)
93
+ self.record_event(McpLifecycleEvent(
94
+ server_name=server_name,
95
+ event_type="disconnected",
96
+ ))
@@ -0,0 +1,318 @@
1
+ """MCP (Model Context Protocol) server manager with JSON-RPC over stdio.
2
+
3
+ Maps to: rust/crates/runtime/src/mcp_stdio.rs
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ MCP_PROTOCOL_VERSION = "2024-11-05"
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # JSON-RPC types
21
+ # ---------------------------------------------------------------------------
22
+
23
+ @dataclass
24
+ class JsonRpcRequest:
25
+ method: str
26
+ params: dict[str, Any] | None = None
27
+ id: str | int | None = None
28
+ jsonrpc: str = "2.0"
29
+
30
+ def to_dict(self) -> dict[str, Any]:
31
+ d: dict[str, Any] = {"jsonrpc": self.jsonrpc, "method": self.method}
32
+ if self.id is not None:
33
+ d["id"] = self.id
34
+ if self.params is not None:
35
+ d["params"] = self.params
36
+ return d
37
+
38
+
39
+ @dataclass
40
+ class JsonRpcError:
41
+ code: int
42
+ message: str
43
+ data: Any = None
44
+
45
+
46
+ @dataclass
47
+ class JsonRpcResponse:
48
+ id: str | int | None = None
49
+ result: Any = None
50
+ error: JsonRpcError | None = None
51
+ jsonrpc: str = "2.0"
52
+
53
+ @classmethod
54
+ def from_dict(cls, data: dict[str, Any]) -> JsonRpcResponse:
55
+ error = None
56
+ if "error" in data:
57
+ e = data["error"]
58
+ error = JsonRpcError(code=e["code"], message=e["message"], data=e.get("data"))
59
+ return cls(
60
+ id=data.get("id"),
61
+ result=data.get("result"),
62
+ error=error,
63
+ )
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # MCP types
68
+ # ---------------------------------------------------------------------------
69
+
70
+ @dataclass
71
+ class McpTool:
72
+ name: str
73
+ description: str = ""
74
+ input_schema: dict[str, Any] = field(default_factory=dict)
75
+
76
+
77
+ @dataclass
78
+ class McpResource:
79
+ uri: str
80
+ name: str
81
+ description: str = ""
82
+ mime_type: str | None = None
83
+
84
+
85
+ @dataclass
86
+ class McpToolCallResult:
87
+ content: list[dict[str, Any]] = field(default_factory=list)
88
+ is_error: bool = False
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # MCP Server Connection
93
+ # ---------------------------------------------------------------------------
94
+
95
+ class McpServerConnection:
96
+ """Manages a single MCP server connection over stdio."""
97
+
98
+ def __init__(
99
+ self,
100
+ name: str,
101
+ command: str,
102
+ args: list[str] | None = None,
103
+ env: dict[str, str] | None = None,
104
+ ) -> None:
105
+ self.name = name
106
+ self.command = command
107
+ self.args = args or []
108
+ self.env = env
109
+ self._process: asyncio.subprocess.Process | None = None
110
+ self._reader: asyncio.StreamReader | None = None
111
+ self._writer: asyncio.StreamWriter | None = None
112
+ self._request_id = 0
113
+ self._pending: dict[int, asyncio.Future[JsonRpcResponse]] = {}
114
+ self._tools: list[McpTool] = []
115
+ self._resources: list[McpResource] = []
116
+ self._read_task: asyncio.Task[None] | None = None
117
+
118
+ async def connect(self) -> None:
119
+ """Start the MCP server process and initialize."""
120
+ import os
121
+
122
+ env = {**os.environ, **(self.env or {})}
123
+ self._process = await asyncio.create_subprocess_exec(
124
+ self.command,
125
+ *self.args,
126
+ stdin=asyncio.subprocess.PIPE,
127
+ stdout=asyncio.subprocess.PIPE,
128
+ stderr=asyncio.subprocess.PIPE,
129
+ env=env,
130
+ )
131
+ self._reader = self._process.stdout
132
+ self._writer = self._process.stdin
133
+
134
+ # Start reading responses
135
+ self._read_task = asyncio.create_task(self._read_loop())
136
+
137
+ # Initialize
138
+ _init_result = await self._send_request("initialize", {
139
+ "protocolVersion": MCP_PROTOCOL_VERSION,
140
+ "capabilities": {},
141
+ "clientInfo": {"name": "axion-code", "version": "1.0.0"},
142
+ })
143
+
144
+ # Send initialized notification
145
+ await self._send_notification("notifications/initialized", {})
146
+
147
+ # List tools
148
+ tools_result = await self._send_request("tools/list", {})
149
+ if tools_result and tools_result.result:
150
+ for t in tools_result.result.get("tools", []):
151
+ self._tools.append(McpTool(
152
+ name=t["name"],
153
+ description=t.get("description", ""),
154
+ input_schema=t.get("inputSchema", {}),
155
+ ))
156
+
157
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> McpToolCallResult:
158
+ """Call a tool on the MCP server."""
159
+ result = await self._send_request("tools/call", {
160
+ "name": name,
161
+ "arguments": arguments,
162
+ })
163
+ if result and result.error:
164
+ return McpToolCallResult(
165
+ content=[{"type": "text", "text": result.error.message}],
166
+ is_error=True,
167
+ )
168
+ if result and result.result:
169
+ return McpToolCallResult(
170
+ content=result.result.get("content", []),
171
+ is_error=result.result.get("isError", False),
172
+ )
173
+ return McpToolCallResult(is_error=True)
174
+
175
+ @property
176
+ def tools(self) -> list[McpTool]:
177
+ return self._tools
178
+
179
+ @property
180
+ def resources(self) -> list[McpResource]:
181
+ return self._resources
182
+
183
+ async def disconnect(self) -> None:
184
+ """Shutdown the MCP server."""
185
+ if self._read_task:
186
+ self._read_task.cancel()
187
+ if self._process:
188
+ try:
189
+ self._process.terminate()
190
+ await asyncio.wait_for(self._process.wait(), timeout=5.0)
191
+ except (asyncio.TimeoutError, ProcessLookupError):
192
+ self._process.kill()
193
+
194
+ def _next_id(self) -> int:
195
+ self._request_id += 1
196
+ return self._request_id
197
+
198
+ async def _send_request(
199
+ self, method: str, params: dict[str, Any]
200
+ ) -> JsonRpcResponse | None:
201
+ req_id = self._next_id()
202
+ request = JsonRpcRequest(method=method, params=params, id=req_id)
203
+
204
+ future: asyncio.Future[JsonRpcResponse] = asyncio.get_event_loop().create_future()
205
+ self._pending[req_id] = future
206
+
207
+ await self._write_message(request.to_dict())
208
+
209
+ try:
210
+ return await asyncio.wait_for(future, timeout=30.0)
211
+ except asyncio.TimeoutError:
212
+ self._pending.pop(req_id, None)
213
+ logger.error("MCP request timed out: %s", method)
214
+ return None
215
+
216
+ async def _send_notification(self, method: str, params: dict[str, Any]) -> None:
217
+ request = JsonRpcRequest(method=method, params=params)
218
+ await self._write_message(request.to_dict())
219
+
220
+ async def _write_message(self, data: dict[str, Any]) -> None:
221
+ if self._writer is None:
222
+ return
223
+ content = json.dumps(data)
224
+ message = f"Content-Length: {len(content)}\r\n\r\n{content}"
225
+ self._writer.write(message.encode())
226
+ await self._writer.drain()
227
+
228
+ async def _read_loop(self) -> None:
229
+ """Read JSON-RPC responses from the server."""
230
+ if self._reader is None:
231
+ return
232
+
233
+ buffer = b""
234
+ while True:
235
+ try:
236
+ chunk = await self._reader.read(4096)
237
+ if not chunk:
238
+ break
239
+ buffer += chunk
240
+
241
+ # Parse Content-Length header
242
+ while b"\r\n\r\n" in buffer:
243
+ header_end = buffer.index(b"\r\n\r\n")
244
+ header = buffer[:header_end].decode()
245
+ content_length = 0
246
+ for line in header.split("\r\n"):
247
+ if line.lower().startswith("content-length:"):
248
+ content_length = int(line.split(":")[1].strip())
249
+
250
+ body_start = header_end + 4
251
+ if len(buffer) < body_start + content_length:
252
+ break # Not enough data yet
253
+
254
+ body = buffer[body_start:body_start + content_length].decode()
255
+ buffer = buffer[body_start + content_length:]
256
+
257
+ try:
258
+ data = json.loads(body)
259
+ response = JsonRpcResponse.from_dict(data)
260
+ if response.id is not None and response.id in self._pending:
261
+ future = self._pending.pop(response.id)
262
+ if not future.done():
263
+ future.set_result(response)
264
+ except json.JSONDecodeError:
265
+ logger.warning("Invalid JSON from MCP server")
266
+
267
+ except asyncio.CancelledError:
268
+ break
269
+ except Exception as exc:
270
+ logger.error("MCP read error: %s", exc)
271
+ break
272
+
273
+
274
+ # ---------------------------------------------------------------------------
275
+ # MCP Server Manager
276
+ # ---------------------------------------------------------------------------
277
+
278
+ class McpServerManager:
279
+ """Manages multiple MCP server connections.
280
+
281
+ Maps to: rust/crates/runtime/src/mcp_stdio.rs::McpServerManager
282
+ """
283
+
284
+ def __init__(self) -> None:
285
+ self._servers: dict[str, McpServerConnection] = {}
286
+
287
+ async def connect(
288
+ self,
289
+ name: str,
290
+ command: str,
291
+ args: list[str] | None = None,
292
+ env: dict[str, str] | None = None,
293
+ ) -> McpServerConnection:
294
+ """Connect to an MCP server."""
295
+ conn = McpServerConnection(name=name, command=command, args=args, env=env)
296
+ await conn.connect()
297
+ self._servers[name] = conn
298
+ return conn
299
+
300
+ def get(self, name: str) -> McpServerConnection | None:
301
+ return self._servers.get(name)
302
+
303
+ def all_servers(self) -> list[McpServerConnection]:
304
+ return list(self._servers.values())
305
+
306
+ def all_tools(self) -> list[tuple[str, McpTool]]:
307
+ """Get all tools from all connected servers."""
308
+ tools = []
309
+ for server in self._servers.values():
310
+ for tool in server.tools:
311
+ tools.append((server.name, tool))
312
+ return tools
313
+
314
+ async def disconnect_all(self) -> None:
315
+ """Disconnect all servers."""
316
+ for server in self._servers.values():
317
+ await server.disconnect()
318
+ self._servers.clear()
@@ -0,0 +1,79 @@
1
+ """MCP tool registry bridge.
2
+
3
+ Maps to: rust/crates/runtime/src/mcp_tool_bridge.rs
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import enum
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+
13
+ class McpConnectionStatus(enum.Enum):
14
+ DISCONNECTED = "disconnected"
15
+ CONNECTING = "connecting"
16
+ CONNECTED = "connected"
17
+ AUTH_REQUIRED = "auth_required"
18
+ ERROR = "error"
19
+
20
+
21
+ @dataclass
22
+ class McpToolInfo:
23
+ name: str
24
+ description: str = ""
25
+ input_schema: dict[str, Any] = field(default_factory=dict)
26
+
27
+
28
+ @dataclass
29
+ class McpResourceInfo:
30
+ uri: str
31
+ name: str
32
+ description: str = ""
33
+ mime_type: str | None = None
34
+
35
+
36
+ @dataclass
37
+ class McpServerState:
38
+ """State of a connected MCP server."""
39
+
40
+ server_name: str
41
+ status: McpConnectionStatus = McpConnectionStatus.DISCONNECTED
42
+ tools: list[McpToolInfo] = field(default_factory=list)
43
+ resources: list[McpResourceInfo] = field(default_factory=list)
44
+ error_message: str | None = None
45
+
46
+
47
+ class McpToolRegistry:
48
+ """Tracks connected MCP servers and their available tools.
49
+
50
+ Maps to: rust/crates/runtime/src/mcp_tool_bridge.rs::McpToolRegistry
51
+ """
52
+
53
+ def __init__(self) -> None:
54
+ self._servers: dict[str, McpServerState] = {}
55
+
56
+ def register_server(self, state: McpServerState) -> None:
57
+ self._servers[state.server_name] = state
58
+
59
+ def get_server(self, name: str) -> McpServerState | None:
60
+ return self._servers.get(name)
61
+
62
+ def all_servers(self) -> list[McpServerState]:
63
+ return list(self._servers.values())
64
+
65
+ def all_tools(self) -> list[tuple[str, McpToolInfo]]:
66
+ """Get all tools with their server name."""
67
+ tools = []
68
+ for state in self._servers.values():
69
+ if state.status == McpConnectionStatus.CONNECTED:
70
+ for tool in state.tools:
71
+ tools.append((state.server_name, tool))
72
+ return tools
73
+
74
+ def find_tool(self, tool_name: str) -> tuple[str, McpToolInfo] | None:
75
+ """Find a tool by name across all servers."""
76
+ for server_name, tool in self.all_tools():
77
+ if tool.name == tool_name:
78
+ return (server_name, tool)
79
+ return None