These examples are executed by moon cram test tests/cram. The Moon wrapper
builds the native package at cmd/openseek first, then exposes the executable
on PATH as openseek.exe.
openseek is the headless automation CLI: a subcommand tree under run,
serve, review, subrun, mcp, and sessions (the interactive terminal UI
is the separate openseek_tui binary, maintained in its own repository). Every
command here is offline: it either prints help or fails argument validation
before the agent contacts DeepSeek, so the suite needs no API key and makes no
network calls. The live, API-backed examples live in
tests/live/deepseek.md.
openseek --help is generated by argparse — the whole CLI is one command tree,
so the top level lists the subcommands and the engine-shared options. It exits
successfully.
$ openseek.exe --help
Usage: openseek [options] <command>
DeepSeek-backed MoonBit coding agent (headless automation CLI).
Commands:
run Run one task headlessly; stream JSONL events on stdout.
serve Session server: read JSONL commands (prompt/steer/cancel/compact/goal) from stdin.
mcp List configured MCP servers and the tools they expose.
review Read-only code review of base...HEAD; prints a JSON ReviewReport.
subrun INTERNAL: run one subagent kind as a child of this engine (input JSON on stdin; stdin EOF cancels).
sessions Manage durable sessions.
Options:
-h, --help Show help information.
--api-key <api-key> API key for the selected chat provider. [default: ]
--model <model> Chat model: deepseek-v4-flash, deepseek-v4-pro, kimi-k2.7-code, kimi-k2.7-code-highspeed, glm-5.3, or glm-5.3-flash. [env: OPENSEEK_MODEL] [default: deepseek-v4-flash]
--api-url <api-url> OpenAI-compatible chat completions endpoint. [env: OPENSEEK_API_URL] [default: ]
--retry-attempts <retry-attempts> Total tries per model request before giving up on a retryable failure (429, 5xx, or a transport error); 1 disables retrying. Omit for the client default. [env: OPENSEEK_RETRY_ATTEMPTS]
--retry-backoff-ms <retry-backoff-ms> Delay before the first model-request retry; it doubles per attempt, capped at 60s. Omit for the client default. [env: OPENSEEK_RETRY_BACKOFF_MS]
--max-steps <max-steps> Maximum agent steps per turn; omit to bound turns by the model's context window instead (a checkpoint summary carries each turn into the next). [env: OPENSEEK_MAX_STEPS]
--thinking <thinking> Model thinking mode: no, high, or max; GLM maps no to low effort. [env: OPENSEEK_THINKING] [default: high]
--session <session> Create or resume this durable session id.
--session-root <session-root> Directory containing durable OpenSeek sessions. [default: .openseek]
The root carries the engine-shared options (--api-key, --model, …) as
globals, so any subcommand sees them. The interactive UI's own options
(--prompt, --engine, --continue) live on the separate openseek_tui
binary (its own repository, moonbitlang/openseek_tui).
There is no free-form top-level prompt: a bare word that is not a subcommand is rejected by argparse rather than silently opening the UI with that word as a prompt. (argparse does not yet suggest a near-miss subcommand the way it does for options — tracked upstream in moonbitlang/core — so the message is a plain rejection for now.)
$ sh <<'EOF'
> stderr=$(mktemp)
> if env -u DEEPSEEK openseek.exe serv 2> "$stderr" >/dev/null; then echo exit-zero; else echo exit-non-zero; fi
> sed -n '1p' "$stderr"
> rm -f "$stderr"
> EOF
exit-non-zero
error: unexpected value 'serv' found; no more were expected
Because the UI options live on the separate openseek_tui binary, a UI flag
given to openseek is rejected — not silently accepted and then ignored before
an engine subcommand. So openseek --prompt x run … is an error; use
openseek_tui --prompt "…" for the UI, and openseek run "…" for a headless
task.
$ sh <<'EOF'
> stderr=$(mktemp)
> if env DEEPSEEK=test-key openseek.exe --prompt x run TASK 2> "$stderr" >/dev/null; then echo exit-zero; else echo exit-non-zero; fi
> sed -n '1p' "$stderr"
> rm -f "$stderr"
> EOF
exit-non-zero
error: unexpected argument '--prompt' found
With no free-form positional, -- cannot smuggle a prompt: a value after it is
rejected by the parser.
$ sh <<'EOF'
> stderr=$(mktemp)
> if env DEEPSEEK=test-key openseek.exe -- something 2> "$stderr" >/dev/null; then echo exit-zero; else echo exit-non-zero; fi
> sed -n '1p' "$stderr"
> rm -f "$stderr"
> EOF
exit-non-zero
error: unexpected value 'something' found; no more were expected
openseek is automation-only: there is no default subcommand, so a bare
invocation is an error (launch the interactive UI with openseek_tui).
$ sh <<'EOF'
> stderr=$(mktemp)
> if openseek.exe 2> "$stderr" >/dev/null; then echo exit-zero; else echo exit-non-zero; fi
> sed -n '1p' "$stderr"
> rm -f "$stderr"
> EOF
exit-non-zero
error: the following required argument was not provided: 'subcommand'
openseek run --help prints the options, environment variables, and defaults
behind a headless run.
$ openseek.exe run --help
Usage: openseek run [options] [task...]
Run one task headlessly; stream JSONL events on stdout.
Arguments:
task... Task description.
Options:
-h, --help Show help information.
--api-key <api-key> API key for the selected chat provider. [default: ]
--model <model> Chat model: deepseek-v4-flash, deepseek-v4-pro, kimi-k2.7-code, kimi-k2.7-code-highspeed, glm-5.3, or glm-5.3-flash. [env: OPENSEEK_MODEL] [default: deepseek-v4-flash]
--api-url <api-url> OpenAI-compatible chat completions endpoint. [env: OPENSEEK_API_URL] [default: ]
--retry-attempts <retry-attempts> Total tries per model request before giving up on a retryable failure (429, 5xx, or a transport error); 1 disables retrying. Omit for the client default. [env: OPENSEEK_RETRY_ATTEMPTS]
--retry-backoff-ms <retry-backoff-ms> Delay before the first model-request retry; it doubles per attempt, capped at 60s. Omit for the client default. [env: OPENSEEK_RETRY_BACKOFF_MS]
--max-steps <max-steps> Maximum agent steps per turn; omit to bound turns by the model's context window instead (a checkpoint summary carries each turn into the next). [env: OPENSEEK_MAX_STEPS]
--thinking <thinking> Model thinking mode: no, high, or max; GLM maps no to low effort. [env: OPENSEEK_THINKING] [default: high]
--session <session> Create or resume this durable session id.
--session-root <session-root> Directory containing durable OpenSeek sessions. [default: .openseek]
--no-session Run ephemerally: do not record this run to a durable session.
--review-gate On goal(met), audit the worktree against the goal with a review subagent and inject the findings as an advisory notice.
--dir <dir> Workspace directory for relative paths; creates only the final path component if its parent exists. [default: .]
--system-prompt-file <system-prompt-file> Read the complete system prompt from this file instead of the built-in prompt. [env: OPENSEEK_SYSTEM_PROMPT_FILE] [default: ]
--system-prompt-addendum-file <system-prompt-addendum-file> Append this file to the selected system prompt for prompt experiments. [env: OPENSEEK_SYSTEM_PROMPT_ADDENDUM_FILE] [default: ]
--global-skills-dir <global-skills-dir> User-level skills directory advertised alongside workspace skills; empty means $HOME/.openseek/skills, or %USERPROFILE%/.openseek/skills on Windows. [env: OPENSEEK_GLOBAL_SKILLS_DIR] [default: ]
--mcp-config <mcp-config> Path to a JSON file of MCP servers ({"mcpServers": {"<name>": {"command", "args", "env"} | {"url", "headers"}}}); each server's tools (stdio subprocess or Streamable HTTP) are exposed to the agent, namespaced mcp__<server>__<tool>. Empty disables MCP. [env: OPENSEEK_MCP_CONFIG] [default: ]
--review-deadline <review-deadline> Wall-clock deadline in milliseconds for one review audit (the review tool or --review-gate); default 900000 (15 minutes).
--approval <approval> What happens when a tool asks permission to run without its sandbox: never (default; refuse without asking, and do not offer the argument to the model), ask (prompt the controller over the command stream and wait), always (grant without asking). [env: OPENSEEK_APPROVAL] [default: never]
--concurrency <concurrency> Run the task in N sibling copies of --dir concurrently (best-of-N). Any explicit value, including 1, copies --dir into <dir>_run_<i> and never writes to --dir itself; omit the flag for a single in-place run. [default: 1]
--approval ask is refused here rather than accepted and then never honoured:
a headless run reads no commands, so there is nobody the engine could ask,
and the alternative is a turn that stops for five minutes to wait out a
deadline no one was ever going to beat.
$ env DEEPSEEK=test-key openseek.exe run --approval ask "probe" 2>&1
error: --approval ask needs a controller to ask: this command reads no commands, so use never (refuse escalation) or always (grant it unasked)
[1]
The [env: OPENSEEK_APPROVAL] fallback is the same option arriving through the
process environment instead of argv, so it is refused the same way. This is the
one piece of main that argparse cannot pick up on its own: parse reads the
process arguments itself but defaults the environment to empty, so the binary
must hand it @env.get_env_vars() — and this case fails if it ever stops.
$ env DEEPSEEK=test-key OPENSEEK_APPROVAL=ask openseek.exe run "probe" 2>&1
error: --approval ask needs a controller to ask: this command reads no commands, so use never (refuse escalation) or always (grant it unasked)
[1]
With no --api-key flag and no DEEPSEEK in the environment, a run reports the
missing key and exits non-zero. The message names the model that wanted a key,
not the variable that would have supplied one — DEEPSEEK here, KIMI for a
Kimi model. Those options are hidden from --help, so this is where they are
written down.
$ sh <<'EOF'
> stdout=$(mktemp)
> stderr=$(mktemp)
> if env -u DEEPSEEK -u KIMI -u OPENSEEK_MODEL openseek.exe run "summarize this project" > "$stdout" 2> "$stderr"; then echo exit-zero; else echo exit-non-zero; fi
> cat "$stderr"
> if test -s "$stdout"; then echo stdout-not-empty; else echo stdout-empty; fi
> rm -f "$stdout" "$stderr"
> EOF
exit-non-zero
error: an API key is required for deepseek-v4-flash: pass --api-key
stdout-empty
The task is free-form after option parsing has stopped, but a leading option-looking token is still an option. This catches stale or misspelled flags instead of silently turning them into prompt text.
$ sh <<'EOF'
> stdout=$(mktemp)
> stderr=$(mktemp)
> if env -u DEEPSEEK openseek.exe run --xxy he > "$stdout" 2> "$stderr"; then echo exit-zero; else echo exit-non-zero; fi
> sed -n '1p' "$stderr"
> if test -s "$stdout"; then echo stdout-not-empty; else echo stdout-empty; fi
> rm -f "$stdout" "$stderr"
> EOF
exit-non-zero
error: unexpected argument '--xxy' found
stdout-empty
When the task itself must start with -, use the normal option delimiter.
Here parsing succeeds and the run reaches the later API-key validation.
$ sh <<'EOF'
> stdout=$(mktemp)
> stderr=$(mktemp)
> if env -u DEEPSEEK -u KIMI -u OPENSEEK_MODEL openseek.exe run -- '--xxy he' > "$stdout" 2> "$stderr"; then echo exit-zero; else echo exit-non-zero; fi
> cat "$stderr"
> if test -s "$stdout"; then echo stdout-not-empty; else echo stdout-empty; fi
> rm -f "$stdout" "$stderr"
> EOF
exit-non-zero
error: an API key is required for deepseek-v4-flash: pass --api-key
stdout-empty
Session inspection and compaction operate on typed session files and do not
require a API key for the selected chat provider. Hand-written log lines carry the 0 sentinel stamp;
the summary event the compaction appends is stamped with the wall clock, so
both sessions show calls strip ts to stay deterministic.
$ sh <<'EOF'
> tmp=$(mktemp -d)
> mkdir -p "$tmp/sessions/demo"
> cat > "$tmp/sessions/demo/openseek_session-demo.jsonl" <<'JSONL'
> {"version":1,"id":"demo","system_prompt":"system"}
> {"sequence":1,"ts":0,"item":{"kind":"user","payload":{"content":"hello"}}}
> {"sequence":2,"ts":0,"item":{"kind":"assistant","payload":{"content":"answer","tool_calls":[]}}}
> JSONL
> printf 'hello and answer' > "$tmp/summary.txt"
> env -u DEEPSEEK openseek.exe sessions list --session-root "$tmp" | cut -f1
> env -u DEEPSEEK openseek.exe sessions show demo --session-root "$tmp" | sed -E 's/"ts":[0-9]+,//g'
> env -u DEEPSEEK openseek.exe sessions compact demo --session-root "$tmp" --file "$tmp/summary.txt" --from 1 --to 2
> env -u DEEPSEEK openseek.exe sessions show demo --session-root "$tmp" | sed -E 's/"ts":[0-9]+,//g'
> rm -rf "$tmp"
> EOF
demo
{"version":1,"id":"demo","system_prompt":"system","events":[{"sequence":1,"item":{"kind":"user","payload":{"content":"hello"}}},{"sequence":2,"item":{"kind":"assistant","payload":{"content":"answer","tool_calls":[]}}}]}
compacted session demo events 1..2; last_sequence=3
{"version":1,"id":"demo","system_prompt":"system","events":[{"sequence":1,"item":{"kind":"user","payload":{"content":"hello"}}},{"sequence":2,"item":{"kind":"assistant","payload":{"content":"answer","tool_calls":[]}}},{"sequence":3,"item":{"kind":"summary","payload":{"content":"hello and answer","from_sequence":1,"to_sequence":2}}}]}
openseek run "task" records its conversation to a generated
cli-YYYYMMDD-HHMMSS-mmm session under --session-root (default .openseek),
exactly as if --session had been passed — "what did the agent do?" is usually
asked after the run, when an unrecorded answer is gone for good. The run
announces the recording with a session_started event on stdout, so the id is
in the event stream; afterwards the run is
visible to sessions list, sessions show, the viz server, and --session <id> resumption.
This example stays offline by pointing --api-url at a closed local port: the
engine names its session, durably records the user prompt, and only then fails
to reach the API. The generated id's timestamp is normalized for determinism.
A refused connection is retryable, so every example below that targets the
closed port pins OPENSEEK_RETRY_ATTEMPTS=1: none of them is testing the retry
budget, and the suite's runtime should not track its default.
$ sh <<'EOF'
> tmp=$(mktemp -d)
> cd "$tmp"
> if env DEEPSEEK=test-key OPENSEEK_RETRY_ATTEMPTS=1 openseek.exe run --api-url "http://127.0.0.1:9/chat/completions" "say hi" > out.jsonl 2>/dev/null; then echo exit-zero; else echo exit-non-zero; fi
> grep -c '"event":"session_started"' out.jsonl
> env -u DEEPSEEK openseek.exe sessions list | cut -f1 | sed -E 's/cli-[0-9]{8}-[0-9]{6}-[0-9]{3}(-[A-Za-z0-9]+)?/cli-<stamp>/'
> rm -rf "$tmp"
> EOF
exit-non-zero
1
cli-<stamp>
The stdout stream is a protocol, not a log: events are written straight to
stdout by their own writer, and the engine links no logger at all, so a logging
environment variable cannot silence them. This case guards against the stream
ever being routed back through one: once, events went through @xlog, and
MOON_XLOG=warn dropped the whole stream — a TUI attached to that engine
rendered nothing with no error to explain it.
$ sh <<'EOF'
> tmp=$(mktemp -d)
> cd "$tmp"
> for level in warn error; do
> env DEEPSEEK=test-key OPENSEEK_RETRY_ATTEMPTS=1 MOON_XLOG=$level openseek.exe run --api-url "http://127.0.0.1:9/chat/completions" --dir "$tmp/$level" "say hi" > "out-$level.jsonl" 2>/dev/null
> echo "$level: $(grep -c '"event":"agent_step"' "out-$level.jsonl")"
> done
> rm -rf "$tmp"
> EOF
warn: 1
error: 1
--no-session turns recording off: the same failing run leaves no session root
behind.
$ sh <<'EOF'
> tmp=$(mktemp -d)
> cd "$tmp"
> env DEEPSEEK=test-key OPENSEEK_RETRY_ATTEMPTS=1 openseek.exe run --no-session --api-url "http://127.0.0.1:9/chat/completions" "say hi" >/dev/null 2>&1
> if test -d .openseek; then echo recorded; else echo ephemeral; fi
> rm -rf "$tmp"
> EOF
ephemeral
--dir defaults to ., but it can point a run at another workspace. When the
final path component is missing and the parent exists, OpenSeek creates that one
directory, logs workspace_created, and resolves the default session root under
it.
$ sh <<'EOF'
> tmp=$(mktemp -d)
> mkdir -p "$tmp/parent"
> if env DEEPSEEK=test-key OPENSEEK_RETRY_ATTEMPTS=1 openseek.exe run --dir "$tmp/parent/new" --api-url "http://127.0.0.1:9/chat/completions" "say hi" > "$tmp/out.jsonl" 2>/dev/null; then echo exit-zero; else echo exit-non-zero; fi
> if test -d "$tmp/parent/new"; then echo dir-created; else echo dir-missing; fi
> grep -c '"event":"workspace_created"' "$tmp/out.jsonl"
> env -u DEEPSEEK openseek.exe sessions list --dir "$tmp/parent/new" | cut -f1 | sed -E 's/cli-[0-9]{8}-[0-9]{6}-[0-9]{3}(-[A-Za-z0-9]+)?/cli-<stamp>/'
> env -u DEEPSEEK openseek.exe sessions list --dir "$tmp/parent/fresh" > "$tmp/session-list.out"
> if test -d "$tmp/parent/fresh"; then echo session-dir-created; else echo session-dir-missing; fi
> wc -l < "$tmp/session-list.out" | tr -d ' '
> rm -rf "$tmp"
> EOF
exit-non-zero
dir-created
1
cli-<stamp>
session-dir-created
0
Asking for both recording behaviors at once is rejected before any work happens.
$ env DEEPSEEK=test-key openseek.exe run --session demo --no-session "say hi" 2>&1
error: --no-session contradicts --session; pick one behavior
[1]
serve turns the engine into a long-lived session server: prompts, steers, and
cancels arrive as JSONL commands on stdin, and the usual event stream leaves on
stdout. The command surface is testable offline — a malformed command is
reported as a command_error event rather than killing the server, an idle
cancel is a no-op, and stdin EOF shuts the server down cleanly (exit 0)
without ever touching the network. The grep -o keeps only the stable event
tag, since a line also carries the event's payload fields.
$ printf '{"command":"reboot"}\n{"command":"cancel"}\n' | env DEEPSEEK=test-key openseek.exe serve 2>/dev/null | grep -o '"event":"command_error"'
"event":"command_error"
A steer with no turn to land in is rejected rather than converted into a prompt:
it can only arrive idle by racing a turn's terminal event through the pipes, and
an engine must never start a turn the controller did not ask for. The rejection
carries a steer_dropped event so the controller can ask the user to resubmit —
and, usefully for this offline test, no turn means no network.
$ printf '{"command":"steer","text":"too late"}\n' | env DEEPSEEK=test-key openseek.exe serve 2>/dev/null | grep -o '"event":"steer_dropped"'
"event":"steer_dropped"
serve takes no positional; a stray task is rejected by the parser before
anything runs.
$ sh <<'EOF'
> stderr=$(mktemp)
> if env DEEPSEEK=test-key openseek.exe serve "do something" 2> "$stderr" >/dev/null; then echo exit-zero; else echo exit-non-zero; fi
> sed -n '1p' "$stderr"
> rm -f "$stderr"
> EOF
exit-non-zero
error: unexpected value 'do something' found; no more were expected
Native MoonBit CLIs should use moonbitlang/core/argparse and call
@argparse.parse(...) on a Command instead of hand-rolling argument parsing
with @env.args(). This tiny package verifies the current FlagArg,
PositionArg, and Matches shape.
$ sh <<'EOF'
> tmp=$(mktemp -d)
> mkdir -p "$tmp/cmd/echoargs"
> cat > "$tmp/moon.mod" <<'MOD'
> name = "example/cli"
> version = "0.1.0"
> MOD
> cat > "$tmp/cmd/echoargs/moon.pkg" <<'PKG'
> import {
> "moonbitlang/core/argparse"
> }
> warnings = "+unnecessary_annotation"
> supported_targets = "+native"
> options(
> "is-main": true,
> )
> PKG
> cat > "$tmp/cmd/echoargs/main.mbt" <<'MBT'
> ///|
> fn main raise {
> let matches = @argparse.parse(
> Command(
> "echoargs",
> about="Tiny argparse example.",
> flags=[FlagArg("stdin", long="stdin", about="Read stdin.")],
> positionals=[PositionArg("input", default_values=["-"])],
> ),
> )
> let input = match matches.values.get("input") {
> Some([value, ..]) => value
> _ => fail("missing input")
> }
> let stdin = match matches.flags.get("stdin") {
> Some(value) => value
> None => false
> }
> println("input=\{input}")
> println("stdin=\{stdin}")
> }
> MBT
> (cd "$tmp" && moon run --target native cmd/echoargs -- --stdin sample.toml)
> rm -rf "$tmp"
> EOF
input=sample.toml
stdin=true