fixr-cli 0.1.3__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.
fixr/__init__.py ADDED
File without changes
fixr/auth.py ADDED
@@ -0,0 +1,57 @@
1
+ import webbrowser
2
+ import httpx
3
+ import json
4
+ from pathlib import Path
5
+ from . import config as cfg
6
+
7
+ # OAuth configs per provider (add more as providers support it)
8
+ OAUTH_CONFIGS = {
9
+ "google": {
10
+ "auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
11
+ "token_url": "https://oauth2.googleapis.com/token",
12
+ "scope": "https://www.googleapis.com/auth/generative-language",
13
+ "client_id_env": "GOOGLE_CLIENT_ID",
14
+ "client_secret_env": "GOOGLE_CLIENT_SECRET",
15
+ }
16
+ }
17
+
18
+ def set_api_key(provider: str, key: str) -> None:
19
+ cfg.set_key(provider, key)
20
+
21
+ def get_api_key(provider: str) -> str | None:
22
+ return cfg.get_key(provider)
23
+
24
+ def oauth_login(provider: str) -> str | None:
25
+ """
26
+ Opens browser for OAuth flow. Returns access token or None.
27
+ Currently supports: google (Gemini)
28
+ """
29
+ import os
30
+ oc = OAUTH_CONFIGS.get(provider)
31
+ if not oc:
32
+ raise ValueError(f"OAuth not supported for provider: {provider}")
33
+
34
+ client_id = os.environ.get(oc["client_id_env"])
35
+ if not client_id:
36
+ raise ValueError(f"Set {oc['client_id_env']} env var to use OAuth for {provider}")
37
+
38
+ # Build auth URL (device flow simplified)
39
+ params = {
40
+ "client_id": client_id,
41
+ "response_type": "code",
42
+ "scope": oc["scope"],
43
+ "redirect_uri": "urn:ietf:wg:oauth:2.0:oob",
44
+ "access_type": "offline",
45
+ }
46
+ from urllib.parse import urlencode
47
+ url = f"{oc['auth_url']}?{urlencode(params)}"
48
+ webbrowser.open(url)
49
+ return url # User pastes code manually in CLI
50
+
51
+ def save_oauth_token(provider: str, token: str) -> None:
52
+ c = cfg.load()
53
+ c.setdefault("auth_tokens", {})[provider] = token
54
+ cfg.save(c)
55
+
56
+ def get_oauth_token(provider: str) -> str | None:
57
+ return cfg.load().get("auth_tokens", {}).get(provider)
fixr/cache.py ADDED
@@ -0,0 +1,42 @@
1
+ import json
2
+ import hashlib
3
+ from pathlib import Path
4
+ from .config import FIXR_DIR
5
+
6
+ CACHE_FILE = FIXR_DIR / "cache.json"
7
+
8
+ def _normalize(error: str) -> str:
9
+ """Strip line numbers and memory addresses for stable cache keys."""
10
+ import re
11
+ s = re.sub(r'line \d+', 'line N', error)
12
+ s = re.sub(r'0x[0-9a-fA-F]+', '0xADDR', s)
13
+ s = re.sub(r'\s+', ' ', s).strip().lower()
14
+ return s
15
+
16
+ def _key(error: str) -> str:
17
+ return hashlib.sha256(_normalize(error).encode()).hexdigest()[:16]
18
+
19
+ def _load() -> dict:
20
+ FIXR_DIR.mkdir(exist_ok=True)
21
+ if not CACHE_FILE.exists():
22
+ return {}
23
+ try:
24
+ return json.loads(CACHE_FILE.read_text())
25
+ except Exception:
26
+ return {}
27
+
28
+ def _save(cache: dict) -> None:
29
+ CACHE_FILE.write_text(json.dumps(cache, indent=2))
30
+
31
+ def get(error: str) -> str | None:
32
+ return _load().get(_key(error))
33
+
34
+ def set(error: str, solution: str) -> None:
35
+ cache = _load()
36
+ cache[_key(error)] = solution
37
+ _save(cache)
38
+
39
+ def clear() -> int:
40
+ count = len(_load())
41
+ _save({})
42
+ return count
fixr/config.py ADDED
@@ -0,0 +1,61 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ FIXR_DIR = Path.home() / ".fixr"
5
+ CONFIG_FILE = FIXR_DIR / "config.json"
6
+
7
+ DEFAULTS = {
8
+ "provider": "groq",
9
+ "model": "groq/llama-3.3-70b-versatile",
10
+ "api_keys": {},
11
+ "auth_tokens": {},
12
+ }
13
+
14
+ def load() -> dict:
15
+ FIXR_DIR.mkdir(exist_ok=True)
16
+ if not CONFIG_FILE.exists():
17
+ save(DEFAULTS.copy())
18
+ return DEFAULTS.copy()
19
+ return json.loads(CONFIG_FILE.read_text())
20
+
21
+ def save(cfg: dict) -> None:
22
+ FIXR_DIR.mkdir(exist_ok=True)
23
+ CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
24
+
25
+ def set_key(provider: str, key: str) -> None:
26
+ cfg = load()
27
+ cfg["api_keys"][provider] = key
28
+ save(cfg)
29
+
30
+ def get_key(provider: str) -> str | None:
31
+ cfg = load()
32
+ return cfg["api_keys"].get(provider)
33
+
34
+ def set_default(provider: str, model: str) -> None:
35
+ cfg = load()
36
+ cfg["provider"] = provider
37
+ cfg["model"] = model
38
+ save(cfg)
39
+ def get_models(provider: str) -> list:
40
+ cfg = load()
41
+ defaults = {
42
+ "groq": ["groq/llama-3.3-70b-versatile", "groq/llama-3.1-8b-instant", "groq/mixtral-8x7b-32768"],
43
+ "gemini": ["gemini/gemini-2.0-flash", "gemini/gemini-2.5-pro", "gemini/gemini-1.5-pro"],
44
+ "mistral": ["mistral/mistral-small-latest", "mistral/mistral-large-latest", "mistral/codestral-latest"],
45
+ "openai": ["openai/gpt-4o-mini", "openai/gpt-4o", "openai/o4-mini"],
46
+ "anthropic": ["anthropic/claude-haiku-4-5-20251001", "anthropic/claude-sonnet-4-6", "anthropic/claude-opus-4-6"],
47
+ "openrouter": ["openrouter/meta-llama/llama-3.3-70b-instruct:free", "openrouter/mistralai/mistral-7b-instruct:free", "openrouter/google/gemma-3-27b-it:free"],
48
+ "cerebras": ["cerebras/llama-3.3-70b", "cerebras/llama-3.1-8b", "cerebras/llama-3.1-70b"],
49
+ "nvidia": ["nvidia_nim/meta/llama-3.3-70b-instruct", "nvidia_nim/mistralai/mistral-7b-instruct-v0.3", "nvidia_nim/google/gemma-3-27b-it"],
50
+ "ollama": ["ollama/llama3.3", "ollama/mistral", "ollama/codellama"],
51
+ "cohere": ["cohere/command-r-plus", "cohere/command-r", "cohere/command-a-03-2025"],
52
+ }
53
+ custom = cfg.get("custom_models", {}).get(provider, [])
54
+ return defaults.get(provider, []) + custom
55
+
56
+ def add_model(provider: str, model: str) -> None:
57
+ cfg = load()
58
+ cfg.setdefault("custom_models", {}).setdefault(provider, [])
59
+ if model not in cfg["custom_models"][provider]:
60
+ cfg["custom_models"][provider].append(model)
61
+ save(cfg)
fixr/llm.py ADDED
@@ -0,0 +1,91 @@
1
+ import os
2
+ from . import config as cfg
3
+
4
+ PROVIDER_MODELS = {
5
+ "groq": "groq/llama-3.3-70b-versatile",
6
+ "gemini": "gemini/gemini-2.0-flash",
7
+ "mistral": "mistral/mistral-small-latest",
8
+ "openai": "openai/gpt-4o-mini",
9
+ "anthropic": "anthropic/claude-haiku-4-5-20251001",
10
+ "ollama": "ollama/llama3",
11
+ "openrouter": "openrouter/meta-llama/llama-3.3-70b-instruct:free",
12
+ "nvidia": "nvidia_nim/meta/llama-3.3-70b-instruct",
13
+ "cerebras": "cerebras/llama3.3-70b",
14
+ "cohere": "cohere/command-r-plus",
15
+ }
16
+
17
+ PROVIDER_ENV_KEYS = {
18
+ "groq": "GROQ_API_KEY",
19
+ "gemini": "GEMINI_API_KEY",
20
+ "mistral": "MISTRAL_API_KEY",
21
+ "openai": "OPENAI_API_KEY",
22
+ "anthropic": "ANTHROPIC_API_KEY",
23
+ "openrouter": "OPENROUTER_API_KEY",
24
+ "nvidia": "NVIDIA_NIM_API_KEY",
25
+ "cerebras": "CEREBRAS_API_KEY",
26
+ "cohere": "COHERE_API_KEY",
27
+ }
28
+
29
+ PROMPT = """\
30
+ You are a senior software engineer. Analyze the error below. Be precise, technical, and concise.
31
+
32
+ Respond in EXACTLY this format — no extra text, no deviation:
33
+
34
+ ERROR TYPE: <NameError | TypeError | SyntaxError | ImportError | RuntimeError | LogicError | Other>
35
+ SEVERITY: <Critical | High | Medium | Low>
36
+
37
+ EXPLANATION:
38
+ <2 sentences max. Name the exact variable, line, or function. Say WHY it failed, not just what failed.>
39
+
40
+ ROOT CAUSE:
41
+ <1 sentence. The single deepest technical reason.>
42
+
43
+ FIX:
44
+ ```python
45
+ <minimal working code that fixes the issue>
46
+ ```
47
+
48
+ PREVENTION:
49
+ <1 sentence. Actionable rule. Start with "Always" or "Never".>
50
+
51
+ ERROR:
52
+ {error}
53
+ """
54
+
55
+ def resolve_key(provider: str) -> str | None:
56
+ """Check config store first, then env var."""
57
+ key = cfg.get_key(provider)
58
+ if key:
59
+ return key
60
+ env = PROVIDER_ENV_KEYS.get(provider)
61
+ return os.environ.get(env) if env else None
62
+
63
+ def _set_env_key(provider: str, key: str) -> None:
64
+ env = PROVIDER_ENV_KEYS.get(provider)
65
+ if env and key:
66
+ os.environ[env] = key
67
+
68
+ def ask(error: str, provider: str | None = None, model: str | None = None) -> str:
69
+ from litellm import completion
70
+
71
+ conf = cfg.load()
72
+ provider = provider or conf.get("provider", "groq")
73
+ model = model or conf.get("model") or PROVIDER_MODELS.get(provider, "groq/llama-3.3-70b-versatile")
74
+
75
+ key = resolve_key(provider)
76
+ if not key:
77
+ raise ValueError(
78
+ f"No API key found for '{provider}'.\n"
79
+ f"Run: fxr config --provider {provider} --api-key YOUR_KEY\n"
80
+ f"Or: fxr setup"
81
+ )
82
+ _set_env_key(provider, key)
83
+
84
+
85
+
86
+ resp = completion(
87
+ model=model,
88
+ messages=[{"role": "user", "content": PROMPT.format(error=error.strip())}],
89
+ max_tokens=600,
90
+ )
91
+ return resp.choices[0].message.content.strip()
fixr/main.py ADDED
@@ -0,0 +1,221 @@
1
+ import sys
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.markdown import Markdown
8
+ from rich.panel import Panel
9
+
10
+ from . import cache, llm, auth, config as cfg
11
+ from .llm import PROVIDER_MODELS
12
+
13
+ app = typer.Typer(help="fixr — AI error explainer with smart caching")
14
+ console = Console()
15
+
16
+
17
+ def _read_stdin() -> str | None:
18
+ if not sys.stdin.isatty():
19
+ return sys.stdin.read().strip()
20
+ return None
21
+
22
+ def _run_fix(err: str, provider=None, model=None, no_cache=False):
23
+ if not no_cache:
24
+ cached = cache.get(err)
25
+ if cached:
26
+ _display(cached, cached=True)
27
+ return
28
+ with console.status("[cyan]Analyzing error...[/cyan]"):
29
+ try:
30
+ solution = llm.ask(err, provider=provider, model=model)
31
+ except ValueError as e:
32
+ console.print(f"[bold red]✗ Config error:[/bold red] {e}")
33
+ raise typer.Exit(1)
34
+ except Exception as e:
35
+ console.print(f"[bold red]✗ LLM error:[/bold red] {e}")
36
+ raise typer.Exit(1)
37
+
38
+
39
+ def _display(solution: str, cached: bool = False):
40
+ from rich.syntax import Syntax
41
+ from rich.rule import Rule
42
+ import re
43
+
44
+ title = "[green]fixr ⚡ cached[/green]" if cached else "[cyan]fixr[/cyan]"
45
+ console.print()
46
+ console.rule(f"[bold]{title}[/bold]")
47
+
48
+ # Extract and style each section
49
+ sections = {
50
+ "ERROR TYPE": "bold red",
51
+ "SEVERITY": "bold yellow",
52
+ "EXPLANATION": "white",
53
+ "ROOT CAUSE": "bold white",
54
+ "FIX": None, # handled separately for code
55
+ "PREVENTION": "green",
56
+ }
57
+
58
+ current = solution
59
+ for section, style in sections.items():
60
+ pattern = rf"{section}:\n?(.*?)(?=\n[A-Z ]+:|$)"
61
+ match = re.search(pattern, current, re.DOTALL)
62
+ if not match:
63
+ continue
64
+ content = match.group(1).strip()
65
+
66
+ if section == "FIX":
67
+ console.print(f"\n[bold cyan]● FIX[/bold cyan]")
68
+ # extract code block
69
+ code_match = re.search(r"```(?:\w+)?\n?(.*?)```", content, re.DOTALL)
70
+ if code_match:
71
+ code = code_match.group(1).strip()
72
+ syntax = Syntax(code, "python", theme="dracula", line_numbers=True)
73
+ console.print(syntax)
74
+ else:
75
+ console.print(content)
76
+ elif section in ("ERROR TYPE", "SEVERITY"):
77
+ console.print(f"[{style}]{section}:[/{style}] {content}")
78
+ else:
79
+ console.print(f"\n[bold cyan]● {section}[/bold cyan]")
80
+ console.print(f"[{style}]{content}[/{style}]")
81
+
82
+ console.rule()
83
+ console.print()
84
+
85
+ @app.callback(invoke_without_command=True)
86
+ def main(ctx: typer.Context):
87
+ """fixr — paste an error or a script file, get a fix."""
88
+ if ctx.invoked_subcommand is not None:
89
+ return
90
+
91
+ stdin_error = _read_stdin()
92
+ if stdin_error:
93
+ _run_fix(stdin_error)
94
+ return
95
+ console.print("[yellow]Usage: fxr 'error message' or fxr script.py[/yellow]")
96
+
97
+ @app.command(name="fix", help="Explain an error and suggest a fix.")
98
+ def fix(
99
+ error: str = typer.Argument(None, help="Error string to analyze"),
100
+ provider: str = typer.Option(None, "--provider", "-p", help="LLM provider"),
101
+ model: str = typer.Option(None, "--model", "-m", help="Model string"),
102
+ no_cache: bool = typer.Option(False, "--no-cache", help="Skip cache lookup"),
103
+ ):
104
+ stdin_error = _read_stdin()
105
+ err = stdin_error or error
106
+ if not err:
107
+ err = typer.prompt("Paste your error")
108
+ _run_fix(err, provider=provider, model=model, no_cache=no_cache)
109
+
110
+
111
+ @app.command()
112
+ def config(
113
+ provider: str = typer.Option(None, "--provider", "-p", help="Set default provider"),
114
+ model: str = typer.Option(None, "--model", "-m", help="Set default model"),
115
+ api_key: str = typer.Option(None, "--api-key", "-k", help="Set API key for provider"),
116
+ show: bool = typer.Option(False, "--show", help="Show current config"),
117
+ ):
118
+ """Configure default provider, model, and API keys."""
119
+ if show:
120
+ c = cfg.load()
121
+ keys = {k: v[:8] + "..." for k, v in c.get("api_keys", {}).items() if v}
122
+ console.print(Panel(
123
+ f"Provider: [cyan]{c.get('provider')}[/cyan]\n"
124
+ f"Model: [cyan]{c.get('model')}[/cyan]\n"
125
+ f"Keys: {keys}",
126
+ title="fixr config"
127
+ ))
128
+ return
129
+ if api_key and provider:
130
+ auth.set_api_key(provider, api_key)
131
+ console.print(f"[green]✓[/green] API key saved for [cyan]{provider}[/cyan]")
132
+ if provider or model:
133
+ p = provider or cfg.load().get("provider", "groq")
134
+ m = model or PROVIDER_MODELS.get(p, "groq/llama-3.3-70b-versatile")
135
+ cfg.set_default(p, m)
136
+ console.print(f"[green]✓[/green] Default set to [cyan]{p}[/cyan] / [cyan]{m}[/cyan]")
137
+
138
+
139
+ @app.command()
140
+ def login(
141
+ provider: str = typer.Argument(..., help="Provider to OAuth login (e.g. google)"),
142
+ ):
143
+ """Login via OAuth (browser-based). Currently supports: google."""
144
+ try:
145
+ url = auth.oauth_login(provider)
146
+ console.print(f"[cyan]Browser opened.[/cyan] If not, visit:\n{url}")
147
+ code = typer.prompt("Paste the auth code here")
148
+ auth.save_oauth_token(provider, code)
149
+ console.print(f"[green]✓[/green] Token saved for [cyan]{provider}[/cyan]")
150
+ except ValueError as e:
151
+ console.print(f"[red]{e}[/red]")
152
+ raise typer.Exit(1)
153
+
154
+
155
+ @app.command()
156
+ def providers():
157
+ """List all supported providers and their default models."""
158
+ console.print("\n[bold]Supported Providers[/bold]\n")
159
+ free = {"groq", "gemini", "mistral", "openrouter", "nvidia", "cerebras"}
160
+ for p, m in PROVIDER_MODELS.items():
161
+ tier = "[green]free tier[/green]" if p in free else "[yellow]paid[/yellow]"
162
+ console.print(f" [cyan]{p:<12}[/cyan] {tier:<20} {m}")
163
+ console.print()
164
+
165
+
166
+ @app.command()
167
+ def clear_cache():
168
+ """Clear the local error cache."""
169
+ n = cache.clear()
170
+ console.print(f"[green]✓[/green] Cleared {n} cached entries.")
171
+
172
+
173
+ @app.command()
174
+ def setup():
175
+ """Interactive setup wizard."""
176
+ console.print("\n[bold cyan]fxr setup[/bold cyan]\n")
177
+
178
+ providers_list = list(PROVIDER_MODELS.keys())
179
+ free = {"groq", "gemini", "mistral", "openrouter", "cerebras", "ollama", "nvidia"}
180
+ for i, p in enumerate(providers_list):
181
+ tier = "[green]free[/green]" if p in free else "[yellow]paid[/yellow]"
182
+ console.print(f" {i+1}. {p} ({tier})")
183
+
184
+ choice = typer.prompt("\nSelect provider number", default="1")
185
+ provider = providers_list[int(choice) - 1]
186
+
187
+ models = cfg.get_models(provider)
188
+ console.print(f"\n[bold]Models for {provider}:[/bold]")
189
+ for i, m in enumerate(models):
190
+ console.print(f" {i+1}. {m}")
191
+ console.print(f" {len(models)+1}. Enter custom model")
192
+
193
+ mchoice = typer.prompt("Select model number", default="1")
194
+ midx = int(mchoice) - 1
195
+ if midx == len(models):
196
+ model = typer.prompt("Enter model string")
197
+ cfg.add_model(provider, model)
198
+ else:
199
+ model = models[midx]
200
+
201
+ api_key = typer.prompt(f"\nPaste your {provider} API key", hide_input=True)
202
+ auth.set_api_key(provider, api_key)
203
+ cfg.set_default(provider, model)
204
+ console.print(f"\n[green]✓[/green] Config saved — {provider} / {model}")
205
+ console.print("\n[bold #50fa7b]✓ Setup complete! Run: fxr \"your error\" or fxr script.py[/bold #50fa7b]")
206
+
207
+
208
+ @app.command(name="add-model")
209
+ def add_model_cmd(
210
+ provider: str = typer.Argument(..., help="Provider name"),
211
+ model: str = typer.Argument(..., help="Model string"),
212
+ ):
213
+ """Add a custom model to a provider's list."""
214
+ cfg.add_model(provider, model)
215
+ console.print(f"[green]✓[/green] Added [cyan]{model}[/cyan] to [cyan]{provider}[/cyan]")
216
+
217
+ def cli():
218
+ known = {"fix", "config", "login", "providers", "clear-cache", "setup", "add-model", "--help", "-h"}
219
+ if len(sys.argv) > 1 and sys.argv[1] not in known and not sys.argv[1].startswith("--"):
220
+ sys.argv.insert(1, "fix")
221
+ app()
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: fixr-cli
3
+ Version: 0.1.3
4
+ Summary: AI-powered CLI that explains programming errors using a persistent cache and LLMs.
5
+ Project-URL: Homepage, https://github.com/Udhay090/fixr
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: httpx>=0.27.0
8
+ Requires-Dist: litellm>=1.40.0
9
+ Requires-Dist: rich>=13.0.0
10
+ Requires-Dist: typer>=0.12.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # fixr ⚡
14
+
15
+ > AI-powered CLI that explains errors and suggests fixes — using a hashtable cache + LLM hybrid.
16
+
17
+ ```bash
18
+ pip install fixr-cli
19
+ ```
20
+
21
+ ---
22
+
23
+ ## How it works
24
+
25
+ ```
26
+ your error
27
+
28
+
29
+ SHA256 cache lookup ─> hit ─> instant fix ⚡ (no LLM call)
30
+
31
+ miss
32
+
33
+ LiteLLM → Groq / Gemini / Mistral / OpenAI / Anthropic / ...
34
+
35
+
36
+ cache result → show fix
37
+ ```
38
+
39
+ Identical errors are resolved instantly from cache. The tool gets faster the more you use it.
40
+
41
+ ---
42
+
43
+ ## Usage
44
+
45
+ ```bash
46
+ # Run any file — fxr captures the error automatically
47
+ fxr script.py
48
+ fxr main.rs
49
+ fxr app.js
50
+ fxr main.cpp
51
+ fxr Main.java
52
+ fxr main.go
53
+
54
+ # Paste an error directly
55
+ fxr "TypeError: unsupported operand type(s) for +: 'int' and 'str'"
56
+
57
+ # Pipe any command
58
+ python script.py 2>&1 | fxr
59
+ cargo build 2>&1 | fxr
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Setup
65
+
66
+ ```bash
67
+ # 1. Install
68
+ pip install fixr-cli
69
+ # or
70
+ uv add fixr-cli
71
+
72
+ # 2. Run setup wizard (select provider, model, paste API key)
73
+ fxr setup
74
+ ```
75
+
76
+ Setup takes 30 seconds. Free API keys work — no credit card needed.
77
+
78
+ ---
79
+
80
+ ## Free Tier Providers
81
+
82
+ | Provider | Free API | Speed |
83
+ |---|---|---|
84
+ | [Groq](https://console.groq.com) | ✅ | ⚡⚡ Faster (free) |
85
+ | [Cerebras](https://inference.cerebras.ai) | ✅ | ⚡ Fast (free) |
86
+ | [Gemini](https://aistudio.google.com) | ✅ | ✅ Good |
87
+ | [Mistral](https://console.mistral.ai) | ✅ | ✅ Good |
88
+ | [OpenRouter](https://openrouter.ai) | ✅ | ✅ Good |
89
+ | Ollama | ✅ Local | Depends on hardware |
90
+ | OpenAI | ❌ Paid | ⚡⚡⚡ Fastest overall |
91
+ | Anthropic | ❌ Paid | ⚡ Fast |
92
+
93
+ Default: **Groq → llama-3.3-70b-versatile**
94
+
95
+ ---
96
+
97
+ ## Commands
98
+
99
+ ```bash
100
+ fxr setup # interactive setup wizard
101
+ fxr providers # list all providers + models
102
+ fxr config --show # show current config
103
+ fxr config --provider groq --api-key # set API key
104
+ fxr add-model <provider> <model> # add custom model
105
+ fxr clear-cache # wipe local cache
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Languages Supported
111
+
112
+ Python · JavaScript · TypeScript · Rust · C · C++ · Java · Go · Ruby · PHP · Bash · Lua · Perl · R · Swift · Kotlin
113
+
114
+ ---
115
+
116
+ ## Architecture
117
+
118
+ ```
119
+ fixr/
120
+ ├── main.py # Typer CLI — commands + cli() entrypoint
121
+ ├── cache.py # SHA256 hashtable — ~/.fixr/cache.json
122
+ ├── llm.py # LiteLLM routing — 10+ providers
123
+ ├── config.py # Config store — ~/.fixr/config.json
124
+ └── auth.py # API key storage + OAuth scaffold
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Stack
130
+
131
+ Python · Typer · LiteLLM · Rich · Hatchling · uv
132
+
133
+ ---
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,10 @@
1
+ fixr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ fixr/auth.py,sha256=z-4bnuLTfDQXh8Ulhl44x4yid_kc-7uTL0-gc4N5Lmw,1808
3
+ fixr/cache.py,sha256=FDgl9pHR9m0zJjC-hVCNz_wHIfYpfOaJw_tDxg14I_o,1053
4
+ fixr/config.py,sha256=57y9ucmX9EHZIAWqk3arl842Lkf0EGtcNjD_X6BBGZs,2475
5
+ fixr/llm.py,sha256=K7l5nerD_jA5IB3A8Y3-lv1T1EFu9eKspDRndjrZXrc,2760
6
+ fixr/main.py,sha256=0XoA5phWK5APDe_kr-pMrvXDcnzTxx_qkuJv80owl3o,8055
7
+ fixr_cli-0.1.3.dist-info/METADATA,sha256=P2QZBG3SL9K_ipQurhe8X_8m4hyYi0Mq9d6C_Orx4I8,2992
8
+ fixr_cli-0.1.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
9
+ fixr_cli-0.1.3.dist-info/entry_points.txt,sha256=eDTlLn5OI5nvANOmLTlDtYhkdep-v3ONWLELx48z-8w,59
10
+ fixr_cli-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ fixr = fixr.main:cli
3
+ fxr = fixr.main:cli