Skip to content

Latest commit

 

History

23 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

moon-code

CI

A terminal AI coding agent written in MoonBit, styled after Claude Code.

Streams from the Anthropic Messages API or any OpenAI-compatible endpoint, runs six tools against your working directory (read, write, edit, bash, grep, ls), and renders the whole thing append-only to your scrollback so native selection and scrollback keep working.

Build

curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
source ~/.bashrc

moon build --target native --release
./_build/native/release/build/cmd/main/main.exe

The result is a single ELF that links only libc; TLS is statically vendored.

Run

export ANTHROPIC_API_KEY=sk-ant-...
moon-code

/help lists the slash commands, /clear resets the conversation, /status shows the resolved setup, /exit quits. Esc interrupts a streaming response. Mutating tools (write, edit, bash) ask before they run, showing the diff for a proposed file change; option 3 lets you deny with a note the model sees.

A prompt given as an argument runs one turn and exits, which is what makes the binary scriptable:

moon-code 'read moon.mod and tell me the module name'
moon-code --model claude-opus-4-1 'summarise the diff'

When stdin is not a terminal there is nobody to prompt, so file edits run unasked and shell commands are refused. Set MOON_CODE_ALLOW_BASH_UNATTENDED=1 to allow them.

That default exists because the file tools (read, write, edit, grep, ls) are confined to the directory moon-code was started in, and bash is not. Asked for a path outside the project, a model gets the refusal and then simply reaches for the shell; interactively you see that command in the prompt and can deny it, but a scripted run would hand it over silently.

Paths resolve through realpath first, so a symlink inside the project pointing outside it is refused rather than followed. Be clear on what this buys: it stops a prompt injection from quietly reading or writing outside your project, and it is not a sandbox. Anything with shell access has the machine, so run untrusted work in a container.

Each session is appended to ~/.moon-code/sessions/<epoch-ms>-<id>.jsonl, one JSON object per turn; if that write fails, moon-code warns once and carries on.

Provider and model resolution

There is one resolution order, and the flags are shorthand for the same environment variables:

Variable Flag Effect
ANTHROPIC_API_KEY Selects Anthropic when set
OPENAI_API_KEY Selects OpenAI when ANTHROPIC_API_KEY is not set
MOON_CODE_PROVIDER --provider Forces anthropic or openai; any other value is an error
MOON_CODE_BASE_URL Endpoint override, trailing slash trimmed
MOON_CODE_MODEL --model Model override
MOON_CODE_NO_CACHE Set to 1 to stop sending Anthropic prompt-caching markers
MOON_CODE_ALLOW_BASH_UNATTENDED Set to 1 to allow shell commands when stdin is not a terminal

Defaults are https://api.anthropic.com/v1 with claude-sonnet-5 for Anthropic, and https://api.openai.com/v1 with gpt-4o for OpenAI. With neither key set, moon-code prints what to export and exits non-zero.

MOON_CODE_BASE_URL plus OPENAI_API_KEY is what makes OpenRouter, Groq, Together, Fireworks, vLLM, llama.cpp and LM Studio work:

export OPENAI_API_KEY=sk-or-...
export MOON_CODE_BASE_URL=https://openrouter.ai/api/v1
export MOON_CODE_MODEL=anthropic/claude-sonnet-4.5
moon-code

Prompt caching

On the Anthropic path the request carries cache_control breakpoints on the last tool definition, on the system prompt, and on the last block of the most recent user message. The tools and the system prompt are byte-identical every turn, so after the first request they are read from the cache rather than re-charged at full input price; the conversation breakpoint extends that to the accumulated history. /status reports how many input tokens came back from the cache this session.

Nothing changes on the OpenAI path: cache_control is Anthropic-specific and several OpenAI-compatible endpoints reject unknown fields, so that request builder never emits it. For an Anthropic-shaped gateway that proxies /v1/messages without supporting caching, MOON_CODE_NO_CACHE=1 drops every breakpoint and restores the plain-string system field.

Caching only engages above a model-dependent minimum prefix (1024 tokens on Sonnet, 4096 on Haiku 4.5), so short sessions on a small model may report no cache reads even though the markers were sent.

Status

Early. It works and it is used daily, but every dependency is pre-1.0 and moonbitlang/async says outright that its API will change, so expect breakage on a toolchain bump. CI pins the toolchain deliberately for that reason.

Deliberately out of scope for now: MCP, subagents, a plugin system for slash commands, multi-provider beyond the two wire formats, and an alt-screen mode.

Known rough edges: the input box hard-wraps mid-word (a tradeoff that keeps the cursor column correct with CJK and emoji), and /clear on a resumed session keeps appending to the same transcript.

Security

The file tools (read, write, edit, grep, ls) are confined to the directory moon-code was started in, resolving symlinks first so a link out of the project is refused rather than followed. bash is not confined and cannot usefully be: it gets a shell. Interactively it goes through the permission prompt; with no terminal to ask, shell commands are refused unless you opt in.

So the boundary is real but narrow: it stops a prompt injection from quietly reading or writing outside your project. It is not a sandbox. Anything with shell access has the machine, so run untrusted work in a container.

License

MIT


Not affiliated with, endorsed by, or connected to Anthropic. moon-code is an independent project that imitates the Claude Code terminal interface; "Claude" and "Claude Code" are Anthropic's.

About

A Claude Code-style terminal AI coding agent, written in MoonBit

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages