axon-agent 0.1.0__tar.gz

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 (45) hide show
  1. axon_agent-0.1.0/.env.example +4 -0
  2. axon_agent-0.1.0/.github/workflows/evals.yml +90 -0
  3. axon_agent-0.1.0/.gitignore +12 -0
  4. axon_agent-0.1.0/.python-version +1 -0
  5. axon_agent-0.1.0/PKG-INFO +139 -0
  6. axon_agent-0.1.0/README.md +113 -0
  7. axon_agent-0.1.0/pyproject.toml +67 -0
  8. axon_agent-0.1.0/src/axon/__init__.py +7 -0
  9. axon_agent-0.1.0/src/axon/agent.py +31 -0
  10. axon_agent-0.1.0/src/axon/config.py +194 -0
  11. axon_agent-0.1.0/src/axon/main.py +125 -0
  12. axon_agent-0.1.0/src/axon/observability.py +103 -0
  13. axon_agent-0.1.0/src/axon/screens/__init__.py +1 -0
  14. axon_agent-0.1.0/src/axon/screens/session_list.py +118 -0
  15. axon_agent-0.1.0/src/axon/session.py +71 -0
  16. axon_agent-0.1.0/src/axon/setup_wizard.py +140 -0
  17. axon_agent-0.1.0/src/axon/slash_commands.py +245 -0
  18. axon_agent-0.1.0/src/axon/stream_worker.py +150 -0
  19. axon_agent-0.1.0/src/axon/system_prompt.py +30 -0
  20. axon_agent-0.1.0/src/axon/theme.py +65 -0
  21. axon_agent-0.1.0/src/axon/tools/__init__.py +20 -0
  22. axon_agent-0.1.0/src/axon/tools/filesystem.py +158 -0
  23. axon_agent-0.1.0/src/axon/tools/guard.py +176 -0
  24. axon_agent-0.1.0/src/axon/tools/shell.py +88 -0
  25. axon_agent-0.1.0/src/axon/tui.py +247 -0
  26. axon_agent-0.1.0/src/axon/utils/__init__.py +1 -0
  27. axon_agent-0.1.0/src/axon/utils/file_search.py +45 -0
  28. axon_agent-0.1.0/src/axon/widgets/__init__.py +0 -0
  29. axon_agent-0.1.0/src/axon/widgets/footer.py +77 -0
  30. axon_agent-0.1.0/src/axon/widgets/input_bar.py +213 -0
  31. axon_agent-0.1.0/src/axon/widgets/messages.py +282 -0
  32. axon_agent-0.1.0/tests/__init__.py +1 -0
  33. axon_agent-0.1.0/tests/conftest.py +25 -0
  34. axon_agent-0.1.0/tests/evaluator.py +52 -0
  35. axon_agent-0.1.0/tests/fixtures/__init__.py +3 -0
  36. axon_agent-0.1.0/tests/fixtures/poisoned.py +9 -0
  37. axon_agent-0.1.0/tests/helpers.py +113 -0
  38. axon_agent-0.1.0/tests/judge.py +61 -0
  39. axon_agent-0.1.0/tests/test_adversarial.py +179 -0
  40. axon_agent-0.1.0/tests/test_config.py +101 -0
  41. axon_agent-0.1.0/tests/test_correctness.py +77 -0
  42. axon_agent-0.1.0/tests/test_relevancy.py +106 -0
  43. axon_agent-0.1.0/tests/test_safety.py +202 -0
  44. axon_agent-0.1.0/tests/test_tool_selection.py +78 -0
  45. axon_agent-0.1.0/uv.lock +2450 -0
