obiobi 1.0.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.
- obiobi-1.0.1/LICENSE +21 -0
- obiobi-1.0.1/PKG-INFO +519 -0
- obiobi-1.0.1/README.md +492 -0
- obiobi-1.0.1/obiobi/__init__.py +2 -0
- obiobi-1.0.1/obiobi/__main__.py +5 -0
- obiobi-1.0.1/obiobi/backends.py +361 -0
- obiobi-1.0.1/obiobi/cli.py +354 -0
- obiobi-1.0.1/obiobi/config.py +207 -0
- obiobi-1.0.1/obiobi/executor.py +83 -0
- obiobi-1.0.1/obiobi/history.py +235 -0
- obiobi-1.0.1/obiobi/index.py +146 -0
- obiobi-1.0.1/obiobi/installer.py +136 -0
- obiobi-1.0.1/obiobi/nl2cmd.py +133 -0
- obiobi-1.0.1/obiobi/safety.py +66 -0
- obiobi-1.0.1/obiobi/ui.py +463 -0
- obiobi-1.0.1/obiobi/wizard.py +185 -0
- obiobi-1.0.1/obiobi.egg-info/PKG-INFO +519 -0
- obiobi-1.0.1/obiobi.egg-info/SOURCES.txt +29 -0
- obiobi-1.0.1/obiobi.egg-info/dependency_links.txt +1 -0
- obiobi-1.0.1/obiobi.egg-info/entry_points.txt +2 -0
- obiobi-1.0.1/obiobi.egg-info/requires.txt +4 -0
- obiobi-1.0.1/obiobi.egg-info/top_level.txt +1 -0
- obiobi-1.0.1/pyproject.toml +41 -0
- obiobi-1.0.1/setup.cfg +4 -0
- obiobi-1.0.1/tests/test_api_backend.py +253 -0
- obiobi-1.0.1/tests/test_complete.py +60 -0
- obiobi-1.0.1/tests/test_core.py +101 -0
- obiobi-1.0.1/tests/test_index.py +364 -0
- obiobi-1.0.1/tests/test_render.py +61 -0
- obiobi-1.0.1/tests/test_ui2.py +318 -0
- obiobi-1.0.1/tests/test_wizard.py +104 -0
obiobi-1.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ali Sehran
|
|
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.
|
obiobi-1.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: obiobi
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: 帯 — a quiet band around your shell. Ask in plain English; the command appears in grey, Tab accepts it.
|
|
5
|
+
Author: Ali Sehran
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ali-sehran/ObiObi
|
|
8
|
+
Project-URL: Source, https://github.com/ali-sehran/ObiObi
|
|
9
|
+
Project-URL: Issues, https://github.com/ali-sehran/ObiObi/issues
|
|
10
|
+
Keywords: shell,terminal,cli,llm,natural-language,ollama,openai
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: System :: Shells
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: prompt_toolkit>=3.0.0
|
|
24
|
+
Provides-Extra: local
|
|
25
|
+
Requires-Dist: llama-cpp-python>=0.2.79; extra == "local"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# obiobi 帯
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/obiobi/)
|
|
31
|
+
[](https://pypi.org/project/obiobi/)
|
|
32
|
+
[](https://pypi.org/project/obiobi/)
|
|
33
|
+
[](https://github.com/ali-sehran/ObiObi/actions/workflows/tests.yml)
|
|
34
|
+
[](LICENSE)
|
|
35
|
+
[](https://github.com/ali-sehran/ObiObi/stargazers)
|
|
36
|
+
[](https://makeapullrequest.com)
|
|
37
|
+
|
|
38
|
+
> **帯** *obi* — the sash tied around a kimono, and the paper band wrapped
|
|
39
|
+
> around a bundle of notes or a book. One quiet strip that holds the whole
|
|
40
|
+
> thing together without being the thing you look at.
|
|
41
|
+
|
|
42
|
+
That is the whole design brief. obiobi is a band around your shell: it binds
|
|
43
|
+
your terminal, your history, the tools you have installed and a language model
|
|
44
|
+
into one line of dim grey text, and then gets out of the way.
|
|
45
|
+
|
|
46
|
+
A terminal prompt that turns plain-English questions into shell commands using a
|
|
47
|
+
small language model — local, or any endpoint you point it at.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
obi ~/Desktop/obiobi ❯ ??ask: what containers are running
|
|
51
|
+
docker ps [Tab]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The second line is the suggestion, drawn in dim grey. **`Tab`** turns it into the
|
|
55
|
+
real input line, **`Enter`** runs it. Nothing executes until you accept it.
|
|
56
|
+
|
|
57
|
+
Type an ordinary command and the grey text is your **last matching command**
|
|
58
|
+
instead — `doc` → `doc`**`ker ps`**. Same `Tab` to take it.
|
|
59
|
+
|
|
60
|
+
Tab completes like a shell when there's no grey text to take: the first word of
|
|
61
|
+
a line against every executable on `$PATH`, everything after it against the
|
|
62
|
+
filesystem. It knows a command position from an argument, including after a
|
|
63
|
+
`|`, `;` or `&&`:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
lso<Tab> → lsof
|
|
67
|
+
cat READ<Tab> → cat README.md
|
|
68
|
+
cd tes<Tab> → cd tests/
|
|
69
|
+
ls | gre<Tab> → ls | grep
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Tab inserts the candidate straight away and cycles through the rest on further
|
|
73
|
+
presses, the way zsh's menu-complete does. There is no floating menu on purpose:
|
|
74
|
+
prompt_toolkit reserves eight blank rows under the prompt to hold one, whether
|
|
75
|
+
or not anything is completing. While a completion is open **Enter picks, it does
|
|
76
|
+
not run** — you get to read what you picked and press Enter again to execute:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
cat READ → [Tab] cat README.md (menu open)
|
|
80
|
+
→ [Enter] takes it, line sits there
|
|
81
|
+
→ [Enter] runs it
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The grey history replay takes priority over the completion menu, so `doc<Tab>`
|
|
85
|
+
gives you `docker ps` (your last matching command) rather than a list. Use `→`
|
|
86
|
+
if you want the replay without disturbing an open menu.
|
|
87
|
+
|
|
88
|
+
Anything that deletes, sudos, kills or installs is offered with a reason and
|
|
89
|
+
takes **two** deliberate Tabs, so it can't be accepted by reflex:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
obi ~/projects ❯ ??ask: recursively delete the node_modules folder
|
|
93
|
+
rm -rf node_modules ⚠ deletes files [Tab][Tab]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The first Tab leaves the line alone and changes the note to
|
|
97
|
+
`⚠ deletes files - [Tab] again to accept`. Typing anything cancels it.
|
|
98
|
+
|
|
99
|
+
| key | what it does |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `Tab` | accept the grey text; if there is none, **complete** a command or path |
|
|
102
|
+
| `Tab` `Tab` | a **risky** command needs two presses — the first only arms it |
|
|
103
|
+
| `→` | accept the grey text without touching completion (fish/zsh habit) |
|
|
104
|
+
| `Enter` | with a completion menu open: take the highlighted item, don't run |
|
|
105
|
+
| `Enter` | otherwise run the line. It never accepts a suggestion; that is Tab's job |
|
|
106
|
+
| `Ctrl-G` / `Esc` | dismiss the suggestion |
|
|
107
|
+
| `Ctrl-D` | exit |
|
|
108
|
+
| `??docs` | write this session's questions and answers to a file |
|
|
109
|
+
| `history [n]` | your real shell history |
|
|
110
|
+
| `:help` `:backend` `:dry` | in-shell meta commands |
|
|
111
|
+
|
|
112
|
+
## Install
|
|
113
|
+
|
|
114
|
+
One command, any OS with Python 3.9+ — see [INSTALL.md](INSTALL.md) for the
|
|
115
|
+
step-by-step version.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pipx install obiobi # or: uv tool install obiobi / pip install obiobi
|
|
119
|
+
obiobi config --set backend=api \
|
|
120
|
+
--set api_base=https://openrouter.ai/api/v1 \
|
|
121
|
+
--set api_model=nvidia/nemotron-3-nano-30b-a3b:free \
|
|
122
|
+
--set api_key_env=OPENROUTER_API_KEY
|
|
123
|
+
obiobi config --set-key # prompts, saves to ~/.config/obiobi/credentials (600)
|
|
124
|
+
obiobi
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
That is the whole setup. `obiobi config` on its own prints what is set, what is
|
|
128
|
+
missing and how to fix it; the first `--set` also indexes what is installed on
|
|
129
|
+
the machine. One pure-python dependency (`prompt_toolkit`), a 33 KB wheel, no
|
|
130
|
+
model bundled.
|
|
131
|
+
|
|
132
|
+
The key goes in its own 0600 file, never in `config.json` — so `config.json`
|
|
133
|
+
stays safe to commit or copy between machines. An environment variable still
|
|
134
|
+
wins if you set one. `obiobi config --forget-key` removes it.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
obiobi # interactive shell
|
|
138
|
+
obiobi ask "how much disk space is left" # one-shot, prints the command
|
|
139
|
+
obiobi ask "..." --run # print and execute
|
|
140
|
+
obiobi index --show # re-scan, and print what the model is told
|
|
141
|
+
obiobi doctor # what's installed, which backend is live
|
|
142
|
+
obiobi config # show / change settings
|
|
143
|
+
obiobi --dry-run # never execute, just show
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`./install.sh` is still there for a self-contained venv install from a checkout
|
|
147
|
+
(`./install.sh ollama|llama-cpp|heuristic`), but it is no longer the main path.
|
|
148
|
+
|
|
149
|
+
## Your history is the shell's history
|
|
150
|
+
|
|
151
|
+
obiobi keeps no history of its own. It reads `~/.bash_history` or
|
|
152
|
+
`~/.zsh_history` directly and appends to it, so anything you run here shows up
|
|
153
|
+
in `history` in every other terminal, and Up-Arrow walks the commands you ran
|
|
154
|
+
outside obiobi. `??ask:` lines and meta commands are filtered out — only real
|
|
155
|
+
commands are stored.
|
|
156
|
+
|
|
157
|
+
`history` inside obiobi lists the same commands your terminal does, in the same
|
|
158
|
+
order. The **numbers are close but cannot be made identical**, for three
|
|
159
|
+
independent reasons, all verified against a live bash:
|
|
160
|
+
|
|
161
|
+
1. A shell's history number is a per-session counter whose origin depends on
|
|
162
|
+
how long the file was when *that* terminal opened. Two terminals number the
|
|
163
|
+
same command differently.
|
|
164
|
+
2. bash keeps only the last `HISTSIZE` entries (default **500**) and renumbers
|
|
165
|
+
from 1, so once your file passes that the numbers shift. Raising `HISTSIZE`
|
|
166
|
+
and `HISTFILESIZE` in your rc removes this one.
|
|
167
|
+
3. A file line is not a history entry: bash skips blank lines and folds
|
|
168
|
+
backslash-continued commands into one entry.
|
|
169
|
+
|
|
170
|
+
obiobi shows the one number two processes can agree on — the entry's position
|
|
171
|
+
in the file — and says so under the listing. Don't feed them to `!123`; that
|
|
172
|
+
resolves against your shell's counter, not the file.
|
|
173
|
+
|
|
174
|
+
zsh's two on-disk formats are both handled; the existing file decides which one
|
|
175
|
+
is appended. **bash users:** add `shopt -s histappend` to your rc. Without it
|
|
176
|
+
bash *overwrites* the history file when a terminal exits and would drop the
|
|
177
|
+
entries obiobi added while that terminal was open.
|
|
178
|
+
|
|
179
|
+
fish is not supported — its history format is different enough that obiobi
|
|
180
|
+
stays out of it and falls back to its own file.
|
|
181
|
+
|
|
182
|
+
## What it knows about your machine
|
|
183
|
+
|
|
184
|
+
`obiobi index` writes a plain list of names to `~/.local/share/obiobi/tools.json`
|
|
185
|
+
and every request hands that list to the model. A name is enough — the model
|
|
186
|
+
already knows what `docker` is; it only needs to know that you have it.
|
|
187
|
+
|
|
188
|
+
| source | what it gives |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `$PATH` directory scan | executable names, **excluding** `/usr/bin` and friends |
|
|
191
|
+
| `importlib.metadata` | installed python distributions |
|
|
192
|
+
| `npm ls -g --depth=0` | global node packages |
|
|
193
|
+
| `brew list --formula` | homebrew formulae |
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
✓ indexed 269 commands, 48 packages
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The base OS is filtered out on purpose: every machine has `awk`, so listing it
|
|
200
|
+
teaches the model nothing. What matters is that *this* machine has `docker`,
|
|
201
|
+
`kubectl` and `psql`. ~270 names is roughly 800 tokens, small enough to send in
|
|
202
|
+
full rather than guess which ones are relevant.
|
|
203
|
+
|
|
204
|
+
**Nothing unknown is ever executed.** obiobi does not run `--help` on the
|
|
205
|
+
binaries it finds, and does not read man pages. The only commands it runs are
|
|
206
|
+
the three package managers named above. Running strangers to read their banner
|
|
207
|
+
is how you get a keychain prompt out of `docker-credential-osxkeychain`; a test
|
|
208
|
+
asserts that only `python3`, `npm` and `brew` are ever invoked.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
obiobi index --show # rebuild, and print exactly what the model gets
|
|
212
|
+
obiobi config --set use_index=false # or turn the whole thing off
|
|
213
|
+
obiobi config --set index_limit=800 # most names sent per list
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Re-run `obiobi index` after installing new tools; nothing watches for them.
|
|
217
|
+
Only the first `python3` on `$PATH` is asked, so pyenv and per-project venvs
|
|
218
|
+
contribute whichever one is active when you run it.
|
|
219
|
+
|
|
220
|
+
## ??docs
|
|
221
|
+
|
|
222
|
+
`??docs` writes the session's questions and the commands they produced to a
|
|
223
|
+
timestamped markdown file in the current directory:
|
|
224
|
+
|
|
225
|
+
```markdown
|
|
226
|
+
# obiobi session - 2026-08-10 20:30:10
|
|
227
|
+
_1 question_
|
|
228
|
+
|
|
229
|
+
## 1. how much free memory
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
free -h | awk '/^Mem:/ {print $4}'
|
|
233
|
+
```
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Quiet by default
|
|
237
|
+
|
|
238
|
+
The band is meant to be barely visible. Everything obiobi does is designed to
|
|
239
|
+
stay out of your way and off your machine:
|
|
240
|
+
|
|
241
|
+
- **Nothing runs on its own.** Every suggestion is grey text until you press
|
|
242
|
+
`Tab`. Anything that deletes, sudos, kills or installs takes `Tab` twice.
|
|
243
|
+
- **Nothing unknown is executed.** obiobi never runs the binaries it finds to
|
|
244
|
+
learn about them — it reads their names. The only commands it invokes are
|
|
245
|
+
`python3`, `npm` and `brew`, and a test enforces that.
|
|
246
|
+
- **No telemetry, no account, no daemon.** One process, started by you, gone
|
|
247
|
+
when you press `Ctrl-D`.
|
|
248
|
+
- **Your key stays yours.** It lives in a `0600` file that `config.json` never
|
|
249
|
+
touches, so the config is safe to commit or copy between machines.
|
|
250
|
+
- **Your shell stays yours.** obiobi keeps no history of its own; it reads and
|
|
251
|
+
appends to the one your terminal already uses.
|
|
252
|
+
- **It tells you when it leaves the machine.** A remote endpoint is announced
|
|
253
|
+
on startup, because your question and your cwd travel with it.
|
|
254
|
+
|
|
255
|
+
## Backends — bring your own model
|
|
256
|
+
|
|
257
|
+
Nothing here is hardcoded to one provider. Four backends, resolved in this order
|
|
258
|
+
when `backend = "auto"`:
|
|
259
|
+
|
|
260
|
+
1. **api** — any OpenAI-compatible `/chat/completions` endpoint. Only tried in
|
|
261
|
+
`auto` if you've configured one, since it's a deliberate choice.
|
|
262
|
+
2. **llama.cpp** — a local `.gguf` in `~/.local/share/obiobi/models/`, loaded
|
|
263
|
+
through `llama-cpp-python`. Fully offline.
|
|
264
|
+
3. **ollama** — a daemon at `$OLLAMA_HOST` (default `http://127.0.0.1:11434`).
|
|
265
|
+
4. **heuristic** — ~25 regex rules (package inventories, disk, memory, ports,
|
|
266
|
+
processes, git, docker). No model, no latency, platform-aware (`free -h` on
|
|
267
|
+
Linux, `vm_stat` on macOS). The always-available fallback.
|
|
268
|
+
|
|
269
|
+
Force one with `--backend api|llama-cpp|ollama|heuristic`.
|
|
270
|
+
|
|
271
|
+
### Your own GGUF
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
obiobi install --gguf-url https://your.host/some-model-Q4_K_M.gguf
|
|
275
|
+
# or just drop any .gguf into ~/.local/share/obiobi/models/
|
|
276
|
+
obiobi config --set gguf_path=/path/to/model.gguf
|
|
277
|
+
export OBIOBI_MODEL_URL=https://... # env override
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
With no `gguf_path` set, the smallest `.gguf` in the models directory is used —
|
|
281
|
+
smallest means fastest to answer.
|
|
282
|
+
|
|
283
|
+
### OpenAI (or any compatible endpoint)
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
export OPENAI_API_KEY=sk-...
|
|
287
|
+
obiobi config --set api_base=https://api.openai.com/v1 --set api_model=gpt-4o-mini
|
|
288
|
+
obiobi --backend api
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The same backend covers everything that speaks the OpenAI dialect — swap
|
|
292
|
+
`api_base`:
|
|
293
|
+
|
|
294
|
+
| endpoint | `api_base` | key needed |
|
|
295
|
+
| --- | --- | --- |
|
|
296
|
+
| OpenAI | `https://api.openai.com/v1` | yes |
|
|
297
|
+
| OpenRouter | `https://openrouter.ai/api/v1` | yes |
|
|
298
|
+
| Groq | `https://api.groq.com/openai/v1` | yes |
|
|
299
|
+
| Together | `https://api.together.xyz/v1` | yes |
|
|
300
|
+
| `llama-server` (llama.cpp) | `http://127.0.0.1:8080/v1` | no |
|
|
301
|
+
| LM Studio | `http://127.0.0.1:1234/v1` | no |
|
|
302
|
+
| vLLM | `http://127.0.0.1:8000/v1` | no |
|
|
303
|
+
| Ollama's OpenAI shim | `http://127.0.0.1:11434/v1` | no |
|
|
304
|
+
|
|
305
|
+
You do **not** need the key set when you run `obiobi config` — that only writes
|
|
306
|
+
JSON. The key is read at the moment a request is made, so order doesn't matter.
|
|
307
|
+
Put the export in your `~/.zshrc` or `~/.bashrc` to make it stick, or prefix a
|
|
308
|
+
single run: `OPENAI_API_KEY=sk-... obiobi`. Run `obiobi config` or `obiobi doctor`
|
|
309
|
+
to see whether it currently resolves.
|
|
310
|
+
|
|
311
|
+
`--backend api` is only needed to *force* the endpoint. With `backend = auto` a
|
|
312
|
+
configured endpoint is already tried first; forcing it means you get a hard error
|
|
313
|
+
instead of a quiet fall back to a weaker backend. Make it permanent with
|
|
314
|
+
`obiobi config --set backend=api`. (`--backend openai` is accepted as an alias;
|
|
315
|
+
the backend is called `api` because it isn't OpenAI-specific.) Whenever a backend
|
|
316
|
+
is skipped, the reason is printed — you'll never get regex answers while thinking
|
|
317
|
+
you're talking to GPT.
|
|
318
|
+
|
|
319
|
+
Keys are read from the environment — `$OPENAI_API_KEY` by default, renameable via
|
|
320
|
+
`api_key_env`, or `$OBIOBI_API_KEY` — and are **never written to the config
|
|
321
|
+
file**. Localhost endpoints don't require one. When a remote host is in use, the
|
|
322
|
+
banner says so on startup, because your questions (and the cwd in the system
|
|
323
|
+
prompt) leave the machine. Env overrides: `OBIOBI_API_BASE`,
|
|
324
|
+
`OBIOBI_API_MODEL`, `OBIOBI_BACKEND`.
|
|
325
|
+
|
|
326
|
+
### OpenRouter, on the free tier
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
obiobi config --set backend=api \
|
|
330
|
+
--set api_base=https://openrouter.ai/api/v1 \
|
|
331
|
+
--set api_model=nvidia/nemotron-3-nano-30b-a3b:free \
|
|
332
|
+
--set api_key_env=OPENROUTER_API_KEY
|
|
333
|
+
export OPENROUTER_API_KEY=sk-or-v1-... # your own key; put it in ~/.zshrc
|
|
334
|
+
obiobi
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The config file holds the *name* of the variable, never the key — so it is safe
|
|
338
|
+
to commit or share, and everyone who clones this brings their own.
|
|
339
|
+
|
|
340
|
+
Small free models refuse at random — the same question that answers `ls` once
|
|
341
|
+
comes back `# cannot` the next time. obiobi asks once more when it sees a
|
|
342
|
+
refusal, which measured 1-in-6 wrong refusals down to 0-in-6 while still
|
|
343
|
+
refusing things that genuinely have no shell command. Turn it off with
|
|
344
|
+
`obiobi config --set retry_refusals=false`. If refusals still bother you,
|
|
345
|
+
`nvidia/nemotron-3-super-120b-a12b:free` is steadier at ~1.2 s versus ~0.6 s.
|
|
346
|
+
|
|
347
|
+
Every `:free` model on OpenRouter is a hybrid reasoning model, so obiobi sends
|
|
348
|
+
`reasoning: {enabled: false}` to that host — otherwise the whole `max_tokens`
|
|
349
|
+
budget goes to thinking and the reply comes back empty. Free models are also
|
|
350
|
+
rate-limited by a shared pool. When it is exhausted OpenRouter answers **HTTP
|
|
351
|
+
200 with an error body**, so obiobi reads the body rather than the status code,
|
|
352
|
+
retries once after a second, and then says `the model is busy right now (free
|
|
353
|
+
tier is shared)` instead of printing a truncated JSON dict. Raise `debounce_ms`
|
|
354
|
+
further if you still hit it. `nemotron-3-nano-30b-a3b:free` answers in ~1.5 s;
|
|
355
|
+
`poolside/laguna-xs-2.1:free` is a touch faster, `gemma-4-*-it:free` is ~6 s.
|
|
356
|
+
|
|
357
|
+
### Assisted setup
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
obiobi config --reset
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
obiobi setup - Enter takes the suggested value, Tab shows the alternatives
|
|
365
|
+
|
|
366
|
+
ollama found, 2 model(s) pulled
|
|
367
|
+
local-server vLLM / LM Studio / llama-server
|
|
368
|
+
hosted-api OpenRouter, OpenAI, Groq, Together
|
|
369
|
+
|
|
370
|
+
connect via [ollama]:
|
|
371
|
+
model [llama3.2:3b]:
|
|
372
|
+
✓ backend ollama:llama3.2:3b is reachable
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
It checks what is already running and recommends accordingly: if an ollama
|
|
376
|
+
daemon answers, that is the default and the models it lists become the choices;
|
|
377
|
+
if you pick a local server it queries `/v1/models` and offers what that server
|
|
378
|
+
is actually serving. **Enter** takes the recommendation, **Tab** fills it in so
|
|
379
|
+
you can edit it and shows the alternatives after it, typing replaces it.
|
|
380
|
+
|
|
381
|
+
A local GGUF through llama-cpp-python is deliberately not offered — it compiles
|
|
382
|
+
a C extension and downloads gigabytes, and `ollama pull` does the same job.
|
|
383
|
+
`obiobi install --backend llama-cpp` is still there if you want it.
|
|
384
|
+
|
|
385
|
+
### Settings
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
obiobi config # what is set, and where the key comes from
|
|
389
|
+
obiobi config --set debounce_ms=500 --set confirm_risky=false
|
|
390
|
+
obiobi config --all # plus the settings other backends use
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Stored in `~/.config/obiobi/config.json`. The listing only shows settings the
|
|
394
|
+
active backend actually uses — `gguf_url` and `ollama_model` are noise on an API
|
|
395
|
+
endpoint, and nobody set them. `--all` adds them back, marked `(unused with
|
|
396
|
+
backend=api)`.
|
|
397
|
+
|
|
398
|
+
## How the suggestion works
|
|
399
|
+
|
|
400
|
+
Typing schedules a translation after an 800 ms pause (`debounce_ms`). Every
|
|
401
|
+
keystroke cancels the pending timer, so only a real pause spends a request —
|
|
402
|
+
and the window has to be longer than a mid-sentence pause or each pause fires
|
|
403
|
+
one. Measured on a 43-character question typed with 0.55 s pauses: **450 ms
|
|
404
|
+
cost 8 requests, 7 of them for half-typed fragments that were thrown away;
|
|
405
|
+
800 ms costs 1.** `Tab` still means "ask now" if you don't want to wait it out.
|
|
406
|
+
It runs on a worker thread, so the prompt never blocks; a spinner shows while
|
|
407
|
+
the model thinks. Only the newest question's answer is kept — if you keep editing, stale
|
|
408
|
+
answers are dropped. Buffer mutations always happen on the UI thread, during
|
|
409
|
+
render, never from the worker.
|
|
410
|
+
|
|
411
|
+
The model is asked for exactly one command, and the reply is sanitised: code
|
|
412
|
+
fences, `$ ` prompt markers, backticks and prose lines are stripped. If nothing
|
|
413
|
+
usable comes back, the ghost line says so instead of guessing.
|
|
414
|
+
|
|
415
|
+
## Your shell's functions and aliases
|
|
416
|
+
|
|
417
|
+
Commands run through **`$SHELL -lc`**, a login shell, so `~/.bash_profile` and
|
|
418
|
+
`~/.zshrc` are sourced and the functions and aliases you defined there exist:
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
obi ~/Desktop ❯ skey
|
|
422
|
+
live: git_internal
|
|
423
|
+
have: git_internal sa_key
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
A plain `bash -c` is neither login nor interactive and reads no profile at all,
|
|
427
|
+
which is why a personal function used to die with `command not found`. bash also
|
|
428
|
+
ignores aliases when it is not interactive, so obiobi turns on `expand_aliases`.
|
|
429
|
+
|
|
430
|
+
The profile is re-sourced for each command. Measured on a profile that loads
|
|
431
|
+
nvm: **~130 ms** versus ~2 ms without. If your profile is heavy and you would
|
|
432
|
+
rather have the milliseconds back, `obiobi config --set login_shell=false`.
|
|
433
|
+
|
|
434
|
+
## Safety
|
|
435
|
+
|
|
436
|
+
The real guarantee is structural: **nothing runs until you press a key.** A
|
|
437
|
+
suggestion is grey text, not a queued command. On top of that, every command is
|
|
438
|
+
screened at the single point where it would execute:
|
|
439
|
+
|
|
440
|
+
- **Blocked** — replaced with `refused: <reason>`, never runnable: `rm -rf /`,
|
|
441
|
+
`mkfs`, raw writes to a block device, fork bombs, `curl … | sh`, `chmod 777 /`.
|
|
442
|
+
- **Risky** — offered with a `⚠` and a reason, and gated: `Tab` twice to accept,
|
|
443
|
+
then a typed `y` before it runs. Covers sudo, deletes, `kill`, package
|
|
444
|
+
installs, `git reset --hard`, force pushes, writes into system directories,
|
|
445
|
+
docker removals, `dd`, `find -delete`.
|
|
446
|
+
|
|
447
|
+
Be clear-eyed about what the screen is: a **denylist**, defense-in-depth against
|
|
448
|
+
a model that suggests something reckless — not a sandbox. It catches the worst
|
|
449
|
+
shapes by pattern, and a sufficiently creative command can still land unflagged.
|
|
450
|
+
That is why the design never auto-runs anything: the human keystroke is the
|
|
451
|
+
guarantee, the screen is the safety net. `confirm_risky` and `dry_run` are
|
|
452
|
+
configurable; `dry_run` prints instead of running, which is the safest way to
|
|
453
|
+
try it. Commands run through `$SHELL -lc`; `cd` is handled in-process.
|
|
454
|
+
|
|
455
|
+
## Tests
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
python3 -m unittest discover -s tests # 119 tests
|
|
459
|
+
python3 tests/pty_demo.py # drives a real pty, shows the output
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
`tests/test_api_backend.py` runs a real HTTP server on localhost and asserts the
|
|
463
|
+
request shape, bearer auth, key-less localhost access, and that a 401 produces a
|
|
464
|
+
readable message, plus that a refusal or a blocked command never reaches stdout.
|
|
465
|
+
`tests/test_index.py` covers the installed-command index.
|
|
466
|
+
`tests/test_ui2.py` drives the actual prompt through a pipe input and asserts the
|
|
467
|
+
full flow: ghost appears → `Tab` solidifies → `Enter` submits; `Tab` before the
|
|
468
|
+
debounce forces generation; editing the question replaces a stale suggestion.
|
|
469
|
+
|
|
470
|
+
## Notes / known limits
|
|
471
|
+
|
|
472
|
+
- The default GGUF download URL is unverified — if it 404s, the installer falls
|
|
473
|
+
back to the heuristic backend and tells you. `ollama pull` is the safer path.
|
|
474
|
+
- A 0.5B model is fast but modest. It handles everyday inventory/inspection
|
|
475
|
+
questions well and gets creative with rare flags; read the grey line before
|
|
476
|
+
pressing Tab. A 1.5B–3B model is noticeably better if you have the RAM.
|
|
477
|
+
- The index is a snapshot: install a new tool and it stays invisible until you
|
|
478
|
+
re-run `obiobi index`.
|
|
479
|
+
- `Esc` to dismiss depends on the terminal flushing a lone escape byte; `Ctrl-G`
|
|
480
|
+
always works.
|
|
481
|
+
- prompt_toolkit hides its own `bottom_toolbar` when the renderer height is
|
|
482
|
+
unknown (terminals that don't answer cursor-position requests), which would
|
|
483
|
+
make the suggestion silently vanish — so the ghost row is part of the layout
|
|
484
|
+
instead of being a toolbar.
|
|
485
|
+
|
|
486
|
+
## Publishing a release
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
python -m build # -> dist/*.whl and dist/*.tar.gz
|
|
490
|
+
python -m twine check dist/* # must say PASSED for both
|
|
491
|
+
python -m twine upload dist/* # username: __token__, password: pypi-...
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Bump `version` in `pyproject.toml` first — PyPI refuses to overwrite a version
|
|
495
|
+
that already exists. Try it against TestPyPI if you want a dry run:
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
python -m twine upload --repository testpypi dist/*
|
|
499
|
+
pip install --index-url https://test.pypi.org/simple/ obiobi
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Get the token from <https://pypi.org/manage/account/token/>. Scope it to this
|
|
503
|
+
project once the project exists; the first upload needs an account-wide token.
|
|
504
|
+
|
|
505
|
+
## Files
|
|
506
|
+
|
|
507
|
+
```
|
|
508
|
+
obiobi/config.py paths, defaults, persisted config
|
|
509
|
+
obiobi/backends.py api (OpenAI-compatible) / llama.cpp / ollama / heuristic
|
|
510
|
+
obiobi/nl2cmd.py prompt template, output sanitising, ??ask: parsing
|
|
511
|
+
obiobi/ui.py the prompt, the debounced Suggester thread, the ghost row
|
|
512
|
+
obiobi/safety.py blocked and risky command patterns
|
|
513
|
+
obiobi/executor.py confirmation and execution
|
|
514
|
+
obiobi/installer.py model download, pip, ollama pull
|
|
515
|
+
obiobi/index.py $PATH + package-manager scan, names only
|
|
516
|
+
obiobi/history.py the real shell history, read and appended; ??docs
|
|
517
|
+
obiobi/wizard.py the assisted setup behind `config --reset`
|
|
518
|
+
obiobi/cli.py install / ask / index / doctor / config / run
|
|
519
|
+
```
|