ezshell 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: ezshell
3
+ Version: 0.2.0
4
+ Summary: Terminal assistant: explains failed commands and suggests a fix you review before running
5
+ Author: Salomon
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: groq>=0.11.0
11
+ Requires-Dist: prompt_toolkit>=3.0.40
12
+ Requires-Dist: rich>=13.7.0
13
+ Requires-Dist: pyte>=0.8.2
14
+ Requires-Dist: python-dotenv>=1.0.0
15
+ Requires-Dist: textual>=8.0.0
16
+ Requires-Dist: customtkinter>=6.0.0
17
+ Requires-Dist: pillow>=10.0.0
18
+ Requires-Dist: pydantic>=2.5
19
+ Dynamic: license-file
20
+
21
+ <div align="center">
22
+
23
+ # ezShell
24
+
25
+ **Ton terminal t'explique ses erreurs.**
26
+ Une commande échoue ? `Alt+E` explique la cause et place une correction dans ton prompt. Tu relis, tu valides : rien n'est exécuté sans toi.
27
+
28
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
29
+ ![Licence](https://img.shields.io/badge/licence-MIT-green)
30
+ ![Version](https://img.shields.io/badge/version-0.2.0-orange)
31
+
32
+ [Site](https://ezshell.vercel.app) · [Fonctionnalités](#fonctionnalités) · [Installation](#installation) · [Utilisation](#utilisation) · [Licence](#licence)
33
+
34
+ <img src="docs/preview.webp" alt="Aperçu d'ezShell : après « npm run dev » qui échoue avec « vite: command not found », le panneau ezShell explique que vite n'est pas installé et propose la commande npm install -D vite" width="860">
35
+
36
+ </div>
37
+
38
+ ---
39
+
40
+ ## Fonctionnalités
41
+
42
+ - **Diagnostic de la dernière erreur** : la cause probable, en français ou en anglais selon ta locale.
43
+ - **Correction insérée dans le prompt** : la commande arrive dans ta ligne de saisie ; c'est toi qui appuies sur Entrée.
44
+ - **Langage naturel** : tape ta demande (« lister les ports ouverts ») puis `Alt+E` pour obtenir la commande.
45
+ - **Niveau de risque** : faible, moyen ou élevé. Les règles locales d'ezShell relèvent le niveau si le modèle le sous-estime, et une commande à risque élevé exige une confirmation explicite (`y`).
46
+ - **Sans clé API** : ezShell passe par son service hébergé ; ton appareil reçoit un jeton anonyme avec un quota quotidien gratuit.
47
+ - **Secrets masqués** : mots de passe, clés API, jetons, identifiants dans les URL et clés privées sont masqués avant l'envoi, puis une seconde fois sur le serveur.
48
+ - **bash, zsh et fish** sur Linux et macOS, **PowerShell** sur Windows (bêta). Une fenêtre de bureau et une interface plein terminal sont aussi disponibles.
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ Prérequis : Python 3.10 ou plus récent.
55
+
56
+ ```bash
57
+ pipx install ezshell
58
+ ```
59
+
60
+ Puis branche ezShell sur ton shell, en ajoutant la ligne correspondante à la fin de ton fichier de configuration :
61
+
62
+ | Shell | Fichier | Ligne |
63
+ |---|---|---|
64
+ | zsh | `~/.zshrc` | `eval "$(ezsh init zsh)"` |
65
+ | bash | `~/.bashrc` | `eval "$(ezsh init bash)"` |
66
+ | fish | `~/.config/fish/config.fish` | `ezsh init fish \| source` |
67
+ | PowerShell (Windows) | `$PROFILE` | `Invoke-Expression (& ezsh init powershell \| Out-String)` |
68
+
69
+ Pour bash, zsh et PowerShell, `ezsh init --install` ajoute la ligne à ta place. Ouvre ensuite un nouveau terminal.
70
+
71
+ **Windows (bêta) :** installe Python 3.10+ et [pipx](https://pipx.pypa.io/stable/), puis la même commande `pipx install` dans PowerShell (5.1 ou 7, avec PSReadLine). Si PowerShell refuse de charger ton profil, autorise les scripts locaux pour ton compte : `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`. Sous WSL, suis simplement les instructions Linux.
72
+
73
+ **Mettre à jour :** lance `ezsh --update`. ezShell affiche les nouveautés, te demande confirmation (réponse par défaut : non), puis met à jour. Ajoute `--yes` pour passer la confirmation dans un script. Quand une nouvelle version existe, une ligne discrète te le rappelle au plus une fois par jour ; `EZSH_NO_UPDATE_CHECK=1` la désactive. Sous Windows, la commande à lancer dans un nouveau terminal t'est affichée (un programme ne peut pas se remplacer pendant qu'il tourne). Si tu as installé ezShell depuis l'URL git avant la version 0.2.0, réinstalle une dernière fois : `pipx install --force ezshell`.
74
+
75
+ <details>
76
+ <summary>Autres méthodes d'installation</summary>
77
+
78
+ **Dernière version de développement** (pas encore publiée sur PyPI) : `pipx install git+https://github.com/Waddle-io/ezShell.git`
79
+
80
+ **Installateur en une ligne** (installe dans `~/.local/share/ezshell`, commandes dans `~/.local/bin`) :
81
+
82
+ ```bash
83
+ curl -fsSL https://raw.githubusercontent.com/Waddle-io/ezShell/main/install.sh | bash
84
+ ```
85
+
86
+ **Depuis les sources :**
87
+
88
+ ```bash
89
+ git clone https://github.com/Waddle-io/ezShell.git
90
+ cd ezShell
91
+ python3 -m venv .venv
92
+ source .venv/bin/activate
93
+ pip install -e .
94
+ ```
95
+
96
+ La fenêtre de bureau nécessite Tk (`sudo apt install python3-tk` sur Debian/Ubuntu).
97
+ </details>
98
+
99
+ Désinstaller : retire la ligne `ezsh init` de ton fichier de configuration, puis `pipx uninstall ezshell`.
100
+
101
+ ---
102
+
103
+ ## Utilisation
104
+
105
+ ### Dans ton terminal (`Alt+E`)
106
+
107
+ Lance une commande qui échoue (par exemple `git psuh`), puis appuie sur `Alt+E`.
108
+
109
+ | Touche | Action |
110
+ |---|---|
111
+ | `Entrée` | Insérer la commande dans le prompt (risque faible ou moyen) |
112
+ | `y` | Insérer une commande à risque élevé ou sur plusieurs lignes |
113
+ | `e` | Modifier la commande avant de l'insérer |
114
+ | `c` | Copier dans le presse-papiers |
115
+ | `/` | Poser une autre question |
116
+ | `q` / `Échap` | Fermer sans rien insérer |
117
+
118
+ Pour un diagnostic plus précis, lance la commande via `ezsh run` : la fin de sa sortie d'erreur est gardée (et masquée) pour l'analyse.
119
+
120
+ ```bash
121
+ ezsh run -- npm run build
122
+ ```
123
+
124
+ `ezsh suggest --ask "trouver les fichiers de plus de 1 Go"` ouvre le même panneau sans raccourci.
125
+
126
+ ### Autres modes
127
+
128
+ ```bash
129
+ ezsh # fenêtre de bureau (interface terminal si aucun affichage)
130
+ ezsh --tui # interface plein terminal, pratique en SSH
131
+ ezsh --explain "docker: Error response from daemon: port is already allocated"
132
+ ezsh --check-key # vérifie que le service ezShell répond
133
+ ezsh --version
134
+ ```
135
+
136
+ `ezsh --help` liste toutes les options.
137
+
138
+ ### Configuration
139
+
140
+ Aucune configuration n'est nécessaire. Variables utiles :
141
+
142
+ | Variable | Rôle |
143
+ |---|---|
144
+ | `EZSHELL_LANG` | Force la langue (`fr` ou `en`) ; sinon déduite de `$LANG` |
145
+ | `EZSH_API_URL` | URL du service (défaut : `https://ezshell.vercel.app`) |
146
+ | `EZSH_DIRECT=1` | Appelle Groq directement avec ta propre clé (`GROQ_API_KEY` ou `ezsh --set-key`) |
147
+ | `EZSH_MODEL` | Modèle en mode direct (défaut : `openai/gpt-oss-120b`) |
148
+ | `EZSH_STATE_DIR` | Dossier d'état par terminal (défaut : `$XDG_RUNTIME_DIR/ezsh`, `%LOCALAPPDATA%\ezsh` sous Windows) |
149
+ | `EZSH_MOCK_LLM=1` | Simule le modèle, sans réseau (développement) |
150
+
151
+ ---
152
+
153
+ ## Confidentialité
154
+
155
+ Rien n'est envoyé tant que tu n'appuies pas sur `Alt+E`. Sont alors envoyés : ton OS et ton shell, le dossier courant (avec `~` à la place de ton nom), la dernière commande, son code de sortie et, si tu passes par `ezsh run`, la fin de sa sortie d'erreur (4 Ko max). Le service ne conserve pas le contenu des requêtes : seulement un jeton d'appareil haché et des compteurs de quota.
156
+
157
+ ---
158
+
159
+ ## Développement
160
+
161
+ ```bash
162
+ pip install -e . -r requirements-dev.txt
163
+ pytest
164
+ ruff check src tests
165
+ EZSH_MOCK_LLM=1 ezsh suggest --ask "lister les gros fichiers"
166
+ ```
167
+
168
+ Le service hébergé (Next.js + Postgres) est dans [`web/`](web/README.md). Voir aussi [`CONTRIBUTING.md`](CONTRIBUTING.md) et [`DEVELOPMENT.md`](DEVELOPMENT.md).
169
+
170
+ ---
171
+
172
+ ## Licence
173
+
174
+ Distribué sous licence [MIT](LICENSE). © 2026 Salomon.
@@ -0,0 +1,37 @@
1
+ ezshell-0.2.0.dist-info/licenses/LICENSE,sha256=rJwmH6Oshoq2zSpsApfNxIrviaxdUznrORLXWmmyiyk,1068
2
+ src/__init__.py,sha256=iVWzzlVkw7bRh-LeQzDbq8UTa0Sm40bqjOCL-VxyuCw,125
3
+ src/app.py,sha256=TKk8y4727qvz1bLn1oYKv5WxSdAIC60BalvxGk58jyE,10910
4
+ src/i18n.py,sha256=tTANnFhuzWFfN5HF6FhkQR8zjZEpn3QF9tGdaX_qBo0,13253
5
+ src/main.py,sha256=od9jtsTwa3-Vcbme_YInfgzOwfRpq2tvsnJt3Bxtntk,11273
6
+ src/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ src/core/assistant.py,sha256=YGRRN_vk0PQvnW1UvQzVlt6dzVRt8wRDWW0IRF90ljQ,21295
8
+ src/core/capture.py,sha256=3dBnOYrPIjwLJAyn-isCUHf9aqtxjVkOtPyZiGCexvg,7221
9
+ src/core/chat_bridge.py,sha256=Iiz4qIn_7XY25RsRci4KLiHox2onQmN88PvwKihPUXc,3798
10
+ src/core/config.py,sha256=I27XP0W_V_uGVNW6tv3F-Sc66r__sU1YQJS4xs6c92c,9826
11
+ src/core/context.py,sha256=wI2n317cNptzXXGoFe-w_iDHuYrtkY23PDdjyqPruB0,7235
12
+ src/core/groq_client.py,sha256=ppodCJWHPaEEBy2tUgdzSTZWndLHh_lDijC173eIm8g,15258
13
+ src/core/llm_client.py,sha256=Bd0wwHoK_nLeJal6NI4wKmH0Vdmqz4R8vVel5WohIFE,3275
14
+ src/core/prompts.py,sha256=MrX0_t4fOK15_l1eu8usfbGKeK-GbKny7ZrtQ7HxCfo,11519
15
+ src/core/provider.py,sha256=NHsNjZfIUhnkdfh0cc1vfhfzM7a6YKWOS3rvaknOKH8,8431
16
+ src/core/proxy_client.py,sha256=00L8gswliny7b-DBLBBazuBuhxAO1G0ESZ3uXpArEZI,6804
17
+ src/core/pty_shell.py,sha256=SWQGqYxsLGHdRHyPkJ0YuWa4WsJD8DEfWTpVYpbYato,8352
18
+ src/core/redact.py,sha256=SJXMzCl47o1EuwsYWMLHoABJNNNdvpYsdAIGL1pYyvU,4232
19
+ src/core/risk.py,sha256=xdLzblYnHKSvO0k5J2AYpALjSKO2W9O9n-io7pyblZg,14809
20
+ src/core/safety.py,sha256=6jyX-A4i-mwU-4Z8g4Vg-tMOYn2iIVzWs-_5qsf8384,11355
21
+ src/core/sanitize.py,sha256=jEOc5vmb55IbfLC4zk_zx7oWCRbGxqxO93KDXrRXe58,1404
22
+ src/core/schema.py,sha256=Gg1Evw9wTectI4RzbOArOFydm2oEaYhCqLJzhUI_ifc,2049
23
+ src/core/shell_hook.py,sha256=6OlNeIfm0HFyKdHctm54Rllus6X8E4QSOgPhTRYX6QY,15400
24
+ src/core/suggest.py,sha256=6GkqrzymH1lrVqs9NnmJ2nUPkRt82ICS_xTVBCBV11w,3155
25
+ src/core/updater.py,sha256=97ymdYieE8oIH5t4obJ8A8-qvbi2bjSH5mBogYePoWk,16961
26
+ src/ui/chat_tui.py,sha256=QrWYC76dxQpbMGemoDiT7zVfSjHXxqZUIzxCz6qKyAU,20116
27
+ src/ui/gui_window.py,sha256=3MfaEPTM3h710hD-CiuxrRCzP3S0jCmyx2QUyVRsGCI,38987
28
+ src/ui/inline.py,sha256=GZI_JyfSylpQf3y6HHUqfG27D0wGrFzX8NwTR4hiRfE,11076
29
+ src/ui/logo.py,sha256=OnljST95cm73CDvwNGSrKR4b3Do6oRhk_BunmSter-c,7655
30
+ src/ui/panel.py,sha256=J8sR5y5ir8XC2TCTq9w_KZUCddPolaar3r1Z_rbUm6A,7509
31
+ src/ui/prompt.py,sha256=fpoHoGRIFjW3HWIPFbjeNnpRZyiExm0m6vkEu82qAso,5529
32
+ src/ui/render.py,sha256=J_IoWssbeBlwpPv3sHeJbBWyodXN1V2bzpI5K_dL4pE,9092
33
+ ezshell-0.2.0.dist-info/METADATA,sha256=Q64N6GiYSfVDl3LUpdaJqXYSybgw-vPeTD4NeSxobeo,7449
34
+ ezshell-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
35
+ ezshell-0.2.0.dist-info/entry_points.txt,sha256=gP6RfCPZAXVSYoTeIHSCkHecWjQqnKkPyxixomyAmzc,63
36
+ ezshell-0.2.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
37
+ ezshell-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ ezsh = src.main:main
3
+ ezshell = src.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 waddle Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ src
src/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """ezShell. `__version__` est l'UNIQUE source du numéro de version (pyproject.toml le lit d'ici)."""
2
+
3
+ __version__ = "0.2.0"
src/app.py ADDED
@@ -0,0 +1,286 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ from rich.panel import Panel
8
+
9
+ from src.core.assistant import ShellAssistant
10
+ from src.core.config import EzShellConfig
11
+ from src.core.context import CommandHistoryEntry, SystemContext
12
+ from src.core.pty_shell import PTYExecutor
13
+ from src.ui.prompt import create_prompt_session, get_prompt_text
14
+ from src.ui.render import (
15
+ console,
16
+ prompt_fix_action,
17
+ render_api_key_missing_card,
18
+ render_ask_card,
19
+ render_banner,
20
+ render_error,
21
+ render_error_card,
22
+ render_info,
23
+ render_success,
24
+ )
25
+
26
+
27
+ class EzShellApp:
28
+ """Main application orchestrator for ezShell interactive sessions."""
29
+
30
+ def __init__(self, config: Optional[EzShellConfig] = None):
31
+ self.config = config or EzShellConfig.load()
32
+ self.assistant: Optional[ShellAssistant] = None
33
+ self.history_entries: List[CommandHistoryEntry] = []
34
+ self.last_exit_code: int = 0
35
+ self.last_suggested_fix: Optional[str] = None
36
+
37
+ if self.config.get_effective_api_key():
38
+ self.assistant = ShellAssistant(config=self.config)
39
+
40
+ def _handle_cd(self, args: List[str]) -> int:
41
+ """Handles the 'cd' builtin command so directory changes persist."""
42
+ target = args[0] if args else str(Path.home())
43
+ if target == "~":
44
+ target = str(Path.home())
45
+ elif target == "-":
46
+ target = os.environ.get("OLDPWD", str(Path.home()))
47
+
48
+ expanded = os.path.expanduser(os.path.expandvars(target))
49
+ try:
50
+ old_pwd = os.getcwd()
51
+ os.chdir(expanded)
52
+ os.environ["OLDPWD"] = old_pwd
53
+ os.environ["PWD"] = os.getcwd()
54
+ return 0
55
+ except Exception as e:
56
+ render_error(f"cd: {e}")
57
+ return 1
58
+
59
+ def _handle_export(self, line: str) -> int:
60
+ """Handles 'export KEY=VALUE' builtin."""
61
+ parts = line[6:].strip().split("=", 1)
62
+ if len(parts) == 2:
63
+ key, val = parts[0].strip(), parts[1].strip().strip('"').strip("'")
64
+ os.environ[key] = val
65
+ render_success(f"Exported {key}={val}")
66
+ return 0
67
+ else:
68
+ render_error("Usage: export KEY=VALUE")
69
+ return 1
70
+
71
+ def _show_help(self) -> None:
72
+ """Displays ezShell usage guide."""
73
+ help_text = """
74
+ [bold cyan]ezShell Built-in Commands:[/bold cyan]
75
+ [bold]!ask <question>[/bold] Ask AI any command-line, DevOps, or system question
76
+ [bold]!fix[/bold] Re-run or review the last suggested fix command
77
+ [bold]!config[/bold] Inspect active configuration and model settings
78
+ [bold]!help[/bold] Display this help overview
79
+ [bold]clear[/bold] Clear the terminal screen
80
+ [bold]exit[/bold] or [bold]quit[/bold] Leave ezShell
81
+
82
+ [bold cyan]Key Features:[/bold cyan]
83
+ • Automatic failure detection with root cause diagnosis via Groq ([green]openai/gpt-oss-120b[/green])
84
+ • One-click fix execution with built-in destructive command guard
85
+ • Command and path autocompletion ([italic]Tab[/italic]), auto-suggestions ([italic]Right Arrow[/italic])
86
+ • Press [bold]Ctrl+Space[/bold] to quickly begin an [bold]!ask[/bold] query
87
+ """
88
+ console.print(Panel(help_text.strip(), title="ezShell Help", border_style="cyan"))
89
+
90
+ def _show_config(self) -> None:
91
+ """Displays the active configuration."""
92
+ has_key = bool(self.config.get_effective_api_key())
93
+ masked_key = (
94
+ f"...{self.config.get_effective_api_key()[-6:]}"
95
+ if has_key and len(self.config.get_effective_api_key() or "") > 6
96
+ else "[red]Not configured[/red]"
97
+ )
98
+
99
+ cfg_text = f"""
100
+ [bold cyan]Model:[/bold cyan] {self.config.model}
101
+ [bold cyan]API Key:[/bold cyan] {masked_key}
102
+ [bold cyan]Temperature:[/bold cyan] {self.config.temperature}
103
+ [bold cyan]Max Tokens:[/bold cyan] {self.config.max_tokens}
104
+ [bold cyan]Auto Suggest Fix:[/bold cyan] {self.config.auto_suggest_fix}
105
+ [bold cyan]Stream Explanation:[/bold cyan] {self.config.stream_explanation}
106
+ """
107
+ console.print(Panel(cfg_text.strip(), title="ezShell Configuration", border_style="cyan"))
108
+
109
+ def execute_single_command(self, cmd_str: str) -> int:
110
+ """Executes a single command line and returns the exit code."""
111
+ cmd = cmd_str.strip()
112
+ if not cmd:
113
+ return 0
114
+
115
+ # Builtin: cd
116
+ if cmd == "cd" or cmd.startswith("cd "):
117
+ args = cmd.split()[1:]
118
+ code = self._handle_cd(args)
119
+ self.last_exit_code = code
120
+ return code
121
+
122
+ # Builtin: export
123
+ if cmd.startswith("export ") and "=" in cmd:
124
+ code = self._handle_export(cmd)
125
+ self.last_exit_code = code
126
+ return code
127
+
128
+ # Builtin: clear
129
+ if cmd == "clear":
130
+ console.clear()
131
+ return 0
132
+
133
+ # Standard execution via PTY
134
+ result = PTYExecutor.execute(cmd, cwd=os.getcwd())
135
+ self.last_exit_code = result.exit_code
136
+
137
+ # Record in history
138
+ import time
139
+ self.history_entries.append(
140
+ CommandHistoryEntry(command=cmd, exit_code=result.exit_code, timestamp=time.time())
141
+ )
142
+ if len(self.history_entries) > self.config.max_history_items:
143
+ self.history_entries.pop(0)
144
+
145
+ # Handle failure if command failed
146
+ if result.exit_code != 0:
147
+ self._handle_command_failure(result.command, result.exit_code, result.clean_output)
148
+
149
+ return result.exit_code
150
+
151
+ def _handle_command_failure(self, command: str, exit_code: int, clean_output: str) -> None:
152
+ """Triggers AI diagnosis and presents fix options to the user."""
153
+ if not self.config.get_effective_api_key():
154
+ console.print(
155
+ "\n[dim #3b4261]─[/] [#e0af68] Command failed[/] "
156
+ "[dim #565f89]• Set GROQ_API_KEY in .env.local to enable instant AI fixes[/]\n"
157
+ )
158
+ return
159
+
160
+ if self.assistant is None:
161
+ self.assistant = ShellAssistant(config=self.config)
162
+
163
+ context = SystemContext.collect(history=self.history_entries)
164
+
165
+ with console.status(
166
+ f"[cyan]Analyzing failure with Groq ({self.config.model})...[/cyan]",
167
+ spinner="dots",
168
+ ):
169
+ try:
170
+ diagnostic = self.assistant.diagnose_failure(
171
+ command=command,
172
+ exit_code=exit_code,
173
+ error_output=clean_output,
174
+ context=context,
175
+ )
176
+ except Exception as e:
177
+ render_error(f"Error consulting Groq assistant: {e}")
178
+ return
179
+
180
+ # Render diagnostic card
181
+ render_error_card(diagnostic)
182
+
183
+ if diagnostic.suggested_fix and self.config.auto_suggest_fix:
184
+ self.last_suggested_fix = diagnostic.suggested_fix
185
+ action = prompt_fix_action(
186
+ fix_command=diagnostic.suggested_fix,
187
+ safety_report=diagnostic.safety_report,
188
+ )
189
+ if action == "run":
190
+ console.print(f"[cyan]Executing fix:[/cyan] [bold]{diagnostic.suggested_fix}[/bold]\n")
191
+ self.execute_single_command(diagnostic.suggested_fix)
192
+ elif action == "edit":
193
+ # Will return control back to prompt with suggested command as default
194
+ render_info("Tip: Copy and edit the suggested fix command above.")
195
+
196
+ def run(self) -> None:
197
+ """Starts the interactive ezShell REPL loop."""
198
+ initial_context = SystemContext.collect()
199
+ render_banner(self.config.model, initial_context.shell_name)
200
+
201
+ if not self.config.get_effective_api_key():
202
+ render_api_key_missing_card()
203
+
204
+ session = create_prompt_session()
205
+
206
+ while True:
207
+ try:
208
+ context = SystemContext.collect(history=self.history_entries)
209
+ prompt_html = get_prompt_text(context, self.last_exit_code)
210
+
211
+ user_input = session.prompt(prompt_html)
212
+ line = user_input.strip()
213
+
214
+ if not line:
215
+ continue
216
+
217
+ # Exit check
218
+ if line in ("exit", "quit"):
219
+ console.print("[dim]Goodbye from ezShell![/dim]")
220
+ break
221
+
222
+ # Builtin: help
223
+ if line == "!help":
224
+ self._show_help()
225
+ continue
226
+
227
+ # Builtin: config
228
+ if line == "!config":
229
+ self._show_config()
230
+ continue
231
+
232
+ # Builtin: fix
233
+ if line == "!fix":
234
+ if self.last_suggested_fix:
235
+ console.print(f"[cyan]Running last suggested fix:[/cyan] {self.last_suggested_fix}\n")
236
+ self.execute_single_command(self.last_suggested_fix)
237
+ else:
238
+ render_info("No recent fix available.")
239
+ continue
240
+
241
+ # Builtin: ask
242
+ if line.startswith("!ask"):
243
+ query = line[4:].strip()
244
+ if not query:
245
+ render_info("Usage: !ask <your question here>")
246
+ continue
247
+ self._handle_ask(query, context)
248
+ continue
249
+
250
+ # Standard command execution
251
+ self.execute_single_command(line)
252
+
253
+ except (KeyboardInterrupt, EOFError):
254
+ console.print("\n[dim]ezShell session terminated.[/dim]")
255
+ break
256
+ except Exception as e:
257
+ render_error(f"Unexpected error: {e}")
258
+
259
+ def _handle_ask(self, query: str, context: SystemContext) -> None:
260
+ """Handles natural language inquiry with AI assistant."""
261
+ if not self.config.get_effective_api_key():
262
+ render_error("Cannot execute !ask: GROQ_API_KEY is not configured.")
263
+ return
264
+
265
+ if self.assistant is None:
266
+ self.assistant = ShellAssistant(config=self.config)
267
+
268
+ with console.status(f"[cyan]Asking Groq ({self.config.model})...[/cyan]", spinner="dots"):
269
+ try:
270
+ result = self.assistant.ask(query, context)
271
+ except Exception as e:
272
+ render_error(f"Error querying Groq: {e}")
273
+ return
274
+
275
+ render_ask_card(result)
276
+
277
+ if result.suggested_fix:
278
+ self.last_suggested_fix = result.suggested_fix
279
+ action = prompt_fix_action(
280
+ fix_command=result.suggested_fix,
281
+ safety_report=result.safety_report,
282
+ )
283
+ if action == "run":
284
+ console.print(f"[cyan]Executing:[/cyan] [bold]{result.suggested_fix}[/bold]\n")
285
+ self.execute_single_command(result.suggested_fix)
286
+
src/core/__init__.py ADDED
File without changes