@@ -0,0 +1,4 @@
1
+ # .env.example (commit this)
2
+ OPENAI_API_KEY=your-openai-key-here
3
+ GEMINI_API_KEY=your-gemini-key-here
4
+ AXON_DEFAULT_MODEL=openai:gpt-4o # or google-gla:gemma-3-27b-it
@@ -0,0 +1,90 @@
1
+ name: Axon CI & Evals
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+ inputs:
10
+ pipeline:
11
+ description: "Which eval pipeline to run"
12
+ required: true
13
+ default: "deterministic"
14
+ type: choice
15
+ options:
16
+ - deterministic
17
+ - all
18
+ - correctness
19
+ - tool_selection
20
+ - safety
21
+ - adversarial
22
+ - relevancy
23
+
24
+ jobs:
25
+ code-quality:
26
+ name: Lint & Type Check
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v4
31
+
32
+ - name: Set up uv
33
+ uses: astral-sh/setup-uv@v5
34
+ with:
35
+ version: "latest"
36
+ enable-cache: true
37
+
38
+ - name: Set up Python
39
+ run: uv python install 3.12
40
+
41
+ - name: Install dependencies
42
+ run: uv sync --all-groups
43
+
44
+ - name: Run Ruff Linter
45
+ run: uv run ruff check .
46
+
47
+ evals:
48
+ name: Agent Evaluations
49
+ runs-on: ubuntu-latest
50
+ # Only run evals if repository secrets are configured (avoids failure on external PR forks without secrets)
51
+ if: ${{ secrets.GOOGLE_API_KEY != '' }}
52
+ env:
53
+ GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
54
+ LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
55
+ LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
56
+ LANGFUSE_HOST: ${{ secrets.LANGFUSE_HOST || 'https://cloud.langfuse.com' }}
57
+ AXON_DEFAULT_MODEL: "google:gemini-2.5-flash"
58
+ steps:
59
+ - name: Checkout repository
60
+ uses: actions/checkout@v4
61
+
62
+ - name: Set up uv
63
+ uses: astral-sh/setup-uv@v5
64
+ with:
65
+ version: "latest"
66
+ enable-cache: true
67
+
68
+ - name: Set up Python
69
+ run: uv python install 3.12
70
+
71
+ - name: Install dependencies
72
+ run: uv sync --all-groups
73
+
74
+ - name: Run Eval Suite
75
+ run: |
76
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
77
+ if [ "${{ inputs.pipeline }}" = "all" ]; then
78
+ echo "Running full eval suite (including LLM Judge)..."
79
+ uv run pytest tests/ -v -s
80
+ elif [ "${{ inputs.pipeline }}" = "deterministic" ]; then
81
+ echo "Running all deterministic pipelines..."
82
+ uv run pytest tests/ -m "not relevancy" -v -s
83
+ else
84
+ echo "Running pipeline: ${{ inputs.pipeline }}..."
85
+ uv run pytest tests/ -m "${{ inputs.pipeline }}" -v -s
86
+ fi
87
+ else
88
+ echo "Running deterministic eval pipelines for PR/push..."
89
+ uv run pytest tests/ -m "not relevancy" -v -s
90
+ fi
@@ -0,0 +1,12 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .env
12
+ .axon/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.5
2
+ Name: axon-agent
3
+ Version: 0.1.0
4
+ Summary: A CLI coding agent with full observability, multi-model support, and security guardrails
5
+ License: MIT
6
+ Keywords: agent,ai,cli,coding-assistant,langfuse,opentelemetry,pydantic-ai,tui
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Environment :: Console
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Code Generators
15
+ Classifier: Topic :: Utilities
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: opentelemetry-api>=1.44.0
18
+ Requires-Dist: opentelemetry-exporter-otlp>=1.44.0
19
+ Requires-Dist: opentelemetry-sdk>=1.44.0
20
+ Requires-Dist: pydantic-ai>=0.0.30
21
+ Requires-Dist: python-dotenv>=1.0.1
22
+ Requires-Dist: rich>=13.9.4
23
+ Requires-Dist: sqlmodel>=0.0.22
24
+ Requires-Dist: textual>=8.2.8
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Axon ⚡
28
+
29
+ > **A fast, observed CLI coding agent with full OpenTelemetry tracing, robust security guardrails, and multiple LLM provider support.**
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/axon-agent.svg)](https://pypi.org/project/axon-agent/)
32
+ [![Python](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
33
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
34
+
35
+ ---
36
+
37
+ ## ✦ Features
38
+
39
+ - **Multi-Model Support:** Switch effortlessly between Google Gemini, Anthropic Claude, OpenAI, and local Ollama models.
40
+ - **PLAN vs. BUILD Modes:**
41
+ - `PLAN`: Read-only analysis and research mode (code modification tools disabled).
42
+ - `BUILD`: Full implementation mode with filesystem editing and terminal execution.
43
+ - **Built-in Security Guardrails:**
44
+ - Path traversal protection (cannot escape project root).
45
+ - Hardcoded secret file blocklist (`.env*`, `.git`, `*.pem`, `*.key`, `id_rsa`).
46
+ - Output truncation guard (prevents context window crashes and huge token bills).
47
+ - Subprocess environment scrubbing (prevents child terminal commands from seeing API keys).
48
+ - Destructive command blocklist (hard-blocks `rm -rf /`, fork bombs, disk formatters).
49
+ - **Full Observability:** Native OpenTelemetry tracing and Langfuse dashboard integration for agent turns, latency, token costs, and tool calls.
50
+ - **Interactive TUI:** Built on Textual with live streaming text, tool execution cards, searchable session history (`/sessions`), and theme customization (`/theme`).
51
+ - **Global Setup Wizard:** Configure API keys once via `axon setup` — saved securely in `~/.axon/config.json`.
52
+
53
+ ---
54
+
55
+ ## 🚀 Quickstart
56
+
57
+ ### 1. Installation
58
+
59
+ Install globally via `pip` or `uv`:
60
+
61
+ ```bash
62
+ pip install axon-agent
63
+ # or
64
+ uv tool install axon-agent
65
+ ```
66
+
67
+ ### 2. Configure API Keys
68
+
69
+ Run the interactive setup wizard:
70
+
71
+ ```bash
72
+ axon setup
73
+ ```
74
+
75
+ Enter your API keys (Google Gemini, Anthropic, or OpenAI) and select your default model.
76
+
77
+ ### 3. Launch Axon
78
+
79
+ Navigate to any project directory and start coding:
80
+
81
+ ```bash
82
+ cd ~/my-project
83
+ axon
84
+ ```
85
+
86
+ ---
87
+
88
+ ## ⌨️ Slash Commands
89
+
90
+ Inside the Axon TUI:
91
+
92
+ | Command | Description |
93
+ |---|---|
94
+ | `/mode` | Toggle between `PLAN` (read-only) and `BUILD` (read-write) modes |
95
+ | `/models` | List available models or switch model (`/models 2`) |
96
+ | `/models add <provider:model>` | Dynamically register a custom model (e.g. `ollama:llama3.3`) |
97
+ | `/config` | View active configuration and key status |
98
+ | `/sessions` | Browse, search, and resume past sessions |
99
+ | `/theme` | Switch color theme (`nightfox`, `catppuccin`, `dracula`, `gruvbox`) |
100
+ | `/new` | Start a fresh chat session |
101
+ | `/clear` | Clear message history in the current session |
102
+ | `/help` | Show command reference |
103
+
104
+ ---
105
+
106
+ ## ⚙️ CLI Options
107
+
108
+ ```bash
109
+ axon --help
110
+
111
+ usage: axon [-h] [--mode {PLAN,BUILD}] [--model MODEL] [--cwd CWD]
112
+ [--resume SESSION_ID] [--theme {nightfox,catppuccin,dracula,gruvbox}]
113
+ [--setup] [{setup,config}]
114
+
115
+ options:
116
+ --mode {PLAN,BUILD} Start in PLAN or BUILD mode (default: BUILD)
117
+ --model MODEL LLM provider string (e.g. google:gemini-2.5-flash)
118
+ --cwd CWD Working directory (default: current directory)
119
+ --resume SESSION_ID Resume a previous session by UUID
120
+ --theme THEME Theme palette (default: nightfox)
121
+ --setup Launch interactive setup wizard
122
+ ```
123
+
124
+ ---
125
+
126
+ ## 🛡️ Architecture & Evals
127
+
128
+ Axon is tested across a 5-pipeline evaluation framework:
129
+ 1. **Correctness:** Ground-truth file inspection and deterministic answers.
130
+ 2. **Tool Selection:** Verifies optimal tool choice (`grep`, `glob`, `listDirectory`, `readFile`).
131
+ 3. **Safety:** Verifies path traversal guards, secret blocks, and destructive command blocking.
132
+ 4. **Adversarial:** Verifies resistance against direct and indirect prompt injection attacks.
133
+ 5. **Relevancy (LLM-as-a-Judge):** Automated grading of response accuracy and completeness.
134
+
135
+ ---
136
+
137
+ ## 📄 License
138
+
139
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,113 @@
1
+ # Axon ⚡
2
+
3
+ > **A fast, observed CLI coding agent with full OpenTelemetry tracing, robust security guardrails, and multiple LLM provider support.**
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/axon-agent.svg)](https://pypi.org/project/axon-agent/)
6
+ [![Python](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
7
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
8
+
9
+ ---
10
+
11
+ ## ✦ Features
12
+
13
+ - **Multi-Model Support:** Switch effortlessly between Google Gemini, Anthropic Claude, OpenAI, and local Ollama models.
14
+ - **PLAN vs. BUILD Modes:**
15
+ - `PLAN`: Read-only analysis and research mode (code modification tools disabled).
16
+ - `BUILD`: Full implementation mode with filesystem editing and terminal execution.
17
+ - **Built-in Security Guardrails:**
18
+ - Path traversal protection (cannot escape project root).
19
+ - Hardcoded secret file blocklist (`.env*`, `.git`, `*.pem`, `*.key`, `id_rsa`).
20
+ - Output truncation guard (prevents context window crashes and huge token bills).
21
+ - Subprocess environment scrubbing (prevents child terminal commands from seeing API keys).
22
+ - Destructive command blocklist (hard-blocks `rm -rf /`, fork bombs, disk formatters).
23
+ - **Full Observability:** Native OpenTelemetry tracing and Langfuse dashboard integration for agent turns, latency, token costs, and tool calls.
24
+ - **Interactive TUI:** Built on Textual with live streaming text, tool execution cards, searchable session history (`/sessions`), and theme customization (`/theme`).
25
+ - **Global Setup Wizard:** Configure API keys once via `axon setup` — saved securely in `~/.axon/config.json`.
26
+
27
+ ---
28
+
29
+ ## 🚀 Quickstart
30
+
31
+ ### 1. Installation
32
+
33
+ Install globally via `pip` or `uv`:
34
+
35
+ ```bash
36
+ pip install axon-agent
37
+ # or
38
+ uv tool install axon-agent
39
+ ```
40
+
41
+ ### 2. Configure API Keys
42
+
43
+ Run the interactive setup wizard:
44
+
45
+ ```bash
46
+ axon setup
47
+ ```
48
+
49
+ Enter your API keys (Google Gemini, Anthropic, or OpenAI) and select your default model.
50
+
51
+ ### 3. Launch Axon
52
+
53
+ Navigate to any project directory and start coding:
54
+
55
+ ```bash
56
+ cd ~/my-project
57
+ axon
58
+ ```
59
+
60
+ ---
61
+
62
+ ## ⌨️ Slash Commands
63
+
64
+ Inside the Axon TUI:
65
+
66
+ | Command | Description |
67
+ |---|---|
68
+ | `/mode` | Toggle between `PLAN` (read-only) and `BUILD` (read-write) modes |
69
+ | `/models` | List available models or switch model (`/models 2`) |
70
+ | `/models add <provider:model>` | Dynamically register a custom model (e.g. `ollama:llama3.3`) |
71
+ | `/config` | View active configuration and key status |
72
+ | `/sessions` | Browse, search, and resume past sessions |
73
+ | `/theme` | Switch color theme (`nightfox`, `catppuccin`, `dracula`, `gruvbox`) |
74
+ | `/new` | Start a fresh chat session |
75
+ | `/clear` | Clear message history in the current session |
76
+ | `/help` | Show command reference |
77
+
78
+ ---
79
+
80
+ ## ⚙️ CLI Options
81
+
82
+ ```bash
83
+ axon --help
84
+
85
+ usage: axon [-h] [--mode {PLAN,BUILD}] [--model MODEL] [--cwd CWD]
86
+ [--resume SESSION_ID] [--theme {nightfox,catppuccin,dracula,gruvbox}]
87
+ [--setup] [{setup,config}]
88
+
89
+ options:
90
+ --mode {PLAN,BUILD} Start in PLAN or BUILD mode (default: BUILD)
91
+ --model MODEL LLM provider string (e.g. google:gemini-2.5-flash)
92
+ --cwd CWD Working directory (default: current directory)
93
+ --resume SESSION_ID Resume a previous session by UUID
94
+ --theme THEME Theme palette (default: nightfox)
95
+ --setup Launch interactive setup wizard
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 🛡️ Architecture & Evals
101
+
102
+ Axon is tested across a 5-pipeline evaluation framework:
103
+ 1. **Correctness:** Ground-truth file inspection and deterministic answers.
104
+ 2. **Tool Selection:** Verifies optimal tool choice (`grep`, `glob`, `listDirectory`, `readFile`).
105
+ 3. **Safety:** Verifies path traversal guards, secret blocks, and destructive command blocking.
106
+ 4. **Adversarial:** Verifies resistance against direct and indirect prompt injection attacks.
107
+ 5. **Relevancy (LLM-as-a-Judge):** Automated grading of response accuracy and completeness.
108
+
109
+ ---
110
+
111
+ ## 📄 License
112
+
113
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "axon-agent"
3
+ version = "0.1.0"
4
+ description = "A CLI coding agent with full observability, multi-model support, and security guardrails"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.12"
8
+ keywords = ["ai", "agent", "coding-assistant", "cli", "tui", "opentelemetry", "langfuse", "pydantic-ai"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Topic :: Software Development :: Code Generators",
18
+ "Topic :: Utilities",
19
+ ]
20
+ dependencies = [
21
+ "pydantic-ai>=0.0.30",
22
+ "python-dotenv>=1.0.1",
23
+ "sqlmodel>=0.0.22",
24
+ "rich>=13.9.4",
25
+ "textual>=8.2.8",
26
+ "opentelemetry-api>=1.44.0",
27
+ "opentelemetry-sdk>=1.44.0",
28
+ "opentelemetry-exporter-otlp>=1.44.0",
29
+ ]
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "pytest",
34
+ "pytest-asyncio",
35
+ "langfuse",
36
+ "ruff",
37
+ "pyright",
38
+ "hatch",
39
+ "twine",
40
+ "build",
41
+ ]
42
+
43
+ [project.scripts]
44
+ axon = "axon.main:main" # creates the `axon` CLI command
45
+
46
+ [build-system]
47
+ requires = ["hatchling"]
48
+ build-backend = "hatchling.build"
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/axon"]
52
+
53
+ [tool.ruff]
54
+ line-length = 88
55
+
56
+ [tool.pyright]
57
+ pythonVersion = "3.12"
58
+
59
+ [tool.pytest.ini_options]
60
+ asyncio_mode = "auto"
61
+ markers = [
62
+ "correctness: Tests that verify the agent produces the right answer",
63
+ "tool_selection: Tests that verify the agent picks the correct tool",
64
+ "safety: Tests that verify the agent respects guardrails",
65
+ "adversarial: Tests that verify the agent resists prompt injection",
66
+ "relevancy: Tests that use LLM-as-a-Judge to grade response quality",
67
+ ]
@@ -0,0 +1,7 @@
1
+ """
2
+ Axon — A CLI coding agent with full observability.
3
+ """
4
+ from axon.config import __version__
5
+ from axon.agent import create_agent
6
+
7
+ __all__ = ["__version__", "create_agent"]
@@ -0,0 +1,31 @@
1
+ """
2
+ Axon agent factory.
3
+
4
+ Instantiates and configures a PydanticAI Agent with:
5
+ - Dynamic system prompt (PLAN vs BUILD mode)
6
+ - OpenTelemetry instrumentation for Langfuse tracing
7
+ - Modular tool registration (filesystem + shell)
8
+ """
9
+ from pydantic_ai import Agent
10
+ from pydantic_ai.capabilities import Instrumentation
11
+
12
+ from axon.system_prompt import build_system_prompt
13
+ from axon.tools import register_tools
14
+
15
+
16
+ def create_agent(mode: str, model_str: str, cwd: str) -> Agent:
17
+ """
18
+ Create an agent instance configured for the given mode, model, and directory.
19
+
20
+ Args:
21
+ mode: "PLAN" (read-only) or "BUILD" (read-write).
22
+ model_str: Model provider string (e.g. "google:gemini-2.5-flash").
23
+ cwd: Working directory root for filesystem and shell operations.
24
+
25
+ Returns:
26
+ Configured PydanticAI Agent ready for execution.
27
+ """
28
+ prompt = build_system_prompt(mode)
29
+ agent = Agent(model_str, system_prompt=prompt, capabilities=[Instrumentation()])
30
+ register_tools(agent, mode=mode, cwd=cwd)
31
+ return agent
@@ -0,0 +1,194 @@
1
+ """
2
+ Central configuration and persistent user settings for Axon.
3
+
4
+ Manages:
5
+ - Constants, defaults, and paths (~/.axon/config.json)
6
+ - Automatic loading of global user API keys into os.environ
7
+ - Dynamic model lists (default models + user-added custom models)
8
+ - Secure config saving with restricted file permissions (0600)
9
+ """
10
+ import json
11
+ import os
12
+ import stat
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Any
15
+
16
+ # Version
17
+ __version__ = "0.1.0"
18
+
19
+ # Defaults
20
+ DEFAULT_FALLBACK_MODEL = "google:gemini-2.5-flash"
21
+ DEFAULT_THEME = "nightfox"
22
+ DEFAULT_MODE = "BUILD"
23
+
24
+ # Standard default models
25
+ DEFAULT_MODELS = [
26
+ "google:gemini-2.5-flash",
27
+ "google:gemini-1.5-pro",
28
+ "google:gemma-4-31b-it",
29
+ "anthropic:claude-3-5-haiku-latest",
30
+ "anthropic:claude-opus-4-5",
31
+ "openai:gpt-4o-mini",
32
+ "openai:gpt-4o",
33
+ ]
34
+
35
+ # Paths
36
+ AXON_DIR = Path.home() / ".axon"
37
+ AXON_DIR.mkdir(parents=True, exist_ok=True)
38
+ CONFIG_PATH = AXON_DIR / "config.json"
39
+ DB_PATH = AXON_DIR / "sessions.db"
40
+ DATABASE_URL = f"sqlite:///{DB_PATH}"
41
+
42
+ # Known API key environment variable names
43
+ KNOWN_KEY_NAMES = [
44
+ "GOOGLE_API_KEY",
45
+ "ANTHROPIC_API_KEY",
46
+ "OPENAI_API_KEY",
47
+ "LANGFUSE_PUBLIC_KEY",
48
+ "LANGFUSE_SECRET_KEY",
49
+ "LANGFUSE_HOST",
50
+ ]
51
+
52
+
53
+ def get_default_config() -> Dict[str, Any]:
54
+ """Return initial default configuration dictionary."""
55
+ return {
56
+ "default_model": DEFAULT_FALLBACK_MODEL,
57
+ "default_theme": DEFAULT_THEME,
58
+ "default_mode": DEFAULT_MODE,
59
+ "models": list(DEFAULT_MODELS),
60
+ "keys": {
61
+ "GOOGLE_API_KEY": "",
62
+ "ANTHROPIC_API_KEY": "",
63
+ "OPENAI_API_KEY": "",
64
+ "LANGFUSE_PUBLIC_KEY": "",
65
+ "LANGFUSE_SECRET_KEY": "",
66
+ "LANGFUSE_HOST": "https://cloud.langfuse.com",
67
+ },
68
+ }
69
+
70
+
71
+ def load_config() -> Dict[str, Any]:
72
+ """Load configuration from ~/.axon/config.json or return defaults."""
73
+ if not CONFIG_PATH.exists():
74
+ return get_default_config()
75
+
76
+ try:
77
+ data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
78
+ # Ensure all required top-level keys exist
79
+ default_cfg = get_default_config()
80
+ for k, v in default_cfg.items():
81
+ if k not in data:
82
+ data[k] = v
83
+ if isinstance(data.get("keys"), dict):
84
+ for key_name, default_val in default_cfg["keys"].items():
85
+ if key_name not in data["keys"]:
86
+ data["keys"][key_name] = default_val
87
+ return data
88
+ except Exception:
89
+ return get_default_config()
90
+
91
+
92
+ def save_config(config_data: Dict[str, Any]) -> None:
93
+ """Save configuration to ~/.axon/config.json with secure 0600 file permissions."""
94
+ AXON_DIR.mkdir(parents=True, exist_ok=True)
95
+ temp_path = CONFIG_PATH.with_suffix(".tmp")
96
+
97
+ # Write JSON formatted
98
+ temp_path.write_text(json.dumps(config_data, indent=2), encoding="utf-8")
99
+
100
+ # Set restricted permissions (owner read/write only) on non-Windows platforms
101
+ try:
102
+ if os.name != "nt":
103
+ temp_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
104
+ except Exception:
105
+ pass
106
+
107
+ temp_path.replace(CONFIG_PATH)
108
+
109
+
110
+ def inject_env_keys() -> None:
111
+ """
112
+ Inject keys from ~/.axon/config.json into os.environ.
113
+ Does NOT overwrite keys already set in the active shell environment.
114
+ """
115
+ cfg = load_config()
116
+ keys = cfg.get("keys", {})
117
+ for key_name, key_val in keys.items():
118
+ if key_val and not os.getenv(key_name):
119
+ os.environ[key_name] = str(key_val).strip()
120
+
121
+
122
+ def get_api_key(name: str) -> Optional[str]:
123
+ """Get an API key by name from os.environ or config.json."""
124
+ if os.getenv(name):
125
+ return os.getenv(name)
126
+ cfg = load_config()
127
+ return cfg.get("keys", {}).get(name) or None
128
+
129
+
130
+ def set_api_key(name: str, value: str) -> None:
131
+ """Save an API key into ~/.axon/config.json and update os.environ."""
132
+ cfg = load_config()
133
+ if "keys" not in cfg:
134
+ cfg["keys"] = {}
135
+ cfg["keys"][name] = value.strip()
136
+ save_config(cfg)
137
+ os.environ[name] = value.strip()
138
+
139
+
140
+ def get_models() -> List[str]:
141
+ """Get full list of available models (defaults + user custom models)."""
142
+ cfg = load_config()
143
+ models = cfg.get("models", DEFAULT_MODELS)
144
+ return list(models) if isinstance(models, list) else list(DEFAULT_MODELS)
145
+
146
+
147
+ def add_custom_model(model_str: str) -> bool:
148
+ """
149
+ Add a custom model identifier string (e.g. 'ollama:llama3.3' or 'openai:gpt-4o')
150
+ to ~/.axon/config.json. Returns True if added, False if already present.
151
+ """
152
+ model_clean = model_str.strip()
153
+ if not model_clean:
154
+ return False
155
+
156
+ cfg = load_config()
157
+ models = cfg.get("models", list(DEFAULT_MODELS))
158
+ if model_clean not in models:
159
+ models.append(model_clean)
160
+ cfg["models"] = models
161
+ save_config(cfg)
162
+ return True
163
+ return False
164
+
165
+
166
+ def has_valid_key() -> bool:
167
+ """
168
+ Check if the user has configured at least one LLM provider key
169
+ (Google Gemini, Anthropic, or OpenAI).
170
+ """
171
+ # Check env first
172
+ if os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY"):
173
+ return True
174
+ if os.getenv("ANTHROPIC_API_KEY"):
175
+ return True
176
+ if os.getenv("OPENAI_API_KEY"):
177
+ return True
178
+
179
+ # Check config.json
180
+ cfg = load_config()
181
+ keys = cfg.get("keys", {})
182
+ return bool(
183
+ keys.get("GOOGLE_API_KEY")
184
+ or keys.get("ANTHROPIC_API_KEY")
185
+ or keys.get("OPENAI_API_KEY")
186
+ )
187
+
188
+
189
+ # Automatically inject stored keys on module import
190
+ inject_env_keys()
191
+
192
+ # Active defaults resolved after injection
193
+ DEFAULT_MODEL = os.getenv("AXON_DEFAULT_MODEL", load_config().get("default_model", DEFAULT_FALLBACK_MODEL))
194
+ SUPPORTED_MODELS = get_models()