Metadata-Version: 2.5
Name: wisemonkey
Version: 2026.11.03
Summary: A simple, extensible, and hackable AI agent for the Linux and macOS terminal
License-File: LICENSE
Requires-Python: >=3.13
Requires-Dist: anthropic>=0.70.0
Requires-Dist: chromadb>=0.5.0
Requires-Dist: ollama>=0.4.0
Requires-Dist: openai>=1.0.0
Requires-Dist: pillow>=12.2.0
Requires-Dist: prompt-toolkit>=3.0.52
Requires-Dist: pymupdf>=1.24.0
Requires-Dist: pypubsub>=4.0.7
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich>=15.0.0
Requires-Dist: textual[syntax]>=2.0.0
Requires-Dist: tiktoken>=0.13.0
Requires-Dist: xdg-base-dirs>=6.0.2
Description-Content-Type: text/markdown

<h3 align="center"><img src="icon.png" alt="Wisemonkey" width="130px"><br>Wisemonkey - <i>A dead simple CLI agent for Linux and macOS</i></h3>

<p align="center">
<a href="https://codeberg.org/langurmonkey/wisemonkey/releases"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcodeberg.org%2Fapi%2Fv1%2Frepos%2Flangurmonkey%2Fwisemonkey%2Freleases%2Flatest&query=%24.tag_name&label=latest%20release" alt="Latest release" /></a>
<a href="https://codeberg.org/langurmonkey/wisemonkey/issues"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcodeberg.org%2Fapi%2Fv1%2Frepos%2Flangurmonkey%2Fwisemonkey%2Fissues&query=%24.length&label=open%20issues" alt="Open issues" /></a>
<a href="https://pypi.org/project/wisemonkey/"><img src="https://img.shields.io/pypi/v/wisemonkey?label=PyPI" alt="PyPI" /></a>
<a href="https://opensource.org/licenses/MPL-2.0"><img src="https://img.shields.io/badge/license-MIT-brightgreen.svg" alt="License: MPL2.0" /></a>
<img alt="Static Badge" src="https://img.shields.io/badge/OS-Linux-blue">
<img alt="Static Badge" src="https://img.shields.io/badge/OS-macOS-yellow">

</p>

---

