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
axion/cli/input.py ADDED
@@ -0,0 +1,290 @@
1
+ """Interactive input with fixed bottom toolbar and clean prompt.
2
+
3
+ Simple, reliable approach:
4
+ - Clean prompt (no box-drawing — they break with line wrapping)
5
+ - Fixed bottom toolbar showing model, tokens, cost
6
+ - Block cursor
7
+ - Tab completion for slash commands
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from prompt_toolkit import PromptSession
16
+ from prompt_toolkit.completion import Completer, Completion, merge_completers
17
+ from prompt_toolkit.cursor_shapes import CursorShape
18
+ from prompt_toolkit.document import Document
19
+ from prompt_toolkit.formatted_text import HTML
20
+ from prompt_toolkit.history import FileHistory
21
+ from prompt_toolkit.key_binding import KeyBindings
22
+ from prompt_toolkit.keys import Keys
23
+ from prompt_toolkit.styles import Style
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Slash command completer
27
+ # ---------------------------------------------------------------------------
28
+
29
+ def _get_slash_commands() -> list[str]:
30
+ """Get all slash commands from the registry (always up to date)."""
31
+ from axion.commands.registry import get_command_registry
32
+ reg = get_command_registry()
33
+ return [f"/{name}" for name in reg.command_names()]
34
+
35
+ MODEL_COMPLETIONS = [
36
+ # Anthropic
37
+ "opus", "sonnet", "haiku",
38
+ # OpenAI GPT-4
39
+ "gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano",
40
+ # OpenAI GPT-5
41
+ "gpt-5", "gpt-5-mini", "gpt-5-nano", "gpt-5-pro", "gpt-5.4", "gpt-5.4-mini",
42
+ # OpenAI Codex
43
+ "codex", "gpt-5-codex",
44
+ # OpenAI reasoning
45
+ "o1", "o3", "o3-mini", "o3-pro", "o4-mini",
46
+ # xAI
47
+ "grok-2", "grok-3",
48
+ # Local (Ollama)
49
+ "llama3.1", "llama4", "mistral", "codellama", "deepseek", "phi", "gemma", "qwen",
50
+ ]
51
+
52
+
53
+ class SlashCommandCompleter(Completer):
54
+ """Context-aware completer for slash commands."""
55
+
56
+ def get_completions(self, document: Document, complete_event: Any) -> list[Completion]:
57
+ text = document.text_before_cursor.lstrip()
58
+ if not text.startswith("/"):
59
+ return []
60
+
61
+ parts = text.split(maxsplit=1)
62
+ cmd = parts[0].lower()
63
+
64
+ if len(parts) == 1 and not text.endswith(" "):
65
+ return [
66
+ Completion(c, start_position=-len(cmd))
67
+ for c in _get_slash_commands() if c.startswith(cmd)
68
+ ]
69
+
70
+ arg_text = parts[1] if len(parts) > 1 else ""
71
+ candidates: list[str] = []
72
+ if cmd == "/model":
73
+ candidates = MODEL_COMPLETIONS
74
+ elif cmd == "/session":
75
+ candidates = ["list", "show", "fork", "switch", "delete", "new"]
76
+ elif cmd == "/plan":
77
+ candidates = ["execute", "exit", "status"]
78
+ elif cmd == "/plugins":
79
+ candidates = ["list", "install", "enable", "disable"]
80
+ elif cmd == "/resume":
81
+ candidates = ["latest"]
82
+ elif cmd == "/mcp":
83
+ candidates = ["list", "show", "help"]
84
+ elif cmd == "/permissions":
85
+ candidates = ["allow", "prompt", "read-only", "workspace-write"]
86
+ elif cmd == "/share":
87
+ candidates = ["file", "json", "import"]
88
+ elif cmd == "/undo":
89
+ candidates = ["hard"]
90
+ elif cmd == "/init-project" or cmd == "/scaffold":
91
+ candidates = ["react", "nextjs", "django", "fastapi", "express", "cli", "flask"]
92
+ elif cmd == "/export":
93
+ candidates = ["transcript.md"]
94
+
95
+ return [
96
+ Completion(c, start_position=-len(arg_text))
97
+ for c in candidates if c.startswith(arg_text.lower())
98
+ ]
99
+
100
+
101
+ class FileTagCompleter(Completer):
102
+ """Completer for @file references — suggests files/dirs in the current or specified directory."""
103
+
104
+ def get_completions(self, document: Document, complete_event: Any) -> list[Completion]:
105
+ text_before = document.text_before_cursor
106
+
107
+ # Find the last @ token that is either at the start or preceded by whitespace
108
+ at_idx = -1
109
+ for i in range(len(text_before) - 1, -1, -1):
110
+ if text_before[i] == "@" and (i == 0 or text_before[i - 1] in (" ", "\t")):
111
+ at_idx = i
112
+ break
113
+
114
+ if at_idx == -1:
115
+ return []
116
+
117
+ # The partial path typed after @
118
+ partial = text_before[at_idx + 1:]
119
+
120
+ # Determine the directory to list and the prefix to match
121
+ if "/" in partial or "\\" in partial:
122
+ # User typed a partial path like @src/ma — split into dir + prefix
123
+ sep = partial.rfind("/")
124
+ if sep == -1:
125
+ sep = partial.rfind("\\")
126
+ dir_part = partial[: sep + 1]
127
+ name_prefix = partial[sep + 1:]
128
+ search_dir = Path(dir_part) if dir_part else Path(".")
129
+ else:
130
+ dir_part = ""
131
+ name_prefix = partial
132
+ search_dir = Path(".")
133
+
134
+ try:
135
+ entries = sorted(search_dir.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
136
+ except (OSError, PermissionError):
137
+ return []
138
+
139
+ completions: list[Completion] = []
140
+ for entry in entries:
141
+ name = entry.name
142
+ # Skip hidden files/dirs
143
+ if name.startswith("."):
144
+ continue
145
+ if not name.lower().startswith(name_prefix.lower()):
146
+ continue
147
+
148
+ display_name = name + "/" if entry.is_dir() else name
149
+ completion_text = dir_part + display_name
150
+
151
+ completions.append(
152
+ Completion(
153
+ completion_text,
154
+ start_position=-len(partial),
155
+ display=display_name,
156
+ )
157
+ )
158
+
159
+ return completions
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Key bindings
164
+ # ---------------------------------------------------------------------------
165
+
166
+ def create_key_bindings() -> KeyBindings:
167
+ bindings = KeyBindings()
168
+
169
+ @bindings.add(Keys.ControlD)
170
+ def _(event: Any) -> None:
171
+ event.app.exit(exception=EOFError())
172
+
173
+ return bindings
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Style
178
+ # ---------------------------------------------------------------------------
179
+
180
+ # Navy/Cyan terminal theme
181
+ INPUT_STYLE = Style.from_dict({
182
+ "prompt": "#00d4aa bold", # Teal prompt
183
+ "bottom-toolbar": "bg:#0a192f fg:#8892b0", # Navy bg, muted text
184
+ "bottom-toolbar.text": "bg:#0a192f fg:#8892b0",
185
+ "placeholder": "#4a5568", # Dark gray placeholder
186
+ })
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # Input session
191
+ # ---------------------------------------------------------------------------
192
+
193
+ class InputSession:
194
+ """Clean input with fixed bottom toolbar showing live stats."""
195
+
196
+ def __init__(self, history_path: Path | None = None) -> None:
197
+ history = FileHistory(str(history_path)) if history_path else None
198
+
199
+ self._status_model = ""
200
+ self._status_tokens = 0
201
+ self._status_cost = 0.0
202
+ self._status_turn = 0
203
+
204
+ combined_completer = merge_completers(
205
+ [SlashCommandCompleter(), FileTagCompleter()]
206
+ )
207
+
208
+ self.session: PromptSession[str] = PromptSession(
209
+ history=history,
210
+ completer=combined_completer,
211
+ complete_while_typing=True,
212
+ key_bindings=create_key_bindings(),
213
+ style=INPUT_STYLE,
214
+ cursor=CursorShape.BLOCK,
215
+ bottom_toolbar=self._toolbar,
216
+ )
217
+
218
+ def _toolbar(self) -> HTML:
219
+ """Build the fixed bottom toolbar content."""
220
+ if self._status_turn == 0:
221
+ return HTML(
222
+ " <b>axion</b> │ /help for commands │ Ctrl+C to interrupt"
223
+ )
224
+ mode = getattr(self, "_status_auth_mode", "")
225
+ model_lc = (self._status_model or "").lower()
226
+ if mode == "subscription":
227
+ # ChatGPT for codex, Pro/Max for Claude
228
+ sub_label = "ChatGPT" if "codex" in model_lc else "Pro/Max"
229
+ cost_part = sub_label
230
+ elif mode == "local":
231
+ cost_part = "local"
232
+ elif mode == "api":
233
+ cost_part = f"API · ${self._status_cost:.4f}"
234
+ else:
235
+ cost_part = f"${self._status_cost:.4f}"
236
+ return HTML(
237
+ f" <b>{self._status_model}</b>"
238
+ f" │ turn {self._status_turn}"
239
+ f" │ {self._status_tokens:,} tokens"
240
+ f" │ {cost_part}"
241
+ )
242
+
243
+ def update_status(
244
+ self,
245
+ model: str,
246
+ tokens: int,
247
+ cost: float,
248
+ turn: int,
249
+ auth_mode: str = "",
250
+ ) -> None:
251
+ """Update toolbar data (called after each turn)."""
252
+ self._status_model = model
253
+ self._status_tokens = tokens
254
+ self._status_cost = cost
255
+ self._status_turn = turn
256
+ self._status_auth_mode = auth_mode
257
+
258
+ async def prompt(self, prompt_text: str = "axion") -> str | None:
259
+ """Get input with a clean prompt, placeholder hint, and fixed toolbar."""
260
+ import random
261
+
262
+ placeholders = [
263
+ "Try: fix the bug in main.py",
264
+ "Try: explain this codebase",
265
+ "Try: add tests for the API",
266
+ "Try: search for TODO comments",
267
+ "Try: read README.md and summarize",
268
+ "Try: /plan add authentication",
269
+ "Try: /help for all commands",
270
+ ]
271
+ hint = random.choice(placeholders)
272
+
273
+ try:
274
+ result = await self.session.prompt_async(
275
+ HTML(f"<prompt>{prompt_text} &gt; </prompt>"),
276
+ placeholder=HTML(f"<style bg='' fg='#555555'>{hint}</style>"),
277
+ )
278
+ return result
279
+ except (EOFError, KeyboardInterrupt):
280
+ return None
281
+
282
+ def push_history(self, text: str) -> None:
283
+ if self.session.history:
284
+ self.session.history.append_string(text)
285
+
286
+ @staticmethod
287
+ def default_history_path() -> Path:
288
+ history_dir = Path.home() / ".axion"
289
+ history_dir.mkdir(parents=True, exist_ok=True)
290
+ return history_dir / "repl_history"