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/api/sse.py ADDED
@@ -0,0 +1,98 @@
1
+ """Server-Sent Events (SSE) parser.
2
+
3
+ Maps to: rust/crates/api/src/sse.rs
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+
10
+ from axion.api.error import InvalidSseFrameError
11
+ from axion.api.types import StreamEvent
12
+
13
+
14
+ class SseParser:
15
+ """Incremental SSE frame parser that handles chunked delivery."""
16
+
17
+ def __init__(self) -> None:
18
+ self._buffer = bytearray()
19
+
20
+ def push(self, chunk: bytes) -> list[StreamEvent]:
21
+ """Push a chunk of data and return any complete events."""
22
+ self._buffer.extend(chunk)
23
+ events: list[StreamEvent] = []
24
+
25
+ while True:
26
+ frame = self._next_frame()
27
+ if frame is None:
28
+ break
29
+ event = parse_frame(frame)
30
+ if event is not None:
31
+ events.append(event)
32
+
33
+ return events
34
+
35
+ def finish(self) -> list[StreamEvent]:
36
+ """Flush any remaining data in the buffer."""
37
+ if not self._buffer:
38
+ return []
39
+
40
+ trailing = self._buffer.decode("utf-8", errors="replace")
41
+ self._buffer.clear()
42
+
43
+ event = parse_frame(trailing)
44
+ return [event] if event is not None else []
45
+
46
+ def _next_frame(self) -> str | None:
47
+ """Extract the next complete frame from the buffer."""
48
+ # Look for \n\n separator
49
+ pos = self._buffer.find(b"\n\n")
50
+ sep_len = 2
51
+
52
+ if pos == -1:
53
+ # Try \r\n\r\n
54
+ pos = self._buffer.find(b"\r\n\r\n")
55
+ sep_len = 4
56
+
57
+ if pos == -1:
58
+ return None
59
+
60
+ frame_bytes = bytes(self._buffer[: pos])
61
+ del self._buffer[: pos + sep_len]
62
+ return frame_bytes.decode("utf-8", errors="replace")
63
+
64
+
65
+ def parse_frame(frame: str) -> StreamEvent | None:
66
+ """Parse a single SSE frame into a StreamEvent."""
67
+ trimmed = frame.strip()
68
+ if not trimmed:
69
+ return None
70
+
71
+ data_lines: list[str] = []
72
+ event_name: str | None = None
73
+
74
+ for line in trimmed.splitlines():
75
+ if line.startswith(":"):
76
+ continue
77
+ if line.startswith("event:"):
78
+ event_name = line[len("event:"):].strip()
79
+ continue
80
+ if line.startswith("data:"):
81
+ data_lines.append(line[len("data:"):].lstrip())
82
+
83
+ if event_name == "ping":
84
+ return None
85
+
86
+ if not data_lines:
87
+ return None
88
+
89
+ payload = "\n".join(data_lines)
90
+ if payload == "[DONE]":
91
+ return None
92
+
93
+ try:
94
+ data = json.loads(payload)
95
+ except json.JSONDecodeError as exc:
96
+ raise InvalidSseFrameError(f"Invalid JSON in SSE data: {exc}") from exc
97
+
98
+ return StreamEvent.from_dict(data)
axion/api/types.py ADDED
@@ -0,0 +1,451 @@
1
+ """API data models for Anthropic and OpenAI-compatible providers.
2
+
3
+ Maps to: rust/crates/api/src/types.rs
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Literal
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Request types
13
+ # ---------------------------------------------------------------------------
14
+
15
+ @dataclass
16
+ class MessageRequest:
17
+ model: str
18
+ max_tokens: int
19
+ messages: list[InputMessage]
20
+ system: str | None = None
21
+ tools: list[ToolDefinition] | None = None
22
+ tool_choice: ToolChoice | None = None
23
+ stream: bool = False
24
+
25
+ def with_streaming(self) -> MessageRequest:
26
+ self.stream = True
27
+ return self
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ d: dict[str, Any] = {
31
+ "model": self.model,
32
+ "max_tokens": self.max_tokens,
33
+ "messages": [m.to_dict() for m in self.messages],
34
+ }
35
+ if self.system is not None:
36
+ d["system"] = self.system
37
+ if self.tools is not None:
38
+ d["tools"] = [t.to_dict() for t in self.tools]
39
+ if self.tool_choice is not None:
40
+ d["tool_choice"] = self.tool_choice.to_dict()
41
+ if self.stream:
42
+ d["stream"] = True
43
+ return d
44
+
45
+
46
+ @dataclass
47
+ class InputMessage:
48
+ role: str
49
+ content: list[InputContentBlock]
50
+
51
+ @classmethod
52
+ def user_text(cls, text: str) -> InputMessage:
53
+ return cls(role="user", content=[TextInputBlock(text=text)])
54
+
55
+ @classmethod
56
+ def user_tool_result(
57
+ cls, tool_use_id: str, content: str, is_error: bool = False
58
+ ) -> InputMessage:
59
+ return cls(
60
+ role="user",
61
+ content=[
62
+ ToolResultBlock(
63
+ tool_use_id=tool_use_id,
64
+ content=[ToolResultTextContent(text=content)],
65
+ is_error=is_error,
66
+ )
67
+ ],
68
+ )
69
+
70
+ def to_dict(self) -> dict[str, Any]:
71
+ return {
72
+ "role": self.role,
73
+ "content": [b.to_dict() for b in self.content],
74
+ }
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Input content blocks (tagged union via subclasses)
79
+ # ---------------------------------------------------------------------------
80
+
81
+ @dataclass
82
+ class InputContentBlock:
83
+ """Base class for input content blocks."""
84
+
85
+ def to_dict(self) -> dict[str, Any]:
86
+ raise NotImplementedError
87
+
88
+
89
+ @dataclass
90
+ class TextInputBlock(InputContentBlock):
91
+ text: str
92
+
93
+ def to_dict(self) -> dict[str, Any]:
94
+ return {"type": "text", "text": self.text}
95
+
96
+
97
+ @dataclass
98
+ class ToolUseInputBlock(InputContentBlock):
99
+ id: str
100
+ name: str
101
+ input: Any
102
+
103
+ def to_dict(self) -> dict[str, Any]:
104
+ return {"type": "tool_use", "id": self.id, "name": self.name, "input": self.input}
105
+
106
+
107
+ @dataclass
108
+ class ImageInputBlock(InputContentBlock):
109
+ """Image content block — base64-encoded image for vision models.
110
+
111
+ Works with both Anthropic (source.type=base64) and OpenAI (image_url).
112
+ """
113
+ media_type: str # e.g. "image/png", "image/jpeg"
114
+ data: str # base64-encoded image data
115
+
116
+ def to_dict(self) -> dict[str, Any]:
117
+ # Anthropic format
118
+ return {
119
+ "type": "image",
120
+ "source": {
121
+ "type": "base64",
122
+ "media_type": self.media_type,
123
+ "data": self.data,
124
+ },
125
+ }
126
+
127
+ def to_openai_dict(self) -> dict[str, Any]:
128
+ """OpenAI format for image_url with base64 data URL."""
129
+ return {
130
+ "type": "image_url",
131
+ "image_url": {
132
+ "url": f"data:{self.media_type};base64,{self.data}",
133
+ },
134
+ }
135
+
136
+
137
+ @dataclass
138
+ class ToolResultBlock(InputContentBlock):
139
+ tool_use_id: str
140
+ content: list[ToolResultContent]
141
+ is_error: bool = False
142
+
143
+ def to_dict(self) -> dict[str, Any]:
144
+ d: dict[str, Any] = {
145
+ "type": "tool_result",
146
+ "tool_use_id": self.tool_use_id,
147
+ "content": [c.to_dict() for c in self.content],
148
+ }
149
+ if self.is_error:
150
+ d["is_error"] = True
151
+ return d
152
+
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # Tool result content blocks
156
+ # ---------------------------------------------------------------------------
157
+
158
+ @dataclass
159
+ class ToolResultContent:
160
+ def to_dict(self) -> dict[str, Any]:
161
+ raise NotImplementedError
162
+
163
+
164
+ @dataclass
165
+ class ToolResultTextContent(ToolResultContent):
166
+ text: str
167
+
168
+ def to_dict(self) -> dict[str, Any]:
169
+ return {"type": "text", "text": self.text}
170
+
171
+
172
+ @dataclass
173
+ class ToolResultJsonContent(ToolResultContent):
174
+ value: Any
175
+
176
+ def to_dict(self) -> dict[str, Any]:
177
+ return {"type": "json", "value": self.value}
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Tool definitions
182
+ # ---------------------------------------------------------------------------
183
+
184
+ @dataclass
185
+ class ToolDefinition:
186
+ name: str
187
+ input_schema: dict[str, Any]
188
+ description: str | None = None
189
+
190
+ def to_dict(self) -> dict[str, Any]:
191
+ d: dict[str, Any] = {"name": self.name, "input_schema": self.input_schema}
192
+ if self.description is not None:
193
+ d["description"] = self.description
194
+ return d
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # Tool choice
199
+ # ---------------------------------------------------------------------------
200
+
201
+ @dataclass
202
+ class ToolChoice:
203
+ type: Literal["auto", "any", "tool"]
204
+ name: str | None = None
205
+
206
+ @classmethod
207
+ def auto(cls) -> ToolChoice:
208
+ return cls(type="auto")
209
+
210
+ @classmethod
211
+ def any(cls) -> ToolChoice:
212
+ return cls(type="any")
213
+
214
+ @classmethod
215
+ def tool(cls, name: str) -> ToolChoice:
216
+ return cls(type="tool", name=name)
217
+
218
+ def to_dict(self) -> dict[str, Any]:
219
+ d: dict[str, Any] = {"type": self.type}
220
+ if self.name is not None:
221
+ d["name"] = self.name
222
+ return d
223
+
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # Response types
227
+ # ---------------------------------------------------------------------------
228
+
229
+ @dataclass
230
+ class MessageResponse:
231
+ id: str
232
+ type: str
233
+ role: str
234
+ content: list[OutputContentBlock]
235
+ model: str
236
+ usage: Usage
237
+ stop_reason: str | None = None
238
+ stop_sequence: str | None = None
239
+ request_id: str | None = None
240
+
241
+ def total_tokens(self) -> int:
242
+ return self.usage.total_tokens()
243
+
244
+ @classmethod
245
+ def from_dict(cls, data: dict[str, Any]) -> MessageResponse:
246
+ return cls(
247
+ id=data["id"],
248
+ type=data.get("type", "message"),
249
+ role=data.get("role", "assistant"),
250
+ content=[OutputContentBlock.from_dict(b) for b in data.get("content", [])],
251
+ model=data["model"],
252
+ usage=Usage.from_dict(data["usage"]),
253
+ stop_reason=data.get("stop_reason"),
254
+ stop_sequence=data.get("stop_sequence"),
255
+ request_id=data.get("request_id"),
256
+ )
257
+
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # Output content blocks (tagged union via subclasses)
261
+ # ---------------------------------------------------------------------------
262
+
263
+ @dataclass
264
+ class OutputContentBlock:
265
+ """Base class for output content blocks."""
266
+
267
+ @classmethod
268
+ def from_dict(cls, data: dict[str, Any]) -> OutputContentBlock:
269
+ block_type = data.get("type", "")
270
+ if block_type == "text":
271
+ return TextOutputBlock(text=data["text"])
272
+ if block_type == "tool_use":
273
+ return ToolUseOutputBlock(id=data["id"], name=data["name"], input=data["input"])
274
+ if block_type == "thinking":
275
+ return ThinkingBlock(thinking=data.get("thinking", ""), signature=data.get("signature"))
276
+ if block_type == "redacted_thinking":
277
+ return RedactedThinkingBlock(data=data.get("data"))
278
+ return TextOutputBlock(text=str(data))
279
+
280
+
281
+ @dataclass
282
+ class TextOutputBlock(OutputContentBlock):
283
+ text: str = ""
284
+
285
+
286
+ @dataclass
287
+ class ToolUseOutputBlock(OutputContentBlock):
288
+ id: str = ""
289
+ name: str = ""
290
+ input: Any = None
291
+
292
+
293
+ @dataclass
294
+ class ThinkingBlock(OutputContentBlock):
295
+ thinking: str = ""
296
+ signature: str | None = None
297
+
298
+
299
+ @dataclass
300
+ class RedactedThinkingBlock(OutputContentBlock):
301
+ data: Any = None
302
+
303
+
304
+ # ---------------------------------------------------------------------------
305
+ # Usage / tokens
306
+ # ---------------------------------------------------------------------------
307
+
308
+ @dataclass
309
+ class Usage:
310
+ input_tokens: int = 0
311
+ output_tokens: int = 0
312
+ cache_creation_input_tokens: int = 0
313
+ cache_read_input_tokens: int = 0
314
+
315
+ def total_tokens(self) -> int:
316
+ return (
317
+ self.input_tokens
318
+ + self.output_tokens
319
+ + self.cache_creation_input_tokens
320
+ + self.cache_read_input_tokens
321
+ )
322
+
323
+ @classmethod
324
+ def from_dict(cls, data: dict[str, Any]) -> Usage:
325
+ return cls(
326
+ input_tokens=data.get("input_tokens", 0),
327
+ output_tokens=data.get("output_tokens", 0),
328
+ cache_creation_input_tokens=data.get("cache_creation_input_tokens", 0),
329
+ cache_read_input_tokens=data.get("cache_read_input_tokens", 0),
330
+ )
331
+
332
+
333
+ # ---------------------------------------------------------------------------
334
+ # Stream events (tagged union via subclasses)
335
+ # ---------------------------------------------------------------------------
336
+
337
+ @dataclass
338
+ class StreamEvent:
339
+ """Base class for SSE stream events."""
340
+
341
+ @classmethod
342
+ def from_dict(cls, data: dict[str, Any]) -> StreamEvent:
343
+ event_type = data.get("type", "")
344
+ if event_type == "message_start":
345
+ return MessageStartEvent(
346
+ message=MessageResponse.from_dict(data["message"])
347
+ )
348
+ if event_type == "message_delta":
349
+ return MessageDeltaEvent(
350
+ delta=MessageDelta.from_dict(data["delta"]),
351
+ usage=Usage.from_dict(data["usage"]),
352
+ )
353
+ if event_type == "content_block_start":
354
+ return ContentBlockStartEvent(
355
+ index=data["index"],
356
+ content_block=OutputContentBlock.from_dict(data["content_block"]),
357
+ )
358
+ if event_type == "content_block_delta":
359
+ return ContentBlockDeltaEvent(
360
+ index=data["index"],
361
+ delta=ContentBlockDelta.from_dict(data["delta"]),
362
+ )
363
+ if event_type == "content_block_stop":
364
+ return ContentBlockStopEvent(index=data["index"])
365
+ if event_type == "message_stop":
366
+ return MessageStopEvent()
367
+ return StreamEvent()
368
+
369
+
370
+ @dataclass
371
+ class MessageStartEvent(StreamEvent):
372
+ message: MessageResponse | None = None
373
+
374
+
375
+ @dataclass
376
+ class MessageDelta:
377
+ stop_reason: str | None = None
378
+ stop_sequence: str | None = None
379
+
380
+ @classmethod
381
+ def from_dict(cls, data: dict[str, Any]) -> MessageDelta:
382
+ return cls(
383
+ stop_reason=data.get("stop_reason"),
384
+ stop_sequence=data.get("stop_sequence"),
385
+ )
386
+
387
+
388
+ @dataclass
389
+ class MessageDeltaEvent(StreamEvent):
390
+ delta: MessageDelta = field(default_factory=MessageDelta)
391
+ usage: Usage = field(default_factory=Usage)
392
+
393
+
394
+ @dataclass
395
+ class ContentBlockStartEvent(StreamEvent):
396
+ index: int = 0
397
+ content_block: OutputContentBlock | None = None
398
+
399
+
400
+ @dataclass
401
+ class ContentBlockDelta:
402
+ """Base for content block deltas."""
403
+
404
+ @classmethod
405
+ def from_dict(cls, data: dict[str, Any]) -> ContentBlockDelta:
406
+ delta_type = data.get("type", "")
407
+ if delta_type == "text_delta":
408
+ return TextDelta(text=data.get("text", ""))
409
+ if delta_type == "input_json_delta":
410
+ return InputJsonDelta(partial_json=data.get("partial_json", ""))
411
+ if delta_type == "thinking_delta":
412
+ return ThinkingDelta(thinking=data.get("thinking", ""))
413
+ if delta_type == "signature_delta":
414
+ return SignatureDelta(signature=data.get("signature", ""))
415
+ return ContentBlockDelta()
416
+
417
+
418
+ @dataclass
419
+ class TextDelta(ContentBlockDelta):
420
+ text: str = ""
421
+
422
+
423
+ @dataclass
424
+ class InputJsonDelta(ContentBlockDelta):
425
+ partial_json: str = ""
426
+
427
+
428
+ @dataclass
429
+ class ThinkingDelta(ContentBlockDelta):
430
+ thinking: str = ""
431
+
432
+
433
+ @dataclass
434
+ class SignatureDelta(ContentBlockDelta):
435
+ signature: str = ""
436
+
437
+
438
+ @dataclass
439
+ class ContentBlockDeltaEvent(StreamEvent):
440
+ index: int = 0
441
+ delta: ContentBlockDelta = field(default_factory=ContentBlockDelta)
442
+
443
+
444
+ @dataclass
445
+ class ContentBlockStopEvent(StreamEvent):
446
+ index: int = 0
447
+
448
+
449
+ @dataclass
450
+ class MessageStopEvent(StreamEvent):
451
+ pass
axion/cli/__init__.py ADDED
File without changes
axion/cli/init_cmd.py ADDED
@@ -0,0 +1,50 @@
1
+ """Repository initialization command — creates AXION.md."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from rich.console import Console
8
+
9
+
10
+ def init_project(cwd: Path | None = None, console: Console | None = None) -> None:
11
+ """Initialize a new project with AXION.md and .axion/ directory."""
12
+ actual_cwd = cwd or Path.cwd()
13
+ con = console or Console()
14
+
15
+ # Check for existing instruction files
16
+ axion_md = actual_cwd / "AXION.md"
17
+ claude_md = actual_cwd / "CLAUDE.md"
18
+
19
+ if axion_md.exists():
20
+ con.print("[yellow]AXION.md already exists[/yellow]")
21
+ return
22
+ if claude_md.exists():
23
+ con.print("[yellow]CLAUDE.md found — rename to AXION.md? (Axion reads both)[/yellow]")
24
+ return
25
+
26
+ # Create .axion directory
27
+ axion_dir = actual_cwd / ".axion"
28
+ axion_dir.mkdir(exist_ok=True)
29
+
30
+ # Create AXION.md
31
+ axion_md.write_text(
32
+ "# AXION.md\n\n"
33
+ "This file provides guidance to Axion Code when working with this codebase.\n\n"
34
+ "## Project overview\n\n"
35
+ "<!-- Describe your project here -->\n\n"
36
+ "## Build & test\n\n"
37
+ "<!-- Add build and test commands -->\n\n"
38
+ "## Code conventions\n\n"
39
+ "<!-- Add coding style guidelines -->\n",
40
+ encoding="utf-8",
41
+ )
42
+ con.print("[green]Created AXION.md[/green]")
43
+
44
+ # Create .axion/settings.json if it doesn't exist
45
+ settings = axion_dir / "settings.json"
46
+ if not settings.exists():
47
+ settings.write_text(
48
+ '{\n "permissions": {\n "defaultMode": "prompt"\n }\n}\n'
49
+ )
50
+ con.print("[green]Created .axion/settings.json[/green]")