[Wisemonkey](https://tonisagrista.com/projects/wisemonkey) is a simple, open, and hackable AI agent for the Linux and macOS terminal. It connects to any service providing an OpenAI, Anthropic, or Ollama-compatible endpoint. It features **session management**, **persistent memory management**, **vector store** for document embedding, native and MCP **tools**, **skills**, and much more.

The sections of this document are:

- [Quickstart](#quickstart)
- [Run from source](#run-from-source)
- [Run in a container](#run-in-a-container)
- [Configuration](#configuration)
- [Soul file](#soul-file)
- [Usage and commands](#usage-and-commands)
- [Mid-turn steering](#mid-turn-steering)
- [Global memory](#global-memory)
- [Rolling chat memory](#rolling-chat-memory)
- [Extend agent](#extend-agent)

## Quickstart

Wisemonkey has been tested to work on Linux and macOS.

### Requirements

- Python 3.13+
- `uv` for dependency management

### Installation

On Linux or macOS, install `uv` and run the agent:

```bash
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Run wmk
uvx wisemonkey
```

Wisemonkey is available on [PyPI](https://pypi.org/project/wisemonkey/).

You can also install it with our script:

```bash
curl -fsSL https://codeberg.org/langurmonkey/wisemonkey/raw/branch/master/install.sh | sh
```
This installs wisemonkey to `~/.local/share/wisemonkey/repository`. It adds a `wmk` binary to `~/.local/bin/wmk`. If Docker is installed, it also adds a `wmkc` binary to `~/.local/bin/wmkc` to run the agent in a container (see [Run in a container](#run-in-a-container)). If you have `~/.local/bin` in your `$PATH`, you can launch the onboarding process to configure the agent interactively:

```bash
wmk --onboard
```

### Running

Run the agent with the default session:

```bash
# Using uvx
uvx wisemonkey
# If installed, simply do
wmk
```
For the rest of this document, we assume that `wmk` is in your path. You can substitute it with `uvx wisemonkey` if you use the `uvx` method.

If you need an API key to access the endpoint, put it in the `.env` file. Wisemonkey looks for the `.env` file in the following locations, in order:

- Current directory, `./.env`
- Config directory, `$XDG_CONFIG_HOME/wisemonkey/.env`
- Home directory, `$HOME/.env`

Create the `.env` file with the API key:

```bash
echo "OPENAI_API_KEY=your-api-key-here" > .env
echo "ANTHROPIC_API_KEY=your-api-key-here" > .env
```

> The agent uses `python-dotenv` to load `.env` at startup. The `openai` package reads `OPENAI_API_KEY` from the environment automatically. You can also set `OPENAI_API_KEY` in your shell profile. Same goes for `ANTHROPIC_API_KEY`.

### API keys for several endpoints

Wisemonkey is usually driven through a router — OpenRouter, OpenCode, a local vLLM — and each of those wants its own key. A single global variable cannot cover that: you would have to rewrite it on every switch (into shell history), and one endpoint's key would be sent to another the moment the provider changed. So the key is resolved from an **ordered, per-provider chain**, first non-empty wins:

1. `model.api_key_env` — the environment variable **name** given in `config.yaml`. Explicit always wins.
2. `WM_API_KEY_<PROVIDER>` — namespaced, so several keys coexist. Providers are `GENERIC`, `OPENAI`, `ANTHROPIC` (`WM_API_KEY_GENERIC`, …). This is the one that makes router hopping painless.
3. The provider's conventional variable — `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`. Existing setups keep working untouched.
4. `WM_API_KEY` — a generic fallback, tried **last**, for every key-bearing provider.

Local providers (Ollama, LM Studio) need no key.

The rule is that only step 4 is shared: `WM_API_KEY_GENERIC` never satisfies an Anthropic request, so switching provider cannot send the wrong credential anywhere.

**Recommended setup for routers.** Give each endpoint its own variable and point at it:

```bash
cat >> .env <<'EOF'
OPENROUTER_API_KEY=sk-or-...
OPENCODE_API_KEY=sk-oc-...
EOF
```

```yaml
model:
  provider: generic
  base_url: https://openrouter.ai/api/v1
  api_key_env: OPENROUTER_API_KEY   # name only, never the key itself
  name: anthropic/claude-sonnet-4.5
```

Switching to OpenCode is then two lines in `config.yaml`, with both keys still in `.env` and neither moving to your shell history. If you do not care which key is which, the namespaced variables need no config at all:

```bash
WM_API_KEY_GENERIC=sk-or-...   # used by any OpenAI-compatible endpoint
WM_API_KEY_ANTHROPIC=sk-ant-...
```

In the agent loop, `/model` configures a whole endpoint in one flow: URL → provider → key variable → model (`/url` is an alias for the same flow). A canonical URL picks the provider for you, an unknown host asks whether it is OpenAI-compatible, and a local endpoint asks no key question at all. The key step is a list, not free text: it offers the host's conventional name first (`OPENROUTER_API_KEY` for `openrouter.ai`, `OPENCODE_API_KEY` for `opencode.ai`), marks the variables that are actually set in your environment, and keeps auto-detect as the default.

Endpoints you use regularly can be listed under `model.endpoints`, and the URL step then becomes a picker with "Enter a new URL..." as its last entry:

```yaml
model:
  endpoints:
    - name: Local (LM Studio)
      base_url: http://127.0.0.1:1234/v1
      provider: lmstudio          # skips the provider question
    - name: OpenRouter
      base_url: https://openrouter.ai/api/v1
      provider: generic
      api_key_env: OPENROUTER_API_KEY   # the key this endpoint wants
    - https://api.openai.com/v1         # bare URL = entry with no extras
```

Picking an entry sets its URL, provider and key variable at once, so switching back and forth is one selection. The key list likewise shows **every** `*_API_KEY` variable found in `.env` or the environment, not just the presets. Two entries are always appended for anything new: "Enter a different variable name..." and "Enter a new API key", which prompts for the value and writes it to `~/.config/wisemonkey/.env` (mode `600`), so a new key never needs a hand-edited file or a restart.

Storing a key in `config.yaml` is not supported, deliberately: `config.yaml` gets shared and committed, `.env` does not. See `.env.example`.

If a key cannot be found, Wisemonkey names every variable it tried, e.g.

```
Missing API key for generic. Tried: WM_API_KEY_GENERIC, OPENAI_API_KEY,
WM_API_KEY. Set one in your environment or .env file, or point
model.api_key_env at a variable holding the key, or run /onboard.
```


## Run from source

```bash
# Clone the repo, then build the project:
uv build
# Set an API key (any of the names in "API keys for several endpoints"):
export WM_API_KEY_GENERIC=your-api-key
# Run the agent with the default session:
uv run wmk
```

## Run in a container

The `wmkc` script runs the agent inside a container, so it needs no Python or `uv` on the host, and the agent only ever sees the directory you launch it from. On first use it builds the image from the `Dockerfile` (a few minutes) and caches it; later runs start instantly.

```bash
# From the directory you want the agent to work in:
cd ~/my-project
wmkc                      # same arguments as wmk
wmkc my-project           # a named session
wmkc --version
```

The container runs as **your** user, not root, so files it writes into the workspace and into the session history come out owned by you. Three host directories are mounted:

- **the current directory** — the workspace; it is the only part of the host the agent can see and modify
- **`$XDG_CONFIG_HOME/wisemonkey`** — `config.yaml`, `mcp.json` and any `.env` already in use
- **`$XDG_DATA_HOME/wisemonkey`** — sessions, chat history, notes and vector stores, so conversations survive the container

Your `HOME`, `USER`, `TERM`, `EDITOR`/`VISUAL`, the XDG variables and your API keys (`WM_API_KEY_*`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`) are forwarded from the environment. Keys are passed **by name**, never as `NAME=value`, so they do not show up in `ps` or in your shell history.

The agent runs sandboxed: no Linux capabilities, no privilege escalation, and a bounded process count.

| Variable | Effect |
|-|-|
| `WMKC_IMAGE` | Use a different image (default `wisemonkey-agent:local`) |
| `WMKC_BUILD=0` | Never build; fail if the image is missing (for scripts) |
| `WMKC_REBUILD=1` | Rebuild the image even if it already exists |
| `WISEMONKEY_HOME` | Where the `Dockerfile` lives (set by the `install.sh` wrapper) |

> The image ships the agent, not the checkout: editing `agent/*.py` does **not** change a container that is already built. Rebuild with `WMKC_REBUILD=1 wmkc` to run changed code.

> Tool hooks (`agent.hooks`) run *inside* the container, so a guard binary installed only on the host (e.g. `~/.local/bin/dcg`) is not found there — and a hook that cannot start fails open. If you rely on a host-side guard, run `wmk` directly, or extend `wmkc` to mount the binary in.

> Client/server mode also works, but a daemon started *inside* a container is private to that container: `XDG_RUNTIME_DIR` is deliberately not forwarded, so the socket goes to `/tmp/wisemonkey-<user>`.

## Configuration

You can configure the agent interactively before the first run with `wmk --onboard`. On first run, the configuration file is created in `$XDG_CONFIG_HOME/wisemonkey/config.yaml` from the default configuration (`config.yaml`) in the root of this repository.

Additionally, the configuration directory holds the `mcp.json` (see next section), and the `.updates.yml`, which holds information about the last update time and status.

### Model Context Protocol (MCP)

Wisemonkey also supports MCP. Use the following commands to manage the MCP integration:

- `/mcp`: Show the current MCP configuration
- `/mcp edit`: Edit the MCP configuration file (`~/.config/wisemonkey/mcp.json`)
- `/mcp tools`: List all MCP tools available. Alias: `/tools mcp`

MCP servers are started when the agent boots. You need to restart the agent if you add new servers.

### Tool hooks (`agent.hooks`)

Wisemonkey can run external guard programs around every tool call — before it
runs (`pre_tool_use`) and after (`post_tool_use`). The protocol is compatible
with Claude Code / Codex hooks: the hook gets JSON on stdin and answers on
stdout, so an already-installed tool like [`dcg`](https://github.com/dcg)
(the destructive command guard) works with no adapter.

```yaml
agent:
  hooks:
    pre_tool_use:
      - matcher: "run_command|spawn"   # regex over tool names; empty or "*" = all
        hooks:
          - type: command
            command: /usr/local/bin/dcg   # use an absolute path
            timeout: 5                   # seconds
            honor_unsafe: false          # see below
    post_tool_use: []                    # logging/audit; output is ignored
```

- A `pre_tool_use` hook may `deny`, `ask` or `allow` via
  `{"hookSpecificOutput": {"permissionDecision": "...", "permissionDecisionReason": "..."}}`
  on stdout, or block with exit code 2 and the reason on stderr. Silent stdout
  means "no opinion". A `deny` is fed back to the model as a tool error (like a
  declined confirmation), so the turn continues and the model can adapt; an
  `ask` turns into an ordinary wisemonkey confirmation prompt.
- Shell tools (`run_command`, `spawn`) are presented to a hook under the Claude
  name **`Bash`** with the command in `tool_input.command` — that is the name a
  stock `dcg` matches on.
- A hook that **cannot start** fails open with a warning (a broken install does
  not refuse every call). A hook that **hangs past `timeout` or prints garbage**
  fails closed, because it has not cleared the call; set `fail_open: true` on a
  handler to invert that.
- `honor_unsafe: true` makes a handler skip while `agent.unsafe` is set. The
  default (`false`) is that hooks **always run**, so `agent.unsafe` never
  silently disables a guard that did not ask to be switched off.

## File references (`@path`)

You can attach files directly to a prompt with `@`-references: writing `@agent/agent.py` (or the backtick-quoted `@\`agent/agent.py\``) in your message inlines the file content into the context sent to the model — no extra tool-call round-trip needed.

- **Small files** (up to `agent.at_file_max_chars`, default 8000) are inlined as fenced code blocks appended to your message.
- **Large files** inject only a stub (path, size, first lines) so the model knows the file exists and can read it itself with the `read_file` tool.
- **Directories** inject a shallow listing.
- Non-existent or handle-like tokens (`@langur`, `a@b.com`) are left untouched.

The typed text stays as-is on screen and in chat history — only the message sent to the model carries the attachment. Set `agent.at_file_max_chars: 0` to disable the feature. Path completion works after `@` too (`@ag` → `@agent/`).

## Soul file

Wisemonkey supports a **soul file** — a markdown file that defines the agent's identity and persona. It is injected into the system prompt *before* the workspace instructions, so it takes precedence over `AGENTS.md` without replacing it.

Two soul files are considered, in order:

1. **Global soul** — `$XDG_CONFIG_HOME/wisemonkey/SOUL.md`. Applies to every session on the machine.
2. **Workspace soul** — `SOUL.md` in the working directory or any parent directory (same walk-up lookup as `AGENTS.md`). Applies to the project.

Both are loaded when present; the global soul comes first. If neither exists, nothing is added.

```markdown
<!-- ~/.config/wisemonkey/SOUL.md -->
# Soul

You are Wisemonkey, a terse and precise engineering assistant.
You prefer concrete answers over hedging, and you never invent facts.
```

Configure the file names (or disable with an empty string) in `config.yaml`:

```yaml
agent:
  soul_file: SOUL.md          # Workspace soul file name
  global_soul_file: SOUL.md   # Global soul file name in the config directory
```

> The soul file is identity, not task instructions. Keep workspace rules in `AGENTS.md` and task-specific procedures in skills.

## Usage and commands

Run the agent, and then you can enter your prompt. You can use the following key bindings during input:

- <kbd>Enter</kbd>: submit the prompt
- <kbd>Alt</kbd> / <kbd>Shift</kbd> / <kbd>Ctrl</kbd> + <kbd>Enter</kbd>, or <kbd>Ctrl</kbd> + <kbd>j</kbd>: add a new line
- <kbd>Ctrl</kbd> + <kbd>q</kbd>: quit

#### Newline keys and your terminal

A terminal sends the *same* byte for <kbd>Enter</kbd>, <kbd>Shift</kbd>+<kbd>Enter</kbd> and <kbd>Ctrl</kbd>+<kbd>Enter</kbd>, so the agent cannot tell them apart unless the terminal is asked to. On start-up it enables the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) (supported by kitty, Ghostty, Alacritty, WezTerm, foot and Rio, no configuration needed) and disables it again on exit, so your shell is left exactly as it was.

| Your terminal | Newline keys |
|-|-|
| kitty, Ghostty, Alacritty, WezTerm, foot, Rio, iTerm2 | <kbd>Shift</kbd>+<kbd>Enter</kbd>, <kbd>Ctrl</kbd>+<kbd>Enter</kbd>, <kbd>Alt</kbd>+<kbd>Enter</kbd>, <kbd>Ctrl</kbd>+<kbd>j</kbd> |
| Terminals sending xterm `modifyOtherKeys` | <kbd>Shift</kbd>+<kbd>Enter</kbd>, <kbd>Alt</kbd>+<kbd>Enter</kbd>, <kbd>Ctrl</kbd>+<kbd>j</kbd> |
| macOS Terminal.app | <kbd>Alt</kbd>+<kbd>Enter</kbd> (needs *Use Option as Meta Key*), <kbd>Ctrl</kbd>+<kbd>j</kbd> |
| GNOME Terminal, PuTTY, screen | <kbd>Ctrl</kbd>+<kbd>j</kbd> |
| tmux | as the outer terminal, plus `set -g extended-keys on` and `set -g extended-keys-format csi-u` |

When a terminal ignores the protocol, <kbd>Shift</kbd>+<kbd>Enter</kbd> simply arrives as <kbd>Enter</kbd> and submits, so <kbd>Ctrl</kbd>+<kbd>j</kbd> is the reliable one. Nothing is printed about this: the fallback is silent.

During inference, you can cancel the turn and return to the input prompt with <kbd>Ctrl</kbd> + <kbd>c</kbd>, or type into the footer to steer the running turn (see [mid-turn steering](#mid-turn-steering)).

#### Questions asked mid-session

Commands like `/model` and `/config` ask you things (a URL, a key variable, a yes/no). Those questions are fully line-edited — arrows, <kbd>Backspace</kbd>, <kbd>Ctrl</kbd>+<kbd>u</kbd>, history — with the question itself styled like the rest of the UI. The one thing that does not work there is a modified <kbd>Enter</kbd>: the questions are read by `readline`, which cannot tell <kbd>Shift</kbd>+<kbd>Enter</kbd> from a plain <kbd>Enter</kbd>, so <kbd>Enter</kbd> submits.

### Sessions

Internally, Wisemonkey uses sessions to separate different memory histories. Sessions are **named by the user**. By default, the agent uses the `default` session. You can start in a different session (either create a new one, or restore it if it exists) by passing its name as a positional argument:

```bash
# Start in a specific session named 'my-project'
wmk my-project
```

The default session's name is `default`, so the following two commands are equivalent:
```bash
# These two commands start the 'default' session
wmk
wmk default
```

You can also list the existing sessions with `-ls`:

```bash
# List sessions
wmk --ls           
Sessions:
- my-project - ~/.local/share/wisemonkey/sessions/my-project
- default - ~/.local/share/wisemonkey/sessions/default
```

Or from inside a session, with `/sessions` (which also shows message counts and
marks the current one):

```
/sessions
```

Sessions can be switched without restarting, with `/resume`:

```
/resume                    # resume the most recently used other session
/resume my-project         # switch to 'my-project'
```

`/resume` rebinds the chat history, notes, user profile and vector store of the
running process, and sends the previous session to disk first. An unknown name
creates a new session, exactly like `wmk my-project`. It is not available in
client/server mode: there the daemon owns the session, so start a server for the
target session (`wmk --server my-project`) and attach to that instead.

Sessions contain:

- The input history
- Chat memory (see [chat memory](#chat-memory))
- Vector store (see [document embedding](#document-embedding))
- Notes (see [session memory](#session-memory))
- User profile (see [session memory](#session-memory))

The configuration file can be session-specific. When a session starts, the agent first looks for
`<session_dir>/config.yaml`. If it exists, that file is used; otherwise, the global config
`<config_dir>/wisemonkey/config.yaml` is used.

> Sessions are matched by the directory name in the sessions location (`~/.local/share/wisemonkey/sessions`). You can rename a session by just renaming the directory! 

### Client/server mode

Wisemonkey can run as a client/server split: a **server** owns the session (chat memory, profile, notes, vector store, model connection), and **clients** are thin frontends that connect to it and render the UI.

Start a persistent daemon server with `--server`:

```bash
# Start a daemon server for the 'default' session
wmk --server
# Or for a named session
wmk --server my-project
```

The server binds a Unix domain socket at `$XDG_RUNTIME_DIR/wisemonkey/$SESSION.sock` (e.g. `/run/user/1000/wisemonkey/default.sock`) and stays running until stopped.

Any client started afterwards **tries to attach to an existing daemon for its session first**. If a server is up, the client runs as a thin remote client: turns, slash commands, cancellation, and memory stats all flow over the socket, while shell commands (`!`) run locally and their results are recorded in the server's chat history. If no server is up, the client falls back to the normal mode, where an ephemeral server is spawned for the duration of the client's lifespan.

```bash
# Terminal 1: start the daemon
wmk --server my-project
# Terminal 2: attach to it (same session name!)
wmk my-project
```

> Clients attach by **session name**: a client for `session2` will never connect to a daemon running `session1` — it simply starts its own independent session.

This also means several clients can share a single session state through one daemon, and the session's chat history survives client restarts while the daemon keeps running.

### `vi` mode

You can enable `vi` mode for the current session with the [command](#commands) `/vi on`, or permanently in the [configuration](#configuration).

**External editor**---In `vi` mode, exit INSERT mode (<kbd>Esc</kbd>), then press <kbd>v</kbd> to edit your prompt in an external editor (uses your `$VISUAL` or `$EDITOR` variable).

### Slash commands

There are a few commands available to use in the agent loop. You can list them with `/help`. Also, use `/[command-name] help` (e.g. `/config help`) to show additional help for a command.

### Correcting a turn

A prompt you already sent is easy to reach but hard to take back: <kbd>↑</kbd> re-sends it verbatim, and the bad exchange stays in the agent's context either way, so the model keeps building on it. Three commands deal with that:

| Command | What it does |
|-|-|
| `/edit` | Open the last prompt in `$EDITOR`, and re-run it — the edited prompt **replaces** the old exchange, so the model never sees both versions |
| `/retry` | Re-run the last prompt unchanged. `--drop` also removes the old answer first, so the re-run starts from the same context the original did |
| `/undo [n]` | Drop the last *n* exchanges from the context entirely, without running anything |

`/undo` drops *whole* exchanges — the prompt, the answer, and any tool calls and results that belonged to it — never a fragment, which would leave a tool result in the prompt with no matching call.

Slash commands are still written to `history.txt`, but they are filtered out of what <kbd>↑</kbd> walks through, so the commands you ran early on no longer push your real prompts out of reach.

### Shell commands

Prefix an input with `!` to run it directly in the shell instead of sending it to the model:

```
!git status
!ls -la
!docker ps
```

The command runs synchronously (60 s timeout by default), its stdout, stderr, and exit code are printed, and the command together with its result is appended to the session chat memory, so the model can use it as context on the next turn. Since you type the command yourself, no confirmation is requested — this is the same trust model as running it in a terminal.

## Mid-turn steering

You don't have to wait for the agent to finish a turn before adding to it.

While the model is working, Wisemonkey reserves the bottom four terminal rows as a **sticky footer**:

```
──────────────────────────────
 ⇒ stealth/space-bunny  Mem…
 ⠹ Processing prompt...
 Ctrl+C: cancel turn   |   type + ↵ to steer
```

The footer stays pinned while the response scrolls, using the terminal's own scroll region (`DECSTBM`) — no output interception, so streaming, tools, and markdown rendering all behave exactly as they do otherwise.

### Steering a running turn

Type in the footer's input row while the turn runs and press <kbd>Enter</kbd>. The line is queued and shown as `↳ queued: <text>  (next tool result)`.

It does **not** have to wait for the turn to end. Wisemonkey delivers it to the model at the **next tool-call seam** — right after the current batch of tools finishes, just before the model is asked again. The line becomes a regular `user` message, so it is persisted to chat history and counts tokens like any other user message, and it is echoed on screen at the moment the model actually receives it:

```
⤷ Read the CSV first, then plot the two columns. [steered mid-turn]
```

That makes a genuine mid-turn correction rather than a new turn: the model already has its earlier work in context when your line lands.

If the turn finishes before another tool call happens, nothing is lost. The line stays queued, and it runs as an ordinary next prompt.

### Details

- The footer input row is deliberately **not** a line editor: no history, no completion, and nothing is echoed to stdout while you type. It only draws the transient row. The stage spinner and the `💡 Thinking...` indicator live on the bottom hints row, to the right of the key bindings, so they never overwrite what you are typing.
- <kbd>Ctrl</kbd> + <kbd>c</kbd> still cancels the running turn, even mid-turn. The key is deliberately left enabled in cbreak mode for exactly this reason.
- Lines you type are captured as raw keystrokes, so an accidental keypress is harmless; it just does not appear until you press <kbd>Enter</kbd>.
- Steering works in both modes: local, and attached to a daemon server (the client forwards the line over the socket, and the server confirms delivery before the client drops its own copy).
- Nothing is lost on cancellation — a queued line survives a cancelled turn.

### Tool output

Each tool prints one line when it is activated:

```
↸ Activating tool:  read_file  (agent/core.py)
```

That is the only line a *successful, quick* tool produces. A "finished in 0.0s" line under every call turns a fifteen-tool turn into fifteen lines of noise, so the result line is printed only when it tells you something:

- **Failures** are always reported, with the error text and the duration.
- **Slow tools** — at least `agent.tool_slow_threshold` seconds (default 1 s) — print `Tool X finished in Y.Ys`, so the tool that made you wait is the one you can name.

```yaml
agent:
  tool_slow_threshold: 1.0   # 0 = always print a result line; a huge value = errors only
```

This is purely about what you see. The tool's full result still goes to the model and into chat history either way.

You can turn the whole thing off with `agent.steer_midturn: false` in the [configuration](#configuration).

> If your terminal renders the footer oddly, set `agent.footer_debug_bytes: true` and look at `$XDG_STATE_HOME/wisemonkey/footer.log`, which records every byte the footer writes.

## Session memory

Persistent memory follows XDG Base Directory spec in `~/.local/share/wisemonkey/session/$SESSION_NAME`:

- `user_profile.json`---User information
- `notes.json`---Persistent notes (added via `save_note` tool)
- `tmp/`---Scratch space for tool subprocesses (see below)

Every command the agent runs (`run_command`, `spawn`) gets `TMPDIR` pointed at `<session_dir>/tmp`, created `0700` on first use. Anything a command leaves in the temp directory---`mktemp`, Python's `tempfile`, a compiler's intermediates---therefore stays in the session instead of the shared, world-readable `/tmp`, and is cleaned up with the session. Writing there needs no confirmation, because the agent already owns it; a path that is literally `/tmp` still prompts.

That scratch directory and the working directory you launched the agent in are the whole of "the session". Every tool that would reach past them asks you first --- the shell tools (`run_command`, `spawn`) and the file tools (`read_file`, `write_file`, `patch_file`, `list_dir`, `find_files`, `search_content`) alike, so reaching an outside path is an explicit decision whichever tool names it. `/unsafe` turns those prompts off for unattended runs.

**Lifecycle:**
- Memory is loaded into the system prompt each turn
- `save_note` tool adds notes during a session
- `save_memory` tool explicitly persists memory to disk
- Memory is auto-saved when the agent exits (interactive mode)

## Document embedding

Wisemonkey can embed documents into a per-session vector store, allowing the agent to search and reference their contents during conversation. Use `/embed` to add a document:

```bash
/embed ~/documents/research_paper.pdf
/embed ./notes.md
```

The agent uses the `search_knowledge` tool to query embedded documents when answering questions about previously indexed files. Supported formats include PDF, Markdown, and plain text. Embeddings are powered by the configured embedding model and stored in the session directory under `vectordb/`.

## Chat history

In addition to persistent memory, the agent maintains a **chat history** of recent user input and assistant output pairs. This provides context that survives beyond the LLM's context window. Here is how it works:

- Each user message and assistant response is stored in memory
- Reasoning is omitted from chat memory
- Automatically compacted when exceeding the configured character limit
- The user can trigger the compaction any time with `/history compact`
- Chat memory is attached to the system prompt on each turn, as the **last section** (after identity, workspace instructions, memory, and skills) so the static prompt prefix stays stable and provider prompt caches are not invalidated on every turn
- The agent displays the last 10 exchanges, with long messages truncated
- `/history` prints the whole session chat history; `/history n` prints only the last **n turns** (a prompt plus its final answer, with tool calls and results left out, like every other count in the `/history-*` commands)
- Tool results are truncated in the formatted history unless `chat_history_full_tool_results` is enabled (see below)
- Large file reads can be limited with the `read_file` tool's `max_lines` parameter (like `head`), or read as a window with `offset` (a 1-based start line, like `tail -n +N`)
- Targeted edits use `patch_file`, which can match text (whitespace-tolerant, exact-match required) or patch by line range with `start_line`/`end_line`
- `search_content` searches file contents by literal substring (default) or by Python regular expression (`regex: true`), with context lines, glob filters, depth limits and a reported result cap
- The token count is live: the footer shows a per-turn Δ while the model streams, the status line shows the exact delta, provider-reported prompt tokens and (if `model.pricing` is configured) the turn's cost
- All color comes from one palette (`agent/palette.py`); the Rich theme, the prompt, the sticky footer, and other components cannot drift apart
- Long-running commands can be backgrounded with `spawn`, watched incrementally with `poll_job`, and stopped with `kill_job` (which signals the whole process group and returns the output printed before death)

**Persistence:**
- Chat history is persisted to `~/.local/share/wisemonkey/session/$SESSION_NAME/chat_history.json`
- Automatically loaded on startup
- Saved after every exchange (user input or assistant response)
- Compacted history is also persisted to disk

**Configuration:**
```yaml
agent:
  max_chat_history: 80000                     # Maximum history tokens to keep for context
  chat_history_full_tool_results: false       # Include full tool call/result content in the prompt
  chat_history_tool_result_max_chars: 500     # Max chars per tool result when the above is false
```

> Chat history is accounted in **tokens**, counted exactly with tiktoken (o200k encoding) when available, falling back to a chars/4 estimate otherwise. The token accounting used for compaction matches what is actually injected into the prompt: when `chat_history_full_tool_results` is `false`, tool results are counted (and injected) truncated to `chat_history_tool_result_max_chars`, so a single large tool result does not trigger premature compaction.

## Protocol

The client/server wire protocol (Unix domain socket, newline-delimited JSON) is specified in [docs/PROTOCOL.md](docs/PROTOCOL.md). It is stable enough to build third-party frontends against: attach to a session's socket, send an `attach` request, and drive turns via `prompt` while streaming events come back.

## Structure

Wisemonkey is built to be modular and hackable. Here is an overview of the main parts and their mapping to the file system.

```
wisemonkey/
├── agent/                  # Core agent code.
│   ├── agent.py            # REPL frontend: agent loop, prompt handling, key bindings.
│   ├── client.py           # Client-side connection to a daemon server.
│   ├── server.py           # Daemon server (UDS) owning the session Core.
│   ├── ipc.py              # IPC protocol: messages, payloads, transports.
│   ├── emitter.py          # Turn event emitter: core callbacks -> IPC events.
│   ├── commands.py         # Slash commands (e.g. /embed, /quit).
│   ├── completion.py       # Smart path completion for the prompt.
│   ├── at_files.py         # @file/@dir reference expansion.
│   ├── shellcmd.py         # `!` shell command handling.
│   ├── startup.py          # Startup banner and info rendering.
│   ├── tokens.py           # Token counting (tiktoken).
│   ├── history.py          # Input history handling.
│   ├── config.py           # Configuration loading and handling.
│   ├── console.py          # Rich console output with themed formatting.
│   ├── output.py           # Output adapter protocol (REPL, IPC).
│   ├── footer.py           # Sticky footer (DECSTBM scroll region) + spinner.
│   ├── steer.py            # Mid-turn steering input (cbreak key capture).
│   ├── keys.py             # Terminal key protocol (modified Enter, kitty keys).
│   ├── core.py             # Core agent functions, like API connection and tool calls.
│   ├── mcp.py              # MCP server support.
│   ├── mdstream.py         # Streaming markdown renderer for the REPL.
│   ├── memory.py           # Session memory, paste file creation.
│   ├── router.py           # API router implementation for OpenAI, Ollama, and Anthropic.
│   ├── skills.py           # Skill loading and management.
│   ├── tools.py            # Tool definitions.
│   ├── update.py           # Update management.
│   ├── utils.py            # Utility functions.
│   └── vectorstore.py      # Vector store wrapper.
├── tools/                  # Tool implementations available to the model.
│   ├── basic.py            # Basic and example tools.
│   ├── files.py            # File read/write tools.
│   ├── memory.py            # search_knowledge tool.
│   ├── network.py          # URL fetching.
│   ├── screenshot.py       # Screen capture tool (base64 JPEG, user-confirmed).
│   ├── terminal.py         # Shell command execution.
│   └── vectorstore.py      # Vector store tool handler.
├── skills/                 # Skill definitions. Add new skills here.
│   ├── example.md
│   └── rolldice.md
├── tests/                  # Contains all `unittest` tests.
│   └── [...]
├── config.yaml             # Default config file.
├── README.md
├── wmkc                    # Run the agent in a container (wrapper around docker).
├── Dockerfile              # Container image for wmkc.
├── pyproject.toml
├── install.sh              # Installer script.
└── .env.example
```

## Extend the agent

This agent is simple enough that it can be easily customized and extended by adding new tools, commands, and skills.

If you create a cool new tool, skill, or slash command, consider contributing it via a merge request!

### Adding tools

Create a file in `tools/` or use one of the existing ones. To create a tool,
create a method and decorate it with `@tool(name, description, params)`:

```python
from agent.tools import tool

@tool(
    name="my_tool",
    description="Does something useful. Be exhaustive here, as it is what the LLM will read to know about your tool.",
    parameters={
        "type": "object",
        "properties": {
            "input": {
                "type": "string",
                "description": "The input parameter."
            }
        },
        "required": ["input"],
    },
)
def my_handler(args):
    input = args.get("input", "no input provided")
    return {"result": f"{input}"}
```

Tools are auto-discovered on startup.

### Adding slash commands

The process is very similar to tools. You need to create your method, preferably in `agent/commands.py`, and decorate it with `@cmd(name, description, aliases, examples, can_complete)`.

A slash command must return, in that order, `ok:bool`, `msg:str`, `content:str`, `markdown:str`:

1. `ok`: a `bool` indicating if the command succeeded or failed.
2. `msg`: an optional short status message. It is printed with `OK` or `ERROR`.
3. `content`: an optional `str` with the Python Rich-formatted content, it is printed to the output.
4. `markdown`: an optional `str` formatted in Markdown, it is printed to the output.

```python
@cmd(
    "/my-command",
    "This is the description",
    aliases=["/mycmd"],
)
def _cmd_my_command(agent, params) -> (bool, str, str, str):
    """This command returns a message but no content"""
    return True, "This is awesome!", None, None
```

Decorated commands are automatically registered, and auto-completed in the input prompt.

### Adding skills

Skills are loaded from the `skills/` directory of the wisemonkey project, as well as the `./skills/` folder within the current working directory. Skill loading can be disabled entirely with `agent.skills: false` in the config — no skills directories are created or scanned, and no skills enter the system prompt.

To create a new skill, add a `.md` file in `skills/` with YAML front matter, following the [agentskills.io](https://agentskills.io) standard:

```markdown
---
name: my-skill
description: What this skill does
---

# My skill

## When to use

...

## Steps

1. ...
```

The front matter `name` and `description` are parsed and shown in the
skills list. The body is injected into the system prompt.
