procyon 0.1.1

Terminal development harness for Stellar and Soroban smart contracts, driven by a language model
# 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.

<img src="docs/screens/demo.gif" alt="Procyon's TUI: the boot banner, the command palette, and /status — network, account, contract, provider, model and MCP servers" width="706">

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

```bash
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.

```bash
cd ~/my-soroban-project
procyon
```

Optional, and only if you want the tools that use them: the
[`stellar` CLI](https://developers.stellar.org/docs/tools/developer-tools/cli/stellar-cli) for
account keys and contract interfaces, and Node/`npx` for the [Caatinga](#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]#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:

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

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

```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.

## 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:

```bash
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:

```bash
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.

- **Code**`list_dir`, `glob`, `grep`, `read_file`, `write_file`, `edit_file`
- **Projects**`project_init`, `project_info`
- **Contracts**`caatinga_build`, `caatinga_deploy`, `caatinga_read`, `caatinga_invoke`,
  `caatinga_doctor`, `stellar_invoke`, `generate_bindings`
- **Accounts**`account_create`, `account_list`, `account_balance`
- **Inspection**`run_tests`, `get_contract_events`, `filter_contract_events`, `generate_docs`
- **Delegation**`list_skills`, `run_skill`, `list_personas`, `talk_to`, `party_mode`,
  `spawn_agent`
- **Housekeeping**`list_plugins`, `check_update`

## Caatinga

Deploys go through [`@caatinga/cli`](https://www.npmjs.com/package/@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.

```bash
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](https://raven.stellar.org) is the intended companion. Declare it with
`auth = "oauth"` and sign in once:

```bash
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.toml` — `api_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

```bash
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](LICENSE).