procyon 0.1.2

Terminal development harness for Stellar and Soroban smart contracts, driven by a language model
procyon-0.1.2 is not a library.

Procyon

A terminal harness for Stellar and Soroban development, with a language model driving the tools.

You describe what you want in plain language. Procyon gives the model tools scoped to your workspace — search and edit code, build, deploy, invoke, inspect events, look facts up over MCP — and shows the work as it happens.

The interface is a transcript, a prompt and one status line. Where you are — network, provider, model, contract — lives in that line rather than in a panel, and the network is coloured, with mainnet called out. Everything else is one keystroke away instead of permanently on screen.

Ctrl+K opens the palette; / completes commands inline as you type; ? on an empty prompt prints the shortcuts. /status is where the full picture lives.

Install

cargo install procyon

Needs Rust 1.88 or newer. The build takes a few minutes the first time. Then run it from a project directory — Procyon treats the directory you launch it from as the workspace, and every file tool is confined to it.

cd ~/my-soroban-project
procyon

Optional, and only if you want the tools that use them: the stellar CLI for account keys and contract interfaces, and Node/npx for the Caatinga tools.

Status

Early. The core loop works — streaming, tool calling, workspace confinement, context compaction, MCP, session persistence, twelve providers — with 687 tests and CI. What to expect:

  • No per-operation approval. File writes are confined to the workspace but not individually confirmed. Mainnet is a switch you set once, not a prompt. See Safety.
  • Deploy and invoke are not proven end to end. The Caatinga flags are verified against the pinned CLI version, but the paths depend on your project's own config.
  • Personas have no tools. talk_to and party_mode can reason and answer, but cannot read your files or run a build.
  • Skills and MCP servers load once per run. Adding either means restarting.
  • Session logs are plain JSONL and grow with the conversation.

Configure

A credential is all you need. Procyon reads .env from the working directory, or the environment:

ANTHROPIC_API_KEY=sk-ant-...       # or the variable for your provider

Everything else is optional, in ~/.config/procyon/config.toml:

default_network = "testnet"          # local | testnet | mainnet
theme = "dark"                       # dark | light
provider = "anthropic"               # see Providers
default_model = "claude-sonnet-5"
allow_mainnet = false                # see Safety

# MCP servers. Tools arrive namespaced <name>__<tool>.
[[mcp_servers]]
name = "raven"
url = "https://raven.stellar.org/mcp"
auth = "oauth"                       # or token_env = "PROCYON_MCP_TOKEN"

A malformed config fails before the terminal switches to raw mode, so you get a readable error rather than a scrambled screen.

Providers

Twelve, over two protocols: Anthropic's /v1/messages and the OpenAI /chat/completions dialect. Naming a vendor presets its endpoint and credential variable, so you never look either up.

anthropic · openai · deepseek · groq · openrouter · cerebras · fireworks · togetherai · xai · ollama · lmstudio · openai-compatible

The credential variable follows the vendor — DEEPSEEK_API_KEY, GROQ_API_KEY, and so on — or point api_key_env at your own. ollama and lmstudio need no credential at all, which makes them the shortest path to running Procyon with no provider account. An explicit base_url overrides any preset, for a proxy or self-hosted gateway.

Switch at runtime with /model; the conversation carries over.

One caveat for ollama. The context window a request gets is the server's, not the model's: ollama serve defaults to 4,096 tokens no matter what the weights advertise, and the OpenAI-compatible endpoint Procyon speaks over cannot ask for more — options.num_ctx is not forwarded. Worse, Ollama truncates an over-long prompt and answers 200 rather than erroring, so the failure looks like a model that ignored half its instructions. Procyon reads the real window back from /api/ps each turn, shows it as ctx 2.1k/4k on the status line and in /status, warns when the prompt no longer fits, and narrows the tool list for this provider. The fix on your side is one line:

OLLAMA_CONTEXT_LENGTH=32768 ollama serve

