mycode-cli 0.17.0__py3-none-any.whl → 0.18.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 +57 -108
- mycode_cli/main.py +17 -33
- mycode_cli/server/routers/chat.py +2 -8
- mycode_cli/server/static/assets/{index-DCe0gQLC.js → index-C7MhyuLY.js} +2 -2
- mycode_cli/server/static/index.html +1 -1
- mycode_cli/state.py +105 -0
- mycode_cli/tools.py +11 -4
- mycode_cli/tui/chat.py +183 -333
- mycode_cli/tui/markdown.py +362 -0
- mycode_cli/tui/render.py +521 -564
- mycode_cli/tui/terminal.py +628 -0
- mycode_cli/tui/theme.py +72 -89
- {mycode_cli-0.17.0.dist-info → mycode_cli-0.18.0.dist-info}/METADATA +34 -25
- {mycode_cli-0.17.0.dist-info → mycode_cli-0.18.0.dist-info}/RECORD +17 -15
- mycode_cli/tui/state.py +0 -38
- {mycode_cli-0.17.0.dist-info → mycode_cli-0.18.0.dist-info}/WHEEL +0 -0
- {mycode_cli-0.17.0.dist-info → mycode_cli-0.18.0.dist-info}/entry_points.txt +0 -0
- {mycode_cli-0.17.0.dist-info → mycode_cli-0.18.0.dist-info}/licenses/LICENSE +0 -0
mycode_cli/config.py
CHANGED
|
@@ -99,8 +99,6 @@ class WebConfig:
|
|
|
99
99
|
@dataclass(frozen=True)
|
|
100
100
|
class Settings:
|
|
101
101
|
providers: dict[str, ProviderConfig]
|
|
102
|
-
default_provider: str | None
|
|
103
|
-
default_model: str | None
|
|
104
102
|
port: int
|
|
105
103
|
cwd: str
|
|
106
104
|
project: str
|
|
@@ -305,10 +303,14 @@ def _clean_config_layer(data: Any) -> dict[str, Any]:
|
|
|
305
303
|
|
|
306
304
|
out: dict[str, Any] = {}
|
|
307
305
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
306
|
+
compact_threshold = data.get("compact_threshold")
|
|
307
|
+
if compact_threshold is False:
|
|
308
|
+
out["compact_threshold"] = False
|
|
309
|
+
elif compact_threshold is not None:
|
|
310
|
+
parsed = None if isinstance(compact_threshold, bool) else parse_compact_threshold(compact_threshold)
|
|
311
|
+
if parsed is None:
|
|
312
|
+
raise ValueError("compact_threshold must be a number in [0, 1] or false")
|
|
313
|
+
out["compact_threshold"] = parsed
|
|
312
314
|
|
|
313
315
|
if data.get("permission") is not None:
|
|
314
316
|
out["permission"] = _validate_permission_config(data["permission"])
|
|
@@ -345,30 +347,6 @@ def _optional_config_string(raw: dict[str, Any], key: str, label: str) -> str |
|
|
|
345
347
|
return value.strip() or None
|
|
346
348
|
|
|
347
349
|
|
|
348
|
-
def _validate_default_config(raw: Any) -> dict[str, Any]:
|
|
349
|
-
if not isinstance(raw, dict):
|
|
350
|
-
raise ValueError("default must be an object")
|
|
351
|
-
|
|
352
|
-
out: dict[str, Any] = {}
|
|
353
|
-
for key in ("provider", "model"):
|
|
354
|
-
value = _optional_config_string(raw, key, f"default.{key}")
|
|
355
|
-
if value:
|
|
356
|
-
out[key] = value
|
|
357
|
-
|
|
358
|
-
compact_threshold = raw.get("compact_threshold")
|
|
359
|
-
if compact_threshold is False:
|
|
360
|
-
out["compact_threshold"] = False
|
|
361
|
-
elif compact_threshold is not None:
|
|
362
|
-
if isinstance(compact_threshold, bool):
|
|
363
|
-
raise ValueError("default.compact_threshold must be a number in [0, 1] or false")
|
|
364
|
-
parsed = parse_compact_threshold(compact_threshold)
|
|
365
|
-
if parsed is None:
|
|
366
|
-
raise ValueError("default.compact_threshold must be a number in [0, 1] or false")
|
|
367
|
-
out["compact_threshold"] = parsed
|
|
368
|
-
|
|
369
|
-
return out
|
|
370
|
-
|
|
371
|
-
|
|
372
350
|
def _validate_permission_config(raw: Any) -> PermissionLevel | dict[str, Any]:
|
|
373
351
|
if isinstance(raw, str):
|
|
374
352
|
return normalize_permission_level(raw)
|
|
@@ -498,8 +476,6 @@ def get_settings(cwd: str | None = None) -> Settings:
|
|
|
498
476
|
resolved_project = str(resolve_project(resolved_cwd))
|
|
499
477
|
|
|
500
478
|
raw_providers: dict[str, dict[str, Any]] = {}
|
|
501
|
-
default_provider: str | None = None
|
|
502
|
-
default_model: str | None = None
|
|
503
479
|
compact_threshold: float | None = None
|
|
504
480
|
permission = PermissionConfig()
|
|
505
481
|
raw_web: dict[str, Any] = {}
|
|
@@ -534,13 +510,8 @@ def get_settings(cwd: str | None = None) -> Settings:
|
|
|
534
510
|
|
|
535
511
|
raw_providers[name] = merged
|
|
536
512
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
default_provider = default["provider"]
|
|
540
|
-
if "model" in default:
|
|
541
|
-
default_model = default["model"]
|
|
542
|
-
if "compact_threshold" in default:
|
|
543
|
-
compact_threshold = float(default["compact_threshold"])
|
|
513
|
+
if "compact_threshold" in data:
|
|
514
|
+
compact_threshold = float(data["compact_threshold"])
|
|
544
515
|
|
|
545
516
|
if "permission" in data:
|
|
546
517
|
permission = parse_permission(data.get("permission"), permission)
|
|
@@ -561,8 +532,6 @@ def get_settings(cwd: str | None = None) -> Settings:
|
|
|
561
532
|
|
|
562
533
|
return Settings(
|
|
563
534
|
providers=_build_providers(raw_providers),
|
|
564
|
-
default_provider=default_provider,
|
|
565
|
-
default_model=default_model,
|
|
566
535
|
compact_threshold=compact_threshold,
|
|
567
536
|
permission=permission,
|
|
568
537
|
web=_build_web_config(raw_web),
|
|
@@ -691,44 +660,35 @@ def resolve_provider(
|
|
|
691
660
|
api_key: str | None = None,
|
|
692
661
|
api_base: str | None = None,
|
|
693
662
|
) -> ResolvedProvider:
|
|
694
|
-
"""Resolve provider, model, api_key, and api_base from settings and overrides.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
663
|
+
"""Resolve provider, model, api_key, and api_base from settings and overrides.
|
|
664
|
+
|
|
665
|
+
Without a provider name, the first available provider is used.
|
|
666
|
+
"""
|
|
667
|
+
|
|
668
|
+
selected_name = (provider_name or "").strip()
|
|
669
|
+
if not selected_name:
|
|
670
|
+
refs = _available_provider_references(settings)
|
|
671
|
+
if not refs:
|
|
672
|
+
env_names = list(
|
|
673
|
+
dict.fromkeys(
|
|
674
|
+
env_name
|
|
675
|
+
for provider_id in list_env_discoverable_providers()
|
|
676
|
+
for env_name in provider_env_api_key_names(provider_id)
|
|
677
|
+
)
|
|
705
678
|
)
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if refs:
|
|
713
|
-
return _resolve_provider_runtime(
|
|
714
|
-
settings,
|
|
715
|
-
selected_name=refs[0],
|
|
716
|
-
model=model,
|
|
717
|
-
api_key=api_key,
|
|
718
|
-
api_base=api_base,
|
|
719
|
-
)
|
|
679
|
+
checked = ", ".join(env_names) or "<api key env>"
|
|
680
|
+
raise ValueError(
|
|
681
|
+
"no available providers found; set one of the supported API key env vars "
|
|
682
|
+
+ f"({checked}) or configure a provider in ~/.mycode/config.json or a project .mycode/config.json"
|
|
683
|
+
)
|
|
684
|
+
selected_name = refs[0]
|
|
720
685
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
)
|
|
728
|
-
checked = ", ".join(env_names) or "<api key env>"
|
|
729
|
-
raise ValueError(
|
|
730
|
-
"no available providers found; set one of the supported API key env vars "
|
|
731
|
-
+ f"({checked}) or configure a provider in ~/.mycode/config.json or a project .mycode/config.json"
|
|
686
|
+
return _resolve_provider_runtime(
|
|
687
|
+
settings,
|
|
688
|
+
selected_name=selected_name,
|
|
689
|
+
model=model,
|
|
690
|
+
api_key=api_key,
|
|
691
|
+
api_base=api_base,
|
|
732
692
|
)
|
|
733
693
|
|
|
734
694
|
|
|
@@ -744,41 +704,32 @@ def resolve_provider_choices(settings: Settings) -> list[ResolvedProvider]:
|
|
|
744
704
|
return choices
|
|
745
705
|
|
|
746
706
|
|
|
747
|
-
def
|
|
748
|
-
"""Return
|
|
749
|
-
|
|
750
|
-
available: list[str] = []
|
|
751
|
-
seen: set[str] = set()
|
|
752
|
-
configured_types_with_credentials: set[str] = set()
|
|
707
|
+
def provider_models(settings: Settings, provider: ResolvedProvider) -> list[str]:
|
|
708
|
+
"""Return a resolved provider's selectable models: configured ones, else the adapter defaults."""
|
|
753
709
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
710
|
+
provider_config = settings.providers.get(provider.provider_name or "")
|
|
711
|
+
if provider_config and provider_config.models:
|
|
712
|
+
return list(provider_config.models)
|
|
713
|
+
return list(provider_default_models(provider.provider)) or [provider.model]
|
|
758
714
|
|
|
759
|
-
provider_config = settings.providers.get(cleaned)
|
|
760
|
-
provider_type = provider_config.type if provider_config else cleaned
|
|
761
|
-
if not is_supported_provider(provider_type):
|
|
762
|
-
return
|
|
763
715
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
return
|
|
767
|
-
configured_types_with_credentials.add(provider_type)
|
|
768
|
-
elif not provider_api_key_from_env(provider_type):
|
|
769
|
-
return
|
|
770
|
-
|
|
771
|
-
seen.add(cleaned)
|
|
772
|
-
available.append(cleaned)
|
|
773
|
-
|
|
774
|
-
add(settings.default_provider)
|
|
716
|
+
def _available_provider_references(settings: Settings) -> list[str]:
|
|
717
|
+
"""Return usable provider names: configured providers in order, then env-discovered built-ins."""
|
|
775
718
|
|
|
776
|
-
|
|
777
|
-
|
|
719
|
+
available: list[str] = []
|
|
720
|
+
configured_types: set[str] = set()
|
|
721
|
+
for name, config in settings.providers.items():
|
|
722
|
+
if is_supported_provider(config.type) and provider_is_available(config):
|
|
723
|
+
available.append(name)
|
|
724
|
+
configured_types.add(config.type)
|
|
778
725
|
|
|
779
726
|
for provider_id in list_env_discoverable_providers():
|
|
780
|
-
if
|
|
781
|
-
|
|
727
|
+
if (
|
|
728
|
+
provider_id not in configured_types
|
|
729
|
+
and provider_id not in settings.providers
|
|
730
|
+
and provider_api_key_from_env(provider_id)
|
|
731
|
+
):
|
|
732
|
+
available.append(provider_id)
|
|
782
733
|
|
|
783
734
|
return available
|
|
784
735
|
|
|
@@ -805,8 +756,6 @@ def _resolve_provider_runtime(
|
|
|
805
756
|
resolved_model = requested_model
|
|
806
757
|
elif provider_config and provider_config.models:
|
|
807
758
|
resolved_model = next(iter(provider_config.models))
|
|
808
|
-
elif selected_name == settings.default_provider and (settings.default_model or "").strip():
|
|
809
|
-
resolved_model = str(settings.default_model).strip()
|
|
810
759
|
else:
|
|
811
760
|
defaults = provider_default_models(provider_type)
|
|
812
761
|
if not defaults:
|
mycode_cli/main.py
CHANGED
|
@@ -5,9 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
-
from contextlib import aclosing
|
|
8
|
+
from contextlib import aclosing
|
|
9
9
|
from dataclasses import dataclass, replace
|
|
10
|
-
from typing import
|
|
10
|
+
from typing import Annotated, Any, Literal
|
|
11
11
|
from uuid import uuid4
|
|
12
12
|
|
|
13
13
|
import typer
|
|
@@ -21,17 +21,14 @@ from mycode_cli.config import (
|
|
|
21
21
|
get_settings,
|
|
22
22
|
normalize_reasoning_effort,
|
|
23
23
|
parse_permission,
|
|
24
|
-
resolve_provider,
|
|
25
24
|
resolve_sessions_dir,
|
|
26
25
|
)
|
|
27
26
|
from mycode_cli.permissions import PERMISSION_DENIED_BY_USER_OUTPUT, PERMISSION_DENIED_OUTPUT
|
|
28
27
|
from mycode_cli.sessions import SessionStore
|
|
28
|
+
from mycode_cli.state import load_state, resolve_remembered_provider
|
|
29
29
|
|
|
30
30
|
from .runtime import build_agent
|
|
31
31
|
|
|
32
|
-
if TYPE_CHECKING:
|
|
33
|
-
from .tui.render import TerminalView
|
|
34
|
-
|
|
35
32
|
app = typer.Typer(add_completion=False, pretty_exceptions_enable=False)
|
|
36
33
|
session_app = typer.Typer(help="Session management")
|
|
37
34
|
app.add_typer(session_app, name="session")
|
|
@@ -140,7 +137,6 @@ async def run_noninteractive(agent: Agent, message: str, *, store: SessionStore,
|
|
|
140
137
|
@dataclass
|
|
141
138
|
class _BootstrapContext:
|
|
142
139
|
store: SessionStore
|
|
143
|
-
view: TerminalView
|
|
144
140
|
settings: Settings
|
|
145
141
|
resolved_provider: ResolvedProvider
|
|
146
142
|
resolved_session: ResolvedSession
|
|
@@ -159,21 +155,20 @@ def _bootstrap(
|
|
|
159
155
|
) -> _BootstrapContext:
|
|
160
156
|
"""Shared setup for the chat and run commands."""
|
|
161
157
|
|
|
162
|
-
from .tui.render import TerminalView
|
|
163
|
-
|
|
164
158
|
if session and continue_last:
|
|
165
159
|
raise typer.BadParameter("--session and --continue are mutually exclusive")
|
|
166
160
|
|
|
167
161
|
cwd = os.path.abspath(os.getcwd())
|
|
168
162
|
store = SessionStore(data_dir=resolve_sessions_dir())
|
|
169
|
-
view = TerminalView()
|
|
170
163
|
try:
|
|
171
164
|
settings = get_settings(cwd)
|
|
172
165
|
if permission is not None:
|
|
173
166
|
settings = replace(settings, permission=parse_permission(permission, settings.permission))
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if
|
|
167
|
+
state = load_state()
|
|
168
|
+
resolved_provider = resolve_remembered_provider(settings, state, provider_name=provider, model=model)
|
|
169
|
+
if reasoning_effort is None:
|
|
170
|
+
resolved_effort = state.effort_for(resolved_provider)
|
|
171
|
+
elif (resolved_effort := normalize_reasoning_effort(reasoning_effort)) is not None:
|
|
177
172
|
if not resolved_provider.supports_reasoning_effort:
|
|
178
173
|
raise ValueError(f"provider {resolved_provider.provider!r} does not support reasoning effort")
|
|
179
174
|
if resolved_effort not in resolved_provider.reasoning_efforts:
|
|
@@ -182,6 +177,7 @@ def _bootstrap(
|
|
|
182
177
|
f"reasoning effort {resolved_effort!r} is not supported by model {resolved_provider.model!r}; "
|
|
183
178
|
+ f"supported efforts: {supported}"
|
|
184
179
|
)
|
|
180
|
+
resolved_provider = replace(resolved_provider, reasoning_effort=resolved_effort)
|
|
185
181
|
resolved_session = asyncio.run(
|
|
186
182
|
resolve_session(
|
|
187
183
|
store=store,
|
|
@@ -191,7 +187,9 @@ def _bootstrap(
|
|
|
191
187
|
)
|
|
192
188
|
)
|
|
193
189
|
except ValueError as exc:
|
|
194
|
-
|
|
190
|
+
from .tui.render import console, error_line
|
|
191
|
+
|
|
192
|
+
console.print(error_line(str(exc)))
|
|
195
193
|
raise SystemExit(1) from exc
|
|
196
194
|
|
|
197
195
|
agent = build_agent(
|
|
@@ -202,12 +200,9 @@ def _bootstrap(
|
|
|
202
200
|
session_id=resolved_session.session_id,
|
|
203
201
|
max_turns=max_turns,
|
|
204
202
|
)
|
|
205
|
-
if reasoning_effort is not None:
|
|
206
|
-
agent.reasoning_effort = resolved_effort
|
|
207
203
|
|
|
208
204
|
return _BootstrapContext(
|
|
209
205
|
store=store,
|
|
210
|
-
view=view,
|
|
211
206
|
settings=settings,
|
|
212
207
|
resolved_provider=resolved_provider,
|
|
213
208
|
resolved_session=resolved_session,
|
|
@@ -266,22 +261,11 @@ def chat(
|
|
|
266
261
|
session_id=setup.resolved_session.session_id,
|
|
267
262
|
provider_name=setup.resolved_provider.provider_name,
|
|
268
263
|
reasoning_efforts=setup.resolved_provider.reasoning_efforts,
|
|
269
|
-
view=setup.view,
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
setup.view.print_header(
|
|
273
|
-
provider=setup.resolved_provider.provider,
|
|
274
|
-
model=setup.resolved_provider.model,
|
|
275
264
|
session=setup.resolved_session.session,
|
|
276
265
|
mode=setup.resolved_session.mode,
|
|
277
|
-
|
|
278
|
-
reasoning_effort=setup.agent.reasoning_effort,
|
|
266
|
+
messages=setup.resolved_session.messages,
|
|
279
267
|
)
|
|
280
|
-
|
|
281
|
-
setup.view.print_history_preview(setup.resolved_session.messages)
|
|
282
|
-
|
|
283
|
-
with suppress(KeyboardInterrupt):
|
|
284
|
-
asyncio.run(terminal_chat.run())
|
|
268
|
+
asyncio.run(terminal_chat.run())
|
|
285
269
|
|
|
286
270
|
|
|
287
271
|
@app.command()
|
|
@@ -355,15 +339,15 @@ def session_list(
|
|
|
355
339
|
) -> None:
|
|
356
340
|
"""List saved sessions."""
|
|
357
341
|
|
|
358
|
-
from .tui.render import
|
|
342
|
+
from .tui.render import console, session_list_table
|
|
359
343
|
|
|
360
344
|
cwd = os.path.abspath(os.getcwd())
|
|
361
345
|
store = SessionStore(data_dir=resolve_sessions_dir())
|
|
362
|
-
view = TerminalView()
|
|
363
346
|
|
|
364
347
|
sessions = asyncio.run(store.list_sessions(cwd=None if all_workspaces else cwd))
|
|
365
348
|
heading = "all sessions" if all_workspaces else f"sessions for {cwd}"
|
|
366
|
-
|
|
349
|
+
for renderable in session_list_table(sessions, include_cwd=all_workspaces, heading=heading):
|
|
350
|
+
console.print(renderable)
|
|
367
351
|
|
|
368
352
|
|
|
369
353
|
def main() -> None:
|
|
@@ -30,11 +30,11 @@ from mycode.messages import (
|
|
|
30
30
|
image_block,
|
|
31
31
|
text_block,
|
|
32
32
|
)
|
|
33
|
-
from mycode.providers import provider_default_models
|
|
34
33
|
from mycode_cli.config import (
|
|
35
34
|
ResolvedProvider,
|
|
36
35
|
get_settings,
|
|
37
36
|
normalize_reasoning_effort,
|
|
37
|
+
provider_models,
|
|
38
38
|
resolve_configured_model_metadata,
|
|
39
39
|
resolve_provider,
|
|
40
40
|
resolve_provider_choices,
|
|
@@ -402,13 +402,7 @@ async def get_config(cwd: Annotated[str | None, Query()] = None) -> dict[str, An
|
|
|
402
402
|
providers_info: dict[str, Any] = {}
|
|
403
403
|
for provider in resolve_provider_choices(settings):
|
|
404
404
|
provider_config = settings.providers.get(provider.provider_name or "")
|
|
405
|
-
models = (
|
|
406
|
-
list(provider_config.models)
|
|
407
|
-
if provider_config and provider_config.models
|
|
408
|
-
else list(provider_default_models(provider.provider))
|
|
409
|
-
)
|
|
410
|
-
if not models:
|
|
411
|
-
models = [provider.model]
|
|
405
|
+
models = provider_models(settings, provider)
|
|
412
406
|
|
|
413
407
|
info: dict[str, Any] = {
|
|
414
408
|
"name": provider.provider_name,
|