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.
src/core/capture.py ADDED
@@ -0,0 +1,213 @@
1
+ """Capture de la dernière commande, de son code de sortie et de la fin de sa sortie d'erreur.
2
+
3
+ Prototype M0 (DEVELOPMENT.md §7.3). Deux sources, la plus riche gagne :
4
+
5
+ 1. ``ezsh run -- <cmd>`` : exécute la commande, laisse stdout/stdin au terminal,
6
+ recopie stderr en direct et en garde la fin (~4 Ko). Écrit ``<session>.run.json``.
7
+ 2. Hooks shell (preexec/precmd, PROMPT_COMMAND) : écrivent uniquement la commande
8
+ et le code de sortie (``<session>.cmd`` / ``<session>.code``) : pas de sortie.
9
+
10
+ Aucun contenu n'est envoyé ici : le masquage des secrets se fait avant l'envoi.
11
+ On ne rejoue JAMAIS la commande échouée (effets de bord).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import re
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ import tempfile
23
+ import threading
24
+ import time
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+ from typing import List, Optional
28
+
29
+ STDERR_TAIL_BYTES = 4096
30
+ IS_WINDOWS = os.name == "nt"
31
+
32
+
33
+ def state_dir() -> Path:
34
+ """Dossier d'état par utilisateur (0700), identique à celui calculé par les hooks."""
35
+ override = os.environ.get("EZSH_STATE_DIR")
36
+ if override:
37
+ base = Path(override)
38
+ elif IS_WINDOWS:
39
+ # Même chemin que le hook PowerShell : %LOCALAPPDATA%\ezsh (privé à l'utilisateur).
40
+ local = os.environ.get("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
41
+ base = Path(local) / "ezsh"
42
+ else:
43
+ runtime = os.environ.get("XDG_RUNTIME_DIR")
44
+ base = (
45
+ Path(runtime) / "ezsh"
46
+ if runtime
47
+ else Path(tempfile.gettempdir()) / f"ezsh-{os.getuid()}"
48
+ )
49
+ base.mkdir(mode=0o700, parents=True, exist_ok=True)
50
+ return base
51
+
52
+
53
+ def session_key() -> str:
54
+ """Un état par terminal : EZSH_SESSION est exporté par les hooks (PID du shell)."""
55
+ return os.environ.get("EZSH_SESSION") or str(os.getppid())
56
+
57
+
58
+ @dataclass
59
+ class LastRun:
60
+ command: str
61
+ exit_code: Optional[int]
62
+ stderr_tail: str = ""
63
+ cwd: str = ""
64
+ captured_output: bool = False # True si la sortie d'erreur a été capturée
65
+
66
+
67
+ def _atomic_write(path: Path, text: str) -> None:
68
+ tmp = path.with_suffix(path.suffix + ".tmp")
69
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
70
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
71
+ fh.write(text)
72
+ os.replace(tmp, path)
73
+
74
+
75
+ def run_and_capture(argv: List[str]) -> int:
76
+ """Exécute ``argv`` en gardant la fin de stderr. Retourne le code de sortie de la commande."""
77
+ if not argv:
78
+ print("ezsh run: aucune commande fournie (usage : ezsh run -- <commande>)", file=sys.stderr)
79
+ return 2
80
+ tail = bytearray()
81
+ lock = threading.Lock()
82
+ launch = argv
83
+ if IS_WINDOWS and not shutil.which(argv[0]):
84
+ # Cmdlet ou alias PowerShell (Get-ChildItem, dir…) : on le lance via PowerShell.
85
+ exe = shutil.which("pwsh") or shutil.which("powershell")
86
+ if exe:
87
+ launch = [exe, "-NoLogo", "-NoProfile", "-Command", _powershell_script(argv)]
88
+ try:
89
+ proc = subprocess.Popen(launch, stderr=subprocess.PIPE)
90
+ except FileNotFoundError:
91
+ msg = f"ezsh: commande introuvable : {argv[0]}\n"
92
+ sys.stderr.write(msg)
93
+ _store(argv, 127, msg.encode())
94
+ return 127
95
+ except PermissionError:
96
+ msg = f"ezsh: permission refusée : {argv[0]}\n"
97
+ sys.stderr.write(msg)
98
+ _store(argv, 126, msg.encode())
99
+ return 126
100
+
101
+ def pump() -> None:
102
+ assert proc.stderr is not None
103
+ for chunk in iter(lambda: proc.stderr.read1(4096), b""): # type: ignore[attr-defined]
104
+ try:
105
+ sys.stderr.buffer.write(chunk)
106
+ sys.stderr.buffer.flush()
107
+ except Exception:
108
+ pass
109
+ with lock:
110
+ tail.extend(chunk)
111
+ if len(tail) > STDERR_TAIL_BYTES:
112
+ del tail[: len(tail) - STDERR_TAIL_BYTES]
113
+
114
+ t = threading.Thread(target=pump, daemon=True)
115
+ t.start()
116
+ # Ctrl+C arrive aussi à l'enfant (même terminal) : on le laisse décider de s'arrêter
117
+ # au lieu de le tuer, pour ne pas casser les programmes qui gèrent SIGINT (REPL, psql…).
118
+ while True:
119
+ try:
120
+ code = proc.wait()
121
+ break
122
+ except KeyboardInterrupt:
123
+ continue
124
+ if code < 0: # tué par un signal (Unix) : convention shell 128 + n
125
+ code = 128 - code
126
+ t.join(timeout=2)
127
+ _store(argv, code, bytes(tail))
128
+ return code
129
+
130
+
131
+ def _ps_quote(arg: str) -> str:
132
+ """Argument PowerShell : nu s'il est sûr (garde -Recurse comme paramètre), sinon '…'."""
133
+ if re.fullmatch(r"[\w\-.:/\\=+,@%]+", arg):
134
+ return arg
135
+ return "'" + arg.replace("'", "''") + "'"
136
+
137
+
138
+ def _powershell_script(argv: List[str]) -> str:
139
+ """Script PowerShell qui lance `argv` et reproduit les codes de sortie d'un shell :
140
+ 127 si la commande n'existe pas, le code du programme natif, sinon 1 en cas d'échec."""
141
+ name = argv[0].replace("'", "''")
142
+ return (
143
+ f"if (-not (Get-Command -Name '{name}' -ErrorAction SilentlyContinue)) {{ "
144
+ f"[Console]::Error.WriteLine('ezsh: commande introuvable : {name}'); exit 127 }}; "
145
+ f"& {' '.join(_ps_quote(a) for a in argv)}; "
146
+ "if (-not $?) { if ($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
147
+ )
148
+
149
+
150
+ def _store(argv: List[str], code: int, tail: bytes) -> None:
151
+ if IS_WINDOWS:
152
+ command = subprocess.list2cmdline(argv)
153
+ else:
154
+ import shlex
155
+
156
+ command = " ".join(shlex.quote(a) for a in argv)
157
+ payload = {
158
+ "command": command,
159
+ "exit_code": code,
160
+ "stderr_tail": tail.decode("utf-8", errors="replace"),
161
+ "cwd": os.getcwd(),
162
+ "ts": time.time(),
163
+ }
164
+ try:
165
+ _atomic_write(state_dir() / f"{session_key()}.run.json", json.dumps(payload))
166
+ except OSError:
167
+ pass # la capture est un confort : ne jamais casser la commande
168
+
169
+
170
+ def _read(path: Path) -> Optional[str]:
171
+ try:
172
+ return path.read_text(encoding="utf-8", errors="replace")
173
+ except OSError:
174
+ return None
175
+
176
+
177
+ def read_last_run() -> Optional[LastRun]:
178
+ """Dernière commande de CE terminal. Utilise la capture ``run`` si elle est la plus récente."""
179
+ d, key = state_dir(), session_key()
180
+ cmd_p, code_p, run_p = d / f"{key}.cmd", d / f"{key}.code", d / f"{key}.run.json"
181
+
182
+ run = None
183
+ raw = _read(run_p)
184
+ if raw:
185
+ try:
186
+ run = json.loads(raw)
187
+ except json.JSONDecodeError:
188
+ run = None
189
+
190
+ # Les hooks suppriment run.json dès qu'une autre commande tourne :
191
+ # s'il existe, c'est le plus récent.
192
+ if run:
193
+ return LastRun(
194
+ run["command"],
195
+ run.get("exit_code"),
196
+ run.get("stderr_tail", ""),
197
+ run.get("cwd", ""),
198
+ True,
199
+ )
200
+
201
+ hook_cmd = (_read(cmd_p) or "").strip()
202
+ hook_code = (_read(code_p) or "").strip()
203
+
204
+ if hook_cmd:
205
+ try:
206
+ code = int(hook_code) if hook_code else None
207
+ except ValueError:
208
+ code = None
209
+ return LastRun(hook_cmd, code, "", "", False)
210
+ return None
211
+
212
+
213
+ __all__ = ["run_and_capture", "read_last_run", "LastRun", "state_dir", "session_key"]
@@ -0,0 +1,107 @@
1
+ """Pont entre les interfaces GUI / TUI / --explain et le fournisseur de `Alt+E`.
2
+
3
+ Toutes les interfaces passent ainsi par le même chemin : proxy ezShell (ou Groq direct avec
4
+ EZSH_DIRECT=1, ou mode simulé), validation JSON, nettoyage, plancher de risque. Le résultat
5
+ est converti en `ChatMessageResult` pour que les interfaces existantes n'aient qu'à l'afficher.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional
11
+
12
+ from src.core.assistant import ChatMessageResult
13
+ from src.core.provider import (
14
+ LLMProvider,
15
+ MissingKey,
16
+ ProviderError,
17
+ ProviderOffline,
18
+ QuotaExceeded,
19
+ UpdateRequired,
20
+ get_provider,
21
+ )
22
+ from src.core.redact import redact_secrets
23
+ from src.core.safety import RiskLevel, SafetyReport
24
+ from src.core.schema import InvalidResponse
25
+ from src.core.suggest import build_request, detect_lang, get_suggestion
26
+ from src.ui.panel import tr
27
+
28
+ _RISK = {"low": RiskLevel.SAFE, "medium": RiskLevel.WARNING, "high": RiskLevel.DANGEROUS}
29
+
30
+
31
+ def ask(
32
+ query: str = "",
33
+ *,
34
+ mode: str = "translate",
35
+ command: str = "",
36
+ provider: Optional[LLMProvider] = None,
37
+ ) -> ChatMessageResult:
38
+ """`mode` : translate (question libre), explain_error (dernier échec), explain_command."""
39
+ if mode == "explain_error":
40
+ req = build_request("")
41
+ if command and not req.last_command:
42
+ req.last_command = command
43
+ if query: # texte d'erreur fourni à la main (--explain)
44
+ req.stderr_tail = redact_secrets(query)[-4096:]
45
+ req.exit_code = req.exit_code if req.exit_code is not None else 1
46
+ elif mode == "explain_command":
47
+ req = build_request(command or query)
48
+ req.mode = "explain_command"
49
+ else:
50
+ req = build_request(query)
51
+
52
+ s = get_suggestion(req, provider or get_provider())
53
+ parts = [s.response.explanation]
54
+ if s.response.cause:
55
+ parts.append(f"{tr(req.lang)['cause']} : {s.response.cause}")
56
+ commands = [s.command] if s.command else []
57
+ reports = [
58
+ SafetyReport(
59
+ command=s.command,
60
+ risk_level=_RISK.get(s.risk, RiskLevel.WARNING),
61
+ reason=s.risk_reason,
62
+ requires_explicit_confirmation=s.risk == "high",
63
+ )
64
+ ] if s.command else []
65
+ return ChatMessageResult(
66
+ content="\n\n".join(p for p in parts if p),
67
+ suggested_commands=commands,
68
+ safety_reports=reports,
69
+ )
70
+
71
+
72
+ def provider_label(provider: LLMProvider, config=None) -> str:
73
+ """Libellé court du fournisseur actif, pour les barres d'état."""
74
+ from urllib.parse import urlparse
75
+
76
+ from src.core.provider import MockProvider
77
+ from src.core.proxy_client import ProxyProvider
78
+
79
+ if isinstance(provider, MockProvider):
80
+ return "Mock · offline"
81
+ if isinstance(provider, ProxyProvider):
82
+ return f"ezShell · {urlparse(provider.base_url).hostname or 'proxy'}"
83
+ model = getattr(provider, "model", "") or (config.get_effective_model() if config else "")
84
+ return f"Groq · {model}".rstrip(" ·")
85
+
86
+
87
+ def describe_error(exc: Exception) -> str:
88
+ """Message court et traduit pour une erreur du fournisseur (mêmes textes que `Alt+E`)."""
89
+ t = tr(detect_lang())
90
+ if isinstance(exc, ProviderOffline):
91
+ return t["offline"]
92
+ if isinstance(exc, UpdateRequired):
93
+ return t["update_required"]
94
+ if isinstance(exc, QuotaExceeded):
95
+ if exc.scope == "anonymous":
96
+ return t["trial"]
97
+ return t["quota"].format(scope=exc.scope, reset=exc.reset_at or "?")
98
+ if isinstance(exc, MissingKey):
99
+ return t["nokey"]
100
+ if isinstance(exc, InvalidResponse):
101
+ return t["invalid"]
102
+ if isinstance(exc, ProviderError):
103
+ return t["error"]
104
+ return f"{t['error']} ({type(exc).__name__})"
105
+
106
+
107
+ __all__ = ["ask", "describe_error"]
src/core/config.py ADDED
@@ -0,0 +1,238 @@
1
+ """
2
+ ezShell Configuration Management
3
+ ==================================
4
+ This module defines the ``EzShellConfig`` dataclass, which centralises every
5
+ user-configurable setting for the ezShell application. Configuration is
6
+ resolved through a layered precedence chain so that more specific sources
7
+ always win over more general ones:
8
+
9
+ 1. Local ``.env.local`` / ``.env`` file in the current working directory
10
+ 2. Process-level environment variables (``GROQ_API_KEY``, ``EZSHELL_MODEL``)
11
+ 3. Persisted user config file (``~/.ezshell/config.json`` or a custom path)
12
+ 4. Hard-coded default values defined in this module
13
+
14
+ The :func:`EzShellConfig.load` classmethod merges all sources automatically,
15
+ while :func:`EzShellConfig.save` serialises the current settings back to
16
+ disk as JSON.
17
+
18
+ ---
19
+
20
+ Gestion de la configuration d'ezShell
21
+ =======================================
22
+ Ce module définit le dataclass ``EzShellConfig``, qui centralise tous les
23
+ paramètres configurables par l'utilisateur pour l'application ezShell.
24
+ La configuration est résolue selon une chaîne de priorité : les sources
25
+ plus spécifiques remplacent toujours les sources plus générales.
26
+
27
+ 1. Fichier ``.env.local`` / ``.env`` dans le répertoire de travail courant
28
+ 2. Variables d'environnement du processus (``GROQ_API_KEY``, ``EZSHELL_MODEL``)
29
+ 3. Fichier de configuration utilisateur persisté (``~/.ezshell/config.json``)
30
+ 4. Valeurs par défaut définies dans ce module
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import os
37
+ from dataclasses import asdict, dataclass
38
+ from pathlib import Path
39
+ from typing import Optional
40
+
41
+ from dotenv import load_dotenv
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Module-level constants
45
+ # ---------------------------------------------------------------------------
46
+
47
+ #: Default directory where ezShell stores its persistent configuration.
48
+ DEFAULT_CONFIG_DIR = Path.home() / ".ezshell"
49
+
50
+ #: Full path to the default JSON configuration file.
51
+ DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_DIR / "config.json"
52
+
53
+ #: Groq model identifier used when no model is specified by the user.
54
+ DEFAULT_MODEL = "openai/gpt-oss-120b"
55
+
56
+ #: Supported provider identifiers.
57
+ SUPPORTED_PROVIDERS: list[str] = ["groq", "openai", "anthropic", "ollama"]
58
+
59
+ #: Default model per provider.
60
+ PROVIDER_DEFAULT_MODELS: dict[str, str] = {
61
+ "groq": DEFAULT_MODEL,
62
+ "openai": "gpt-4o-mini",
63
+ "anthropic": "claude-3-5-haiku-20241022",
64
+ "ollama": "llama3",
65
+ }
66
+
67
+
68
+ @dataclass
69
+ class EzShellConfig:
70
+ """Centralised, immutable-style configuration for an ezShell session.
71
+
72
+ All fields have sensible defaults so that ``EzShellConfig()`` is always
73
+ a valid, usable configuration without any environment setup.
74
+
75
+ Attributes:
76
+ groq_api_key (Optional[str]): Groq platform API key used to
77
+ authenticate requests. When ``None``, the value is looked up
78
+ from the ``GROQ_API_KEY`` environment variable at runtime via
79
+ :meth:`get_effective_api_key`.
80
+ model (str): Groq model identifier (e.g. ``"openai/gpt-oss-120b"``).
81
+ Overridable via the ``EZSHELL_MODEL`` environment variable.
82
+ temperature (float): Sampling temperature passed to the language
83
+ model. Lower values (closer to 0) produce more deterministic
84
+ output; higher values increase creativity. Default is ``0.2``.
85
+ max_tokens (int): Maximum number of tokens the model may generate in
86
+ a single response. Default is ``1024``.
87
+ auto_suggest_fix (bool): When ``True``, ezShell automatically
88
+ proposes a corrected command after a shell error without waiting
89
+ for an explicit user request. Default is ``True``.
90
+ stream_explanation (bool): When ``True``, model explanations are
91
+ printed incrementally as tokens arrive rather than buffered and
92
+ displayed all at once. Default is ``True``.
93
+ max_history_items (int): Maximum number of shell-command history
94
+ entries retained in the session context sent to the model.
95
+ Default is ``100``.
96
+ """
97
+
98
+ groq_api_key: Optional[str] = None
99
+ model: str = DEFAULT_MODEL
100
+ temperature: float = 0.2
101
+ max_tokens: int = 1024
102
+ auto_suggest_fix: bool = True
103
+ stream_explanation: bool = True
104
+ max_history_items: int = 100
105
+ # --- multi-provider ---
106
+ provider: str = "groq"
107
+ openai_api_key: Optional[str] = None
108
+ anthropic_api_key: Optional[str] = None
109
+ ollama_base_url: str = "http://localhost:11434"
110
+
111
+ @classmethod
112
+ def load(cls, cwd: Optional[Path] = None, config_path: Optional[Path] = None) -> EzShellConfig:
113
+ """Build an ``EzShellConfig`` instance by merging all configuration sources.
114
+
115
+ Sources are applied from lowest to highest precedence so that each
116
+ subsequent source can selectively override earlier values:
117
+
118
+ 1. User config JSON file (``~/.ezshell/config.json`` or *config_path*)
119
+ 2. Process environment variables (``GROQ_API_KEY``, ``EZSHELL_MODEL``)
120
+ 3. ``.env.local`` / ``.env`` dotenv file in *cwd* (loaded into the
121
+ environment before step 2 is evaluated, but ``override=False``
122
+ prevents it from overwriting already-set variables)
123
+ 4. Dataclass field defaults for any key not supplied by the above
124
+
125
+ Args:
126
+ cwd (Optional[Path]): Directory to search for ``.env.local`` /
127
+ ``.env`` files. Defaults to ``Path.cwd()`` when ``None``.
128
+ config_path (Optional[Path]): Explicit path to a JSON config file.
129
+ When ``None``, :data:`DEFAULT_CONFIG_PATH` is used.
130
+
131
+ Returns:
132
+ EzShellConfig: A fully initialised configuration object whose
133
+ field values reflect the merged result of all available sources.
134
+ """
135
+ # 1. Load local .env files if present
136
+ current_dir = cwd or Path.cwd()
137
+ env_local = current_dir / ".env.local"
138
+ env_default = current_dir / ".env"
139
+ # Prefer .env.local over .env; override=False keeps already-set vars intact
140
+ if env_local.is_file():
141
+ load_dotenv(dotenv_path=env_local, override=False)
142
+ elif env_default.is_file():
143
+ load_dotenv(dotenv_path=env_default, override=False)
144
+
145
+ # 2. Base dict from user config JSON file if it exists
146
+ data: dict = {}
147
+ target_config = config_path or DEFAULT_CONFIG_PATH
148
+ if target_config.is_file():
149
+ try:
150
+ with open(target_config, "r", encoding="utf-8") as f:
151
+ file_data = json.load(f)
152
+ if isinstance(file_data, dict):
153
+ data.update(file_data)
154
+ except Exception:
155
+ # Silently ignore malformed or unreadable config files so that
156
+ # a corrupted file never prevents ezShell from starting.
157
+ pass
158
+
159
+ # 3. Environment variables take precedence over config file
160
+ env_map = {
161
+ "GROQ_API_KEY": "groq_api_key",
162
+ "OPENAI_API_KEY": "openai_api_key",
163
+ "ANTHROPIC_API_KEY": "anthropic_api_key",
164
+ "EZSHELL_MODEL": "model",
165
+ "EZSHELL_PROVIDER": "provider",
166
+ "OLLAMA_BASE_URL": "ollama_base_url",
167
+ }
168
+ for env_var, field_name in env_map.items():
169
+ val = os.environ.get(env_var)
170
+ if val:
171
+ data[field_name] = val
172
+
173
+ valid_keys = {
174
+ "groq_api_key", "model", "temperature", "max_tokens",
175
+ "auto_suggest_fix", "stream_explanation", "max_history_items",
176
+ "provider", "openai_api_key", "anthropic_api_key", "ollama_base_url",
177
+ }
178
+ filtered_data = {k: v for k, v in data.items() if k in valid_keys}
179
+
180
+ return cls(**filtered_data)
181
+
182
+ def save(self, path: Optional[Path] = None) -> None:
183
+ """Persist the current configuration to a JSON file on disk.
184
+
185
+ Args:
186
+ path (Optional[Path]): Destination. Defaults to ``~/.ezshell/config.json``.
187
+
188
+ Raises:
189
+ OSError: If the file cannot be created or written.
190
+ """
191
+ target_path = path or DEFAULT_CONFIG_PATH
192
+ target_path.parent.mkdir(parents=True, exist_ok=True)
193
+ with open(target_path, "w", encoding="utf-8") as f:
194
+ json.dump(asdict(self), f, indent=2)
195
+
196
+ def get_effective_api_key(self) -> Optional[str]:
197
+ """Return the API key for the currently active provider.
198
+
199
+ Ollama is local — it requires no key and returns ``None``.
200
+
201
+ Returns:
202
+ Optional[str]: Active API key, or ``None``.
203
+ """
204
+ if self.provider == "openai":
205
+ return self.openai_api_key or os.environ.get("OPENAI_API_KEY")
206
+ if self.provider == "anthropic":
207
+ return self.anthropic_api_key or os.environ.get("ANTHROPIC_API_KEY")
208
+ if self.provider == "ollama":
209
+ return None
210
+ # Default: groq
211
+ return self.groq_api_key or os.environ.get("GROQ_API_KEY")
212
+
213
+ def get_effective_model(self) -> str:
214
+ """Return the model name to use for the active provider.
215
+
216
+ If :attr:`model` is still the Groq default and the provider has
217
+ changed, returns the canonical default for that provider.
218
+
219
+ Returns:
220
+ str: Model identifier for the active provider.
221
+ """
222
+ if self.model == DEFAULT_MODEL and self.provider in PROVIDER_DEFAULT_MODELS:
223
+ return PROVIDER_DEFAULT_MODELS[self.provider]
224
+ return self.model
225
+
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # Public API surface
229
+ # ---------------------------------------------------------------------------
230
+
231
+ __all__ = [
232
+ "DEFAULT_CONFIG_DIR",
233
+ "DEFAULT_CONFIG_PATH",
234
+ "DEFAULT_MODEL",
235
+ "SUPPORTED_PROVIDERS",
236
+ "PROVIDER_DEFAULT_MODELS",
237
+ "EzShellConfig",
238
+ ]