Skip to main content

Personal AI assistant — Aurum (gold) + Aide (helper)

Project description

AurumAide

Personal AI assistant — Aurum (Latin for gold) + Aide (helper).

Chats with any number of named backends — Gemini, Claude (Anthropic's Messages API), and/or any server that speaks the OpenAI Chat Completions API (Ollama, vLLM, llama.cpp's server, LM Studio, OpenAI itself, ...), local or cloud, each with its own model — switchable per-message, mid-conversation, by name.

Every backend can also read, write, and edit files and run shell commands on the machine aurumaide runs on — see Tools — with no confirmation prompt. This is YOLO by design, in the spirit of Mario Zechner's pi coding agent: a small, direct agent that trusts the loop instead of gating every step behind a permission dialog. Know which backend is active before you use it — see Configuring backends — since whichever one answers a query gets that access.

Prerequisites

  • Python 3.12+
  • A Google AI API key set in a .env file or as an environment variable (GOOGLE_API_KEY), needed for the default zero-config Gemini backend
  • Optionally, one or more OpenAI-compatible endpoints (URL + model name) configured in config.json — see Configuring backends

Setup

uv sync --extra dev

uv.lock is tracked so everyone (and CI) resolves the exact same dependency versions. Without uv, plain pip works too:

python -m venv .venv
source .venv/bin/activate   # Linux/macOS
.venv\Scripts\activate      # Windows

pip install -e ".[dev]"

Usage

aurumaide [options] [arguments...]

Positional arguments are joined by spaces to form the initial query. Without a query the assistant starts in interactive mode.

Options

Option Description
--one-shot Answer the query and exit (requires a query)
--plain Disable markdown rendering, even when stdout is a terminal
--disable-tools Disable the builtin read/write/edit/bash tools, and skip the default ~/.aurumaide/SYSTEM.md (see Tools)
--system-prompt-file FILE_PATH Read system instructions from FILE_PATH instead of the default ~/.aurumaide/SYSTEM.md (see System instructions)
--no-agents-md Don't load the nearest AGENTS.md (see Project instructions)
--resume SESSION_ID Continue a saved conversation by id (see Resuming sessions)
--resume-last Continue the most recently used conversation (see Resuming sessions)

Which backend answers, and what backends exist at all, is entirely config-driven — see Configuring backends and Message conventions.

Examples

# Interactive chat (config.defaultBackend, "gemini" out of the box)
aurumaide

# Ask a question and continue chatting
aurumaide What is the meaning of life

# One-shot: get an answer and exit
aurumaide --one-shot Explain the Python GIL

# Insert a file's contents in place of @<read:PATH> instead of --file
aurumaide --one-shot "Review this diff: @<read:changes.diff>"

# Send a single message to a specific configured backend (see below)
aurumaide "@local hello"

# Pure chat, no filesystem/shell access and no default SYSTEM.md
aurumaide --disable-tools What is the capital of France

# Use a project-specific persona/instructions instead of the global default
aurumaide --system-prompt-file ./project-instructions.md

# Pick up yesterday's conversation right where you left off
aurumaide --resume-last

Configuring backends

Backends live in ~/.aurumaide/config.json under backends — a map from an arbitrary name you choose to its connection details — plus defaultBackend, the name used until an @name prefix switches. A providers block holds shared credentials/endpoints by name, so several backend entries (different models, same account) don't have to repeat an apiKey/baseUrl:

