# setup


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

This module holds what has to happen before a session starts. The
terminal session runs best inside tmux, a terminal multiplexer that
divides one window into panes. tmux gives Ramabana a split for the now
pane, the live view of the turn described in [pane](18_pane.ipynb), and
with its extended keys setting on it delivers `shift+enter` to the
program as a key of its own, which the session uses to queue a message
for the next turn. So the first run starts Ramabana inside a tmux server
of its own, offers to install tmux when it is missing, and
`ramabana --doctor` reports what it found. The module also decides
whether the code index needs rebuilding.

## Extras and scripts

Every console script that the package installs starts in this module, so
it imports only fastcore and the standard library, and never
`ramabana.core`. That keeps a script quick to start and lets it check
for its dependencies before it imports them.

`installed(mod)` says whether a module would import, without importing
it. `need(extra)` checks the modules an extra provides, as listed in
`EXTRAS`, and returns the install command for the ones missing, or `''`
when all are there. `SCRIPTS` names, for each console script other than
`ramabana`, the module whose
[`main`](https://vedicreader.github.io/ramabana/cli.html#main) it runs
and the extra it needs. Looking up one of those names on this module
returns that
[`main`](https://vedicreader.github.io/ramabana/cli.html#main), or, when
its extra is missing, a stand-in that prints the install command and
exits with status 2. `ramabana.core` re-exports
[`installed`](https://vedicreader.github.io/ramabana/setup.html#installed),
[`need`](https://vedicreader.github.io/ramabana/setup.html#need),
`EXTRAS` and `SCRIPTS`.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/core.py#L214"
target="_blank" style="float:right; font-size:smaller">source</a>

### **getattr**

``` python
def __getattr__(
    name
):
```

*The [`main`](https://vedicreader.github.io/ramabana/cli.html#main) a
console script in `SCRIPTS` calls itself, so `call_parse` reads the
command line; or one naming the missing extra.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L45"
target="_blank" style="float:right; font-size:smaller">source</a>

### need

``` python
def need(
    extra, *mods
):
```

*`''` when `mods`, by default all of `extra`’s, are installed; else what
to install.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L40"
target="_blank" style="float:right; font-size:smaller">source</a>

### installed

``` python
def installed(
    mod
):
```

*Whether `mod` would import, without importing it.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L29"
target="_blank" style="float:right; font-size:smaller">source</a>

### env_prefixes

``` python
def env_prefixes():
```

*The env prefixes `core` reads: its own once loaded, where an app may
have changed them, else `ENV_DEFAULT`.*

``` python
assert installed('fastcore') and not installed('no_such_module')
test_eq(need('search', 'fastcore'), '')
test_eq(need('search', 'no_such_module'), "no_such_module not installed: pip install 'ramabana[search]'")
test_eq(env_prefixes(), (sys.modules['ramabana.core'].ENV_PREFIX, sys.modules['ramabana.core'].ENV_FALLBACK) if 'ramabana.core' in sys.modules else ENV_DEFAULT)
```

## The config

Each session gets a tmux server of its own, started as
`tmux -L ramabana-<id>` with the config file `<cfg>/tmux.conf`. A new
server reads the current config and inherits the current environment, so
a second session never picks up the first one’s API keys or virtual
environment. That server never reads `~/.tmux.conf`, and your own tmux
setup is left as it was.

Extended keys are a tmux setting that passes key combinations such as
`shift+enter` through to the program in the pane, instead of folding
them into plain `enter`. `tmux_conf(version)` returns the config for a
given tmux version, adding only the lines that version understands:
`extended-keys` arrived in tmux 3.2 and `extended-keys-format csi-u` in
3.5, and an older tmux prints an error for each line it does not know.
[`tmux_version`](https://vedicreader.github.io/ramabana/setup.html#tmux_version)
reads the version from the output of `tmux -V`. The config claims true
colour (`RGB`) only when `$COLORTERM` says the terminal draws it. The
cell below prints the config for tmux 3.5.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L85"
target="_blank" style="float:right; font-size:smaller">source</a>

### tmux_conf

``` python
def tmux_conf(
    version
):
```

*`TMUX_CONF` with the lines tmux `version` understands.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L81"
target="_blank" style="float:right; font-size:smaller">source</a>

### tmux_version

``` python
def tmux_version(
    s
):
```

*The `(major, minor)` in `tmux -V` output such as `tmux 3.5a`, or None.*

``` python
test_eq([tmux_version(s) for s in ('tmux 3.5a', 'tmux next-3.6', 'tmux master')], [(3, 5), (3, 6), None])
assert 'extended-keys-format csi-u' in tmux_conf((3, 5)) and 'csi-u' not in tmux_conf((3, 4))
assert 'extended-keys on' in tmux_conf((3, 2)) and 'extended-keys' not in tmux_conf((3, 1))
print(tmux_conf((3, 5)))
```

## The relaunch

[`Setup`](https://vedicreader.github.io/ramabana/setup.html#setup)
collects everything about the tmux that Ramabana runs in: the config,
the relaunch into it, the install offer and `--doctor`.

[`Setup.launch`](https://vedicreader.github.io/ramabana/setup.html#setup.launch)
runs before an interactive session starts. Outside tmux it replaces the
current process with `tmux new-session` on a new server, running the
same command line in the same folder. Quitting Ramabana then ends that
tmux session and returns you to your shell. `launch` does nothing for a
one-shot prompt, for `--json`, when stdin or stdout is not a terminal,
and when `--tmux off` or `$RAMABANA_TMUX=off` says to stay out of tmux.
It also needs tmux 3.0 or later. The relaunched process has
`$RAMABANA_WRAPPED` set, so it never relaunches a second time. Inside
tmux, `--pane auto` then opens the now pane.

The server is set to `destroy-unattached`, which ends the session when
you detach or close the window, so Ramabana never keeps running where
nobody can see it. The history is saved, and `--resume latest` reopens
it. The command runs under `sh` with a short wrapper, `HOLD`, that waits
for enter after a non-zero exit, so an error stays on screen instead of
vanishing with the tmux session.

Inside a tmux server of your own, `launch` turns `extended-keys` on for
that server when it is off. The change lasts until the server exits, and
only programs that ask for extended keys receive them.

Every effect on the outside world goes through an argument of
[`Setup`](https://vedicreader.github.io/ramabana/setup.html#setup):
`which`, `system`, `run`, `execvpe`, `ask` and `isatty`, plus `environ`,
`argv` and `cwd`. The examples pass fakes for all of them, so they run
without tmux and change nothing.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L93"
target="_blank" style="float:right; font-size:smaller">source</a>

### Setup

``` python
def Setup(
    cfg:str='~/.config/ramabana', # holds `tmux.conf` and `setup.json`
    environ:NoneType=None, # default `os.environ`
    argv:NoneType=None, # the command line to rerun; default `sys.argv`
    cwd:NoneType=None, # where the session starts; default the current folder
    which:function=which, system:function=system, run:function=run, execvpe:function=execvpe, ask:function=_no_stdin,
    isatty:NoneType=None, # whether stdin and stdout are terminals
):
```

*The tmux ramabana runs in: its config, the relaunch into it, the
install offer, and `--doctor`.*

The example builds a fake machine, `Box`, that has tmux 3.5 on `PATH`
and records each command run, each question asked and the `execvpe` it
would make. `launch` reruns the command line on a new tmux server,
starting in `/work`, with `$RAMABANA_WRAPPED` set and the original
options at the end:

``` python
from types import SimpleNamespace
import tempfile

class Box:
    "Programs on PATH, the commands run, the prompts shown and the exec made."
    def __init__(self, have=('tmux',), version='tmux 3.5a', system='Darwin', answer='', ext='on'):
        self.have, self.version, self.system, self.answer, self.ext = set(have), version, system, answer, ext
        self.calls, self.asked, self.execd = [], [], None
    def which(self, name): return f'/bin/{name}' if name in self.have else None
    def run(self, cmd, **kw):
        self.calls.append(list(cmd))
        if 'install' in cmd: self.have.add('tmux')
        return SimpleNamespace(returncode=0, stdout={'-V': self.version, 'show': self.ext}.get(cmd[1], ''))
    def ask(self, q): return self.asked.append(q) or self.answer
    def execvpe(self, *a): self.execd = a

def mk(box, **kw):
    return Setup(tempfile.mkdtemp(), environ=kw.pop('environ', {'PATH': '/bin'}), argv=['-c', '--pane', 'on'], cwd='/work',
                 which=box.which, system=lambda: box.system, run=box.run, execvpe=box.execvpe, ask=box.ask, isatty=lambda: True)

box = Box()
mk(box).launch()
exe, argv, env = box.execd
test_eq((exe, env['RAMABANA_WRAPPED'], argv[argv.index('-c'):][:2], argv[-2:]), ('/bin/tmux', '1', ['-c', '/work'], ['--pane', 'on']))
argv[:12]
```

    ['tmux',
     '-L',
     'ramabana-3cf00004',
     '-f',
     '/tmp/tmpol68wm_k/tmux.conf',
     'new-session',
     '-s',
     'ramabana',
     '-c',
     '/work',
     'sh',
     '-c']

A one-shot prompt, `--json` and `--tmux off` stay in the current
terminal:

``` python
for kw in (dict(prompt='hi'), dict(as_json=True), dict(tmux='off')):
    box = Box()
    mk(box).launch(**kw)
    test_eq(box.execd, None)
```

The `ramabana` script,
[`run_cli`](https://vedicreader.github.io/ramabana/setup.html#run_cli),
makes the tmux decision before it imports the CLI. Importing the CLI
takes about 0.4 seconds, so deciding first means the process outside
tmux hands over to tmux quickly. `launch_args(argv)` reads just enough
of the command line to know the session is interactive, without the full
option parser, and returns the `cfg` and `tmux` values `launch` needs,
or `None` to leave the decision to the CLI’s
[`main`](https://vedicreader.github.io/ramabana/cli.html#main). A
prompt, `--json`, `--doctor`, an option
[`refusal`](https://vedicreader.github.io/ramabana/setup.html#refusal)
rejects, or any option it is unsure of returns `None`.

[`refusal`](https://vedicreader.github.io/ramabana/setup.html#refusal)
holds the checks
[`main`](https://vedicreader.github.io/ramabana/cli.html#main) makes
before it starts anything, such as `--pii` without `--vault`, and
returns the reason or `''`. Running it here means a bad option is
refused in your terminal, not on tmux’s hold screen.
[`bare_resume`](https://vedicreader.github.io/ramabana/setup.html#bare_resume)
gives a bare `--resume` the value `PICK_SESSION`, which lists this
folder’s sessions to choose from. When
[`run_cli`](https://vedicreader.github.io/ramabana/setup.html#run_cli)
has already called `launch`, it sets `LAUNCHED` so that
[`main`](https://vedicreader.github.io/ramabana/cli.html#main) does not
call it again.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L200"
target="_blank" style="float:right; font-size:smaller">source</a>

### run_cli

``` python
def run_cli():
```

*The `ramabana` script: rerun inside tmux before anything heavy loads,
then parse the command line for `cli.main`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L191"
target="_blank" style="float:right; font-size:smaller">source</a>

### bare_resume

``` python
def bare_resume(
    argv
):
```

*`argv` with a bare `--resume`, one followed by nothing or by another
option, given the value `PICK_SESSION`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L176"
target="_blank" style="float:right; font-size:smaller">source</a>

### launch_args

``` python
def launch_args(
    argv
):
```

*[`Setup`](https://vedicreader.github.io/ramabana/setup.html#setup)’s
`cfg` and `launch`’s `tmux` for an interactive command line
[`main`](https://vedicreader.github.io/ramabana/cli.html#main) accepts,
read without its parser; None leaves it to
[`main`](https://vedicreader.github.io/ramabana/cli.html#main).*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L157"
target="_blank" style="float:right; font-size:smaller">source</a>

### refusal

``` python
def refusal(
    tmux:str='auto', pane:str='auto', profile:str='auto', pii:str='off', vault:bool=False, python:bool=False,
    attach:str='', agent_proxy:bool=False, warm:bool=False, no_warm:bool=False
):
```

*Why [`main`](https://vedicreader.github.io/ramabana/cli.html#main)
refuses these options before starting anything, or ’’.*

## config.toml

`~/.config/ramabana/config.toml` holds this machine’s settings: one
file, edited by hand, that every Ramabana on the machine reads. A
session started with another `--cfg`, and an app that embeds Ramabana
with a config folder of its own, read the same file and never write into
their own folder for it. `$RAMABANA_CONFIG` names another file; the
tests use it.

A value can come from four places, and the first that has one wins:

<table>
<thead>
<tr>
<th>Source</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>a command-line flag</td>
<td><code>--model gpt-5.4</code></td>
</tr>
<tr>
<td>an env var</td>
<td><code>$RAMABANA_MODEL</code>, or <code>$LEELA_MODEL</code> inside
Leela</td>
</tr>
<tr>
<td>config.toml</td>
<td><code>[jobs]</code> <code>turn = "gpt-5.4"</code></td>
</tr>
<tr>
<td>the built-in default</td>
<td><code>claude-opus-5-5</code></td>
</tr>
</tbody>
</table>

`KEYS` lists every key: its type, the env var and the flag that beat it,
what it sets, and the values it may take. The tables are `[jobs]`, the
model each job runs on; `[window]`, the numbers the window guard works
with; `[compaction]`; `[runtime]`, what a model is started with; and
`[cli]`, the defaults of the terminal session’s flags that take a value.
`[jobs] image` is the OpenAI-compatible model `generate_image` draws
with. `[azure]` holds Azure OpenAI’s `endpoint`, which
`$AZURE_OPENAI_ENDPOINT` beats, read as named with no prefix
(`Key.plain`), and the `api_key_env` that names the env var holding its
key. A key an earlier release read, such as `azure.deployments`, is
reported with what to do instead (`RETIRED_KEYS`). A `[model."NAME"]`
table corrects the numbers fastllm’s table gives one model:
`max_input_tokens`, `max_output_tokens` and `supports_images`. Given
`model_id`, the same table defines a model of that name, with `runtime`,
`base_url`, `api_key_env`, `api_name`, `vendor_name` and `ctx`;
[core](core.html) registers it.

[`load_config`](https://vedicreader.github.io/ramabana/setup.html#load_config)
reads the file again only when it changes. An entry it cannot use takes
its default and gives one warning, which
[`Agent.problems`](https://vedicreader.github.io/ramabana/agent.html#agent.problems)
and `ramabana config show` repeat: a key nobody knows, with the nearest
one it might mean; a value of the wrong type or outside its choices; a
file that is not TOML. Nothing raises.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L373"
target="_blank" style="float:right; font-size:smaller">source</a>

### set_table

``` python
def set_table(
    table, values, path:NoneType=None
):
```

*Set `values` in config.toml’s `[table]`, adding the table when it is
missing, comments kept; a file that is not TOML is refused, never
rewritten.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L350"
target="_blank" style="float:right; font-size:smaller">source</a>

### set_setting

``` python
def set_setting(
    key, value, path:NoneType=None
):
```

*Set top-level `key` in config.toml by rewriting its one line, comments
kept. A missing file starts from
[`config_template`](https://vedicreader.github.io/ramabana/setup.html#config_template);
one that is not TOML is refused, never rewritten.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L339"
target="_blank" style="float:right; font-size:smaller">source</a>

### env_names

``` python
def env_names(
    key
):
```

*The env vars that beat `key`, most specific first: its name as given
for a `plain` key, else under each prefix.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L330"
target="_blank" style="float:right; font-size:smaller">source</a>

### model_setting

``` python
def model_setting(
    *names
):
```

*The `[model."NAME"]` corrections for the first of `names` that has any,
else `{}`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L326"
target="_blank" style="float:right; font-size:smaller">source</a>

### setting

``` python
def setting(
    key, dflt:NoneType=None
):
```

*Config.toml’s value for `key`, else `dflt`. An env var or a flag that
beats it is read by the code that owns the key.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L312"
target="_blank" style="float:right; font-size:smaller">source</a>

### load_config

``` python
def load_config(
    path:NoneType=None
):
```

*`(values, problems)` from config.toml, read again only when it changes.
A key that is missing or wrong takes its default; nothing raises.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L225"
target="_blank" style="float:right; font-size:smaller">source</a>

### config_path

``` python
def config_path():
```

*This machine’s config file: `$RAMABANA_CONFIG`, else `CONFIG`. An app
that embeds ramabana reads the same file.*

``` python
import tempfile, warnings
with tempfile.TemporaryDirectory() as d:
    p = Path(d)/'config.toml'
    p.write_text('learning = false\n[jobs]\nsummary = "gpt-5.4"\nturns = "x"\n[window]\nmargin = 0.5\n[model."codex/gpt-6.1-sol"]\nmax_input_tokens = 500000\nwindow = 3\n')
    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter('always')
        vals, bad = load_config(p)
    test_eq(vals, {'learning': False, 'jobs.summary': 'gpt-5.4', 'model': {'codex/gpt-6.1-sol': {'max_input_tokens': 500000}}})
    test_eq(len(bad), 3); test_eq(len(w), 3)
    assert 'did you mean jobs.turn?' in bad[0] and 'margin = 0.5 is ignored: it must be a number, 1 or more' in bad[1]
    p.write_text('[azure]\nendpoint = "https://r.openai.azure.com"\ndeployments = ["gpt-5.4"]\n[model."az"]\nmodel_id = "gpt-5.4"\nbase_url = "https://r.openai.azure.com"\nctx = 9\n')
    with warnings.catch_warnings(record=True):
        warnings.simplefilter('always')
        vals, bad = load_config(p)
    test_eq(vals, {'azure.endpoint': 'https://r.openai.azure.com', 'model': {'az': {'model_id': 'gpt-5.4', 'base_url': 'https://r.openai.azure.com', 'ctx': 9}}})
    assert 'no longer choose models' in bad[0], bad
    p.write_text('learning = [')
    vals, bad = load_config(p)
    test_eq(vals, {}); assert 'not valid TOML' in bad[0]
bad
```

    UserWarning: /tmp/tmpxqop1clh/config.toml: it is not valid TOML (Invalid value (at end of document)), so every setting takes its default
      vals, bad = load_config(p)

    ['/tmp/tmpxqop1clh/config.toml: it is not valid TOML (Invalid value (at end of document)), so every setting takes its default']

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L482"
target="_blank" style="float:right; font-size:smaller">source</a>

### config_cli

``` python
def config_cli(
    argv:tuple=()
):
```

*`ramabana config`: `show` what is in effect, `init` a commented
config.toml, or print its `path`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L458"
target="_blank" style="float:right; font-size:smaller">source</a>

### config_show

``` python
def config_show(
    path:NoneType=None
):
```

*Each setting in effect and where it came from: an env var, config.toml
or the built-in default. A flag is per command line, so it is named, not
read.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L447"
target="_blank" style="float:right; font-size:smaller">source</a>

### config_template

``` python
def config_template():
```

*A config.toml naming every key with its default, all commented out.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L404"
target="_blank" style="float:right; font-size:smaller">source</a>

### config_defaults

``` python
def config_defaults():
```

*Every key’s built-in default, read from the module that owns it.*

`ramabana setup` adds a provider when nothing runs, or another when you
want one. It prints
[`setup_guide`](https://vedicreader.github.io/ramabana/core.html#setup_guide)
from [core](core.html), the providers and each job’s model, and asks
which provider to add. Azure OpenAI, AWS Bedrock’s Mantle endpoint and
any OpenAI-compatible server get a `[model."NAME"]` table
(`SETUP_FIELDS` says what each asks); a provider whose models already
have names, such as OpenAI or Claude Code, needs only the name. Last it
asks which jobs the model serves and writes them into `[jobs]`. Flags
answer the same questions without a terminal, and without a terminal or
flags it prints the guide and exits 2. It writes with
[`set_table`](https://vedicreader.github.io/ramabana/setup.html#set_table),
which edits or adds one table and keeps every comment, and never writes
a key.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L516"
target="_blank" style="float:right; font-size:smaller">source</a>

### setup_cli

``` python
def setup_cli(
    argv:tuple=(), ask:function=_no_stdin, isatty:NoneType=None, out:builtin_function_or_method=print
):
```

*`ramabana setup`: add a provider’s model to config.toml and name the
jobs it serves. Asks only on a terminal; never writes a key.*

``` python
import tomllib
with tempfile.TemporaryDirectory() as d:
    p = Path(d)/'config.toml'; p.write_text('learning = false   # kept\n')
    set_table('jobs', {'turn': 'az'}, p); set_table('az', {'model_id': 'gpt-5.4'}, p)
    test_eq(tomllib.loads(p.read_text()), {'learning': False, 'jobs': {'turn': 'az'}, 'model': {'az': {'model_id': 'gpt-5.4'}}})
    assert '# kept' in p.read_text()
```

[`set_setting`](https://vedicreader.github.io/ramabana/setup.html#set_setting)
writes one top-level key, which only `learning` needs. It rewrites that
key’s line, or the commented-out line the template left, so every
comment stays. A missing file starts from the template. A file that is
not valid TOML is refused with a message saying so, and never rewritten.

``` python
with tempfile.TemporaryDirectory() as d:
    p = Path(d)/'config.toml'
    test_eq(set_setting('learning', False, p), False)
    text = p.read_text()
    assert text.startswith(CONFIG_HEAD) and '\nlearning = false\n' in text and '# turn = ' in text
    test_eq(load_config(p), ({'learning': False}, []))
    set_setting('learning', True, p); test_eq(p.read_text().count('learning = '), 1)
    p.write_text('# mine\nlearning = [')
    try: set_setting('learning', False, p); raise AssertionError('rewrote a file it cannot read')
    except ValueError as e: assert 'not valid TOML' in str(e)
    test_eq(p.read_text(), '# mine\nlearning = [')
print(config_template()[:900])
```

`ramabana config` reaches these before the session’s own options are
parsed. `ramabana config init` writes the template, every key commented
out beside its default, and refuses to replace a file that exists unless
given `--force`, which keeps the old one as `config.toml.bak` and
carries its `learning` across. `ramabana config show` prints each key’s
value in effect and where it came from, with fastllm’s own number beside
a corrected window. `ramabana config path` prints where the file is.

``` python
import os
with tempfile.TemporaryDirectory() as d:
    os.environ['RAMABANA_CONFIG'] = str(p := Path(d)/'config.toml')
    try:
        test_eq(config_cli(['init']), 0); test_eq(config_cli(['init']), 1)
        set_setting('learning', False)
        test_eq(config_cli(['init', '--force']), 0)
        assert (p.parent/'config.toml.bak').exists() and setting('learning') is False
        p.write_text(p.read_text().replace('# summary = ', 'summary = "gpt-5.4" #', 1))
        os.environ['RAMABANA_MODEL'] = 'gpt-5.4-mini'
        out = config_show()
        assert 'jobs.summary = "gpt-5.4"   # config.toml' in out and 'jobs.turn = "gpt-5.4-mini"   # $RAMABANA_MODEL' in out
        assert 'jobs.classify = "claude-haiku-5-5"   # built-in' in out
    finally: os.environ.pop('RAMABANA_CONFIG'); os.environ.pop('RAMABANA_MODEL', None)
print(out)
```

    wrote /tmp/tmpm2bpoc3h/config.toml
    wrote /tmp/tmpm2bpoc3h/config.toml
    # /tmp/tmpm2bpoc3h/config.toml
    learning = false   # config.toml; --no-learned beats it
    jobs.turn = "gpt-5.4-mini"   # $RAMABANA_MODEL; --model beats it
    jobs.oneshot = "gpt-6-luna"   # built-in
    jobs.inline = ""   # built-in
    jobs.completion = "claude-haiku-5-5"   # built-in
    jobs.classify = "claude-haiku-5-5"   # built-in
    jobs.summary = "gpt-5.4"   # config.toml
    jobs.subagent = "claude-sonnet-5-5"   # built-in
    jobs.image = "openai/gpt-image-1"   # built-in
    jobs.video = "gemini/veo-3.0-generate-001"   # built-in
    window.margin = 1.15   # built-in
    window.image_tokens = 1600   # built-in
    window.reserve = 16384   # built-in
    window.keep_recent = 20000   # built-in
    window.local_ctx = 0   # built-in
    compaction.auto = true   # built-in
    compaction.strategy = "summary"   # built-in
    runtime.effort = "high"   # built-in
    runtime.tool_channel = ""   # built-in
    runtime.litert_backend = ""   # built-in
    azure.endpoint = ""   # built-in
    azure.api_key_env = "AZURE_OPENAI_API_KEY"   # built-in
    cli.approve = "ask"   # built-in; --approve beats it
    cli.theme = "auto"   # built-in; --theme beats it
    cli.tmux = "auto"   # built-in; --tmux beats it
    cli.pane = "auto"   # built-in; --pane beats it
    cli.profile = "auto"   # built-in; --profile beats it
    cli.max_tool_calls = "auto"   # built-in; --max-tool-calls beats it
    cli.max_steps = "auto"   # built-in; --max-steps beats it

    /tmp/tmpm2bpoc3h/config.toml exists; `ramabana config init --force` replaces it and keeps it as config.toml.bak

``` python
test_eq(launch_args(['--root', 'a,b', '--profile=full', '--no-web', '--cfg', '/c']), dict(cfg='/c', tmux='auto'))
for argv in (['hi'], ['--vault', 'hi'], ['-'], ['--json'], ['--doctor'], ['--tmux', 'off'], ['--pane', 'sideways']): test_eq(launch_args(argv), None)
```

## Installing tmux

When tmux is missing, `offer_install` asks once whether to run the
command that installs it, and shows the command: `brew install tmux` on
macOS, `sudo apt-get install -y tmux` or `sudo dnf install -y tmux` on
Linux. `installer` picks the command from what is on `PATH`. The command
runs attached to the terminal, so `sudo` can ask for a password.

The answer is stored in `<cfg>/setup.json`. A no is never asked again,
and neither is a yes whose install worked. Ctrl+c at the prompt, or an
install that failed or was interrupted, stores nothing, so the next run
asks again. `ramabana --doctor` asks even after an earlier no. Without
tmux, the session starts in the current terminal and prints one line on
how to get the now pane.

The first example says yes on a Mac with Homebrew, and the session
relaunches in the tmux it installed. The second says no on Linux, and a
second `launch` does not ask again:

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L583"
target="_blank" style="float:right; font-size:smaller">source</a>

### Setup.offer_install

``` python
def offer_install(
    force:bool=False
):
```

*Ask once, or again when `force`, to install tmux: True when tmux is
there now.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L559"
target="_blank" style="float:right; font-size:smaller">source</a>

### Setup.installer

``` python
def installer():
```

*The command that installs tmux here, or None.*

``` python
box = Box(have={'brew'}, answer='y')
mk(box).launch()
test_eq((box.calls[0], box.execd is not None), (['brew', 'install', 'tmux'], True))
box.asked
```

``` python
box = Box(have={'apt-get', 'sudo'}, system='Linux', answer='n')
s = mk(box)
s.launch()
s.launch()
test_eq((len(box.asked), box.execd, s._seen()), (1, None, {'install_tmux': 'no'}))
```

## Your own tmux

When you start Ramabana inside a tmux server you already run, `ext_keys`
reads that server’s `extended-keys` setting and turns it on only when it
is `off`, printing a line to say so. Your `~/.tmux.conf` is never
changed. The example runs `launch` inside tmux with the setting off, on,
and
[`always`](https://vedicreader.github.io/ramabana/agent.html#always),
and only the first turns it on:

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L602"
target="_blank" style="float:right; font-size:smaller">source</a>

### Setup.ext_keys

``` python
def ext_keys():
```

*Turn on `extended-keys` in the tmux server we run inside when it is
off: True if turned on.*

``` python
for ext, sets in (('off', True), ('on', False), ('always', False)):
    box = Box(ext=ext)
    mk(box, environ={'TMUX': '/tmp/tmux-501/default,1,0'}).launch()
    test_eq((['/bin/tmux', 'set', '-s', 'extended-keys', 'on'] in box.calls, box.execd), (sets, None))
```

## Whether the code index needs to run

The `search_code` tool searches a code index that kosha builds over the
installed packages and the open folders. Rebuilding it, called a sync,
takes time, so the session skips it when nothing it reads has changed.

`index_gate(cfg, roots)` decides this without importing kosha. It takes
a fingerprint,
[`index_state`](https://vedicreader.github.io/ramabana/setup.html#index_state),
of what a sync would read: the installed distributions that
`importlib.metadata` finds, with their modification times
([`pkg_state`](https://vedicreader.github.io/ramabana/setup.html#pkg_state)),
and for each folder its git HEAD and a digest of `git status` with the
modification times of the paths it names
([`root_state`](https://vedicreader.github.io/ramabana/setup.html#root_state)).
Outside git, a folder’s fingerprint is the newest modification time
among its entries. It returns the fingerprint and a reason to run kosha,
or `''` to skip it. The reason is `''` when the fingerprint matches the
one the last good sync saved in `<cfg>/kosha-state.json` and each
folder’s `.kosha/code.db` and kosha’s package database,
[`kosha_env_db()`](https://vedicreader.github.io/ramabana/setup.html#kosha_env_db),
exist. Otherwise
[`index_changes`](https://vedicreader.github.io/ramabana/setup.html#index_changes)
says what changed, such as `packages changed: +foo 1.2`.

After a good sync the session calls
[`save_index`](https://vedicreader.github.io/ramabana/setup.html#save_index)
to store the new fingerprint. Either way
[`log_index`](https://vedicreader.github.io/ramabana/setup.html#log_index)
appends one line to `<cfg>/kosha.log`, and `--doctor` shows the last
line through
[`last_index`](https://vedicreader.github.io/ramabana/setup.html#last_index).
The example walks a fresh folder through a first sync, a missing index,
an unchanged state and a new package:

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L697"
target="_blank" style="float:right; font-size:smaller">source</a>

### last_index

``` python
def last_index(
    cfg
):
```

*The last line of `<cfg>/kosha.log`, or ’’.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L689"
target="_blank" style="float:right; font-size:smaller">source</a>

### log_index

``` python
def log_index(
    cfg, decision, why, secs
):
```

*Append `when · run|skip · why · secs` to `<cfg>/kosha.log`, trimmed to
its last `LOG_KEEP` lines past `LOG_MAX` bytes.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L682"
target="_blank" style="float:right; font-size:smaller">source</a>

### save_index

``` python
def save_index(
    cfg, state
):
```

*Keep `state` as the last good sync’s, beside the roots other sessions
indexed.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L667"
target="_blank" style="float:right; font-size:smaller">source</a>

### index_gate

``` python
def index_gate(
    cfg, roots, dirs:NoneType=None
):
```

*`(state, why)`: the fingerprint now, and why kosha must run (a missing
code or env db included), or ’’ to skip it.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L652"
target="_blank" style="float:right; font-size:smaller">source</a>

### index_changes

``` python
def index_changes(
    old, new, most:int=3
):
```

*Why `new` differs from `old`, e.g. `packages changed: +foo 1.2` or
`leela HEAD a1b2→c3d4`; ’’ when it does not.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L646"
target="_blank" style="float:right; font-size:smaller">source</a>

### index_state

``` python
def index_state(
    roots, dirs:NoneType=None
):
```

*The fingerprint of what a sync of `roots` reads.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L635"
target="_blank" style="float:right; font-size:smaller">source</a>

### root_state

``` python
def root_state(
    root
):
```

*A root’s git HEAD and working-tree digest, staged changes included;
outside git, the newest mtime of its entries.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L617"
target="_blank" style="float:right; font-size:smaller">source</a>

### pkg_state

``` python
def pkg_state(
    dirs:NoneType=None
):
```

*`{dist name-version: mtime}` for what `importlib.metadata` finds on
`dirs`, default `sys.path`, as kosha lists packages.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L612"
target="_blank" style="float:right; font-size:smaller">source</a>

### kosha_env_db

``` python
def kosha_env_db():
```

*Where `Kosha()` keeps its package index: `<xdg data>/kosha/env.db`, as
kosha computes it.*

``` python
import tempfile
with tempfile.TemporaryDirectory() as d:
    d = Path(d); (site := d/'site').mkdir(); (site/'fastcore-1.8.0.dist-info').mkdir(); (root := d/'proj').mkdir()
    state, why = index_gate(d/'cfg', [root], [site])
    test_eq((list(state['packages']), why), (['fastcore-1.8.0'], 'first sync'))
    save_index(d/'cfg', state)
    test_eq(index_gate(d/'cfg', [root], [site])[1], 'index missing')
    (root/'.kosha').mkdir(); (root/'.kosha'/'code.db').write_text('')
    if kosha_env_db().exists(): test_eq(index_gate(d/'cfg', [root], [site])[1], '')
    (site/'foo-1.2.dist-info').mkdir()
    assert index_gate(d/'cfg', [root], [site])[1].endswith('packages changed: +foo 1.2')
    log_index(d/'cfg', 'skip', 'unchanged', .01)
    test_eq(last_index(d/'cfg').split(' · ')[1:], ['skip', 'unchanged', '0.01s'])
```

    Updating packages:   0%|          | 0/31 [00:00<?, ?pkg/s]

## `--doctor`

`ramabana --doctor` prints one line for each thing the now pane and the
keys depend on: the tmux version, whether it can split a pane (tmux 3.1
or later), extended keys, the config file, `ramabana-pane`, the missing
extras, and the code index’s last decision. When tmux is missing it then
offers to install it, even after an earlier no. The example runs it on a
fake machine with tmux 3.0, which is too old for the split:

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/setup.py#L704"
target="_blank" style="float:right; font-size:smaller">source</a>

### Setup.doctor

``` python
def doctor():
```

*Print one line per check, offer to install tmux when it is missing, and
return 0.*

``` python
mk(Box(have={'tmux', 'ramabana-pane'}, version='tmux 3.0a')).doctor()
```
