VirtuAI CLI
Run VirtuAI agents from your terminal — chat interactively in a Claude-Code-style TUI, or pipe them into shell scripts. The agent's bash/file tools run on your machine, so files appear in your filesystem and commands hit your real shell.
Installation
pip install virtuai-cli
Or with pipx (recommended for CLI tools — handles PATH automatically):
pipx install virtuai-cli
For local model inference (running agents against Ollama or oMLX on your machine — see below), install the local extra:
pip install 'virtuai-cli[local]' # or: pipx install 'virtuai-cli[local]'
Requires Python 3.11+.
Quick start
1. Pair this machine with a workspace
Open the VirtuAI portal → Settings → CLI → generate a pairing code, then:
virtuai pair <CODE>
The pairing is one-time; the resulting token is stored in your system keychain.
Pairing then sets up the command check (also on the first virtuai chat, or any time with virtuai guard setup). Before the agent runs a command on this machine, a small local model — tev1:0.8b (811 MB) on Ollama ≥ 0.35 — says what the command does:
- reads, or runs the project's tests/build → it runs;
- deletes, installs, writes, uses the network, reads secrets, needs root → you are shown the command and decide. If you don't authorize it, it is left in the input as
!commandfor you to run yourself; its output goes back to the agent.
Commands that only read (ls, cat, git status, rg…) run without the model; inline code (python -c, bash -c, | sh), eval and sudo are always asked. Setup uses llmfit to decide whether the model fits this machine, then offers to install or start Ollama and download the model. If it does not fit, or you decline, there is no command check: what the agent runs is your responsibility, you are asked to accept that, and the chat shows it.
virtuai guard status # on, rules only, or off
virtuai guard test "rm -rf dist" # what it would do: run or ask
2. Chat with an agent
virtuai chat
Opens an interactive TUI. Pick an agent (skipped if your workspace has only one), type a message, watch the response stream in. The agent's tool calls — bash, file reads, file writes — execute on this machine, in the current directory.
Plan mode (Shift+Tab or /plan): the same agent investigates and proposes a plan without changing anything — it may read and run commands that only look; writes, edits and tools that change something are refused by the server. When the plan is ready, choose to carry it out (plan mode turns off) or keep planning.
Every workspace also includes a built-in Plan agent, a separate agent with its own conversation: /agent Plan or virtuai chat --agent Plan. List agents with /agents.
3. Use it from a shell script
virtuai ask "summarize the changes in this branch"
git diff | virtuai ask "what does this change?"
virtuai ask --json "find any bugs" > events.jsonl
ask is non-interactive: it prints the response to stdout, then exits.
Local model inference
Agents can run inference on your own machine — with Ollama, or oMLX on Apple Silicon — instead of a cloud provider: for cost, data locality, or open models. Only the model calls go to the machine; the agent loop, tools, knowledge bases and chat history stay on the server. It works from every channel — web chat, Slack, Telegram, WhatsApp, Google Chat, OpenCode — not only the TUI, as long as a runner that can serve the model is connected.
Set up
- Install the CLI with the local extra (pulls
langchain-ollamaandlangchain-openaifor oMLX;llmfitcomes with the base install):pip install 'virtuai-cli[local]'
- Install Ollama 0.40 or newer and pull a validated model:
ollama pull qwen3:8b
Ollama before 0.40 held the same models in two to three times the memory;virtuai runwarns when it finds one. - In the VirtuAI portal, set the agent's Provider to Local (Ollama) (or Local (oMLX)), enter the model, and pick a cloud fallback model.
- Keep a runner connected:
virtuai run, orvirtuai chat/virtuai ask, which start their own.
Which models
Validated — every one of 10 tool-calling tasks right, 8k context, 16 GB Apple M3 with Ollama 0.40:
| Model | Memory | Median answer |
|---|---|---|
qwen3:8b |
6.6 GB | 2.2 s |
qwen2.5:7b |
5.0 GB | 3.5 s |
qwen2.5-coder:7b |
~5 GB | 4.4 s |
qwen2.5:14b |
10.3 GB | 6.0 s |
Other models run, but are not vouched for. qwen3:4b is accurate but slow (it reasons before every call). Gemma 3 is never used: Ollama cannot call tools with it, so the cloud fallback answers.
Whose turns this machine answers
virtuai run # your own turns only (the default)
virtuai run --serve-models workspace # anyone's in the workspace: a shared Mac mini or GPU box
virtuai run --serve-models none # tools only, no model turns
Each model call goes to the runner of the person the turn is for; if they have none, to a runner started with --serve-models workspace; if there is none either, the agent's cloud fallback answers that call and every channel shows "Modelo local no disponible: responde …". A runner that fails before it answers (Ollama stopped, the model unloaded) costs the call a cloud answer, not an error.
Upgrading from 1.0.2: with a current server, an older runner serves only the person who paired it. A machine that serves a whole team needs 1.0.3 and
--serve-models workspace.
What the runner tells the server
At connect and then every minute: the models it has, the Ollama version, what is loaded and how much memory it really took. With llmfit (part of the base install) it also estimates which pulled models fit this machine, so the server does not send a model to a runner that cannot hold it until it has actually run there. A model that spills to the CPU gets half the context on its next call on that machine (down to 4k).
The context sent to Ollama is 16k by default (set on the server). A model that does not fit at that context — qwen3:14b on a 16 GB Mac spills 7% to the CPU — is capped at half on that machine after its first call, and the server is told in the same reply: from then on it sends prompts that fit, compacting the conversation when they would not, instead of letting Ollama cut the start of the prompt. OLLAMA_NUM_CTX sets a lower context on a machine with little memory. For qwen3 the runner turns Ollama's thinking mode off: left on, it reasoned for minutes before every tool call.
oMLX
The runner finds oMLX through its own settings (~/.omlx/settings.json: port and API key) or VIRTUAI_OMLX_URL / OMLX_API_KEY; the key never leaves the machine. Measured with Qwen3-8B-4bit: 10/10, 2.5 s per answer — on par with Ollama for one person. Its case is a shared host serving several people at once; raise VIRTUAI_MAX_CONCURRENT_MODEL there, since the runner otherwise sends one generation at a time.
Environment variables
Two naming conventions, and the difference matters when sizing a shared
runner: MAX_CONCURRENT_<thing> bounds operations in flight and makes the
extra ones wait; MAX_<thing>S bounds resident objects and makes the
extra ones fail. They are separate knobs on purpose — coupling them would
mean "allow more commands at once" silently launched more browsers.
| Variable | Default | What it does |
|---|---|---|
VIRTUAI_ISOLATE_SESSIONS |
off | Give each conversation its own directory under the workdir, and each person their own browser. Turn it on when one runner serves several people. Also set by --isolate-sessions. |
VIRTUAI_MAX_NAMESPACES |
5 |
Browsers held at once — one per person. Each is a real headful Chrome (roughly 0.4–1 GB resident), so size this to the machine's memory: an OOM-killed Chrome is a worse failure than a clean refusal. Only applies with isolation on. Also set by --max-namespaces. |
VIRTUAI_MAX_CONCURRENT_WORK |
4 |
Commands running at once. Each conversation is already capped at one by its own queue, so in practice this is how many conversations may be executing at the same instant. |
VIRTUAI_MAX_CONCURRENT_MODEL |
1 |
Local model generations at once. Ollama does one well; its own parallelism lives in OLLAMA_NUM_PARALLEL on your machine, which the server cannot see. Raise it for oMLX on a shared host, which batches several at once. |
VIRTUAI_MAX_QUEUED_FRAMES |
64 |
Requests waiting for a slot. Past this the runner answers "busy" rather than queueing without bound. |
VIRTUAI_SESSION_IDLE_TIMEOUT |
off (0) |
Seconds before an idle browser stops holding its slot. Off by default: a slot normally frees only when its browser closes or the runner restarts. |
VIRTUAI_SANDBOX_OWNER |
unset | The one person this runner serves, reported to the server so a browser action reaches their browser. Leave it unset on a laptop or a container shared by a team: the server then routes to it for anyone, which is the behaviour that existed before per-person sandboxes. Set it when a workspace has a sandbox per person, or actions land on whichever runner connected last. |
VIRTUAI_SERVE_MODELS |
own |
Whose local-model turns this runner answers: own, workspace or none. Also set by --serve-models. |
OLLAMA_BASE_URL |
http://localhost:11434 |
Where to reach the local Ollama. |
OLLAMA_NUM_CTX |
unset | The context to ask Ollama for, instead of the server's (16k). Lower it on a machine with little memory. |
VIRTUAI_OMLX_URL |
from ~/.omlx/settings.json, else http://127.0.0.1:8000 |
Where to reach oMLX. |
OMLX_API_KEY |
from ~/.omlx/settings.json |
oMLX's API key. Stays on this machine. |
Server-side, VIRTUAI_ENV_WAIT_SECONDS (default 45) is how long the server
holds a message waiting for a runner to reconnect before telling the user the
environment is not responding.
Commands
| Command | Description |
|---|---|
virtuai chat |
Open the interactive chat TUI |
virtuai ask "..." |
One-shot prompt for scripts (reads stdin if piped) |
virtuai opencode |
Merge this workspace's agents into the local OpenCode TUI config |
virtuai pair <code> |
Pair this machine with a workspace |
virtuai unpair |
Revoke the current pairing |
virtuai run |
Headless runner — let the web app and channels use this machine's tools and local models without opening the TUI (--serve-models own|workspace|none) |
virtuai status |
Show pairing and connection status |
virtuai logs |
Show recent commands the agent executed locally |
virtuai login |
Authenticate with your VirtuAI account (browser flow) |
virtuai config get/set |
Read or set local CLI options (e.g. server_url) |
Inside virtuai chat
The TUI streams tokens, renders tool calls as live cards (running → complete), tracks the agent's todo list, and shows a randomized "✻ Pondering…" indicator while the agent is composing.
Slash commands (type / to see the filtered list, Tab to autocomplete):
| Slash command | What it does |
|---|---|
/help |
Show this list |
/clear, /new |
Drop the current session and start fresh |
/history |
List your recent conversations with this agent |
/load <id> |
Reopen a past conversation by session_id |
/plan |
Switch to the built-in Plan agent |
/agents |
List agents in this workspace |
/agent <name> |
Switch agent without exiting (starts a new conversation) |
/models |
List models available for this agent |
/model <id> |
Switch the model for subsequent messages |
/exit, /quit |
Close the TUI |
Key bindings:
- Esc — cancel the in-flight response
- Ctrl+L — start a new conversation
- Ctrl+C — quit
- Tab — autocomplete the current
/command
virtuai ask for scripting
# Direct prompt
virtuai ask "what's in this folder?"
# Pipe stdin in (concatenated with the argument if both given)
git log --oneline -20 | virtuai ask "summarize these commits"
# Continue a previous session (use --print-session to surface the id)
virtuai ask --session sess-aaa "and apply that fix" --print-session
# Machine-readable: every event as one JSON line
virtuai ask --json "find any bugs" > events.jsonl
# Just the final answer, no streaming (good for capturing into variables)
ANSWER=$(virtuai ask -q "give me the current time")
# Pick a different agent or model
virtuai ask --agent code-review --model claude-opus "review main.py"
# Skip the local runner for a faster startup when no tools are needed
virtuai ask --no-tools "tell me a joke"
Output streams:
stream(default) — tokens go to stdout, tool indicators to stderr (so> out.txtcaptures only the answer)--quiet/-q— accumulate, print only the final assistant text at the end--json— every SSE event as one JSONL line on stdout (parseable)
Exit codes: 0 success · 1 stream error · 2 bad args / agent not found.
Common options
--server <url> Override the saved server URL
--workdir <path> Working directory for tool execution (default: current dir)
--agent <id|name> Pick a specific agent
--model <id> Override the agent's default model
Security model
Read this section before pointing an agent at anything you care about.
The CLI runs the agent's shell commands as your user account, with your full filesystem permissions. There is no sandbox. The agent can do anything you can do at the shell. The guardrails below are best described as speed bumps, not walls.
What is enforced
- Auth: CLI token is stored in your OS keychain (Keychain on macOS, libsecret on Linux, Credential Manager on Windows). All traffic is TLS (WSS + HTTPS) with the
certifiCA bundle. - File-tool path jail: the deepagents file tools —
write_file,edit_file,read_file— route throughupload_files/download_files, which reject absolute paths outside the workdir. This part works. cdjail in bash: the executor wraps every shell command in a bash subshell with a customcdfunction that refuses to leave the workdir. Catches the trivialcd ..case.- Denylist: a regex match on the command string blocks
sudo,rm -rf /, fork bombs,mkfs,dd of=/dev/.... Easy to bypass with aliasing or encoding; mostly there to stop drive-by accidents. - Audit log: every executed command + exit code goes to
~/.virtuai/audit.logwith a timestamp. - Foreground-only: the agent only has access while
virtuai chat,virtuai ask, orvirtuai runis running — no background daemon.
What is NOT enforced
- Shell commands can use absolute paths anywhere. The
cdjail only interceptscd.cat /etc/passwd,ls /Users/you/Documents,rm /tmp/whatever— none of these touchcd, so none are blocked. The agent can read, write, and delete anything you can. - The
cdjail itself is bash-only.sh -c '...',python -c "os.chdir('/')",node -e "..."all bypass it. - Command substitution, eval, env vars, aliases all defeat the denylist.
$(echo s)$(echo udo) ...looks nothing likesudoto a regex. - Network egress is not restricted. The agent can
curl/wgetanywhere, exfiltrate to any host.
If you need real isolation
The only honest options:
- Run the CLI inside a container with the workdir bind-mounted and nothing else:
docker run --rm -it -v "$PWD:/work" -w /work python:3.12 bash -c "pip install virtuai-cli && virtuai pair <code> && virtuai chat"
The container has access only to what you mount. - OS-level sandboxing: wrap
virtuai runinsandbox-execon macOS or run inside alandlock/bubblewrapjail on Linux. - Switch the agent's backend to the E2B remote sandbox in the portal. The agent then runs against a disposable VM, not your laptop. The CLI runner becomes optional (you'd only need it for "interactive" workflows where local files matter).
Use the CLI runner the same way you'd use a powerful shell session you don't fully trust. Point it at a project directory, never at ~, and don't run unattended on a machine with credentials lying around.
Programmatic use
The async building blocks the TUI is built on are importable directly:
import asyncio
from virtuai_cli import config as cfg
from virtuai_cli.chat.sse import stream_chat
async def main():
token = cfg.load_token(cfg._KEYRING_CLI_TOKEN_KEY)
server = cfg.get_server_url()
async for event in stream_chat(server, token, "your-agent-id", "Hello"):
if event.get("type") == "token":
print(event["content"], end="", flush=True)
asyncio.run(main())
A formal SDK (typed client class, sync wrappers) is on the roadmap.
Links
- Homepage: https://imvirtuai.com
- PyPI: https://pypi.org/project/virtuai-cli/
- Issues / docs: contact your workspace admin
Metadata
Release files for virtuai-cli 1.0.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| virtuai_cli-1.0.8.tar.gz | 180.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| virtuai_cli-1.0.8-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 338.7 kB
Release files / virtuai_cli-1.0.8.tar.gz
| Download URL | virtuai_cli-1.0.8.tar.gz |
|---|---|
| Size | 180.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b9d1e4fccc344b22c8e3dbd08993c92b7e390ab8187d476f4c6887cb51ba4291
|
|
BLAKE2b-256 checksum How to use checksums |
c847e62bb99da23879146d5ea313dbbeb002686d66c4c18daac6ac25d07d0f93
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.3
|
Release files / virtuai_cli-1.0.8-py3-none-any.whl
| Download URL | virtuai_cli-1.0.8-py3-none-any.whl |
|---|---|
| Size | 157.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dbb4b6139e7739752111983cb6bf61d5e4015aaf48562338fddf44ac60a353a7
|
|
BLAKE2b-256 checksum How to use checksums |
0cd12d82ec741d9d829c973d719ddae058d617985a6e28e12d1785e53b0769c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.3
|