{
  "defaultBackend": "gemini",
  "providers": {
    "gemini": { "apiKey": "AIza..." },
    "openai": { "apiKey": "sk-..." },
    "anthropic": { "apiKey": "sk-ant-..." },
    "local": { "baseUrl": "http://192.168.1.155:8000/v1" }
  },
  "backends": {
    "gemini": { "type": "gemini", "provider": "gemini" },
    "local": {
      "type": "openai",
      "model": "qwen3.6-27b",
      "provider": "local"
    },
    "gpt5": {
      "type": "openai",
      "model": "gpt-5",
      "provider": "openai"
    },
    "haiku": {
      "type": "anthropic",
      "model": "claude-haiku-4-5",
      "provider": "anthropic"
    },
    "groq": {
      "type": "openai",
      "model": "llama-3.3-70b-versatile",
      "baseUrl": "https://api.groq.com/openai/v1",
      "apiKey": "gsk-..."
    }
  }
}
  • type is "gemini", "openai", or "anthropic" — everything else (the backend's own name: "local", "gpt5", "haiku", "groq" above) is free-form, not reserved words. Name backends however makes sense to you: by location ("home-server"), by provider ("groq"), by model ("gpt-4o-mini") — the name is only ever used as the @name you type to reach it.
  • Any number of entries can share one providergpt5/gpt5nano both pointing at "openai" above, say — or skip provider and set baseUrl/apiKey directly on the entry itself (groq above), whichever reads clearer for your setup. An entry's own baseUrl/apiKey always wins over its provider's.
  • Any number of "openai"-type entries can coexist, each pointing at a different server and model — mix local and cloud freely.
  • A "gemini"-type entry needs no fields beyond type; a missing apiKey falls back to its provider, then a legacy top-level gemini.apiKey block in config.json if present, then the GOOGLE_API_KEY env var. model falls back the same way (minus the env var), then a hardcoded default.
  • An "openai"-type entry needs model and baseUrl at minimum (no universal default exists across providers) — baseUrl falls back to its provider, then a legacy top-level openai.baseUrl block; a missing apiKey falls back the same way, then "unused", since many local servers accept any token.
  • An "anthropic"-type entry needs model at minimum; a missing apiKey falls back to its provider, then a legacy top-level anthropic.apiKey block. Talks to Claude via the Messages API — tool calls work the same manual-loop way as the "openai" backend; extended thinking isn't requested (same scope as the "openai" backend, which also never surfaces a 💭 Thinking block).
  • If config.json predates this section entirely, it's treated as {"defaultBackend": "gemini", "backends": {"gemini": {"type": "gemini"}}} — existing installs keep working unmodified.
  • config.defaultBackend's entry is validated at startup, same as any CLI argument error, so a typo or missing field fails immediately with a clear message. Any other backend, reachable only via @name, is checked lazily the first time it's actually selected — an unconfigured @name prints a warning and leaves you on your current backend rather than crashing the session.

Tools

Every backend — Gemini, Claude, or any OpenAI-compatible endpoint — gets the same four builtin tools, always on unless you pass --disable-tools:

Tool Does
read Read a file. Text files come back cat -n-style (1-indexed line numbers), first 2000 lines by default — pass offset/limit for more. Recognized image files (jpg, png, gif, webp) come back as an attachment the model can actually see, not as text.
write Write content to a file, creating parent directories as needed. Creates the file if missing, overwrites if it exists.
edit Replace one exact, unique occurrence of oldText with newText in a file. Errors (surfaced to the model, not raised) if the text isn't found or matches more than once — add more surrounding context to disambiguate.
bash Run a shell command and return stdout/stderr/exit code. Runs in the current working directory by default, or in workingDirectory if given (must already exist — same error as a failed cd). Accepts an optional timeout in seconds. Picks its shell once at startup — on Windows, Git Bash if installed, else pwsh, else Windows PowerShell, else cmd.exe; any other OS uses its default shell. The tool description tells the model which shell it's talking to, so it emits commands in that shell's own syntax. workingDirectory is set at the OS process level, not via the shell's own cd — sidesteps cmd.exe's cross-drive cd quirk (plain cd silently no-ops across drive letters; only cd /d follows them) entirely. A command whose stdout ends with one or more @<image:PATH> tags gets those images attached to the result instead of left as text — see below.

Attaching images from a bash command's output

Any command bash runs can hand images back the same way a user's own query does: end its stdout with one or more @<image:PATH> tags.

Fixed the login bug. @<image:/tmp/before.png> @<image:/tmp/after.png>

Those tags are stripped from the result and the images actually attached to the model's next turn — not left as inert text it can't see. Useful for a script a skill's instructions point the model at (a ticket-fetching skill that downloads screenshot attachments, say), but works for any command, skill-directed or not. A tag pointing at a missing file or unsupported extension doesn't fail the call — it's dropped and reported back as a Warning: ... line instead, so the model can see what went wrong.

Seeing tool calls (and thinking) as they happen

Every step of a turn streams to the terminal as it happens, each in its own clearly-labeled block — nothing is hidden or buffered until the end:

--- 🔧 Tool call [gemini] ---
Calling bash({'command': 'echo hi'})
----------------

--- ✅ Tool result [gemini] ---
hi

[exit code: 0]
----------------

--- 👇 Response [gemini] ---
The command printed "hi".
----------------

If a model surfaces its own reasoning (Gemini's thought-flagged output, where the model/config supports it), that shows up the same way under a 💭 Thinking block. Only the 👇 Response block's text is ever sent back to the model as conversation history — thinking and tool-call/result blocks are shown for visibility only and never replayed as if the model had said them.

There is no confirmation prompt. A model that decides to call write, edit, or bash does so immediately — it can overwrite files and run arbitrary shell commands on the machine aurumaide runs on without asking first. That's the deliberate trade-off described in the intro: a small, fast, unattended agent instead of one that stops to ask permission at every step (see pi). The risk is scoped to whichever backend is currently active — check config.backends (below) before pointing aurumaide at a query you don't want acted on autonomously, and reach for --disable-tools for pure Q&A sessions where no tool access is wanted at all.

--disable-tools removes all four tools from every backend for that run and skips loading the default ~/.aurumaide/SYSTEM.md (see below) — that file's instructions assume the tools it just removed exist, so loading it anyway would just confuse the model. An explicit --system-prompt-file still applies even with --disable-tools, since a custom prompt may have nothing to do with tool use.

Skills

Drop a Claude Code-style skill anywhere aurumaide looks and every backend can load it via a skill tool, the same way the four builtin tools work. A skill is a directory containing a SKILL.md:

---
name: haiku-writer
description: Write a haiku about a given topic. Use when the user asks for a haiku or poem.
---

Write a haiku (5-7-5 syllables) about: $ARGUMENTS

Always sign it "- aurum, via haiku-writer skill" at the end.

name and description are required — the description is what the model sees to decide when this skill is relevant, so make it specific about what the skill does and when to reach for it. Everything after the frontmatter is the skill's body: instructions the model follows once it loads the skill. $ARGUMENTS in the body is replaced with whatever the model passes as the tool's args — if the body doesn't mention $ARGUMENTS at all, any args given are appended as a note instead.

Skills are discovered from:

  • ~/.aurumaide/skills/*/SKILL.md — available in every project.
  • .aurumaide/skills/*/SKILL.md under the project root (the nearest .git root above the current directory, or the current directory itself if none is found) — this project only.

A skill defined in both places resolves to the project-level one. A malformed SKILL.md (missing frontmatter, missing name/description, invalid YAML) is skipped rather than breaking startup. Skills load automatically whenever tools are enabled — --disable-tools disables them along with everything else, since a skill can only be reached through the tool call that loads it.

System instructions

If ~/.aurumaide/SYSTEM.md exists, its contents are loaded once at startup and sent to every configured backend as its system instruction — free-form prose, not JSON, so it lives outside config.json. A missing or whitespace-only file means no system instruction at all; nothing breaks.

Use --system-prompt-file FILE_PATH to read system instructions from a different file instead — a project-specific persona, a narrower tool-use policy, whatever fits the session. Unlike the default file, a missing --system-prompt-file path is a hard error (it was asked for by name, so silently ignoring a typo would hide it rather than surface it).

--disable-tools skips the default ~/.aurumaide/SYSTEM.md specifically — an explicit --system-prompt-file is still honored alongside it.

Nothing creates or seeds this file for you — save your own at ~/.aurumaide/SYSTEM.md to enable it. A starting point that describes the builtin tools (see Tools) to the model:

You are an expert coding assistant. You help users with coding tasks by
reading files, executing commands, editing code, and writing new files.

Available tools:
- read: Read file contents
- write: Create or overwrite files
- edit: Make surgical edits to files (old text must match exactly)
- bash: Execute shell commands

Guidelines:
- Use bash for file operations like ls, grep, find
- Use read to examine files before editing
- Use edit for precise changes, write only for new files or full rewrites
- Be concise in your responses

Project instructions (AGENTS.md)

On top of your personal SYSTEM.md, aurumaide also honors AGENTS.md — the open, tool-agnostic convention for giving an agent project-specific context (build steps, conventions, house rules). It's plain Markdown with no required schema; write whatever headings and prose you like.

Starting from the current directory and walking up, aurumaide loads the first AGENTS.md it finds and appends it to the system instruction as project context. This is the standard's "closest one takes precedence" rule in its simplest form: the nearest file wins outright and any further-up ones are ignored (a subfolder's AGENTS.md fully replaces the repo root's while you're working there — it doesn't merge with it).

  • Not tied to git — discovery is anchored on your working directory, so it works in a repo, a subfolder of one, or a plain directory that isn't version-controlled at all. Drop a ~/AGENTS.md to act as a personal catch-all whenever nothing closer exists.
  • Layered, not replacing — your SYSTEM.md persona comes first, the project's AGENTS.md after it; a direct request in chat still overrides both.
  • Kept under --disable-tools — unlike the tool-assuming default SYSTEM.md, AGENTS.md is general project context and stays loaded. Pass --no-agents-md to skip it for a run.
  • Empty/whitespace-only files are skipped (the walk continues upward).

Message conventions

Several @-prefixed constructs can appear in any query — the initial one, or any follow-up typed at the interactive prompt:

@name — switch backend mid-session

Prefix a message with the name of any backend from config.backends to send that message (and only that one) to it, and make it the backend for every message after until switched again:

❓ [gemini] Your question: what's the capital of France?
Paris.

❓ [gemini] Your question: @local and what's 17 * 24?
408.

❓ [local] Your question: was my first question about geography or math?
Geography — you asked about the capital of France.

The full conversation's text carries over every switch, so any backend can answer questions about what was said earlier, even by another one. Only the plain text of each turn carries over — provider-specific extras like Gemini's Search grounding or OpenAI's structured tool calls don't have a common format across APIs, so they aren't replayed after a switch.

Naming a backend that doesn't exist, or one whose config is broken (e.g. missing baseUrl), prints a short warning and leaves you on the backend you were already using — it won't crash the session.

Typing the bare name with nothing else (e.g. just @local) switches the active backend without sending a message.

@<image:PATH> — attach an image

Append @<image:PATH> to the end of a message to attach that image file (png, jpeg, webp, heic, heif) to the question:

❓ [gemini] Your question: what color is this flower? @<image:C:\photos\iris.png>
Purple.

Stack multiple tags to attach more than one image to the same question:

❓ [gemini] Your question: which one is bigger? @<image:cat.png> @<image:dog.png>
The dog.

Duplicate paths (even spelled differently, like cat.png and ./cat.png) are collapsed to a single attachment.

PATH accepts either slash direction (C:\photos\iris.png or photos/iris.png), so it works the same typed on Windows or POSIX. The tag(s) must be the last thing in the message — text after the closing > of the final tag isn't part of it.

Combine both tags to attach an image to a message sent to a specific backend:

aurumaide --one-shot "@local what is this? @<image:cat.png>"

A missing file or unsupported extension prints a warning and skips that turn rather than crashing or sending the question without the image.

@<read:PATH> — insert a file's contents

Write @<read:PATH> anywhere in a message to replace it in place with that file's contents — this is how you hand aurumaide a file's worth of context without the removed --file flag:

❓ [gemini] Your question: review this: @<read:diff.patch>

@<#! shell command #> — insert a command's output

Write @<#! shell command #> anywhere in a message to replace it in place with that command's stdout only:

❓ [gemini] Your question: is @<#! git branch --show-current #> the right branch to release from?

@<#!! shell command #> — run a command and show its output directly

Two exclamation marks instead of one runs the command and prints its stdout immediately as its own response block, rather than feeding it to a backend at all. It's shown preformatted (a markdown code fence, in the default markdown-rendering terminal mode) so column-aligned output like dir/ls survives instead of getting reflowed into a run-on line of prose:

❓ [gemini] Your question: @<#!! git status #>

--- 👇 Response [shell] ---
On branch main
nothing to commit, working tree clean
----------------

If the tag is the entire message, nothing gets sent to a backend — you get the command's output and nothing else. Combine it with other text in the same message and everything besides the tag itself still goes to the model as usual.

Both tags run in the same shell the bash tool picked at startup (Git Bash if installed on Windows, else pwsh, else Windows PowerShell, else cmd.exe; whatever the default shell is elsewhere) — @<#!...#>/@<#!!...#> and a model's own bash tool calls always land in the same shell on the same machine, so quoting/piping/control-flow only need to match one syntax. Run @<#! echo $0 #> (POSIX shells) or @<#! echo %COMSPEC% #> (cmd.exe) to see which one that is on your machine.

Both shell tags capture stdout only — non-empty stderr is treated as an error, even from a command that exits 0. This is deliberate: stderr output is usually a warning or diagnostic, not the actual result, and it shouldn't get silently inserted into a prompt or displayed as if it were. If a command legitimately needs its stderr captured too, redirect it yourself with the standard shell syntax:

❓ [gemini] Your question: @<#! mycommand 2>&1 #>

Error handling for @<read:...>/@<#!...#>/@<#!!...#>

A missing file, a command that writes to stderr, a command that exits non-zero, or a command that times out (30s) stops processing immediately: aurumaide prints the error plus your original, unedited prompt, and sends nothing to any backend. Fix the tag and resend. Commands already run before the failing tag keep whatever effect they had — there's no undo for a shell command that already ran.

Interactive input

Interactive mode (no --one-shot) uses a history-aware, auto-suggesting line editor when stdin is a real terminal, backed by prompt_toolkit (the same library behind IPython and ptpython):

  • Persistent history — every line you send is saved to ~/.aurumaide/input_history and recalled with the Up/Down arrows, across separate aurumaide runs, not just within one session. A query given on the command line (aurumaide what is the meaning of life, including with --one-shot) is recorded the same way, so it's still there to recall next time even if that run never touched the interactive prompt.
  • Auto-suggest — as you type, a greyed-out suggestion completes the rest of a matching line from history (fish-shell style); accept it with → or End.
  • Paste-safe — pasting multi-line text lands as one input rather than submitting a message per line.
  • Multi-line composition — plain Enter still submits, same as always; press Escape then Enter to insert a literal newline and keep composing before sending.

--plain disables all of this and falls back to plain input() — same flag that disables markdown rendering on the output side. It's also the automatic fallback whenever stdin isn't a real terminal (piped input, --one-shot, scripts), and whenever the terminal claims to be interactive but isn't one prompt_toolkit can actually drive (Git Bash/MinTTY on Windows is a real example of this) — aurumaide falls back to plain input there rather than crashing.

Resuming sessions

Every conversation is saved as it happens, so you can pick it up in a later run. Each turn is appended to ~/.aurumaide/sessions/<id>.jsonl, where <id> is a 12-character session hash — the filenames in that directory are exactly the ids you pass to --resume (the hash is also line 1 of each saved markdown log). No flag is needed to save — it's automatic (including for --one-shot runs); the flags are for coming back:

# Continue the most recent conversation (what you'll usually want)
aurumaide --resume-last

# Continue a specific saved conversation by its id
aurumaide --resume 3f9a1c2b7e04

# Resume and send a message in the same breath
aurumaide --resume-last "ok, now add error handling"

--resume-last continues whichever session you talked to most recently; --resume <id> targets a specific one (it errors if the id doesn't exist, and --resume-last errors if there are no saved sessions). Either can be combined with a query and with --one-shot. The two flags are mutually exclusive. New turns append to the same file, so a resumed session keeps growing rather than forking a new transcript.

What's restored — and what isn't. Resuming replays the conversation text (your questions and the assistant's answers), which is what the next reply is conditioned on. It deliberately does not restore prior tool calls, tool results, "thinking" output, or attached images — only plain text is persisted, the same way only plain text carries across an @name backend switch mid-session. So a resumed session has the conversational memory of what was discussed, not a replay of every bash/edit the model previously ran.

These per-session transcripts are separate from the human-readable per-turn markdown logs and the input-line history (~/.aurumaide/input_history) — different files, different purposes.

MCP server

Separately from the chat CLI, aurumaide can run as an MCP server exposing Gemini as a tool to other MCP clients (Claude Desktop, another Claude Code session, etc.) — a google_ai(query) tool that answers a question via the Gemini API and logs the exchange the same way the chat CLI does:

python -m aurumaide.google.mcp

Runs over stdio, so point an MCP client's config at that command rather than running it directly. It's independent of config.backends — it always talks to Gemini directly via google-genai, using the same GOOGLE_API_KEY/config.json resolution as the "gemini" backend type.

Development

# Run tests
uv run pytest

# Lint & format
uv run ruff check .
uv run ruff format .

# Type checking
uv run mypy src

(Drop uv run if you installed with plain pip into an activated venv.)

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aurumaide-0.3.0.tar.gz (146.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aurumaide-0.3.0-py3-none-any.whl (75.3 kB view details)

Uploaded Python 3

File details

Details for the file aurumaide-0.3.0.tar.gz.

File metadata

  • Download URL: aurumaide-0.3.0.tar.gz
  • Upload date:
  • Size: 146.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for aurumaide-0.3.0.tar.gz
Algorithm Hash digest
SHA256 cca73424677a4fa2de36a7fa696a50c3a41b23ba8eb062d512c46cb1ee5011b7
MD5 c5f9f24449e6d9821ff9f6769c10020b
BLAKE2b-256 bc89478e25a44ab85aa87c2865d722604538fcc283a7421e3f4b15adee9e462a

See more details on using hashes here.

File details

Details for the file aurumaide-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: aurumaide-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 75.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for aurumaide-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d2982ba8297e9ce46696325d54b263026093c54017c2385eabfadb745e498f2b
MD5 6bdfc53522c8306da8c65ab4b23f4bd8
BLAKE2b-256 64e90be4e76fcd86eeb4b030bd1a6945d2fbf37fb7dadf94ff53be4ea4f6507b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page