Prefer an 8B model over a 4B one while you are there — tool calling is where the small quantized models fail first, and a harness whose model will not call run_tests cannot check its own work.

Using it

Key
Ctrl+B / Ctrl+T / Ctrl+D Ask the agent to build / test / deploy
Ctrl+K Command palette
/ Complete a command inline
? Shortcuts, on an empty prompt
/ PageUp PageDown Scroll the chat
Alt+↑ Alt+↓ Previous / next command
Ctrl+C Quit

Scrolling is anchored — incoming messages do not yank the view while you read back.

Command
/help Commands and shortcuts
/status /project Network, account, contract, provider, MCP servers
/network <local|testnet|mainnet> Switch network
/model [set|provider|model] … Show or switch provider and model
/explain The agent narrates each step before taking it
/clear Clear the chat

Conversations are logged as they happen, so a crash loses nothing:

procyon --sessions       # list sessions for this directory
procyon --resume [id]    # resume the most recent, or a specific one

Without a terminal

One prompt, answer on stdout, no TUI — for a script, a CI step or a benchmark run. The tools, the workspace prompt and the gate are the same as an interactive turn; what is missing is anyone to answer a question, so anything that would need approval is refused unless you say otherwise when the run starts:

procyon --exec "which contracts are in this project?"
procyon --exec "fix the failing test and run the tests" --allow-changes

--allow-changes covers approvals only. It does not reach the refusals — mainnet signing and secret keys in arguments are denials, not questions.

Tools

Built in, plus whatever your MCP servers expose. The agent picks them; you do not call them directly.

  • Codelist_dir, glob, grep, read_file, write_file, edit_file
  • Projectsproject_init, project_info
  • Contractscaatinga_build, caatinga_deploy, caatinga_read, caatinga_invoke, caatinga_doctor, stellar_invoke, generate_bindings
  • Accountsaccount_create, account_list, account_balance
  • Inspectionrun_tests, get_contract_events, filter_contract_events, generate_docs
  • Delegationlist_skills, run_skill, list_personas, talk_to, party_mode, spawn_agent
  • Housekeepinglist_plugins, check_update

Caatinga

Deploys go through @caatinga/cli. The caatinga_* tools only run in a project with a caatinga.config.ts, and they deliberately do not accept:

Not a parameter Comes from
A wasm path caatinga.config.ts, via the contract name
A contract id caatinga.artifacts.json, the source of truth per network
A bindings directory or language caatinga.config.ts

That is the point. An id copied out of a deploy log is stale after the next redeploy, and bindings written somewhere the config does not expect are bindings the app never imports. Deploying without naming a contract deploys all of them in dependency order, then regenerates bindings and syncs frontend env. dry_run estimates cost without submitting.

Signing is always by stellar CLI identity alias, such as alice. A secret key, seed phrase or raw address is rejected before anything spawns, and the rejection does not echo the value back.

Prefer caatinga_read over caatinga_invoke to read a value — it simulates, so it signs nothing and costs nothing. Run caatinga_doctor when something fails for a reason that is not in the contract; most of those are environment drift.

Skills and personas

A skill is a directory with a SKILL.md — frontmatter plus a Markdown body loaded on demand, so a hundred installed skills cost two lines of description each rather than their full text. Discovered from ~/.claude/skills/, ~/.config/procyon/skills/, .procyon/skills/ and .stellar-build/skills/.

Every skill is loaded with its provenance: the version from its own frontmatter and a digest of the whole SKILL.md. run_skill puts that citation in its result, so the transcript says which file the agent was following and what it said at the time — ~/.claude/skills/ is shared with other tools that install and update packs there, so "the same skill" is not a stable claim without it.

procyon --knowledge > .procyon/knowledge.toml   # skills, digests, endpoints, procyon version

Commit that and diff it, and a change in behaviour can be traced to a change in inputs instead of being blamed on the model.

