propio-agent
v1.0.6
Published
Multi-provider AI agent CLI supporting Ollama, Bedrock, OpenRouter, Gemini, xAI, and Cloudflare
Maintainers
Readme
propio-agent
A TypeScript CLI agent that supports multiple LLM providers (Ollama, Amazon Bedrock, OpenRouter, Gemini, and xAI) through a unified interface, with tool calling, an agentic loop, and optional Docker sandbox isolation. Install it as propio-agent, then run the propio command.
Table of Contents
- Prerequisites
- Setup
- Running the Agent
- Configuration
- Usage
- Tools
- Project Structure
- Architecture
- Development
- Sandbox Mode
- Troubleshooting
Prerequisites
- Node.js 20+ with npm
- Docker and Docker Compose (sandbox mode only)
- Ollama (Ollama provider only)
Setup
Install
Install the published CLI package:
npm install -g propio-agentOr run it ad hoc with npm:
npx propio-agent --helpFor local development in this repository:
npm installRun npm start from the directory you want to use as the workspace root. After a global install, run the propio command from any directory; it reads provider settings from ~/.propio/providers.json.
Configure providers
Create the config directory and provider file:
mkdir -p ~/.propioThen create ~/.propio/providers.json. See the Configuration section for the full schema and per-provider examples.
Configure MCP servers
External MCP servers are configured separately from providers in ~/.propio/mcp.json.
mkdir -p ~/.propioThen add MCP servers to ~/.propio/mcp.json. See the MCP section below for the v1 config shape and the Playwright example.
Migrating from an older version
If you previously used a project-local .propio/providers.json:
mkdir -p ~/.propio
cp .propio/providers.json ~/.propio/providers.json
rm -rf .propio # optional cleanupRunning the Agent
Native mode
Runs with full filesystem access — recommended for development on trusted codebases.
npm run build
npm startFor a faster dev loop without a build step:
npm run devSandbox mode
Runs the agent inside Docker, restricting filesystem access to the current working directory. Recommended when working on untrusted codebases.
# From the agent project directory
bin/propio-sandbox
# Or, after a global install, via the installed command
propio --sandboxFor system-wide access from any directory, create a symlink:
ln -s /path/to/propio/bin/propio-sandbox ~/bin/propio-sandboxThe sandbox wrapper automatically rebuilds the Docker image when the installed propio-agent package version differs from the version baked into the existing sandbox image.
When developing locally, rebuild the Docker image after same-version source changes:
docker compose buildVS Code Dev Container
- Open the project in VS Code.
- Click Reopen in Container (or use Dev Containers: Reopen in Container from the Command Palette).
- Run
npm run devinside the container.
Configuration
Agent configuration lives in ~/.propio/providers.json and is shared across all projects.
| Platform | Path |
| ---------- | -------------------------------------- |
| Unix/macOS | ~/.propio/providers.json |
| Windows | %USERPROFILE%\.propio\providers.json |
MCP server configuration lives in ~/.propio/mcp.json:
| Platform | Path |
| ---------- | -------------------------------- |
| Unix/macOS | ~/.propio/mcp.json |
| Windows | %USERPROFILE%\.propio\mcp.json |
Schema
{
"default": "<provider-name>",
"providers": [
{
"name": "string — unique identifier for this entry",
"type": "ollama | bedrock | openrouter | gemini | xai | cloudflare",
"models": [
{
"name": "Human label",
"key": "provider-model-id",
"contextWindowTokens": 128000
}
],
"defaultModel": "provider-model-id"
}
]
}Every model entry must include contextWindowTokens. Provider implementations do not keep built-in model capability tables, so adding a model to an existing provider only requires updating ~/.propio/providers.json.
Ollama
{
"name": "local-ollama",
"type": "ollama",
"host": "http://localhost:11434",
"models": [
{
"name": "Qwen3 Coder 30b",
"key": "qwen3-coder:30b",
"contextWindowTokens": 8192
},
{
"name": "Llama 3.1 8b",
"key": "llama3.1:8b",
"contextWindowTokens": 131072
}
],
"defaultModel": "qwen3-coder:30b"
}Pull a model before use:
ollama pull llama3.1:8b
ollama serveTip: Not all Ollama models support tool calling well. If you see XML-like output (
<function=...>) instead of real tool calls, switch tollama3.1:8bormistral:7b-instruct-v0.3. See Troubleshooting.
Amazon Bedrock
{
"name": "bedrock",
"type": "bedrock",
"region": "us-east-1",
"models": [
{
"name": "Claude Sonnet 4.5",
"key": "global.anthropic.claude-sonnet-4-5-20250929-v1:0",
"contextWindowTokens": 200000
}
],
"defaultModel": "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
}Important: Claude 4.x models require inference profile IDs (e.g.
global.anthropic.claude-sonnet-4-5-...). Direct model IDs will fail with an "on-demand throughput isn't supported" error. To list available profiles:aws bedrock list-inference-profiles --region us-east-1
OpenRouter
Provides access to 300+ models through a single API key.
{
"name": "openrouter",
"type": "openrouter",
"models": [
{
"name": "GPT-4o",
"key": "openai/gpt-4o",
"contextWindowTokens": 128000
},
{
"name": "DeepSeek Chat",
"key": "deepseek/deepseek-chat",
"contextWindowTokens": 128000
}
],
"defaultModel": "openai/gpt-4o",
"apiKey": "sk-or-v1-...",
"httpReferer": "https://myapp.com",
"xTitle": "My App",
"provider": {
"allowFallbacks": true,
"order": ["openai", "anthropic"],
"requireParameters": false
},
"fallbackModels": ["openai/gpt-4o-mini", "openai/gpt-4.1-mini"],
"debugEchoUpstreamBody": false
}The apiKey can also be set via the OPENROUTER_API_KEY environment variable. httpReferer and xTitle are optional and used for OpenRouter leaderboard tracking. xTitle is still the config field name, and the provider sends it as X-OpenRouter-Title.
OpenRouter-specific routing fields:
provider.allowFallbacksmaps to OpenRouterprovider.allow_fallbacksprovider.ordermaps to OpenRouterprovider.orderand should list upstream provider identifiersprovider.requireParametersmaps to OpenRouterprovider.require_parametersfallbackModelsmaps to OpenRoutermodelsdebugEchoUpstreamBodysendsdebug.echo_upstream_bodywhen CLI debug logging is enabled
When OpenRouter returns a 429 or 503 for a tool-enabled request, the provider retries once without tools, shows a visible retry status, and emits a provider_retry diagnostic. The retry only disables tools for that single request; it does not change the provider's default tool behavior.
Gemini
{
"name": "gemini",
"type": "gemini",
"models": [
{
"name": "Gemini 3.1 Pro Preview",
"key": "gemini-3.1-pro-preview",
"contextWindowTokens": 1048576
},
{
"name": "Gemini 3 Flash Preview",
"key": "gemini-3-flash-preview",
"contextWindowTokens": 1048576
},
{
"name": "Gemini 3.1 Flash-Lite Preview",
"key": "gemini-3.1-flash-lite-preview",
"contextWindowTokens": 1048576
}
],
"defaultModel": "gemini-3.1-pro-preview",
"apiKey": "AIza..."
}The apiKey can also be set via the GEMINI_API_KEY environment variable, with GOOGLE_API_KEY as a fallback. These models use Gemini's OpenAI-compatible chat-completions endpoint and support multimodal input.
xAI
{
"name": "xai",
"type": "xai",
"models": [
{
"name": "Grok 4.3",
"key": "grok-4.3",
"contextWindowTokens": 1000000
}
],
"defaultModel": "grok-4.3",
"apiKey": "xai-..."
}The apiKey can also be set via the XAI_API_KEY environment variable.
Cloudflare Workers AI
{
"name": "cloudflare",
"type": "cloudflare",
"models": [
{
"name": "Kimi K2.6",
"key": "cf/moonshotai/kimi-k2.6",
"contextWindowTokens": 262144
}
],
"defaultModel": "cf/moonshotai/kimi-k2.6",
"apiKey": "cf-...",
"accountId": "your-account-id"
}The accountId can also be set via the CLOUDFLARE_ACCOUNT_ID environment variable. The API token can be set via CLOUDFLARE_API_TOKEN, with CLOUDFLARE_AUTH_TOKEN and CLOUDFLARE_API_KEY as fallbacks. Model keys prefixed with cf/ in config are normalized to @cf/... when sent to the Cloudflare API.
MCP
propio loads MCP servers from ~/.propio/mcp.json and exposes them through /mcp. Built-in tools still live under /tools.
V1 config
Only stdio servers are supported in v1:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"],
"enabled": true
}
}
}The common headless variant adds --headless to args:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless"],
"enabled": true
}
}
}MCP commands
| Command | Description |
| ----------------- | ----------------------------------------------- |
| /mcp | Show MCP server status |
| /mcp list | List configured MCP servers |
| /mcp get <name> | Show one MCP server, including discovered tools |
| /mcp tools | List discovered MCP tools |
| /mcp reconnect | Reconnect one MCP server |
| /mcp enable | Enable one MCP server |
| /mcp disable | Disable one MCP server |
/tools continues to manage only built-in tools. MCP tools are discovered and shown through /mcp, but they are still exposed to the model as normal tools during a turn when the server is connected.
Usage
Start the agent and type messages at the prompt. Session context is maintained across turns, with structured context inspection and workspace-scoped session snapshots available from the CLI.
CLI flags
| Flag | Description |
| -------------------------- | ---------------------------------------------------- |
| --help, -h | Show CLI help |
| --version, -v | Print package version and exit |
| --sandbox | Run in Docker sandbox mode |
| --json | Read one prompt from stdin, print JSON to stdout |
| --plain | Disable ANSI colors and spinner |
| --no-interactive | Disable prompts/spinners, read one prompt from stdin |
| --show-status | Show high-level agent status updates |
| --show-reasoning-summary | Show the turn reasoning summary after each response |
| --show-trace | Enable status and reasoning summary output |
| --show-context-stats | Print compact context stats after each turn |
| --show-prompt-plan | Print a compact prompt-plan summary for each request |
| --debug-llm | Emit provider diagnostics to stderr |
| --debug-llm-file <path> | Append provider diagnostics to a file |
# One-shot non-interactive
echo "Summarize this repository." | propio --no-interactive
# Machine-readable JSON output
echo "List top-level files." | propio --json
# Persist diagnostics
propio --debug-llm-file /tmp/propio-debug.logSession commands
| Command | Description |
| -------------------- | ------------------------------------------------------ |
| /help | Show slash-command help |
| /clear | Clear session context |
| /model | Switch the current provider/model or update defaults |
| /context | Show structured context overview |
| /context prompt | Show the latest prompt plan |
| /context memory | Show rolling summary and pinned memory |
| /tools | Enable or disable tools at runtime |
| /session list | List saved session snapshots for the current workspace |
| /session load | Load the latest saved session snapshot |
| /session load <id> | Load a specific saved session snapshot |
| /exit | Save a session snapshot and quit |
Session snapshots are stored under ~/.propio/sessions/ and are scoped by workspace, so different repositories keep separate histories automatically.
Pasting image file paths (chat)
In interactive chat mode, you can drag or paste local image file paths into the prompt:
- Supported formats: PNG, JPEG, GIF, WebP (max 8 MiB per file).
- Paths may use
~/(expanded to your home directory). - The prompt shows an
[Image #N]pill; the model receives[Attached image: filename]plus the image bytes (as a data URL). - Bash mode (
!prefix): paths are inserted as literal text (no image read). - BMP is not supported — convert to PNG or JPEG first.
- Slash commands cannot include images; remove image pills before running
/help,/clear, etc.
Clipboard (macOS): In chat mode, Cmd+V with an image on the clipboard (no text) inserts an [Image #N] pill when your terminal supports bracketed paste. TIFF-only clipboards are not supported in MVP. If AppleScript is insufficient in your environment, install optional pngpaste via Homebrew (brew install pngpaste).
Large paste history: Submissions longer than 1024 characters are stored as paste:<hash> (or !paste:<hash> in bash mode) and restored from ~/.propio/paste-cache/ when you use Up/Down history or accept a reverse-history-search match. The cache is content-addressed and may retain sensitive pasted content indefinitely until you remove it manually (rm -rf ~/.propio/paste-cache/).
How images reach the model
- Prompt pills —
[Image #N]in the buffer is what you see; on submit it expands to[Attached image: filename]in the text sent to the agent, with image bytes attached separately asimageson the user turn. - Providers — Bedrock, Gemini, and Ollama send multimodal user messages (
contentplusimagesas data URLs or bytes). OpenRouter and xAI currently forward text only (imagesare accepted in the prompt and stored in sessions but not sent upstream). The live transcript shows pills (displayText), not expanded bodies or base64. - Session files — Saved sessions under
~/.propio/sessions/store expanded marker text inuserMessage.contentand attachments inuserMessage.images. Image-heavy sessions can grow large; pasted images may contain sensitive data. - One-shot / piped stdin — Non-interactive runs (
echo "hi" | propio) do not accept pasted or dropped images; use the interactive TTY prompt for image input.
Tools
The agent has a built-in tool registry and an agentic loop: it calls tools, processes results, and can chain additional tool calls before returning a final response.
Built-in tools
| Tool | Category | Default | Description |
| ------- | ---------- | -------- | ------------------------------- |
| read | Filesystem | enabled | Read file contents |
| write | Filesystem | enabled | Write content to a file |
| edit | Filesystem | enabled | Replace exact strings in a file |
| bash | Execution | enabled | Execute shell commands ⚠️ |
| grep | Search | disabled | Search file contents |
| find | Search | disabled | Find files by glob pattern |
| ls | Filesystem | disabled | List directory contents |
grep, find, and ls are disabled by default. bash is enabled by default because it is part of the core tool surface, but it can execute arbitrary commands, so use it carefully. Enable or disable tools at runtime with /tools, or programmatically:
agent.enableTool("grep");
agent.enableTool("find");
agent.enableTool("ls");The filesystem tools validate paths by rejecting malformed input and resolving relative paths from the current working directory. To confine filesystem access to the workspace, run the agent in sandbox mode.
Project Structure
propio/
├── bin/
│ └── propio-sandbox # Shell wrapper for Docker sandbox mode
├── src/
│ ├── index.ts # CLI entry point
│ ├── agent.ts # Agent class and agentic loop
│ ├── agentsMd.ts # AGENTS.md loader
│ ├── context/ # Structured context, prompt planning, memory, persistence
│ ├── diagnostics.ts # LLM diagnostics helpers
│ ├── sandboxDelegation.ts # Sandbox delegation logic
│ ├── sessions/ # Session snapshot storage and slash-command handlers
│ ├── cli/
│ │ └── args.ts # CLI argument parsing
│ ├── providers/
│ │ ├── interface.ts # LLMProvider interface
│ │ ├── types.ts # Shared message/request/response types
│ │ ├── config.ts # Provider config types
│ │ ├── configLoader.ts # Config file loading
│ │ ├── factory.ts # Provider factory
│ │ ├── ollama.ts # Ollama provider
│ │ ├── bedrock.ts # Amazon Bedrock provider
│ │ ├── openrouter.ts # OpenRouter provider
│ │ ├── gemini.ts # Gemini provider
│ │ ├── xai.ts # xAI provider
│ │ ├── cloudflare.ts # Cloudflare Workers AI provider
│ │ └── __tests__/
│ ├── tools/
│ │ ├── interface.ts # Tool interface
│ │ ├── types.ts # Tool types
│ │ ├── registry.ts # Tool registry
│ │ ├── factory.ts # Default tool registry factory
│ │ ├── fileSystem.ts # Filesystem tools
│ │ ├── search.ts # Search tools
│ │ ├── bash.ts # Bash execution tool
│ │ └── __tests__/
│ └── ui/
│ ├── banner.ts # Startup banner
│ ├── colors.ts # Color helpers
│ ├── contextInspector.ts # Structured context and prompt-plan views
│ ├── formatting.ts # Output formatting
│ ├── markdownRenderer.ts # Terminal markdown rendering
│ ├── spinner.ts # Ora spinner wrapper
│ ├── symbols.ts # UI symbols
│ ├── terminal.ts # Terminal utilities
│ └── toolMenu.ts # Interactive tool enable/disable menu
├── Dockerfile
├── docker-compose.yml
├── jest.config.js
├── tsconfig.json
└── package.jsonArchitecture
Provider abstraction
All LLM backends implement the LLMProvider interface (src/providers/interface.ts), which exposes a single streamChat() method. The Agent class communicates only through this interface, making providers interchangeable at runtime.
Shared types (src/providers/types.ts) — ChatMessage, ChatTool, ChatRequest, ChatResponse, etc. — provide a provider-agnostic layer. Each provider implementation translates between these types and its own native API format.
Provider-specific errors (ProviderError, ProviderAuthenticationError, ProviderRateLimitError, ProviderModelNotFoundError) are also defined in types.ts and are thrown consistently across providers.
Agentic loop
The Agent class (src/agent.ts) drives a tool-calling loop:
- Send user message to the active provider.
- If the provider returns tool calls, execute them via the tool registry.
- Append tool results to the conversation and repeat.
- Return the final text response to the caller.
Context management
Structured session state is managed under src/context/.
ContextManagerowns turn-based conversation statePromptBuilderassembles provider payloads with budgeting and retry levels- raw tool outputs are stored as artifacts and only inlined when needed
- older conversation can be represented by a rolling summary plus pinned memory
- session state can be serialized and restored structurally
The CLI exposes this state through /context, /context prompt, /context memory, --show-context-stats, and --show-prompt-plan.
Tool registry
src/tools/registry.ts maintains the set of available tools and their enabled/disabled state. Tools can be toggled at runtime via /tools or the agent.enableTool() / agent.disableTool() APIs.
Development
Pre-commit checks
Before committing TypeScript changes on a feature branch, run the full validation set below. A green test run alone is not enough; formatting and Fallow must also be clean on your branch delta.
npm run build
npm test
npm run format:check
npx fallow auditAll four commands should exit 0.
| Check | Command | Required outcome |
| ---------- | ---------------------- | --------------------------------------------- |
| Type-check | npm run build | Compiles with no errors |
| Tests | npm test | All suites pass |
| Formatting | npm run format:check | No Prettier drift (fix with npm run format) |
| Structure | npx fallow audit | See Fallow audit |
Run npx fallow audit after substantial edits, refactors, or agent-generated changes. It complements tests and type-checking; it does not replace them.
Fallow audit
Fallow audits files changed on your branch vs main, not the entire repository on every run. That keeps the gate focused on what you are about to commit.
Target state before commit:
✓ No issues in <N> changed filesIn practice that means:
- Exit code
0fornpx fallow audit - Complexity: no functions above threshold in the changed-file gate (summary should show
complexity 0, notcomplexity N (warn, …)) - Duplication: no clone groups reported as failing the gate (summary should not list
✗ … clone groupsunder Duplication) - Dead code:
0dead files / dead exports in the metrics line
If Fallow reports complexity or duplication failures, fix them in the changed code (extract helpers, dedupe tests, split large functions) rather than relying on a passing test suite alone.
What is out of scope for the default gate
Fallow may note audit gate excluded … inherited findings for complexity or duplication that already exists on main in files you only touched lightly. Those inherited items do not block the default pre-commit audit. To enforce the full repo instead of the branch delta:
npx fallow audit --gate allUse --gate all when doing a broader cleanup; for day-to-day feature work, the default branch-delta audit is the bar to clear before commit.
Suppressions
Prefer refactoring over // fallow-ignore-next-line comments. When a suppression is unavoidable, keep it on the specific line and document why in the PR if the reason is not obvious from the code.
Sandbox Mode
The sandbox runs the agent in Docker with filesystem isolation:
- Read-write: The current working directory is mounted at
/workspace. - Read-only:
~/.propio/is mounted at/app/.propio(provider configs and credentials). - Blocked: All other host paths.
Environment variable passthrough
bin/propio-sandbox automatically forwards these variables when set in your shell:
| Variable | Provider |
| ----------------------------------------------------------------- | ---------- |
| OLLAMA_HOST | Ollama |
| AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN | Bedrock |
| AWS_PROFILE, AWS_DEFAULT_REGION, AWS_REGION | Bedrock |
| GEMINI_API_KEY, GOOGLE_API_KEY | Gemini |
| OPENROUTER_API_KEY | OpenRouter |
| XAI_API_KEY | xAI |
Note: When using
docker compose run --rm agentdirectly, variables are not forwarded automatically — pass them with-e VAR_NAME.
Troubleshooting
Ollama tool calling: XML output instead of tool calls
Symptom:
<function=grep>
<parameter=pattern>some query</parameter>Fix: Switch to a model with better tool calling support:
ollama pull llama3.1:8b
# Update defaultModel in ~/.propio/providers.jsonModels with confirmed good tool calling: llama3.1:8b, llama3.1:70b, mistral:7b-instruct-v0.3, deepseek-coder-v2:16b, qwen2.5:14b.
OpenRouter upstream 429/503 with tools
Symptom:
OpenRouter returns 429 or 503 on the first tool-enabled turn, especially when a provider is overloaded or temporarily unavailable.
Behavior:
The provider now retries once without tools, surfaces a status message in the UI, and logs a provider_retry diagnostic when debug logging is enabled.
What to check:
- Confirm the provider config has the right
providerrouting hints andfallbackModelsif you want OpenRouter to try alternate upstreams. - If you need to debug the upstream request body, set
debugEchoUpstreamBody: trueand run with--debug-llmor--debug-llm-file <path>. - If the retry still fails, the final error will include the upstream provider name and nested error text when OpenRouter provides it.
Docker errors
| Error | Fix |
| ------------------------------------------------- | ------------------------------------------------------------- |
| docker: command not found | Install Docker Desktop |
| Cannot connect to Docker daemon | Start Docker Desktop or the Docker service |
| no such file or directory: ./docker-compose.yml | Run from the agent project directory |
| image not found | Run docker compose build |
Ollama unreachable from sandbox
The sandbox uses host.docker.internal to reach the host. On Linux this may not resolve — use your host's IP instead:
hostname -I
# Set OLLAMA_HOST=http://<your-ip>:11434 before running bin/propio-sandboxAlternatively, add --network=host to the docker run command.
AWS Bedrock auth fails in sandbox
Ensure credentials are exported in your shell before running bin/propio-sandbox:
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
bin/propio-sandboxOr run aws configure and export AWS_PROFILE.
