scootcli 0.1.0__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.
Files changed (97) hide show
  1. scootcli-0.1.0/LICENSE +21 -0
  2. scootcli-0.1.0/PKG-INFO +238 -0
  3. scootcli-0.1.0/README.md +213 -0
  4. scootcli-0.1.0/pyproject.toml +45 -0
  5. scootcli-0.1.0/setup.cfg +4 -0
  6. scootcli-0.1.0/src/scootcli/__init__.py +4 -0
  7. scootcli-0.1.0/src/scootcli/__main__.py +9 -0
  8. scootcli-0.1.0/src/scootcli/activity.py +26 -0
  9. scootcli-0.1.0/src/scootcli/agent.py +350 -0
  10. scootcli-0.1.0/src/scootcli/approvals.py +167 -0
  11. scootcli-0.1.0/src/scootcli/auth.py +59 -0
  12. scootcli-0.1.0/src/scootcli/cli.py +276 -0
  13. scootcli-0.1.0/src/scootcli/clipboard.py +89 -0
  14. scootcli-0.1.0/src/scootcli/commands/__init__.py +56 -0
  15. scootcli-0.1.0/src/scootcli/commands/approve.py +38 -0
  16. scootcli-0.1.0/src/scootcli/commands/auth.py +107 -0
  17. scootcli-0.1.0/src/scootcli/commands/base.py +31 -0
  18. scootcli-0.1.0/src/scootcli/commands/compact.py +40 -0
  19. scootcli-0.1.0/src/scootcli/commands/copy.py +23 -0
  20. scootcli-0.1.0/src/scootcli/commands/exit.py +14 -0
  21. scootcli-0.1.0/src/scootcli/commands/forget.py +28 -0
  22. scootcli-0.1.0/src/scootcli/commands/help.py +29 -0
  23. scootcli-0.1.0/src/scootcli/commands/init.py +50 -0
  24. scootcli-0.1.0/src/scootcli/commands/logo.py +51 -0
  25. scootcli-0.1.0/src/scootcli/commands/model.py +61 -0
  26. scootcli-0.1.0/src/scootcli/commands/panel.py +28 -0
  27. scootcli-0.1.0/src/scootcli/commands/reset.py +20 -0
  28. scootcli-0.1.0/src/scootcli/commands/resume.py +31 -0
  29. scootcli-0.1.0/src/scootcli/commands/save.py +29 -0
  30. scootcli-0.1.0/src/scootcli/commands/sessions.py +42 -0
  31. scootcli-0.1.0/src/scootcli/commands/status.py +59 -0
  32. scootcli-0.1.0/src/scootcli/commands/verbosity.py +57 -0
  33. scootcli-0.1.0/src/scootcli/commands/worktree.py +64 -0
  34. scootcli-0.1.0/src/scootcli/commands/yolo.py +20 -0
  35. scootcli-0.1.0/src/scootcli/config.py +241 -0
  36. scootcli-0.1.0/src/scootcli/context.py +82 -0
  37. scootcli-0.1.0/src/scootcli/credentials.py +79 -0
  38. scootcli-0.1.0/src/scootcli/errors.py +87 -0
  39. scootcli-0.1.0/src/scootcli/images.py +169 -0
  40. scootcli-0.1.0/src/scootcli/keys.py +119 -0
  41. scootcli-0.1.0/src/scootcli/lineeditor.py +577 -0
  42. scootcli-0.1.0/src/scootcli/logo.py +116 -0
  43. scootcli-0.1.0/src/scootcli/models.py +120 -0
  44. scootcli-0.1.0/src/scootcli/panel.py +263 -0
  45. scootcli-0.1.0/src/scootcli/preferences.py +87 -0
  46. scootcli-0.1.0/src/scootcli/presets.py +38 -0
  47. scootcli-0.1.0/src/scootcli/project.py +94 -0
  48. scootcli-0.1.0/src/scootcli/prompts.py +100 -0
  49. scootcli-0.1.0/src/scootcli/providers/__init__.py +20 -0
  50. scootcli-0.1.0/src/scootcli/providers/base.py +370 -0
  51. scootcli-0.1.0/src/scootcli/providers/openai_chat.py +142 -0
  52. scootcli-0.1.0/src/scootcli/providers/openai_responses.py +248 -0
  53. scootcli-0.1.0/src/scootcli/providers/registry.py +173 -0
  54. scootcli-0.1.0/src/scootcli/rendering.py +86 -0
  55. scootcli-0.1.0/src/scootcli/repl.py +801 -0
  56. scootcli-0.1.0/src/scootcli/sessions.py +186 -0
  57. scootcli-0.1.0/src/scootcli/status.py +71 -0
  58. scootcli-0.1.0/src/scootcli/tools/__init__.py +68 -0
  59. scootcli-0.1.0/src/scootcli/tools/base.py +152 -0
  60. scootcli-0.1.0/src/scootcli/tools/edit_file.py +72 -0
  61. scootcli-0.1.0/src/scootcli/tools/list_dir.py +47 -0
  62. scootcli-0.1.0/src/scootcli/tools/read_file.py +56 -0
  63. scootcli-0.1.0/src/scootcli/tools/run_shell.py +73 -0
  64. scootcli-0.1.0/src/scootcli/tools/search.py +170 -0
  65. scootcli-0.1.0/src/scootcli/tools/update_plan.py +104 -0
  66. scootcli-0.1.0/src/scootcli/tools/write_file.py +61 -0
  67. scootcli-0.1.0/src/scootcli/transport.py +312 -0
  68. scootcli-0.1.0/src/scootcli/vision.py +167 -0
  69. scootcli-0.1.0/src/scootcli/workspace.py +105 -0
  70. scootcli-0.1.0/src/scootcli/worktree.py +114 -0
  71. scootcli-0.1.0/src/scootcli.egg-info/PKG-INFO +238 -0
  72. scootcli-0.1.0/src/scootcli.egg-info/SOURCES.txt +95 -0
  73. scootcli-0.1.0/src/scootcli.egg-info/dependency_links.txt +1 -0
  74. scootcli-0.1.0/src/scootcli.egg-info/entry_points.txt +2 -0
  75. scootcli-0.1.0/src/scootcli.egg-info/top_level.txt +1 -0
  76. scootcli-0.1.0/tests/test_agent.py +157 -0
  77. scootcli-0.1.0/tests/test_approvals.py +244 -0
  78. scootcli-0.1.0/tests/test_auth.py +72 -0
  79. scootcli-0.1.0/tests/test_clipboard.py +149 -0
  80. scootcli-0.1.0/tests/test_config.py +124 -0
  81. scootcli-0.1.0/tests/test_images.py +205 -0
  82. scootcli-0.1.0/tests/test_lineeditor.py +425 -0
  83. scootcli-0.1.0/tests/test_logo.py +141 -0
  84. scootcli-0.1.0/tests/test_models.py +108 -0
  85. scootcli-0.1.0/tests/test_panel.py +117 -0
  86. scootcli-0.1.0/tests/test_preferences.py +81 -0
  87. scootcli-0.1.0/tests/test_providers.py +220 -0
  88. scootcli-0.1.0/tests/test_repl.py +255 -0
  89. scootcli-0.1.0/tests/test_resilience.py +121 -0
  90. scootcli-0.1.0/tests/test_sessions.py +179 -0
  91. scootcli-0.1.0/tests/test_smoke.py +116 -0
  92. scootcli-0.1.0/tests/test_streaming.py +251 -0
  93. scootcli-0.1.0/tests/test_tools.py +208 -0
  94. scootcli-0.1.0/tests/test_transport_native.py +158 -0
  95. scootcli-0.1.0/tests/test_vision.py +178 -0
  96. scootcli-0.1.0/tests/test_workspace.py +120 -0
  97. scootcli-0.1.0/tests/test_worktree.py +92 -0
