scootcli 0.8.0__tar.gz → 0.8.1__tar.gz
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.
- {scootcli-0.8.0/src/scootcli.egg-info → scootcli-0.8.1}/PKG-INFO +5 -4
- {scootcli-0.8.0 → scootcli-0.8.1}/README.md +4 -3
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/__init__.py +1 -1
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/cli.py +4 -4
- scootcli-0.8.1/src/scootcli/commands/model.py +115 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/status.py +12 -1
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/config.py +31 -9
- scootcli-0.8.1/src/scootcli/preferences.py +144 -0
- {scootcli-0.8.0 → scootcli-0.8.1/src/scootcli.egg-info}/PKG-INFO +5 -4
- scootcli-0.8.1/tests/test_preferences.py +266 -0
- scootcli-0.8.0/src/scootcli/commands/model.py +0 -65
- scootcli-0.8.0/src/scootcli/preferences.py +0 -87
- scootcli-0.8.0/tests/test_preferences.py +0 -132
- {scootcli-0.8.0 → scootcli-0.8.1}/LICENSE +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/pyproject.toml +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/setup.cfg +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/__main__.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/activity.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/agent.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/approvals.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/auth.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/clipboard.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/__init__.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/approve.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/auth.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/base.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/compact.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/copy.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/exit.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/forget.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/help.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/hooks.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/init.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/logo.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/panel.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/reset.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/resume.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/route.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/save.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/scope.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/sessions.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/verbosity.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/worktree.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/commands/yolo.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/context.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/credentials.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/errors.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/headless.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/hooks.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/images.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/keys.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/lineeditor.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/logo.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/models.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/panel.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/presets.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/pricing.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/project.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/prompts.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/providers/__init__.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/providers/anthropic.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/providers/base.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/providers/openai_chat.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/providers/openai_responses.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/providers/registry.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/providers/router.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/rendering.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/repl.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/resize.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/sessions.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/status.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/tools/__init__.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/tools/base.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/tools/edit_file.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/tools/list_dir.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/tools/open_editor.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/tools/read_file.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/tools/run_shell.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/tools/search.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/tools/update_plan.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/tools/write_file.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/transport.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/updates.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/vision.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/workspace.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli/worktree.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli.egg-info/SOURCES.txt +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli.egg-info/dependency_links.txt +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli.egg-info/entry_points.txt +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/src/scootcli.egg-info/top_level.txt +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_agent.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_anthropic.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_approvals.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_auth.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_clipboard.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_config.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_headless.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_hooks.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_images.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_lineeditor.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_logo.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_models.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_panel.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_providers.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_readiness.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_repl.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_resilience.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_resize.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_router.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_scope.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_sessions.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_small_060.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_smoke.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_streaming.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_tools.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_transport_native.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_vision.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_workspace.py +0 -0
- {scootcli-0.8.0 → scootcli-0.8.1}/tests/test_worktree.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scootcli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: A tiny coding agent that goes where you point it
|
|
5
5
|
Author: Sergey Nes
|
|
6
6
|
License-Expression: MIT
|
|
@@ -115,8 +115,9 @@ scoot --model openai/gpt-5.3-codex "..."
|
|
|
115
115
|
scoot --model auto "..." # pick a model per prompt from the live list (cheap for trivial, strong for edits)
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
Inside the REPL, `/model <provider/model>` switches and is remembered for the
|
|
119
|
-
Starting the REPL with `--model` is remembered the same way, so `scoot --model ollama/llama3.2` once and plain `scoot` afterwards keep using it; a one-shot `scoot --model X "prompt"` does not change the saved choice.
|
|
118
|
+
Inside the REPL, `/model <provider/model>` switches and is remembered for the folder you are in, so each project can have its own model; `/model default` goes back to the provider's preferred model.
|
|
119
|
+
Starting the REPL with `--model` is remembered the same way, so `scoot --model ollama/llama3.2` once and plain `scoot` afterwards keep using it there; a one-shot `scoot --model X "prompt"` does not change the saved choice.
|
|
120
|
+
`/model X everywhere` makes `X` the model for every folder that has no choice of its own, `/model forget` drops the current folder's choice so it follows that, and `/status` says where the active model came from.
|
|
120
121
|
|
|
121
122
|
### Routing
|
|
122
123
|
|
|
@@ -171,7 +172,7 @@ The most useful settings (see [`.env.example`](./.env.example) for all of them):
|
|
|
171
172
|
|---|---|---|
|
|
172
173
|
| `OPENAI_API_KEY` | OpenAI key (or `scoot auth set openai`) | |
|
|
173
174
|
| `SCOOT_PROVIDER` | default provider for bare model names | first with a key, else `ollama` |
|
|
174
|
-
| `SCOOT_MODEL` | `default`, `auto`, or `provider/model
|
|
175
|
+
| `SCOOT_MODEL` | `default`, `auto`, or `provider/model`; overrides the saved per-folder and global choices | `default` |
|
|
175
176
|
| `SCOOT_EFFORT` | reasoning effort | `medium` |
|
|
176
177
|
| `SCOOT_APPROVAL` | `always` · `auto-read` · `auto-edits` · `yolo` | `yolo` |
|
|
177
178
|
| `SCOOT_MAX_STEPS` | tool-call steps per turn before asking to continue | `50` |
|
|
@@ -90,8 +90,9 @@ scoot --model openai/gpt-5.3-codex "..."
|
|
|
90
90
|
scoot --model auto "..." # pick a model per prompt from the live list (cheap for trivial, strong for edits)
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
Inside the REPL, `/model <provider/model>` switches and is remembered for the
|
|
94
|
-
Starting the REPL with `--model` is remembered the same way, so `scoot --model ollama/llama3.2` once and plain `scoot` afterwards keep using it; a one-shot `scoot --model X "prompt"` does not change the saved choice.
|
|
93
|
+
Inside the REPL, `/model <provider/model>` switches and is remembered for the folder you are in, so each project can have its own model; `/model default` goes back to the provider's preferred model.
|
|
94
|
+
Starting the REPL with `--model` is remembered the same way, so `scoot --model ollama/llama3.2` once and plain `scoot` afterwards keep using it there; a one-shot `scoot --model X "prompt"` does not change the saved choice.
|
|
95
|
+
`/model X everywhere` makes `X` the model for every folder that has no choice of its own, `/model forget` drops the current folder's choice so it follows that, and `/status` says where the active model came from.
|
|
95
96
|
|
|
96
97
|
### Routing
|
|
97
98
|
|
|
@@ -146,7 +147,7 @@ The most useful settings (see [`.env.example`](./.env.example) for all of them):
|
|
|
146
147
|
|---|---|---|
|
|
147
148
|
| `OPENAI_API_KEY` | OpenAI key (or `scoot auth set openai`) | |
|
|
148
149
|
| `SCOOT_PROVIDER` | default provider for bare model names | first with a key, else `ollama` |
|
|
149
|
-
| `SCOOT_MODEL` | `default`, `auto`, or `provider/model
|
|
150
|
+
| `SCOOT_MODEL` | `default`, `auto`, or `provider/model`; overrides the saved per-folder and global choices | `default` |
|
|
150
151
|
| `SCOOT_EFFORT` | reasoning effort | `medium` |
|
|
151
152
|
| `SCOOT_APPROVAL` | `always` · `auto-read` · `auto-edits` · `yolo` | `yolo` |
|
|
152
153
|
| `SCOOT_MAX_STEPS` | tool-call steps per turn before asking to continue | `50` |
|
|
@@ -229,9 +229,9 @@ def _interactive(pool: ProviderPool, resume=None, remember_model: Optional[str]
|
|
|
229
229
|
"""Launch the persistent REPL (banner, live status, ESC-interrupt, slash-commands).
|
|
230
230
|
|
|
231
231
|
``remember_model`` is the model named on the command line (``--model``), if any. Launching the REPL
|
|
232
|
-
with it counts as choosing it, exactly like ``/model`` inside the REPL, so
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
with it counts as choosing it for this folder, exactly like ``/model`` inside the REPL, so a plain
|
|
233
|
+
``scoot`` here next time starts on the same model. One-shot prompts and headless runs do not save
|
|
234
|
+
it: a flag on a single command is not a choice for the next session.
|
|
235
235
|
"""
|
|
236
236
|
from .repl import Repl
|
|
237
237
|
|
|
@@ -239,7 +239,7 @@ def _interactive(pool: ProviderPool, resume=None, remember_model: Optional[str]
|
|
|
239
239
|
if remember_model and remember_model.strip():
|
|
240
240
|
from .preferences import set_model
|
|
241
241
|
|
|
242
|
-
set_model(remember_model.strip())
|
|
242
|
+
set_model(remember_model.strip(), root=config.root)
|
|
243
243
|
# resume=auto (SCOOT_RESUME / --resume-last): reload the latest session for this directory
|
|
244
244
|
# unless the user already picked one explicitly via --resume/--continue.
|
|
245
245
|
if resume is None and getattr(config, "resume", "hint") == "auto":
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""/model: list available models or switch the active one.
|
|
2
|
+
|
|
3
|
+
Models are ``provider/model`` ids (``openai/gpt-5.3-codex``, ``ollama/llama3.2``); a bare name means
|
|
4
|
+
the default provider. A switch made here is **persisted** (``~/.config/scoot/preferences.json``) for
|
|
5
|
+
this folder, so the next launch here reuses it; ``/model X everywhere`` saves it for every folder
|
|
6
|
+
without a choice of its own, and ``/model forget`` drops this folder's choice so it follows that. An
|
|
7
|
+
explicit ``--model`` flag or ``SCOOT_MODEL`` still overrides it for that run.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .. import preferences
|
|
13
|
+
from ..providers.base import qualify, split_model_id
|
|
14
|
+
from ..rendering import color
|
|
15
|
+
from . import register
|
|
16
|
+
from .base import SlashCommand
|
|
17
|
+
|
|
18
|
+
_USAGE = "/model <provider/model> · /model default · /model auto (pick per task) · add 'everywhere' for all folders · /model forget"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _root(session):
|
|
22
|
+
return getattr(getattr(session, "config", None), "root", None)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _saved_line(session) -> str:
|
|
26
|
+
folder, everywhere = preferences.saved_models(_root(session))
|
|
27
|
+
return f"saved: this folder {folder or '(none)'} · everywhere {everywhere or '(none)'}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _remember(session, name: str, everywhere: bool) -> str:
|
|
31
|
+
"""Persist ``name`` for this folder (or every folder); return how to say so."""
|
|
32
|
+
root = _root(session)
|
|
33
|
+
if everywhere:
|
|
34
|
+
preferences.set_model_everywhere(name, root=root)
|
|
35
|
+
_note_origin(session, name, preferences.EVERYWHERE)
|
|
36
|
+
return "saved for every folder"
|
|
37
|
+
preferences.set_model(name, root=root)
|
|
38
|
+
_note_origin(session, name, preferences.FOLDER)
|
|
39
|
+
return "saved for this folder"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _note_origin(session, name: str, source: str) -> None:
|
|
43
|
+
"""Keep ``config.model_source`` truthful after a switch, so ``/status`` names the right layer."""
|
|
44
|
+
cfg = getattr(session, "config", None)
|
|
45
|
+
if cfg is not None and hasattr(cfg, "override"):
|
|
46
|
+
session.config = cfg.override(model=name, model_source=source)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _run(session, args: str):
|
|
50
|
+
words = args.split()
|
|
51
|
+
everywhere = False
|
|
52
|
+
if words and words[-1].lower() == "everywhere":
|
|
53
|
+
everywhere = True
|
|
54
|
+
words = words[:-1]
|
|
55
|
+
arg = " ".join(words)
|
|
56
|
+
available = session.available_models()
|
|
57
|
+
|
|
58
|
+
if not arg:
|
|
59
|
+
print(color(f"models (preference: {session.model} → active: {session.active_model})", "bold"))
|
|
60
|
+
print(color(f" {_saved_line(session)}", "gray"))
|
|
61
|
+
current = ""
|
|
62
|
+
for mid in available:
|
|
63
|
+
head, _ = split_model_id(mid)
|
|
64
|
+
if head != current:
|
|
65
|
+
current = head
|
|
66
|
+
print(color(f" {head}", "bold"))
|
|
67
|
+
mark = color(" ← active", "green") if mid == session.active_model else ""
|
|
68
|
+
print(" " + color(mid, "cyan") + mark)
|
|
69
|
+
errors = getattr(getattr(session, "provider", None), "list_errors", None) or {}
|
|
70
|
+
for name, err in sorted(errors.items()):
|
|
71
|
+
print(color(f" {name}: unavailable ({err})", "yellow"))
|
|
72
|
+
print(color(f" use: {_USAGE}", "gray"))
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if arg == "forget":
|
|
76
|
+
preferences.clear_model(root=_root(session))
|
|
77
|
+
session.model = preferences.get_model(_root(session)) or "default"
|
|
78
|
+
session.active_model = session.resolved_model()
|
|
79
|
+
_note_origin(session, session.model, preferences.model_source(_root(session)) or "default")
|
|
80
|
+
print(color(f"this folder's model choice forgotten; now {session.model} → {session.active_model}. "
|
|
81
|
+
f"{_saved_line(session)}", "gray"))
|
|
82
|
+
if hasattr(session, "refresh_readiness") and not session.refresh_readiness():
|
|
83
|
+
print(color(session.setup_message, "yellow"))
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
if arg in ("auto", "default"):
|
|
87
|
+
session.model = arg
|
|
88
|
+
session.active_model = session.resolved_model()
|
|
89
|
+
how = _remember(session, arg, everywhere)
|
|
90
|
+
what = "picked per task from the live list" if arg == "auto" else f"the provider's preferred model, now {session.active_model}"
|
|
91
|
+
print(color(f"model preference set to {arg} ({what}); {how}.", "gray"))
|
|
92
|
+
if hasattr(session, "refresh_readiness") and not session.refresh_readiness():
|
|
93
|
+
print(color(session.setup_message, "yellow"))
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
if arg not in available:
|
|
97
|
+
head, _ = split_model_id(arg)
|
|
98
|
+
if head is None:
|
|
99
|
+
try:
|
|
100
|
+
arg = qualify(session.provider.default_name, arg)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
if arg not in available:
|
|
104
|
+
print(color(f"warning: '{arg}' is not in the models list; setting it anyway.", "yellow"))
|
|
105
|
+
session.model = arg
|
|
106
|
+
session.active_model = arg
|
|
107
|
+
how = _remember(session, arg, everywhere)
|
|
108
|
+
print(color(f"model set to {arg}; {how}.", "gray"))
|
|
109
|
+
if hasattr(session, "refresh_readiness") and not session.refresh_readiness():
|
|
110
|
+
print(color(session.setup_message, "yellow"))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
register(SlashCommand("model", "list or switch model (default|auto|provider/model), per folder or everywhere",
|
|
114
|
+
_run, usage="[provider/model|default|auto] [everywhere] | forget"))
|
|
115
|
+
|
|
@@ -7,6 +7,16 @@ from . import register
|
|
|
7
7
|
from .base import SlashCommand
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
_ORIGINS = {"flag": "--model", "env": "SCOOT_MODEL", "folder": "saved for this folder",
|
|
11
|
+
"everywhere": "saved for every folder", "default": "the default"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _model_origin(session) -> str:
|
|
15
|
+
"""``" (from --model)"`` and the like: which layer chose the model this run started with."""
|
|
16
|
+
src = getattr(getattr(session, "config", None), "model_source", "")
|
|
17
|
+
return f" (from {_ORIGINS[src]})" if src in _ORIGINS else ""
|
|
18
|
+
|
|
19
|
+
|
|
10
20
|
def _run(session, args: str):
|
|
11
21
|
cfg = session.config
|
|
12
22
|
try:
|
|
@@ -20,7 +30,8 @@ def _run(session, args: str):
|
|
|
20
30
|
provider_row = getattr(cfg, "provider", "") or "?"
|
|
21
31
|
rows = [
|
|
22
32
|
("provider", provider_row),
|
|
23
|
-
("model", session.active_model + (" (auto)" if session.model.lower() == "auto" else "")
|
|
33
|
+
("model", session.active_model + (" (auto)" if session.model.lower() == "auto" else "")
|
|
34
|
+
+ _model_origin(session)),
|
|
24
35
|
("effort", getattr(cfg, "effort", "medium")),
|
|
25
36
|
("env files", ", ".join(getattr(cfg, "env_files", ()) or ()) or "none"),
|
|
26
37
|
("update", (f"{session.update_available} available: pipx upgrade scootcli"
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Precedence (highest wins):
|
|
4
4
|
CLI flag → environment variable → project ``.env`` → global ``~/.config/scoot/.env`` → default.
|
|
5
|
+
The model has two more layers between the ``.env`` files and the default: the model saved for this
|
|
6
|
+
workspace root, then the one saved for every folder (see ``preferences.py``); ``model_source`` says
|
|
7
|
+
which layer answered.
|
|
5
8
|
|
|
6
9
|
Two optional ``.env`` files are read: the global one in scoot's config home, and the nearest ``.env``
|
|
7
10
|
found walking up from the current directory (so a repo-root file applies from any subdirectory).
|
|
@@ -129,16 +132,23 @@ def is_model_alias(model: Optional[str]) -> bool:
|
|
|
129
132
|
return (model or "").strip().lower() in (DEFAULT_MODEL_ALIAS, AUTO_MODEL_ALIAS, "")
|
|
130
133
|
|
|
131
134
|
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
MODEL_SOURCES = ("flag", "env", "folder", "everywhere", "default")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _resolve_model_setting(model_env: Optional[str], root=None) -> Tuple[str, str]:
|
|
139
|
+
"""``(model, source)`` below the CLI flag (applied later): SCOOT_MODEL → the model saved for
|
|
140
|
+
``root`` → the model saved for every folder → default."""
|
|
134
141
|
if model_env:
|
|
135
|
-
return model_env
|
|
142
|
+
return model_env, "env"
|
|
136
143
|
try:
|
|
137
|
-
from .preferences import get_model
|
|
144
|
+
from .preferences import get_model, model_source
|
|
138
145
|
|
|
139
|
-
|
|
146
|
+
saved = get_model(root)
|
|
147
|
+
if saved:
|
|
148
|
+
return saved, model_source(root) or "everywhere"
|
|
140
149
|
except Exception:
|
|
141
|
-
|
|
150
|
+
pass
|
|
151
|
+
return DEFAULT_MODEL_ALIAS, "default"
|
|
142
152
|
|
|
143
153
|
|
|
144
154
|
def _resolve_logo_setting(logo_env: Optional[str]) -> bool:
|
|
@@ -161,6 +171,7 @@ class Config:
|
|
|
161
171
|
proxy: str = DEFAULT_PROXY
|
|
162
172
|
provider: str = "" # default provider name; empty → first provider with a key, else ollama
|
|
163
173
|
model: str = DEFAULT_MODEL_ALIAS # "default" | "auto" | a model id, ideally provider/model
|
|
174
|
+
model_source: str = "default" # where ``model`` came from: flag | env | folder | everywhere | default
|
|
164
175
|
effort: str = "medium" # reasoning effort for models that take it: low | medium | high | xhigh
|
|
165
176
|
timeout: int = 120
|
|
166
177
|
max_steps: int = 50
|
|
@@ -194,17 +205,20 @@ class Config:
|
|
|
194
205
|
load_dotenv(env_file)
|
|
195
206
|
|
|
196
207
|
get = os.environ.get
|
|
208
|
+
root = Path(get("SCOOT_ROOT", str(cwd))).resolve()
|
|
209
|
+
model, model_source = _resolve_model_setting(get("SCOOT_MODEL"), root)
|
|
197
210
|
return cls(
|
|
198
211
|
proxy=get("HTTPS_PROXY") or get("https_proxy") or DEFAULT_PROXY,
|
|
199
212
|
env_files=tuple(str(f) for f in files),
|
|
200
213
|
provider=get("SCOOT_PROVIDER", "").strip().lower(),
|
|
201
|
-
model=
|
|
214
|
+
model=model,
|
|
215
|
+
model_source=model_source,
|
|
202
216
|
effort=get("SCOOT_EFFORT", "medium").strip().lower(),
|
|
203
217
|
timeout=int(get("SCOOT_TIMEOUT", "120")),
|
|
204
218
|
max_steps=int(get("SCOOT_MAX_STEPS", "50")),
|
|
205
219
|
compact_at=int(get("SCOOT_COMPACT_AT", "100000")),
|
|
206
220
|
approval=get("SCOOT_APPROVAL", "yolo"),
|
|
207
|
-
root=
|
|
221
|
+
root=root,
|
|
208
222
|
verbose=_as_bool(get("SCOOT_VERBOSE", "false")),
|
|
209
223
|
stream=_as_bool(get("SCOOT_STREAM", "true")),
|
|
210
224
|
panel=_as_bool(get("SCOOT_PANEL", "true")),
|
|
@@ -223,10 +237,18 @@ class Config:
|
|
|
223
237
|
)
|
|
224
238
|
|
|
225
239
|
def override(self, **kwargs) -> "Config":
|
|
226
|
-
"""Return a copy with the given (non-None) fields overridden
|
|
240
|
+
"""Return a copy with the given (non-None) fields overridden: the CLI-flag layer.
|
|
241
|
+
|
|
242
|
+
A ``model`` here is the flag. A new ``root`` (``--root``) without one re-resolves the saved
|
|
243
|
+
model for that folder, unless the flag or ``SCOOT_MODEL`` already decided it.
|
|
244
|
+
"""
|
|
227
245
|
clean = {k: v for k, v in kwargs.items() if v is not None}
|
|
228
246
|
if "root" in clean:
|
|
229
247
|
clean["root"] = Path(clean["root"]).resolve()
|
|
248
|
+
if "model" in clean:
|
|
249
|
+
clean.setdefault("model_source", "flag")
|
|
250
|
+
elif "root" in clean and self.model_source not in ("flag", "env") and clean["root"] != self.root:
|
|
251
|
+
clean["model"], clean["model_source"] = _resolve_model_setting(None, clean["root"])
|
|
230
252
|
return replace(self, **clean)
|
|
231
253
|
|
|
232
254
|
def resolve_model(self, task_hint: str = "") -> str:
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Persisted user preferences (the chosen model, the mascot toggle), surviving across launches.
|
|
2
|
+
|
|
3
|
+
One file at ``~/.config/scoot/preferences.json`` (``SCOOT_CONFIG_DIR`` overrides the location, used by
|
|
4
|
+
tests). The model preference has two layers: ``models`` maps a workspace root to the model chosen
|
|
5
|
+
there (``/model`` or ``--model`` in that folder), and ``model`` is the choice for every folder without
|
|
6
|
+
one (``/model X everywhere``). Writing is best-effort: a failure never breaks the CLI.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from .config import SCOOT_CONFIG_DIR
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _config_dir() -> Path:
|
|
20
|
+
override = os.environ.get("SCOOT_CONFIG_DIR")
|
|
21
|
+
return Path(override).expanduser() if override else SCOOT_CONFIG_DIR
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _prefs_file() -> Path:
|
|
25
|
+
return _config_dir() / "preferences.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_preferences() -> dict:
|
|
29
|
+
try:
|
|
30
|
+
data = json.loads(_prefs_file().read_text())
|
|
31
|
+
return data if isinstance(data, dict) else {}
|
|
32
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _write(prefs: dict) -> None:
|
|
37
|
+
directory = _config_dir()
|
|
38
|
+
try:
|
|
39
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
try:
|
|
41
|
+
os.chmod(directory, 0o700)
|
|
42
|
+
except OSError:
|
|
43
|
+
pass
|
|
44
|
+
path = _prefs_file()
|
|
45
|
+
tmp = directory / ".preferences.tmp"
|
|
46
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
47
|
+
with os.fdopen(fd, "w") as fh:
|
|
48
|
+
json.dump(prefs, fh)
|
|
49
|
+
os.replace(tmp, path) # atomic
|
|
50
|
+
except OSError:
|
|
51
|
+
pass # preferences are best-effort
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def save_preference(key: str, value) -> None:
|
|
55
|
+
prefs = load_preferences()
|
|
56
|
+
if value is None:
|
|
57
|
+
prefs.pop(key, None)
|
|
58
|
+
else:
|
|
59
|
+
prefs[key] = value
|
|
60
|
+
_write(prefs)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── model preference ────────────────────────────────────────────────────────────
|
|
64
|
+
FOLDER = "folder" # the model saved for this workspace root
|
|
65
|
+
EVERYWHERE = "everywhere" # the model saved for every folder without its own
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _root_key(root) -> str:
|
|
69
|
+
return str(Path(root).expanduser().resolve())
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _folder_models(prefs: dict) -> dict:
|
|
73
|
+
models = prefs.get("models")
|
|
74
|
+
return models if isinstance(models, dict) else {}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def saved_models(root=None):
|
|
78
|
+
"""``(folder_model, global_model)``: what is saved for ``root`` (``None`` without a root) and for
|
|
79
|
+
everywhere; either may be ``None``."""
|
|
80
|
+
prefs = load_preferences()
|
|
81
|
+
folder = _folder_models(prefs).get(_root_key(root)) if root is not None else None
|
|
82
|
+
return (folder or None), (prefs.get("model") or None)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_model(root=None) -> Optional[str]:
|
|
86
|
+
"""The saved model that applies in ``root``: the folder's own choice, else the global one."""
|
|
87
|
+
folder, everywhere = saved_models(root)
|
|
88
|
+
return folder or everywhere
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def model_source(root=None) -> Optional[str]:
|
|
92
|
+
"""Which layer :func:`get_model` answered from: ``"folder"``, ``"everywhere"``, or ``None``."""
|
|
93
|
+
folder, everywhere = saved_models(root)
|
|
94
|
+
return FOLDER if folder else (EVERYWHERE if everywhere else None)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def set_model(name: str, root=None) -> None:
|
|
98
|
+
"""Persist the chosen model (including ``auto`` / ``default``) so the next launch reuses it: for
|
|
99
|
+
the folder ``root`` when given, otherwise for every folder (which also drops the folder's own
|
|
100
|
+
entry when ``root`` is passed as ``everywhere=True`` via :func:`set_model_everywhere`)."""
|
|
101
|
+
if root is None:
|
|
102
|
+
save_preference("model", name)
|
|
103
|
+
return
|
|
104
|
+
prefs = load_preferences()
|
|
105
|
+
models = dict(_folder_models(prefs))
|
|
106
|
+
models[_root_key(root)] = name
|
|
107
|
+
prefs["models"] = models
|
|
108
|
+
_write(prefs)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def set_model_everywhere(name: str, root=None) -> None:
|
|
112
|
+
"""Make ``name`` the model for every folder, and forget ``root``'s own choice so it applies there too."""
|
|
113
|
+
prefs = load_preferences()
|
|
114
|
+
prefs["model"] = name
|
|
115
|
+
if root is not None:
|
|
116
|
+
models = dict(_folder_models(prefs))
|
|
117
|
+
models.pop(_root_key(root), None)
|
|
118
|
+
prefs["models"] = models
|
|
119
|
+
_write(prefs)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def clear_model(root=None) -> None:
|
|
123
|
+
"""Forget the saved model: the folder's own entry when ``root`` is given, else the global one."""
|
|
124
|
+
if root is None:
|
|
125
|
+
save_preference("model", None)
|
|
126
|
+
return
|
|
127
|
+
prefs = load_preferences()
|
|
128
|
+
models = dict(_folder_models(prefs))
|
|
129
|
+
if models.pop(_root_key(root), None) is not None:
|
|
130
|
+
prefs["models"] = models
|
|
131
|
+
_write(prefs)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── mascot preference ───────────────────────────────────────────────────────────
|
|
135
|
+
def get_logo() -> Optional[bool]:
|
|
136
|
+
"""``True``/``False`` if the user chose via ``/logo on|off``; ``None`` when unset (default on)."""
|
|
137
|
+
value = load_preferences().get("logo")
|
|
138
|
+
return value if isinstance(value, bool) else None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def set_logo(on: Optional[bool]) -> None:
|
|
142
|
+
"""Persist the mascot toggle (``None`` forgets it → back to the default)."""
|
|
143
|
+
save_preference("logo", on)
|
|
144
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scootcli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: A tiny coding agent that goes where you point it
|
|
5
5
|
Author: Sergey Nes
|
|
6
6
|
License-Expression: MIT
|
|
@@ -115,8 +115,9 @@ scoot --model openai/gpt-5.3-codex "..."
|
|
|
115
115
|
scoot --model auto "..." # pick a model per prompt from the live list (cheap for trivial, strong for edits)
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
Inside the REPL, `/model <provider/model>` switches and is remembered for the
|
|
119
|
-
Starting the REPL with `--model` is remembered the same way, so `scoot --model ollama/llama3.2` once and plain `scoot` afterwards keep using it; a one-shot `scoot --model X "prompt"` does not change the saved choice.
|
|
118
|
+
Inside the REPL, `/model <provider/model>` switches and is remembered for the folder you are in, so each project can have its own model; `/model default` goes back to the provider's preferred model.
|
|
119
|
+
Starting the REPL with `--model` is remembered the same way, so `scoot --model ollama/llama3.2` once and plain `scoot` afterwards keep using it there; a one-shot `scoot --model X "prompt"` does not change the saved choice.
|
|
120
|
+
`/model X everywhere` makes `X` the model for every folder that has no choice of its own, `/model forget` drops the current folder's choice so it follows that, and `/status` says where the active model came from.
|
|
120
121
|
|
|
121
122
|
### Routing
|
|
122
123
|
|
|
@@ -171,7 +172,7 @@ The most useful settings (see [`.env.example`](./.env.example) for all of them):
|
|
|
171
172
|
|---|---|---|
|
|
172
173
|
| `OPENAI_API_KEY` | OpenAI key (or `scoot auth set openai`) | |
|
|
173
174
|
| `SCOOT_PROVIDER` | default provider for bare model names | first with a key, else `ollama` |
|
|
174
|
-
| `SCOOT_MODEL` | `default`, `auto`, or `provider/model
|
|
175
|
+
| `SCOOT_MODEL` | `default`, `auto`, or `provider/model`; overrides the saved per-folder and global choices | `default` |
|
|
175
176
|
| `SCOOT_EFFORT` | reasoning effort | `medium` |
|
|
176
177
|
| `SCOOT_APPROVAL` | `always` · `auto-read` · `auto-edits` · `yolo` | `yolo` |
|
|
177
178
|
| `SCOOT_MAX_STEPS` | tool-call steps per turn before asking to continue | `50` |
|