Add a customize.toml with an [agent] section and the skill becomes a persona. talk_to runs one as a sub-agent; party_mode runs several in parallel and returns each perspective:

You: party mode — should this contract be upgradeable?

🏗 Tyler (2 round trips, 4.1s):
  Upgradeability is a governance question before it is a technical one…

💻 Elliot (1 round trip, 2.8s):
  If you do, the storage layout has to be versioned from day one…

Grounding

The difference between an agent that sounds knowledgeable about Stellar and one that is comes down to whether it can check.

Procyon rebuilds the workspace context every turn — project, network, registered contracts, accounts, installed tooling, connected servers — and is an MCP client, over HTTP or stdio. The system prompt tells the model to look Stellar facts up rather than recall them, and to say so when nothing is connected.

Stellar Raven is the intended companion. Declare it with auth = "oauth" and sign in once:

procyon --authorize raven

That runs OAuth 2.1 with PKCE and stores the refresh token under ~/.local/share/procyon/oauth/ with mode 0600. A server that fails to connect is reported and skipped — an outage never stops Procyon from starting — and connections rebuild themselves after a dropped transport.

Safety

One gate, and it fails closed. Every tool call is graded allow / confirm / deny in one place, from a capability the tool declares about itself. A tool nobody classified — an MCP tool from a server you configured, a plugin from a manifest — is confirmed, not waved through. Approving a write to one file grants that file, not the tool: "always" is remembered per target, not per name.

Workspace confinement. Every file tool resolves paths against the launch directory and rejects anything outside it, including a path behind a symlinked parent pointing out of the tree.

Secret keys are refused, not discouraged. A Stellar secret seed, or a field named for one, anywhere in a tool's arguments — nested objects included — is a refusal rather than a prompt. Once passed it is in the process list and the session log for good, and signing is by CLI identity alias regardless.

A deploy cannot ship code that was never built. Caatinga deploys the wasm recorded by the last build, so caatinga_deploy refuses while source has changed since then: otherwise the deploy puts the previous version of the contract on chain and reports success.

Mainnet is off until you turn it on. Every tool that signs and submits refuses the public network unless allow_mainnet = true is set, or PROCYON_ALLOW_MAINNET=1 for one run. This is a gate, not a prompt instruction: the agent cannot grant it to itself. Those tools also require the network as an explicit argument, because a default Procyon cannot see would make "unspecified" indistinguishable from "mainnet".

Two limits: the gate matches the network by name, so a custom network pointing at the public one under another name escapes it; and it is a switch, not a per-operation confirmation.

Secrets stay out of the repo. .gitignore covers .env and .procyon/. Stellar secret keys stay in the stellar CLI keystore, referenced by name. Provider and MCP credentials are named by variable in config.tomlapi_key_env and token_env hold the name, never the value.

Long conversations. At 80% of the context window Procyon summarizes older turns into a checkpoint. The cut never separates a tool call from its result; if no safe cut exists, it refuses to compact rather than corrupt the transcript.

Development

cargo test                 # 687 tests, no network, no toolchain
cargo test -- --ignored    # 9 more, needing network or an installed toolchain
cargo clippy --all-targets -- -D warnings
cargo fmt --all --check

A tool is a Tool impl under src/tools/, registered in runtime::load_tools. Three rules: declare a capability() — the default is Write, so a read-only tool that forgets costs a prompt rather than the other way round; resolve any caller-supplied path through tools::paths::resolve_in_workspace; and never block the runtime — use tokio::fs, tokio::process or spawn_blocking, or the UI freezes while your tool runs.

There is also a benchmark: bench/README.md explains the Developer Parity design, and procyon --bench runs it. A task's acceptance script is the only thing that decides pass or fail.

src/agent holds the vocabulary a conversation is made of and knows no wire format; src/anthropic and src/openai each hold one client, one wire and one stream. ContentPart is what the session log persists, so its shape is a durability commitment — which is why a provider's request shapes are kept out of it.

License

Apache-2.0.