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.
- axion/__init__.py +3 -0
- axion/api/__init__.py +0 -0
- axion/api/anthropic.py +460 -0
- axion/api/client.py +259 -0
- axion/api/error.py +161 -0
- axion/api/ollama.py +597 -0
- axion/api/openai_compat.py +805 -0
- axion/api/openai_responses.py +627 -0
- axion/api/prompt_cache.py +31 -0
- axion/api/sse.py +98 -0
- axion/api/types.py +451 -0
- axion/cli/__init__.py +0 -0
- axion/cli/init_cmd.py +50 -0
- axion/cli/input.py +290 -0
- axion/cli/main.py +2953 -0
- axion/cli/render.py +489 -0
- axion/cli/tui.py +766 -0
- axion/commands/__init__.py +0 -0
- axion/commands/handlers/__init__.py +0 -0
- axion/commands/handlers/agents.py +51 -0
- axion/commands/handlers/builtin_commands.py +367 -0
- axion/commands/handlers/mcp.py +59 -0
- axion/commands/handlers/models.py +75 -0
- axion/commands/handlers/plugins.py +55 -0
- axion/commands/handlers/skills.py +61 -0
- axion/commands/parsing.py +317 -0
- axion/commands/registry.py +166 -0
- axion/compat_harness/__init__.py +0 -0
- axion/compat_harness/extractor.py +145 -0
- axion/plugins/__init__.py +0 -0
- axion/plugins/hooks.py +22 -0
- axion/plugins/manager.py +391 -0
- axion/plugins/manifest.py +270 -0
- axion/runtime/__init__.py +0 -0
- axion/runtime/bash.py +388 -0
- axion/runtime/bootstrap.py +39 -0
- axion/runtime/claude_subscription.py +300 -0
- axion/runtime/compact.py +233 -0
- axion/runtime/config.py +397 -0
- axion/runtime/conversation.py +1073 -0
- axion/runtime/file_ops.py +613 -0
- axion/runtime/git.py +213 -0
- axion/runtime/hooks.py +235 -0
- axion/runtime/image.py +212 -0
- axion/runtime/lanes.py +282 -0
- axion/runtime/lsp.py +425 -0
- axion/runtime/mcp/__init__.py +0 -0
- axion/runtime/mcp/client.py +76 -0
- axion/runtime/mcp/lifecycle.py +96 -0
- axion/runtime/mcp/stdio.py +318 -0
- axion/runtime/mcp/tool_bridge.py +79 -0
- axion/runtime/memory.py +196 -0
- axion/runtime/oauth.py +329 -0
- axion/runtime/openai_subscription.py +346 -0
- axion/runtime/permissions.py +247 -0
- axion/runtime/plan_mode.py +96 -0
- axion/runtime/policy_engine.py +259 -0
- axion/runtime/prompt.py +586 -0
- axion/runtime/recovery.py +261 -0
- axion/runtime/remote.py +28 -0
- axion/runtime/sandbox.py +68 -0
- axion/runtime/scheduler.py +231 -0
- axion/runtime/session.py +365 -0
- axion/runtime/sharing.py +159 -0
- axion/runtime/skills.py +124 -0
- axion/runtime/tasks.py +258 -0
- axion/runtime/usage.py +241 -0
- axion/runtime/workers.py +186 -0
- axion/telemetry/__init__.py +0 -0
- axion/telemetry/events.py +67 -0
- axion/telemetry/profile.py +49 -0
- axion/telemetry/sink.py +60 -0
- axion/telemetry/tracer.py +95 -0
- axion/tools/__init__.py +0 -0
- axion/tools/lane_completion.py +33 -0
- axion/tools/registry.py +853 -0
- axion/tools/tool_search.py +226 -0
- axion_code-1.0.0.dist-info/METADATA +709 -0
- axion_code-1.0.0.dist-info/RECORD +82 -0
- axion_code-1.0.0.dist-info/WHEEL +4 -0
- axion_code-1.0.0.dist-info/entry_points.txt +2 -0
- 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} > </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"
|