fixr-cli 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.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .fixr/
9
+ *.save
10
+ test.py
11
+ test2.py
12
+ script.py
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: fixr-cli
3
+ Version: 0.1.0
4
+ Summary: AI-powered CLI that explains programming errors using a persistent cache and LLMs.
5
+ Project-URL: Homepage, https://github.com/yourusername/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 combo.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install fixr
21
+ # or
22
+ uv add fixr
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ # Direct
29
+ fixr "TypeError: unsupported operand type(s) for +: 'int' and 'str'"
30
+
31
+ # Pipe any command output
32
+ python script.py 2>&1 | fixr
33
+ cargo build 2>&1 | fixr
34
+ node app.js 2>&1 | fixr
35
+ g++ main.cpp 2>&1 | fixr
36
+ ```
37
+
38
+ ## Setup
39
+
40
+ ```bash
41
+ # Set API key
42
+ fixr config --provider groq --api-key gsk_...
43
+
44
+ # Set default provider
45
+ fixr config --provider gemini
46
+
47
+ # OAuth login (Google/Gemini)
48
+ fixr login google
49
+
50
+ # Show current config
51
+ fixr config --show
52
+
53
+ # List all providers
54
+ fixr providers
55
+ ```
56
+
57
+ ## Free tier providers (no credit card)
58
+
59
+ | Provider | Models |
60
+ |---|---|
61
+ | groq | llama-3.3-70b-versatile |
62
+ | gemini | gemini-2.0-flash |
63
+ | mistral | mistral-small |
64
+ | openrouter | 20+ free models |
65
+ | nvidia | 100+ open models |
66
+ | cerebras | llama3.3-70b |
67
+
68
+ ## How it works
69
+
70
+ ```
71
+ error input
72
+
73
+
74
+ normalize + hash (sha256[:16])
75
+
76
+
77
+ cache hit? ──yes──→ instant response ⚡
78
+
79
+ no
80
+
81
+ LLM call (provider of choice)
82
+
83
+
84
+ store in ~/.fixr/cache.json
85
+
86
+
87
+ display fix
88
+ ```
89
+
90
+ Cache lives at `~/.fixr/cache.json`. Gets smarter over time.
@@ -0,0 +1,78 @@
1
+ # fixr ⚡
2
+
3
+ AI-powered CLI that explains errors and suggests fixes — using a hashtable cache + LLM combo.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install fixr
9
+ # or
10
+ uv add fixr
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ # Direct
17
+ fixr "TypeError: unsupported operand type(s) for +: 'int' and 'str'"
18
+
19
+ # Pipe any command output
20
+ python script.py 2>&1 | fixr
21
+ cargo build 2>&1 | fixr
22
+ node app.js 2>&1 | fixr
23
+ g++ main.cpp 2>&1 | fixr
24
+ ```
25
+
26
+ ## Setup
27
+
28
+ ```bash
29
+ # Set API key
30
+ fixr config --provider groq --api-key gsk_...
31
+
32
+ # Set default provider
33
+ fixr config --provider gemini
34
+
35
+ # OAuth login (Google/Gemini)
36
+ fixr login google
37
+
38
+ # Show current config
39
+ fixr config --show
40
+
41
+ # List all providers
42
+ fixr providers
43
+ ```
44
+
45
+ ## Free tier providers (no credit card)
46
+
47
+ | Provider | Models |
48
+ |---|---|
49
+ | groq | llama-3.3-70b-versatile |
50
+ | gemini | gemini-2.0-flash |
51
+ | mistral | mistral-small |
52
+ | openrouter | 20+ free models |
53
+ | nvidia | 100+ open models |
54
+ | cerebras | llama3.3-70b |
55
+
56
+ ## How it works
57
+
58
+ ```
59
+ error input
60
+
61
+
62
+ normalize + hash (sha256[:16])
63
+
64
+
65
+ cache hit? ──yes──→ instant response ⚡
66
+
67
+ no
68
+
69
+ LLM call (provider of choice)
70
+
71
+
72
+ store in ~/.fixr/cache.json
73
+
74
+
75
+ display fix
76
+ ```
77
+
78
+ Cache lives at `~/.fixr/cache.json`. Gets smarter over time.
File without changes
@@ -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)
@@ -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
@@ -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)
@@ -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()
@@ -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,23 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "fixr-cli"
7
+ version = "0.1.0"
8
+ description = "AI-powered CLI that explains programming errors using a persistent cache and LLMs."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "typer>=0.12.0",
13
+ "litellm>=1.40.0",
14
+ "httpx>=0.27.0",
15
+ "rich>=13.0.0",
16
+ ]
17
+
18
+ [project.scripts]
19
+ fxr = "fixr.main:cli"
20
+ fixr = "fixr.main:cli"
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/yourusername/fixr"