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 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
+ [![PyPI](https://img.shields.io/pypi/v/obiobi?color=5f8787&label=pypi)](https://pypi.org/project/obiobi/)
31
+ [![Python](https://img.shields.io/pypi/pyversions/obiobi?color=5f8787)](https://pypi.org/project/obiobi/)
32
+ [![Downloads](https://img.shields.io/pypi/dm/obiobi?color=5f8787)](https://pypi.org/project/obiobi/)
33
+ [![Tests](https://github.com/ali-sehran/ObiObi/actions/workflows/tests.yml/badge.svg)](https://github.com/ali-sehran/ObiObi/actions/workflows/tests.yml)
34
+ [![License](https://img.shields.io/github/license/ali-sehran/ObiObi?color=5f8787)](LICENSE)
35
+ [![Stars](https://img.shields.io/github/stars/ali-sehran/ObiObi?style=flat&color=5f8787)](https://github.com/ali-sehran/ObiObi/stargazers)
36
+ [![PRs welcome](https://img.shields.io/badge/PRs-welcome-5f8787.svg)](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
+ ```