mycode-cli 0.8.10__py3-none-any.whl → 0.9.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.
- mycode_cli/config.py +278 -115
- mycode_cli/permissions.py +1 -1
- mycode_cli/runtime.py +2 -2
- mycode_cli/server/routers/chat.py +135 -149
- mycode_cli/server/routers/sessions.py +8 -6
- mycode_cli/server/routers/settings.py +2 -174
- mycode_cli/server/run_manager.py +49 -1
- mycode_cli/server/static/assets/{EditDiff-BwTMgMSU.js → EditDiff-B0IHjo9H.js} +1 -1
- mycode_cli/server/static/assets/{angular-html-B6qE7Fgr.js → angular-html-1A3-6kDn.js} +1 -1
- mycode_cli/server/static/assets/{angular-ts-CCYXQBsQ.js → angular-ts-DHbksxnM.js} +1 -1
- mycode_cli/server/static/assets/{apl-BHFdPdwP.js → apl-BaBAc5eF.js} +1 -1
- mycode_cli/server/static/assets/{astro-CdDbCEi0.js → astro-COnAvojq.js} +1 -1
- mycode_cli/server/static/assets/{blade-tLtw_1xC.js → blade-TOINzxFP.js} +1 -1
- mycode_cli/server/static/assets/{c-gShqJt5_.js → c-Nf_dof3C.js} +1 -1
- mycode_cli/server/static/assets/{cobol-CKazzhoF.js → cobol-C5VZrCE3.js} +1 -1
- mycode_cli/server/static/assets/{coffee-DsdLebDY.js → coffee-DZrxMgkq.js} +1 -1
- mycode_cli/server/static/assets/{cpp-Cwf1vUUo.js → cpp-sXfSrOsh.js} +1 -1
- mycode_cli/server/static/assets/{crystal-D2D2rLOi.js → crystal-BjlKCmcB.js} +1 -1
- mycode_cli/server/static/assets/{css-DCF53qDU.js → css-DIFAVgam.js} +1 -1
- mycode_cli/server/static/assets/{edge-DQH05KUf.js → edge-DF8HkBtE.js} +1 -1
- mycode_cli/server/static/assets/{elixir-2VpsbiLV.js → elixir-qnpJ3Nvm.js} +1 -1
- mycode_cli/server/static/assets/{elm-_CHnbifk.js → elm-BL2EN5T8.js} +1 -1
- mycode_cli/server/static/assets/{engine-compile-B24DC8pd.js → engine-compile-F5yGEj2z.js} +3 -3
- mycode_cli/server/static/assets/{erb-Cxmz5KSP.js → erb-QTsb__7o.js} +1 -1
- mycode_cli/server/static/assets/{git-rebase-C04jMORK.js → git-rebase-0I7RXJtl.js} +1 -1
- mycode_cli/server/static/assets/{glimmer-js-D0Nvbzz0.js → glimmer-js-Bs8WRGgm.js} +1 -1
- mycode_cli/server/static/assets/{glimmer-ts-ix22zR5c.js → glimmer-ts-D9emdqIC.js} +1 -1
- mycode_cli/server/static/assets/{glsl-C4sVYnlm.js → glsl-D9ulPUIi.js} +1 -1
- mycode_cli/server/static/assets/{graphql-CmkcYwk_.js → graphql-DUJZ9UiP.js} +1 -1
- mycode_cli/server/static/assets/{hack-BDK0ocxa.js → hack-DYifHe-0.js} +1 -1
- mycode_cli/server/static/assets/{haml-wLZoRSkX.js → haml-Bz_OM7oT.js} +1 -1
- mycode_cli/server/static/assets/{handlebars-Bq6qdew-.js → handlebars-BjKJLgms.js} +1 -1
- mycode_cli/server/static/assets/{highlighter-Dlz8vejz.js → highlighter-w4GLV421.js} +1 -1
- mycode_cli/server/static/assets/{html-j61brix0.js → html-ClcZt6DK.js} +1 -1
- mycode_cli/server/static/assets/{html-derivative-B16zJ1uM.js → html-derivative-BaRvfEoQ.js} +1 -1
- mycode_cli/server/static/assets/{http-0baH_3Dk.js → http-D-6KTEZs.js} +1 -1
- mycode_cli/server/static/assets/{hurl-CR5A6vqw.js → hurl-DG0n-OrQ.js} +1 -1
- mycode_cli/server/static/assets/{index-_MPb8SvH.js → index-CRjupeqA.js} +4 -4
- mycode_cli/server/static/assets/{java-B_NtVZr9.js → java-tcLPA8Yi.js} +1 -1
- mycode_cli/server/static/assets/{javascript-BJVvvhBF.js → javascript-CNvuR0fU.js} +1 -1
- mycode_cli/server/static/assets/{jinja-C_N7wio_.js → jinja-BUA2of-S.js} +1 -1
- mycode_cli/server/static/assets/{jison-DVVdkBIk.js → jison-D-xje2pK.js} +1 -1
- mycode_cli/server/static/assets/{json-sM0x8hNn.js → json-BDX8WktD.js} +1 -1
- mycode_cli/server/static/assets/{jsx-C9BVFirF.js → jsx-B7wMhlSu.js} +1 -1
- mycode_cli/server/static/assets/{julia-ChaP_3F1.js → julia-pLGJrApL.js} +1 -1
- mycode_cli/server/static/assets/{just-CavrxuQ2.js → just-CkNBJHi-.js} +1 -1
- mycode_cli/server/static/assets/{latex-DzKiPX3o.js → latex-q0kn6Pcj.js} +1 -1
- mycode_cli/server/static/assets/{liquid-C7DJS0P7.js → liquid-B--iPi74.js} +1 -1
- mycode_cli/server/static/assets/{lua-BqiiO-68.js → lua-BSzNx2dN.js} +1 -1
- mycode_cli/server/static/assets/{marko-D6qURI0q.js → marko-CHA5nb2O.js} +1 -1
- mycode_cli/server/static/assets/{mdc-CcWXXq58.js → mdc-Ckuw-Vv5.js} +1 -1
- mycode_cli/server/static/assets/{nginx-CpKQIPTm.js → nginx-VrfEBzs7.js} +1 -1
- mycode_cli/server/static/assets/{nim-8_0pTVwq.js → nim-DzAmBQZE.js} +1 -1
- mycode_cli/server/static/assets/{perl-D3eGItpN.js → perl-BUFrzUfQ.js} +1 -1
- mycode_cli/server/static/assets/{php-DxTfLPo4.js → php-Bm4RKslR.js} +1 -1
- mycode_cli/server/static/assets/{pug-Czlo7T9J.js → pug-n_U-KK_5.js} +1 -1
- mycode_cli/server/static/assets/{qml-DcXsrGQw.js → qml-B9YHUmta.js} +1 -1
- mycode_cli/server/static/assets/{r-p_dzGn9P.js → r-Dw_ezJH6.js} +1 -1
- mycode_cli/server/static/assets/{razor-DYXsAW5_.js → razor-yv2hRY5M.js} +1 -1
- mycode_cli/server/static/assets/{regexp-T_fsBtNr.js → regexp-D4Ukx3kJ.js} +1 -1
- mycode_cli/server/static/assets/{rst-ORuymkQc.js → rst-C6v-SMfD.js} +1 -1
- mycode_cli/server/static/assets/{ruby-ClzjSQ0l.js → ruby-CYSDCpk-.js} +1 -1
- mycode_cli/server/static/assets/{sas-BRTK3la1.js → sas-DrkbyhMO.js} +1 -1
- mycode_cli/server/static/assets/{scss-DS6I3QcS.js → scss-SpHo1rm1.js} +1 -1
- mycode_cli/server/static/assets/{shellscript-CiTYCSib.js → shellscript-DbHlMLhv.js} +1 -1
- mycode_cli/server/static/assets/{shellsession-D5DT705X.js → shellsession-DWVDoPWl.js} +1 -1
- mycode_cli/server/static/assets/{soy-NNJOX3cw.js → soy-D1a_iX7F.js} +1 -1
- mycode_cli/server/static/assets/{sql-Buaa9QxE.js → sql-CefQnlGw.js} +1 -1
- mycode_cli/server/static/assets/{stata-ntax-hDY.js → stata-BxFGeD9k.js} +1 -1
- mycode_cli/server/static/assets/{surrealql-DOI7nkTm.js → surrealql-h0ka8_Su.js} +1 -1
- mycode_cli/server/static/assets/{svelte-BtWRbgsA.js → svelte-I0t2JQun.js} +1 -1
- mycode_cli/server/static/assets/{templ-BXLB83Z5.js → templ-BibZqt9i.js} +1 -1
- mycode_cli/server/static/assets/{tex-DkP4hQm8.js → tex-BjKBBM3h.js} +1 -1
- mycode_cli/server/static/assets/{ts-tags-BhFXwTms.js → ts-tags-D1PtKTiF.js} +1 -1
- mycode_cli/server/static/assets/{tsx-Ct4UJ72v.js → tsx-C9yQch3d.js} +1 -1
- mycode_cli/server/static/assets/{twig-BVS_f6WM.js → twig-CNmz35zb.js} +1 -1
- mycode_cli/server/static/assets/{typescript-31zaVSb0.js → typescript-CqchtJT1.js} +1 -1
- mycode_cli/server/static/assets/{vue-axzl95W-.js → vue-CP0lvd8t.js} +1 -1
- mycode_cli/server/static/assets/{vue-html-BZJQggZA.js → vue-html-CEXjbQ_2.js} +1 -1
- mycode_cli/server/static/assets/{vue-vine-C0SHPvOF.js → vue-vine-CZuLbSRK.js} +1 -1
- mycode_cli/server/static/assets/{xml-DXLyvx5e.js → xml-BVrV4sh8.js} +1 -1
- mycode_cli/server/static/assets/{xsl-C1DaZKBE.js → xsl-BycG2Clx.js} +1 -1
- mycode_cli/server/static/assets/{yaml-BSIzUixy.js → yaml-D7btoOKo.js} +1 -1
- mycode_cli/server/static/index.html +1 -1
- mycode_cli/tui/chat.py +30 -52
- {mycode_cli-0.8.10.dist-info → mycode_cli-0.9.0.dist-info}/METADATA +2 -2
- {mycode_cli-0.8.10.dist-info → mycode_cli-0.9.0.dist-info}/RECORD +90 -90
- {mycode_cli-0.8.10.dist-info → mycode_cli-0.9.0.dist-info}/WHEEL +1 -1
- {mycode_cli-0.8.10.dist-info → mycode_cli-0.9.0.dist-info}/entry_points.txt +0 -0
- {mycode_cli-0.8.10.dist-info → mycode_cli-0.9.0.dist-info}/licenses/LICENSE +0 -0
mycode_cli/config.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Configuration document validation, settings loading, and provider resolution."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -22,27 +22,20 @@ from mycode.providers import (
|
|
|
22
22
|
from mycode.utils import as_bool, as_int
|
|
23
23
|
|
|
24
24
|
_DEFAULT_MYCODE_HOME = "~/.mycode"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def resolve_mycode_home() -> Path:
|
|
28
|
-
"""Resolve the mycode home directory (``$MYCODE_HOME`` or ``~/.mycode``)."""
|
|
29
|
-
|
|
30
|
-
raw = os.environ.get("MYCODE_HOME", _DEFAULT_MYCODE_HOME)
|
|
31
|
-
return Path(raw).expanduser().resolve(strict=False)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def resolve_sessions_dir() -> Path:
|
|
35
|
-
"""Resolve the directory used for persisted sessions."""
|
|
36
|
-
|
|
37
|
-
return resolve_mycode_home() / "sessions"
|
|
38
|
-
|
|
39
|
-
|
|
40
25
|
_API_KEY_ENV_REF_RE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
|
|
41
|
-
|
|
42
|
-
#
|
|
26
|
+
|
|
27
|
+
# "auto" means "do not send an explicit effort to the provider"; after
|
|
28
|
+
# normalization it is represented as None.
|
|
43
29
|
REASONING_EFFORT_OPTIONS = ("auto", "none", "low", "medium", "high", "xhigh")
|
|
44
30
|
PERMISSION_LEVEL_OPTIONS = ("readonly", "safe", "standard", "yolo")
|
|
45
31
|
PERMISSION_MODE_OPTIONS = ("ask", "deny")
|
|
32
|
+
MODEL_OVERRIDE_KEYS = (
|
|
33
|
+
"context_window",
|
|
34
|
+
"max_output_tokens",
|
|
35
|
+
"supports_reasoning",
|
|
36
|
+
"supports_image_input",
|
|
37
|
+
"supports_pdf_input",
|
|
38
|
+
)
|
|
46
39
|
_EFFORT_AUTO_ALIASES = frozenset({"", "auto", "default"})
|
|
47
40
|
_EFFORT_OFF_ALIASES = frozenset({"off", "disabled"})
|
|
48
41
|
|
|
@@ -103,14 +96,17 @@ class ResolvedProvider:
|
|
|
103
96
|
model_config: ModelConfig | None = None
|
|
104
97
|
|
|
105
98
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
99
|
+
def resolve_mycode_home() -> Path:
|
|
100
|
+
"""Resolve the mycode home directory (``$MYCODE_HOME`` or ``~/.mycode``)."""
|
|
101
|
+
|
|
102
|
+
raw = os.environ.get("MYCODE_HOME", _DEFAULT_MYCODE_HOME)
|
|
103
|
+
return Path(raw).expanduser().resolve(strict=False)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def resolve_sessions_dir() -> Path:
|
|
107
|
+
"""Resolve the directory used for persisted sessions."""
|
|
108
|
+
|
|
109
|
+
return resolve_mycode_home() / "sessions"
|
|
114
110
|
|
|
115
111
|
|
|
116
112
|
def resolve_project(cwd: str) -> Path:
|
|
@@ -127,7 +123,10 @@ def project_dirs(cwd: str, project: str | Path | None = None) -> list[Path]:
|
|
|
127
123
|
"""Return directories from project to cwd, inclusive."""
|
|
128
124
|
|
|
129
125
|
cwd_path = Path(cwd).expanduser().resolve(strict=False)
|
|
130
|
-
|
|
126
|
+
if project is None:
|
|
127
|
+
project_path = resolve_project(cwd)
|
|
128
|
+
else:
|
|
129
|
+
project_path = Path(project).expanduser().resolve(strict=False)
|
|
131
130
|
|
|
132
131
|
dirs = [cwd_path]
|
|
133
132
|
while dirs[-1] != project_path:
|
|
@@ -139,51 +138,23 @@ def project_dirs(cwd: str, project: str | Path | None = None) -> list[Path]:
|
|
|
139
138
|
return list(reversed(dirs))
|
|
140
139
|
|
|
141
140
|
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
return {}
|
|
145
|
-
|
|
146
|
-
models: dict[str, ModelConfig] = {}
|
|
147
|
-
for model, raw in value.items():
|
|
148
|
-
if not isinstance(model, str):
|
|
149
|
-
continue
|
|
150
|
-
model_id = model.strip()
|
|
151
|
-
if not model_id:
|
|
152
|
-
continue
|
|
153
|
-
if isinstance(raw, ModelConfig):
|
|
154
|
-
models[model_id] = raw
|
|
155
|
-
continue
|
|
156
|
-
raw_config = raw if isinstance(raw, dict) else {}
|
|
157
|
-
models[model_id] = ModelConfig(
|
|
158
|
-
context_window=as_int(raw_config.get("context_window")),
|
|
159
|
-
max_output_tokens=as_int(raw_config.get("max_output_tokens")),
|
|
160
|
-
supports_reasoning=as_bool(raw_config.get("supports_reasoning")),
|
|
161
|
-
supports_image_input=as_bool(raw_config.get("supports_image_input")),
|
|
162
|
-
supports_pdf_input=as_bool(raw_config.get("supports_pdf_input")),
|
|
163
|
-
)
|
|
164
|
-
return models
|
|
141
|
+
def _candidate_config_paths(cwd: str, project: str | Path) -> list[Path]:
|
|
142
|
+
"""Return config files in override order: global, then project to cwd."""
|
|
165
143
|
|
|
144
|
+
paths = [resolve_mycode_home() / "config.json"]
|
|
145
|
+
paths.extend(path / ".mycode" / "config.json" for path in project_dirs(cwd, project))
|
|
146
|
+
return paths
|
|
166
147
|
|
|
167
|
-
def _parse_config_api_key(value: Any) -> tuple[str | None, str | None]:
|
|
168
|
-
if not isinstance(value, str):
|
|
169
|
-
return None, None
|
|
170
148
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return None, None
|
|
149
|
+
def is_api_key_env_ref(value: str) -> str | None:
|
|
150
|
+
"""Return the env var name when ``value`` is a ``${NAME}`` reference."""
|
|
174
151
|
|
|
175
|
-
match = _API_KEY_ENV_REF_RE.fullmatch(
|
|
176
|
-
if match
|
|
177
|
-
return None, match.group(1)
|
|
178
|
-
return cleaned, None
|
|
152
|
+
match = _API_KEY_ENV_REF_RE.fullmatch(value.strip())
|
|
153
|
+
return match.group(1) if match else None
|
|
179
154
|
|
|
180
155
|
|
|
181
156
|
def parse_compact_threshold(value: Any) -> float | None:
|
|
182
|
-
"""Parse compact_threshold from config.
|
|
183
|
-
|
|
184
|
-
Returns ``None`` when the key should keep the current/default value, ``0.0``
|
|
185
|
-
when compaction is explicitly disabled, or a valid float in ``[0, 1]``.
|
|
186
|
-
"""
|
|
157
|
+
"""Parse compact_threshold from config."""
|
|
187
158
|
|
|
188
159
|
if value is None:
|
|
189
160
|
return None
|
|
@@ -201,13 +172,7 @@ def parse_compact_threshold(value: Any) -> float | None:
|
|
|
201
172
|
|
|
202
173
|
|
|
203
174
|
def normalize_reasoning_effort(value: Any) -> str | None:
|
|
204
|
-
"""Normalize a reasoning effort setting.
|
|
205
|
-
|
|
206
|
-
Returns `None` for "auto"/"default"/empty — the sentinel for "do not send an
|
|
207
|
-
explicit effort to the provider". Returns "none" for "off"/"disabled" and
|
|
208
|
-
a canonical tier string for recognized levels. Raises `ValueError` for any
|
|
209
|
-
other string (or non-string, non-None value).
|
|
210
|
-
"""
|
|
175
|
+
"""Normalize a reasoning effort setting."""
|
|
211
176
|
|
|
212
177
|
if value is None:
|
|
213
178
|
return None
|
|
@@ -258,61 +223,151 @@ def parse_permission(value: Any, current: PermissionConfig | None = None) -> Per
|
|
|
258
223
|
raise ValueError(f"permission must be a string or object, got {type(value).__name__}")
|
|
259
224
|
|
|
260
225
|
|
|
261
|
-
def
|
|
262
|
-
|
|
263
|
-
if not env_name:
|
|
264
|
-
return None
|
|
226
|
+
def validate_global_config(data: Any) -> dict[str, Any]:
|
|
227
|
+
"""Validate and clean a global config document before writing it to disk."""
|
|
265
228
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
229
|
+
if data is None:
|
|
230
|
+
return {}
|
|
231
|
+
if not isinstance(data, dict):
|
|
232
|
+
raise ValueError("config must be an object")
|
|
269
233
|
|
|
270
|
-
|
|
271
|
-
raise ValueError(f"missing API key env var {env_name!r} referenced by provider {provider.name!r}")
|
|
272
|
-
return None
|
|
234
|
+
out: dict[str, Any] = {}
|
|
273
235
|
|
|
236
|
+
if data.get("default") is not None:
|
|
237
|
+
default = _validate_default_config(data["default"])
|
|
238
|
+
if default:
|
|
239
|
+
out["default"] = default
|
|
274
240
|
|
|
275
|
-
|
|
276
|
-
|
|
241
|
+
if data.get("permission") is not None:
|
|
242
|
+
out["permission"] = _validate_permission_config(data["permission"])
|
|
277
243
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
244
|
+
raw_providers = data.get("providers")
|
|
245
|
+
if raw_providers is not None:
|
|
246
|
+
if not isinstance(raw_providers, dict):
|
|
247
|
+
raise ValueError("providers must be an object")
|
|
248
|
+
providers: dict[str, dict[str, Any]] = {}
|
|
249
|
+
for name, raw in raw_providers.items():
|
|
250
|
+
if not isinstance(name, str) or not name.strip():
|
|
251
|
+
raise ValueError("provider name must be a non-empty string")
|
|
252
|
+
provider_name = name.strip()
|
|
253
|
+
providers[provider_name] = _validate_provider_config(provider_name, raw)
|
|
254
|
+
if providers:
|
|
255
|
+
out["providers"] = providers
|
|
281
256
|
|
|
257
|
+
return out
|
|
282
258
|
|
|
283
|
-
def _candidate_config_paths(cwd: str, project: str | Path) -> list[Path]:
|
|
284
|
-
paths = [resolve_mycode_home() / "config.json"]
|
|
285
|
-
paths.extend(path / ".mycode" / "config.json" for path in project_dirs(cwd, project))
|
|
286
|
-
return paths
|
|
287
259
|
|
|
260
|
+
def _optional_config_string(raw: dict[str, Any], key: str, label: str) -> str | None:
|
|
261
|
+
if key not in raw:
|
|
262
|
+
return None
|
|
263
|
+
value = raw[key]
|
|
264
|
+
if value is None or value == "":
|
|
265
|
+
return None
|
|
266
|
+
if not isinstance(value, str):
|
|
267
|
+
raise ValueError(f"{label} must be a string")
|
|
268
|
+
return value.strip() or None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _validate_default_config(raw: Any) -> dict[str, Any]:
|
|
272
|
+
if not isinstance(raw, dict):
|
|
273
|
+
raise ValueError("default must be an object")
|
|
274
|
+
|
|
275
|
+
out: dict[str, Any] = {}
|
|
276
|
+
for key in ("provider", "model"):
|
|
277
|
+
value = _optional_config_string(raw, key, f"default.{key}")
|
|
278
|
+
if value:
|
|
279
|
+
out[key] = value
|
|
280
|
+
|
|
281
|
+
effort = raw.get("reasoning_effort")
|
|
282
|
+
if effort not in (None, ""):
|
|
283
|
+
normalize_reasoning_effort(effort)
|
|
284
|
+
out["reasoning_effort"] = effort
|
|
285
|
+
|
|
286
|
+
compact_threshold = raw.get("compact_threshold")
|
|
287
|
+
if compact_threshold is False:
|
|
288
|
+
out["compact_threshold"] = False
|
|
289
|
+
elif compact_threshold is not None:
|
|
290
|
+
if isinstance(compact_threshold, bool):
|
|
291
|
+
raise ValueError("default.compact_threshold must be a number in [0, 1] or false")
|
|
292
|
+
parsed = parse_compact_threshold(compact_threshold)
|
|
293
|
+
if parsed is None:
|
|
294
|
+
raise ValueError("default.compact_threshold must be a number in [0, 1] or false")
|
|
295
|
+
out["compact_threshold"] = parsed
|
|
296
|
+
|
|
297
|
+
return out
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _validate_permission_config(raw: Any) -> PermissionLevel | dict[str, Any]:
|
|
301
|
+
if isinstance(raw, str):
|
|
302
|
+
return normalize_permission_level(raw)
|
|
303
|
+
if not isinstance(raw, dict):
|
|
304
|
+
raise ValueError("permission must be a string or object")
|
|
305
|
+
|
|
306
|
+
out: dict[str, Any] = {}
|
|
307
|
+
if "level" in raw:
|
|
308
|
+
out["level"] = normalize_permission_level(raw.get("level"))
|
|
309
|
+
if "mode" in raw:
|
|
310
|
+
out["mode"] = normalize_permission_mode(raw.get("mode"))
|
|
311
|
+
return out
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _validate_provider_config(name: str, raw: Any) -> dict[str, Any]:
|
|
315
|
+
if not isinstance(raw, dict):
|
|
316
|
+
raise ValueError(f"provider {name!r} must be an object")
|
|
317
|
+
|
|
318
|
+
out: dict[str, Any] = {}
|
|
319
|
+
raw_type = raw.get("type")
|
|
320
|
+
if raw_type in (None, ""):
|
|
321
|
+
if not is_supported_provider(name):
|
|
322
|
+
raise ValueError(f"provider {name!r} must set 'type'")
|
|
323
|
+
elif not isinstance(raw_type, str):
|
|
324
|
+
raise ValueError(f"provider {name!r}: type must be a string")
|
|
325
|
+
elif not is_supported_provider(raw_type):
|
|
326
|
+
supported = ", ".join(list_supported_providers())
|
|
327
|
+
raise ValueError(f"provider {name!r}: unsupported type {raw_type!r}; supported: {supported}")
|
|
328
|
+
else:
|
|
329
|
+
out["type"] = raw_type
|
|
288
330
|
|
|
289
|
-
|
|
290
|
-
|
|
331
|
+
for key in ("api_key", "base_url"):
|
|
332
|
+
value = _optional_config_string(raw, key, f"provider {name!r}: {key}")
|
|
333
|
+
if value:
|
|
334
|
+
out[key] = value
|
|
291
335
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
elif is_supported_provider(name):
|
|
297
|
-
# Built-in providers can be overridden by name without repeating type.
|
|
298
|
-
provider_type = name
|
|
299
|
-
else:
|
|
300
|
-
raise ValueError(f"provider {name!r} must set 'type'")
|
|
336
|
+
effort = raw.get("reasoning_effort")
|
|
337
|
+
if effort not in (None, ""):
|
|
338
|
+
normalize_reasoning_effort(effort)
|
|
339
|
+
out["reasoning_effort"] = effort
|
|
301
340
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
api_key=raw.get("api_key") or None,
|
|
310
|
-
api_key_env_var=raw.get("api_key_env_var") or None,
|
|
311
|
-
base_url=raw.get("base_url") or None,
|
|
312
|
-
reasoning_effort=normalize_reasoning_effort(raw.get("reasoning_effort")),
|
|
313
|
-
)
|
|
341
|
+
raw_models = raw.get("models")
|
|
342
|
+
if raw_models is not None:
|
|
343
|
+
models = _validate_model_config_entries(name, raw_models)
|
|
344
|
+
if models:
|
|
345
|
+
out["models"] = models
|
|
346
|
+
|
|
347
|
+
return out
|
|
314
348
|
|
|
315
|
-
|
|
349
|
+
|
|
350
|
+
def _validate_model_config_entries(name: str, raw: Any) -> dict[str, dict[str, Any]]:
|
|
351
|
+
# The settings UI sends a model list, while stored config uses a model map.
|
|
352
|
+
if isinstance(raw, list):
|
|
353
|
+
items: list[tuple[Any, Any]] = [(model, None) for model in raw]
|
|
354
|
+
elif isinstance(raw, dict):
|
|
355
|
+
items = list(raw.items())
|
|
356
|
+
else:
|
|
357
|
+
raise ValueError(f"provider {name!r}: models must be a list or object")
|
|
358
|
+
|
|
359
|
+
out: dict[str, dict[str, Any]] = {}
|
|
360
|
+
for model_id, overrides in items:
|
|
361
|
+
if not isinstance(model_id, str) or not model_id.strip():
|
|
362
|
+
raise ValueError(f"provider {name!r}: model id must be a non-empty string")
|
|
363
|
+
key = model_id.strip()
|
|
364
|
+
if overrides is None:
|
|
365
|
+
out[key] = {}
|
|
366
|
+
elif isinstance(overrides, dict):
|
|
367
|
+
out[key] = {k: v for k, v in overrides.items() if k in MODEL_OVERRIDE_KEYS and v is not None}
|
|
368
|
+
else:
|
|
369
|
+
raise ValueError(f"provider {name!r}: model {key!r} config must be an object")
|
|
370
|
+
return out
|
|
316
371
|
|
|
317
372
|
|
|
318
373
|
def get_settings(cwd: str | None = None) -> Settings:
|
|
@@ -392,6 +447,100 @@ def get_settings(cwd: str | None = None) -> Settings:
|
|
|
392
447
|
)
|
|
393
448
|
|
|
394
449
|
|
|
450
|
+
def _load_json(path: Path) -> dict[str, Any] | None:
|
|
451
|
+
"""Load a JSON object from a config file; ignore missing or invalid files."""
|
|
452
|
+
|
|
453
|
+
if not path.is_file():
|
|
454
|
+
return None
|
|
455
|
+
try:
|
|
456
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
457
|
+
except Exception:
|
|
458
|
+
return None
|
|
459
|
+
return data if isinstance(data, dict) else None
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _normalize_models(value: Any) -> dict[str, ModelConfig]:
|
|
463
|
+
"""Convert raw model overrides into runtime model config objects."""
|
|
464
|
+
|
|
465
|
+
if not isinstance(value, dict):
|
|
466
|
+
return {}
|
|
467
|
+
|
|
468
|
+
models: dict[str, ModelConfig] = {}
|
|
469
|
+
for model, raw in value.items():
|
|
470
|
+
if not isinstance(model, str):
|
|
471
|
+
continue
|
|
472
|
+
model_id = model.strip()
|
|
473
|
+
if not model_id:
|
|
474
|
+
continue
|
|
475
|
+
if isinstance(raw, ModelConfig):
|
|
476
|
+
models[model_id] = raw
|
|
477
|
+
continue
|
|
478
|
+
raw_config = raw if isinstance(raw, dict) else {}
|
|
479
|
+
models[model_id] = ModelConfig(
|
|
480
|
+
context_window=as_int(raw_config.get("context_window")),
|
|
481
|
+
max_output_tokens=as_int(raw_config.get("max_output_tokens")),
|
|
482
|
+
supports_reasoning=as_bool(raw_config.get("supports_reasoning")),
|
|
483
|
+
supports_image_input=as_bool(raw_config.get("supports_image_input")),
|
|
484
|
+
supports_pdf_input=as_bool(raw_config.get("supports_pdf_input")),
|
|
485
|
+
)
|
|
486
|
+
return models
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _parse_config_api_key(value: Any) -> tuple[str | None, str | None]:
|
|
490
|
+
"""Return a literal API key or an env var reference from config."""
|
|
491
|
+
|
|
492
|
+
if not isinstance(value, str):
|
|
493
|
+
return None, None
|
|
494
|
+
|
|
495
|
+
cleaned = value.strip()
|
|
496
|
+
if not cleaned:
|
|
497
|
+
return None, None
|
|
498
|
+
|
|
499
|
+
env_name = is_api_key_env_ref(cleaned)
|
|
500
|
+
if env_name:
|
|
501
|
+
return None, env_name
|
|
502
|
+
return cleaned, None
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _build_providers(raw_providers: dict[str, dict[str, Any]]) -> dict[str, ProviderConfig]:
|
|
506
|
+
"""Build runtime provider configs after layered config files are merged."""
|
|
507
|
+
|
|
508
|
+
providers: dict[str, ProviderConfig] = {}
|
|
509
|
+
|
|
510
|
+
for name, raw in raw_providers.items():
|
|
511
|
+
raw_type = raw.get("type")
|
|
512
|
+
if raw_type:
|
|
513
|
+
provider_type = str(raw_type)
|
|
514
|
+
elif is_supported_provider(name):
|
|
515
|
+
# Built-in providers can be overridden by name without repeating type.
|
|
516
|
+
provider_type = name
|
|
517
|
+
else:
|
|
518
|
+
raise ValueError(f"provider {name!r} must set 'type'")
|
|
519
|
+
|
|
520
|
+
models = _normalize_models(raw.get("models"))
|
|
521
|
+
if not models:
|
|
522
|
+
models = {model: ModelConfig() for model in provider_default_models(provider_type)}
|
|
523
|
+
providers[name] = ProviderConfig(
|
|
524
|
+
name=name,
|
|
525
|
+
type=provider_type,
|
|
526
|
+
models=models,
|
|
527
|
+
api_key=raw.get("api_key") or None,
|
|
528
|
+
api_key_env_var=raw.get("api_key_env_var") or None,
|
|
529
|
+
base_url=raw.get("base_url") or None,
|
|
530
|
+
reasoning_effort=normalize_reasoning_effort(raw.get("reasoning_effort")),
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
return providers
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def provider_has_api_key(provider: ProviderConfig) -> bool:
|
|
537
|
+
"""Return whether a configured provider can authenticate right now."""
|
|
538
|
+
|
|
539
|
+
if provider.api_key_env_var:
|
|
540
|
+
return bool(_config_api_key_from_env_var(provider))
|
|
541
|
+
return bool(provider.api_key or provider_api_key_from_env(provider.type))
|
|
542
|
+
|
|
543
|
+
|
|
395
544
|
def resolve_provider(
|
|
396
545
|
settings: Settings,
|
|
397
546
|
*,
|
|
@@ -453,6 +602,20 @@ def resolve_provider_choices(settings: Settings) -> list[ResolvedProvider]:
|
|
|
453
602
|
return choices
|
|
454
603
|
|
|
455
604
|
|
|
605
|
+
def _config_api_key_from_env_var(provider: ProviderConfig, *, require: bool = False) -> str | None:
|
|
606
|
+
env_name = provider.api_key_env_var
|
|
607
|
+
if not env_name:
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
value = (os.environ.get(env_name) or "").strip()
|
|
611
|
+
if value:
|
|
612
|
+
return value
|
|
613
|
+
|
|
614
|
+
if require:
|
|
615
|
+
raise ValueError(f"missing API key env var {env_name!r} referenced by provider {provider.name!r}")
|
|
616
|
+
return None
|
|
617
|
+
|
|
618
|
+
|
|
456
619
|
def _available_provider_references(settings: Settings) -> list[tuple[str, ProviderConfig | None]]:
|
|
457
620
|
"""Return usable provider references with the configured default first."""
|
|
458
621
|
|
mycode_cli/permissions.py
CHANGED
|
@@ -9,7 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Literal, NamedTuple
|
|
10
10
|
|
|
11
11
|
from mycode import Hooks, ToolExecutionResult, ToolHookContext
|
|
12
|
-
from mycode.
|
|
12
|
+
from mycode.utils import resolve_path
|
|
13
13
|
from mycode_cli.config import PermissionConfig, PermissionLevel, Settings
|
|
14
14
|
from mycode_cli.system_prompt import discover_skills
|
|
15
15
|
|
mycode_cli/runtime.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from mycode.agent import Agent
|
|
6
6
|
from mycode.session import SessionStore
|
|
7
|
-
from mycode.tools import
|
|
7
|
+
from mycode.tools import bash_tool, edit_tool, read_tool, write_tool
|
|
8
8
|
from mycode_cli.config import ResolvedProvider, Settings
|
|
9
9
|
from mycode_cli.permissions import ToolReviewCallback, build_permission_hooks
|
|
10
10
|
from mycode_cli.system_prompt import build_system_prompt
|
|
@@ -46,6 +46,6 @@ def build_agent(
|
|
|
46
46
|
compact_threshold=settings.compact_threshold,
|
|
47
47
|
max_turns=max_turns,
|
|
48
48
|
system=build_system_prompt(cwd, settings),
|
|
49
|
-
tools=
|
|
49
|
+
tools=[read_tool, write_tool, edit_tool, bash_tool],
|
|
50
50
|
hooks=hooks,
|
|
51
51
|
)
|