scootcli-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sergey Neskoromny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: scootcli
3
+ Version: 0.1.0
4
+ Summary: A tiny coding agent that goes where you point it
5
+ Author: Sergey Nes
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sergenes/scootcli
8
+ Project-URL: Repository, https://github.com/sergenes/scootcli
9
+ Project-URL: Issues, https://github.com/sergenes/scootcli/issues
10
+ Project-URL: Changelog, https://github.com/sergenes/scootcli/blob/main/CHANGELOG.md
11
+ Keywords: coding-agent,cli,terminal,llm,openai,ollama,agent,stdlib
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Topic :: Software Development
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # scoot
27
+
28
+ ```
29
+ ╭───╮ scoot: a tiny coding agent that goes where you point it.
30
+ │o o│
31
+ T──┤───┤ pipx install scootcli
32
+ │ ╰┬─┬╯ scoot
33
+ (o)═══╧═╧═(o)
34
+ ```
35
+
36
+ `scoot` is a terminal coding agent in plain Python.
37
+ You type what you want in natural language; it reads, searches, edits, and runs things in your repo, asking before anything risky.
38
+ It talks to official model APIs (OpenAI today, a local Ollama for free, Anthropic next) and has **zero third-party dependencies**: the whole tool is the Python standard library, and it ships as a single-file zipapp as well as a wheel.
39
+
40
+ ## Where this comes from
41
+
42
+ scoot began as [mini_agent](https://github.com/sergenes/mini_agent), a fifty-line Python script written to answer one question: where exactly does a chatbot turn into an agent?
43
+ The answer was a `while` loop that sends the conversation to a model, runs whatever tool the model asks for, appends the result, and goes around again until the model answers without calling a tool.
44
+ The article [Building an AI Agent from Scratch: No Magic, Just a Deterministic Loop](https://levelup.gitconnected.com/building-an-ai-agent-from-scratch-no-magic-just-a-deterministic-loop-a916161705fb?sk=bd25f188cb4cb68d52ef5b77dd67336a) (free link) walks through that script, swaps the cloud model for a local one, and adds tools and MCP on top of the same loop.
45
+ Its conclusion is the design brief for this tool:
46
+
47
+ > There's no magic. The model observes the conversation history, decides whether it has enough to answer or needs a tool, and repeats until it's done.
48
+ >
49
+ > Build the naive version first. Then decide.
50
+
51
+ The naive version did real work in my repos for long enough that the next step was obvious: keep the deterministic loop at the centre and build the rest of a proper command-line tool around it, approvals, sessions, a REPL, and a provider layer, without adding a framework or a dependency.
52
+
53
+ ## Quick start
54
+
55
+ ```bash
56
+ pipx install scootcli # or: pip install scootcli
57
+ scoot auth set openai # paste your OpenAI API key once (hidden input, validated, stored 0600)
58
+ cd ~/code/your-project
59
+ scoot # open the REPL
60
+ ```
61
+
62
+ Or run entirely local with [Ollama](https://ollama.com), no key at all:
63
+
64
+ ```bash
65
+ ollama pull llama3.2
66
+ scoot --model ollama/llama3.2
67
+ ```
68
+
69
+ The first prompt:
70
+
71
+ ```
72
+ ❯ add a --version flag to cli.py
73
+ I'll read cli.py, then add the flag.
74
+ ● read_file {"path": "cli.py"}
75
+ ✔ read_file 128 lines
76
+ ● edit_file {"path": "cli.py", ...} (+6 -0)
77
+ ✔ edit_file cli.py +6 -0
78
+ 🛴 scoot
79
+ Added --version to the parser; it prints the package version and exits.
80
+ ```
81
+
82
+ ## Providers and models
83
+
84
+ Models are addressed as `provider/model`.
85
+ A bare name means the default provider, which is the first provider that has a key, else Ollama.
86
+
87
+ | provider | how it is reached | key | default model |
88
+ |---|---|---|---|
89
+ | `openai` | OpenAI Responses API | `OPENAI_API_KEY` | `gpt-5.3-codex` |
90
+ | `ollama` | local Ollama, Responses API | none | `llama3.2` |
91
+ | `anthropic` | Messages API | `ANTHROPIC_API_KEY` | arrives in 0.2.0 |
92
+
93
+ ```bash
94
+ scoot models # every configured provider, grouped
95
+ scoot models --provider ollama # one provider
96
+ scoot --model openai/gpt-5.3-codex "..."
97
+ scoot --model auto "..." # pick a model per prompt from the live list (cheap for trivial, strong for edits)
98
+ ```
99
+
100
+ Inside the REPL, `/model <provider/model>` switches and is remembered for the next launch; `/model default` goes back to the provider's preferred model.
101
+ `SCOOT_EFFORT` (`low` | `medium` | `high` | `xhigh`, default `medium`) sets the reasoning effort for models that take it.
102
+
103
+ A note on how this is built: scoot speaks the OpenAI chat format internally and translates at the edge.
104
+ Adding a provider that speaks that format is one registry row; a different wire format is one small adapter (`src/scootcli/providers/`).
105
+
106
+ ## Configuration
107
+
108
+ Settings resolve as **CLI flag → environment variable → project `.env` → global `.env` → default**.
109
+
110
+ Two optional `.env` files are read: `~/.config/scoot/.env` (the stable place for keys) and the nearest `.env` walking up from the current directory.
111
+ Only scoot's own keys are imported from them: `SCOOT_*`, provider API keys, and `HTTPS_PROXY` / `NO_PROXY`.
112
+ A project's other secrets never enter scoot's process through a `.env` file.
113
+
114
+ ```bash
115
+ cp .env.example ~/.config/scoot/.env && chmod 600 ~/.config/scoot/.env
116
+ ```
117
+
118
+ The most useful settings (see [`.env.example`](./.env.example) for all of them):
119
+
120
+ | setting | meaning | default |
121
+ |---|---|---|
122
+ | `OPENAI_API_KEY` | OpenAI key (or `scoot auth set openai`) | |
123
+ | `SCOOT_PROVIDER` | default provider for bare model names | first with a key, else `ollama` |
124
+ | `SCOOT_MODEL` | `default`, `auto`, or `provider/model` | `default` |
125
+ | `SCOOT_EFFORT` | reasoning effort | `medium` |
126
+ | `SCOOT_APPROVAL` | `always` · `auto-read` · `auto-edits` · `yolo` | `yolo` |
127
+ | `SCOOT_MAX_STEPS` | tool-call steps per turn before asking to continue | `50` |
128
+ | `SCOOT_OLLAMA_BASE_URL` | where Ollama listens | `http://localhost:11434/v1` |
129
+ | `HTTPS_PROXY` | proxy for hosted providers; localhost is never proxied | |
130
+
131
+ ## Usage
132
+
133
+ ```bash
134
+ scoot # interactive REPL
135
+ scoot "explain what a Python dataclass is" # one-shot turn, then exit
136
+ scoot explain src/auth.py # preset: explain a file (read-only)
137
+ scoot edit cli.py -m "add a --version flag" # preset: edit with an instruction
138
+ scoot --continue # resume the most recent session for this directory
139
+ scoot --resume <id> # resume a specific saved session
140
+ scoot --yes "fix the failing test" # auto-approve every tool call (scripting/CI)
141
+ scoot --approval auto-edits "..." # auto reads and edits, prompt only for shell
142
+ scoot --json "..." # machine-readable result (never streamed)
143
+ scoot --verbose "..." # model and token usage on stderr
144
+ scoot models --json # machine-readable model list
145
+ scoot auth # which providers have a key
146
+ scoot auth set openai # save a key; scoot auth clear openai forgets it
147
+ scoot --no-logo # hide the mascot; /logo off remembers it
148
+ scoot --no-panel --no-dock # plain prompt, no status bar (also what you get without a TTY)
149
+ ```
150
+
151
+ ### In the REPL
152
+
153
+ ```
154
+ /help list commands /reset clear the conversation
155
+ /status provider, model, tokens /save FILE dump the transcript
156
+ /init scan project → AGENTS.md /compact summarize and shrink context
157
+ /model list or switch model /approve set mode (always|auto-read|auto-edits|yolo)
158
+ /yolo auto-approve all /worktree isolate work in a git worktree
159
+ /auth provider keys /logo show or toggle the mascot
160
+ /sessions list saved sessions /resume resume a saved session ([id])
161
+ /forget delete session(s) /panel toggle the bottom status bar
162
+ /verbosity feed detail (full|compact|quiet)
163
+ /c copy last answer (Ctrl-S) /exit quit (also Ctrl-C)
164
+ ```
165
+
166
+ Press **ESC** while a turn is running to interrupt it; the conversation is kept.
167
+ Type `/` and press **Tab** to complete slash commands.
168
+
169
+ **Terminals.** scoot works in macOS Terminal, iTerm2, and inside tmux; the status bar uses a scroll region, the input dock uses raw mode, and clipboard copy uses the system tool or an OSC-52 escape.
170
+ Under tmux, ESC reaches scoot only after tmux's `escape-time` has passed, so with the default 500 ms the interrupt feels delayed; `set -sg escape-time 10` in `~/.tmux.conf` makes it immediate.
171
+ For clipboard copy through tmux, `set -g set-clipboard on` (or `external`) lets the OSC-52 escape reach the outer terminal.
172
+ Without a TTY, scoot falls back to a plain prompt with no bar and no dock.
173
+
174
+ ### Tools and approvals
175
+
176
+ The agent has seven tools: `read_file`, `list_dir`, `search`, `write_file`, `edit_file`, `run_shell`, and `update_plan` (a progress checklist for multi-step work).
177
+ Every tool is sandboxed to the workspace root.
178
+
179
+ When a call needs approval you can approve once `[a]`, trust that tool for the session `[t]`, approve everything this session `[A]`, edit the arguments `[e]`, skip `[s]`, or quit `[q]`.
180
+ `/approve <mode>` sets how much runs without asking: `always` prompts for everything, `auto-read` auto-approves reads, `auto-edits` also auto-approves file edits, `yolo` runs everything.
181
+ Catastrophic shell commands (a denylist: `rm -rf /`, `git push --force`, piping downloads into a shell, and so on) are re-confirmed in every mode.
182
+
183
+ For risky autonomous runs, `/worktree start` moves the work into a throwaway git worktree; `/worktree merge` or `/worktree discard` when done.
184
+
185
+ ### Streaming, images, sessions
186
+
187
+ Responses stream live and stay interruptible.
188
+ Drag an image into the prompt and a vision-capable model describes it into the turn as text, so even a text-only coding model can act on it; `SCOOT_VISION_MODEL` pins the describer, `--no-images` turns the feature off.
189
+ Every turn auto-saves under `~/.local/state/scoot/sessions/` (owner-only, secrets redacted, last 20 kept); `scoot --continue` or `/resume` picks up where you left off.
190
+
191
+ ### The status bar
192
+
193
+ The bottom row shows the mascot's face (its eyes follow the turn: `o o` idle, `> >` thinking, `- -` stopped), the provider, the workspace and session id, the model, the approval mode, context size against the auto-compact threshold, cumulative tokens, message count, and the last error if any.
194
+
195
+ ## Install options
196
+
197
+ ```bash
198
+ pipx install scootcli # recommended: isolated, `scoot` on PATH
199
+ pip install scootcli # anywhere
200
+ curl -LO https://github.com/sergenes/scootcli/releases/latest/download/scoot.pyz && python3 scoot.pyz # single file, no install
201
+ git clone https://github.com/sergenes/scootcli && cd scootcli && pip install -e . # from source
202
+ ```
203
+
204
+ Requirements: Python 3.9 or newer on macOS or Linux.
205
+ Nothing else: no compiler, no packages, no `curl`.
206
+
207
+ ## Development
208
+
209
+ ```bash
210
+ pip install -e .
211
+ python -m pytest -q # network-free suite
212
+ ./scripts/build.sh # dist/scoot.pyz + wheel + sdist
213
+ ```
214
+
215
+ Design notes live in [`DESIGN.md`](./DESIGN.md), the behaviour spec in [`SPEC.md`](./SPEC.md), what is next in [`ROADMAP.md`](./ROADMAP.md), and the release history in [`CHANGELOG.md`](./CHANGELOG.md).
216
+ New capability is a drop-in file: a tool in `tools/`, a slash command in `commands/`, a provider row or adapter in `providers/`.
217
+
218
+ ## Security
219
+
220
+ - API keys are read from the environment or from `~/.config/scoot/credentials.json` (directory `0700`, file `0600`), validated before saving, and never echoed; `rendering.redact()` masks key-shaped strings in all output.
221
+ - Only allowlisted keys are imported from `.env` files.
222
+ - All file and shell tools are sandboxed to the workspace root; destructive shell commands are always re-confirmed.
223
+ - Saved sessions are owner-only with secrets redacted; `/forget all` removes them.
224
+ - No third-party dependencies means no third-party code to audit.
225
+
226
+ ## Contributing
227
+
228
+ Stars and forks are welcome, and so is using scoot to improve scoot: it is a coding agent, so point it at its own repo and let it do the work while you review.
229
+ Bug reports with a way to reproduce them are the most useful thing you can send.
230
+
231
+ Pull requests are welcome too, with one honest caveat: I intend to keep this tool small, so I will not merge most feature proposals.
232
+ A feature gets in when it is clearly useful to most users of a coding agent, fits the stdlib-only constraint, and comes with tests.
233
+ If you have an idea that does not meet that bar, a fork is the right home for it, and I am happy to link to forks that go somewhere interesting.
234
+
235
+ ## License
236
+
237
+ MIT, see [`LICENSE`](./LICENSE).
238
+ Written by [Sergey Nes](https://www.linkedin.com/in/sergey-neskoromny).
@@ -0,0 +1,213 @@
1
+ # scoot
2
+
3
+ ```
4
+ ╭───╮ scoot: a tiny coding agent that goes where you point it.
5
+ │o o│
6
+ T──┤───┤ pipx install scootcli
7
+ │ ╰┬─┬╯ scoot
8
+ (o)═══╧═╧═(o)
9
+ ```
10
+
11
+ `scoot` is a terminal coding agent in plain Python.
12
+ You type what you want in natural language; it reads, searches, edits, and runs things in your repo, asking before anything risky.
13
+ It talks to official model APIs (OpenAI today, a local Ollama for free, Anthropic next) and has **zero third-party dependencies**: the whole tool is the Python standard library, and it ships as a single-file zipapp as well as a wheel.
14
+
15
+ ## Where this comes from
16
+
17
+ scoot began as [mini_agent](https://github.com/sergenes/mini_agent), a fifty-line Python script written to answer one question: where exactly does a chatbot turn into an agent?
18
+ The answer was a `while` loop that sends the conversation to a model, runs whatever tool the model asks for, appends the result, and goes around again until the model answers without calling a tool.
19
+ The article [Building an AI Agent from Scratch: No Magic, Just a Deterministic Loop](https://levelup.gitconnected.com/building-an-ai-agent-from-scratch-no-magic-just-a-deterministic-loop-a916161705fb?sk=bd25f188cb4cb68d52ef5b77dd67336a) (free link) walks through that script, swaps the cloud model for a local one, and adds tools and MCP on top of the same loop.
20
+ Its conclusion is the design brief for this tool:
21
+
22
+ > There's no magic. The model observes the conversation history, decides whether it has enough to answer or needs a tool, and repeats until it's done.
23
+ >
24
+ > Build the naive version first. Then decide.
25
+
26
+ The naive version did real work in my repos for long enough that the next step was obvious: keep the deterministic loop at the centre and build the rest of a proper command-line tool around it, approvals, sessions, a REPL, and a provider layer, without adding a framework or a dependency.
27
+
28
+ ## Quick start
29
+
30
+ ```bash
31
+ pipx install scootcli # or: pip install scootcli
32
+ scoot auth set openai # paste your OpenAI API key once (hidden input, validated, stored 0600)
33
+ cd ~/code/your-project
34
+ scoot # open the REPL
35
+ ```
36
+
37
+ Or run entirely local with [Ollama](https://ollama.com), no key at all:
38
+
39
+ ```bash
40
+ ollama pull llama3.2
41
+ scoot --model ollama/llama3.2
42
+ ```
43
+
44
+ The first prompt:
45
+
46
+ ```
47
+ ❯ add a --version flag to cli.py
48
+ I'll read cli.py, then add the flag.
49
+ ● read_file {"path": "cli.py"}
50
+ ✔ read_file 128 lines
51
+ ● edit_file {"path": "cli.py", ...} (+6 -0)
52
+ ✔ edit_file cli.py +6 -0
53
+ 🛴 scoot
54
+ Added --version to the parser; it prints the package version and exits.
55
+ ```
56
+
57
+ ## Providers and models
58
+
59
+ Models are addressed as `provider/model`.
60
+ A bare name means the default provider, which is the first provider that has a key, else Ollama.
61
+
62
+ | provider | how it is reached | key | default model |
63
+ |---|---|---|---|
64
+ | `openai` | OpenAI Responses API | `OPENAI_API_KEY` | `gpt-5.3-codex` |
65
+ | `ollama` | local Ollama, Responses API | none | `llama3.2` |
66
+ | `anthropic` | Messages API | `ANTHROPIC_API_KEY` | arrives in 0.2.0 |
67
+
68
+ ```bash
69
+ scoot models # every configured provider, grouped
70
+ scoot models --provider ollama # one provider
71
+ scoot --model openai/gpt-5.3-codex "..."
72
+ scoot --model auto "..." # pick a model per prompt from the live list (cheap for trivial, strong for edits)
73
+ ```
74
+
75
+ Inside the REPL, `/model <provider/model>` switches and is remembered for the next launch; `/model default` goes back to the provider's preferred model.
76
+ `SCOOT_EFFORT` (`low` | `medium` | `high` | `xhigh`, default `medium`) sets the reasoning effort for models that take it.
77
+
78
+ A note on how this is built: scoot speaks the OpenAI chat format internally and translates at the edge.
79
+ Adding a provider that speaks that format is one registry row; a different wire format is one small adapter (`src/scootcli/providers/`).
80
+
81
+ ## Configuration
82
+
83
+ Settings resolve as **CLI flag → environment variable → project `.env` → global `.env` → default**.
84
+
85
+ Two optional `.env` files are read: `~/.config/scoot/.env` (the stable place for keys) and the nearest `.env` walking up from the current directory.
86
+ Only scoot's own keys are imported from them: `SCOOT_*`, provider API keys, and `HTTPS_PROXY` / `NO_PROXY`.
87
+ A project's other secrets never enter scoot's process through a `.env` file.
88
+
89
+ ```bash
90
+ cp .env.example ~/.config/scoot/.env && chmod 600 ~/.config/scoot/.env
91
+ ```
92
+
93
+ The most useful settings (see [`.env.example`](./.env.example) for all of them):
94
+
95
+ | setting | meaning | default |
96
+ |---|---|---|
97
+ | `OPENAI_API_KEY` | OpenAI key (or `scoot auth set openai`) | |
98
+ | `SCOOT_PROVIDER` | default provider for bare model names | first with a key, else `ollama` |
99
+ | `SCOOT_MODEL` | `default`, `auto`, or `provider/model` | `default` |
100
+ | `SCOOT_EFFORT` | reasoning effort | `medium` |
101
+ | `SCOOT_APPROVAL` | `always` · `auto-read` · `auto-edits` · `yolo` | `yolo` |
102
+ | `SCOOT_MAX_STEPS` | tool-call steps per turn before asking to continue | `50` |
103
+ | `SCOOT_OLLAMA_BASE_URL` | where Ollama listens | `http://localhost:11434/v1` |
104
+ | `HTTPS_PROXY` | proxy for hosted providers; localhost is never proxied | |
105
+
106
+ ## Usage
107
+
108
+ ```bash
109
+ scoot # interactive REPL
110
+ scoot "explain what a Python dataclass is" # one-shot turn, then exit
111
+ scoot explain src/auth.py # preset: explain a file (read-only)
112
+ scoot edit cli.py -m "add a --version flag" # preset: edit with an instruction
113
+ scoot --continue # resume the most recent session for this directory
114
+ scoot --resume <id> # resume a specific saved session
115
+ scoot --yes "fix the failing test" # auto-approve every tool call (scripting/CI)
116
+ scoot --approval auto-edits "..." # auto reads and edits, prompt only for shell
117
+ scoot --json "..." # machine-readable result (never streamed)
118
+ scoot --verbose "..." # model and token usage on stderr
119
+ scoot models --json # machine-readable model list
120
+ scoot auth # which providers have a key
121
+ scoot auth set openai # save a key; scoot auth clear openai forgets it
122
+ scoot --no-logo # hide the mascot; /logo off remembers it
123
+ scoot --no-panel --no-dock # plain prompt, no status bar (also what you get without a TTY)
124
+ ```
125
+
126
+ ### In the REPL
127
+
128
+ ```
129
+ /help list commands /reset clear the conversation
130
+ /status provider, model, tokens /save FILE dump the transcript
131
+ /init scan project → AGENTS.md /compact summarize and shrink context
132
+ /model list or switch model /approve set mode (always|auto-read|auto-edits|yolo)
133
+ /yolo auto-approve all /worktree isolate work in a git worktree
134
+ /auth provider keys /logo show or toggle the mascot
135
+ /sessions list saved sessions /resume resume a saved session ([id])
136
+ /forget delete session(s) /panel toggle the bottom status bar
137
+ /verbosity feed detail (full|compact|quiet)
138
+ /c copy last answer (Ctrl-S) /exit quit (also Ctrl-C)
139
+ ```
140
+
141
+ Press **ESC** while a turn is running to interrupt it; the conversation is kept.
142
+ Type `/` and press **Tab** to complete slash commands.
143
+
144
+ **Terminals.** scoot works in macOS Terminal, iTerm2, and inside tmux; the status bar uses a scroll region, the input dock uses raw mode, and clipboard copy uses the system tool or an OSC-52 escape.
145
+ Under tmux, ESC reaches scoot only after tmux's `escape-time` has passed, so with the default 500 ms the interrupt feels delayed; `set -sg escape-time 10` in `~/.tmux.conf` makes it immediate.
146
+ For clipboard copy through tmux, `set -g set-clipboard on` (or `external`) lets the OSC-52 escape reach the outer terminal.
147
+ Without a TTY, scoot falls back to a plain prompt with no bar and no dock.
148
+
149
+ ### Tools and approvals
150
+
151
+ The agent has seven tools: `read_file`, `list_dir`, `search`, `write_file`, `edit_file`, `run_shell`, and `update_plan` (a progress checklist for multi-step work).
152
+ Every tool is sandboxed to the workspace root.
153
+
154
+ When a call needs approval you can approve once `[a]`, trust that tool for the session `[t]`, approve everything this session `[A]`, edit the arguments `[e]`, skip `[s]`, or quit `[q]`.
155
+ `/approve <mode>` sets how much runs without asking: `always` prompts for everything, `auto-read` auto-approves reads, `auto-edits` also auto-approves file edits, `yolo` runs everything.
156
+ Catastrophic shell commands (a denylist: `rm -rf /`, `git push --force`, piping downloads into a shell, and so on) are re-confirmed in every mode.
157
+
158
+ For risky autonomous runs, `/worktree start` moves the work into a throwaway git worktree; `/worktree merge` or `/worktree discard` when done.
159
+
160
+ ### Streaming, images, sessions
161
+
162
+ Responses stream live and stay interruptible.
163
+ Drag an image into the prompt and a vision-capable model describes it into the turn as text, so even a text-only coding model can act on it; `SCOOT_VISION_MODEL` pins the describer, `--no-images` turns the feature off.
164
+ Every turn auto-saves under `~/.local/state/scoot/sessions/` (owner-only, secrets redacted, last 20 kept); `scoot --continue` or `/resume` picks up where you left off.
165
+
166
+ ### The status bar
167
+
168
+ The bottom row shows the mascot's face (its eyes follow the turn: `o o` idle, `> >` thinking, `- -` stopped), the provider, the workspace and session id, the model, the approval mode, context size against the auto-compact threshold, cumulative tokens, message count, and the last error if any.
169
+
170
+ ## Install options
171
+
172
+ ```bash
173
+ pipx install scootcli # recommended: isolated, `scoot` on PATH
174
+ pip install scootcli # anywhere
175
+ curl -LO https://github.com/sergenes/scootcli/releases/latest/download/scoot.pyz && python3 scoot.pyz # single file, no install
176
+ git clone https://github.com/sergenes/scootcli && cd scootcli && pip install -e . # from source
177
+ ```
178
+
179
+ Requirements: Python 3.9 or newer on macOS or Linux.
180
+ Nothing else: no compiler, no packages, no `curl`.
181
+
182
+ ## Development
183
+
184
+ ```bash
185
+ pip install -e .
186
+ python -m pytest -q # network-free suite
187
+ ./scripts/build.sh # dist/scoot.pyz + wheel + sdist
188
+ ```
189
+
190
+ Design notes live in [`DESIGN.md`](./DESIGN.md), the behaviour spec in [`SPEC.md`](./SPEC.md), what is next in [`ROADMAP.md`](./ROADMAP.md), and the release history in [`CHANGELOG.md`](./CHANGELOG.md).
191
+ New capability is a drop-in file: a tool in `tools/`, a slash command in `commands/`, a provider row or adapter in `providers/`.
192
+
193
+ ## Security
194
+
195
+ - API keys are read from the environment or from `~/.config/scoot/credentials.json` (directory `0700`, file `0600`), validated before saving, and never echoed; `rendering.redact()` masks key-shaped strings in all output.
196
+ - Only allowlisted keys are imported from `.env` files.
197
+ - All file and shell tools are sandboxed to the workspace root; destructive shell commands are always re-confirmed.
198
+ - Saved sessions are owner-only with secrets redacted; `/forget all` removes them.
199
+ - No third-party dependencies means no third-party code to audit.
200
+
201
+ ## Contributing
202
+
203
+ Stars and forks are welcome, and so is using scoot to improve scoot: it is a coding agent, so point it at its own repo and let it do the work while you review.
204
+ Bug reports with a way to reproduce them are the most useful thing you can send.
205
+
206
+ Pull requests are welcome too, with one honest caveat: I intend to keep this tool small, so I will not merge most feature proposals.
207
+ A feature gets in when it is clearly useful to most users of a coding agent, fits the stdlib-only constraint, and comes with tests.
208
+ If you have an idea that does not meet that bar, a fork is the right home for it, and I am happy to link to forks that go somewhere interesting.
209
+
210
+ ## License
211
+
212
+ MIT, see [`LICENSE`](./LICENSE).
213
+ Written by [Sergey Nes](https://www.linkedin.com/in/sergey-neskoromny).
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "scootcli"
7
+ dynamic = ["version"]
8
+ description = "A tiny coding agent that goes where you point it"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Sergey Nes" }]
13
+ requires-python = ">=3.9"
14
+ # Zero third-party dependencies: stdlib only. This is a design constraint, not an omission.
15
+ dependencies = []
16
+ keywords = ["coding-agent", "cli", "terminal", "llm", "openai", "ollama", "agent", "stdlib"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "Operating System :: MacOS",
22
+ "Operating System :: POSIX :: Linux",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Topic :: Software Development",
26
+ "Topic :: Utilities",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/sergenes/scootcli"
31
+ Repository = "https://github.com/sergenes/scootcli"
32
+ Issues = "https://github.com/sergenes/scootcli/issues"
33
+ Changelog = "https://github.com/sergenes/scootcli/blob/main/CHANGELOG.md"
34
+
35
+ [project.scripts]
36
+ scoot = "scootcli.cli:main"
37
+
38
+ [tool.setuptools.dynamic]
39
+ version = { attr = "scootcli.__version__" }
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """scoot: a tiny coding agent that goes where you point it."""
2
+
3
+ __version__ = "0.1.0"
4
+
@@ -0,0 +1,9 @@
1
+ """Enable ``python -m scootcli``."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
9
+
@@ -0,0 +1,26 @@
1
+ """Shared 'activity' context manager: spinner + ESC-interrupt around a blocking call.
2
+
3
+ Used by slash-commands (``/init``, ``/compact``) that call the model outside the agent loop. Yields a
4
+ ``cancel_event`` the caller passes to ``client.chat`` so ESC terminates it immediately.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import threading
10
+ from contextlib import contextmanager
11
+
12
+ from .keys import InterruptibleSection
13
+ from .status import Status
14
+
15
+
16
+ @contextmanager
17
+ def activity(message: str):
18
+ cancel = threading.Event()
19
+ status = Status()
20
+ status.start(message)
21
+ try:
22
+ with InterruptibleSection(cancel):
23
+ yield cancel
24
+ finally:
25
+ status.stop()
26
+