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 +0 -0
- fixr/auth.py +57 -0
- fixr/cache.py +42 -0
- fixr/config.py +61 -0
- fixr/llm.py +91 -0
- fixr/main.py +221 -0
- fixr_cli-0.1.3.dist-info/METADATA +137 -0
- fixr_cli-0.1.3.dist-info/RECORD +10 -0
- fixr_cli-0.1.3.dist-info/WHEEL +4 -0
- fixr_cli-0.1.3.dist-info/entry_points.txt +3 -0
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,,
|