This guide documents the master tandem CLI and the direct tandem-engine
runtime using bash commands (macOS/Linux/WSL).
tandem --help
tandem doctor
tandem status
tandem service install
tandem install panel
tandem panel init
tandem panel open
tandem-engine serve --hostname 127.0.0.1 --port 39731
tandem run "Summarize this repository"For the official headless bootstrap path, use the master CLI:
npm i -g @frumu/tandem
tandem install panel
tandem panel initFor a direct engine-only setup, install the master CLI and start the runtime:
npm i -g @frumu/tandem
tandem-engine serve --hostname 127.0.0.1 --port 39731If you need a terminal-first interface instead, install the TUI:
npm i -g @frumu/tandem-tui
tandem-tuiStarts the HTTP/SSE runtime used by desktop and TUI clients.
tandem-engine serve --hostname 127.0.0.1 --port 39731Useful options:
--hostname(alias:--host)--port--state-dir--provider--model--api-key--configTANDEM_ENGINE_HOST(env override)TANDEM_ENGINE_PORT(env override)TANDEM_API_TOKEN(optional API auth token requirement)
The master CLI also understands tandem service status, tandem update,
tandem addon list, and tandem panel open for the add-on flow.
Checks engine health by calling GET /global/health.
tandem-engine status
tandem-engine status --hostname 127.0.0.1 --port 39731Engine runtime now detects host environment once at startup and treats it as canonical:
os:windows|linux|macosshell_family:powershell|posixpath_style:windows|posixarch: host architecture (for examplex86_64,aarch64)
This environment is injected into run prompts, exposed via GET /global/health (environment),
and attached to session.run.started events so all clients (Desktop, TUI, HTTP) see identical OS context.
OS-aware prompt behavior can be controlled with:
TANDEM_OS_AWARE_PROMPTS=1(default on)TANDEM_OS_AWARE_PROMPTS=0to disable environment prompt injection
Runs one prompt and prints the model response.
tandem-engine run "Write a status update" --provider openrouter --model openai/gpt-4o-miniRuns multiple prompts concurrently and prints a JSON summary.
cat > tasks.json << 'JSON'
{
"tasks": [
{ "id": "science", "prompt": "Explain why the sky appears blue in 5 bullet points", "provider": "openrouter" },
{ "id": "writing", "prompt": "Write a concise professional status update for a weekly team sync", "provider": "openrouter" },
{ "id": "planning", "prompt": "Create a simple 3-step plan to learn Rust over 4 weeks", "provider": "openrouter" }
]
}
JSON
tandem-engine parallel --json @tasks.json --concurrency 3Prompt-driven:
tandem-engine run "Summarize this repository https://github.com/frumu-ai/tandem"Direct tools:
tandem-engine tool --json '{"tool":"webfetch","args":{"url":"https://github.com/frumu-ai/tandem","return":"both","mode":"auto"}}'
tandem-engine tool --json '{"tool":"webfetch_html","args":{"url":"https://github.com/frumu-ai/tandem"}}'
tandem-engine tool --json '{"tool":"websearch","args":{"query":"frumu tandem engine architecture","limit":5}}'Executes a built-in tool call.
Input formats:
- raw JSON string
@path/to/file.json-(stdin)
tandem-engine tool --json '{"tool":"workspace_list_files","args":{"path":"."}}'
tandem-engine tool --json @payload.json
cat payload.json | tandem-engine tool --json -Payload shape:
{
"tool": "workspace_list_files",
"args": {
"path": "."
}
}For MCP-backed tools, Tandem can emit an explicit auth-required flow when upstream requires OAuth or account authorization.
- Event contract:
mcp.auth.required - Runtime MCP server state includes:
last_auth_challengemcp_session_id
- MCP reconnect/refresh failures now clear stale tool cache/session state before reporting errors.
Practical implication:
- You can retry MCP tools after completing authorization without restarting the engine.
- If you see repeated authorization loops, verify stable MCP headers (especially provider-specific user identity headers) and reconnect/refresh the MCP server.
Tandem normalizes MCP tool-call argument keys before tools/call using tool schema hints.
Examples of recovered mismatch classes:
taskTitle->task_titlelistId->list_id- common alias fallback such as
name->task_titlewhen required by schema
This behavior runs in the engine runtime, so it applies to all clients (web, TUI, channels), not only CLI calls.
Lists supported provider IDs.
tandem-engine providerstandem-engine does not yet expose a direct key set subcommand.
Use the engine HTTP API while serve is running.
Temporary (in-memory) auth:
PUT /auth/{provider}stores a runtime-only token.- It does not persist across engine restarts.
API="http://127.0.0.1:39731"Set a provider key (example: openrouter):
curl -s -X PUT "$API/auth/openrouter" \
-H 'content-type: application/json' \
-d '{"apiKey":"YOUR_OPENROUTER_KEY"}'Set another provider key (example: openai):
curl -s -X PUT "$API/auth/openai" \
-H 'content-type: application/json' \
-d '{"apiKey":"YOUR_OPENAI_KEY"}'Persistent provider defaults (safe):
PATCH /configfor non-secret defaults only (for exampledefault_provider,default_model).providers.<id>.api_keyis rejected by design.
curl -s -X PATCH "$API/config" \
-H 'content-type: application/json' \
-d '{"default_provider":"openrouter","providers":{"openrouter":{"default_model":"openai/gpt-4o-mini"}}}' | jq .Verify configured providers:
curl -s "$API/config/providers" | jq .Attempting to write secret keys via config now returns:
400 CONFIG_SECRET_REJECTED- Use
PUT /auth/{provider}or environment variables instead.
Delete a provider key:
curl -s -X DELETE "$API/auth/openrouter"WSL note:
- If engine runs on Windows and curl runs in WSL, replace
127.0.0.1with your Windows host IP:
WIN_HOST="$(awk '/nameserver/ {print $2; exit}' /etc/resolv.conf)"
API="http://${WIN_HOST}:39731"Config file locations:
- State/project config (engine-local):
<state_dir>/config.json - Global config (from
dirs::config_dir()): - Linux:
~/.config/tandem/config.json - macOS:
~/Library/Application Support/tandem/config.json - Windows:
%APPDATA%\\tandem\\config.json - Override global location with
TANDEM_GLOBAL_CONFIG
Generate a token:
tandem-engine token generateRun engine with token requirement:
TANDEM_API_TOKEN="tk_your_token_here" tandem-engine serve --hostname 0.0.0.0 --port 39731Send authenticated requests:
curl -s "$API/global/health" -H "Authorization: Bearer $TANDEM_API_TOKEN" | jq .
curl -s "$API/config/providers" -H "X-Tandem-Token: $TANDEM_API_TOKEN" | jq .Rotate token at runtime (requires current token header):
curl -s -X POST "$API/auth/token/generate" \
-H "Authorization: Bearer $TANDEM_API_TOKEN" | jq .Desktop first-run behavior:
- Tandem Desktop auto-generates an engine API token on first launch.
- Desktop passes this token to the sidecar via
TANDEM_API_TOKEN. - Desktop also sends the token on sidecar HTTP requests using
X-Tandem-Token. - TUI uses the same shared token and sends
X-Tandem-Tokenas well. - Token storage is keychain-first with fallback.
- Primary backend is
keychain(Windows Credential Manager / macOS Keychain / Linux Secret Service). - Fallback backend is shared token file storage when keychain is unavailable/locked.
Desktop token storage path:
- Windows:
%APPDATA%\\tandem\\security\\engine_api_token - macOS:
~/Library/Application Support/tandem/security/engine_api_token - Linux:
~/.local/share/tandem/security/engine_api_token
Notes:
- Desktop Settings shows token masked by default with
RevealandCopy. - Settings also shows token storage backend (
keychain,file,env, ormemory). - TUI token commands include
/engine token(masked) and/engine token show(full token + storage backend + fallback path).
Reserved for future interactive REPL support.
Start engine:
tandem-engine serve --hostname 127.0.0.1 --port 39731In a second terminal:
# 1) Create session
SID="$(curl -s -X POST 'http://127.0.0.1:39731/session' -H 'content-type: application/json' -d '{}' | jq -r '.id')"
# 2) Build message payload
MSG='{"parts":[{"type":"text","text":"Give me 3 practical Rust learning tips."}]}'
# 3) Append message
curl -s -X POST "http://127.0.0.1:39731/session/$SID/message" -H 'content-type: application/json' -d "$MSG" >/dev/null
# 4) Start async run and get stream path
RUN_JSON="$(curl -s -X POST "http://127.0.0.1:39731/session/$SID/prompt_async?return=run" -H 'content-type: application/json' -d "$MSG")"
ATTACH_PATH="$(echo "$RUN_JSON" | jq -r '.attachEventStream')"
echo "$RUN_JSON" | jq .
# 5) Stream events
curl -N "http://127.0.0.1:39731${ATTACH_PATH}"Synchronous one-shot response:
RESP="$(curl -s -X POST "http://127.0.0.1:39731/session/$SID/prompt_sync" -H 'content-type: application/json' -d "$MSG")"
echo "$RESP" | jq .Extract latest assistant text from response history:
echo "$RESP" | jq -r '[.[] | select(.info.role=="assistant")][-1].parts[] | select(.type=="text") | .text'When --state-dir is omitted:
--state-dirTANDEM_STATE_DIR- Shared Tandem canonical path
- Local fallback
.tandem
unsupported provider ...: runtandem-engine providerstool is required in input json: include non-emptytoolinvalid hostname or port: verify--hostname/--port
For Windows users, run these commands in WSL for the same behavior.