How to run Cursor Composer 2.5 as the Takopi engine via pi + pi-cursor-sdk, without the Takopi control plane.
Tested on Linux with Takopi installed at /opt/takopi-runtime, pi via nvm, and Telegram transport.
Telegram message
→ Takopi (Python, /opt/takopi-runtime)
→ patched pi runner (takopi-runners-pi.py)
→ subprocess: pi --print --mode json --provider cursor --model cursor/composer-2.5
→ pi-cursor-sdk (npm) → Cursor SDK → Composer 2.5
→ JSONL events on stdout → Takopi progress UI → Telegram edits
Key idea: Takopi does not call the Cursor SDK directly. It shells out to pi, which uses the pi-cursor-sdk extension as a provider. Takopi’s job is to translate pi’s JSONL stream into Telegram progress messages.
| Piece | Purpose |
|---|---|
| Takopi runtime | e.g. /opt/takopi-runtime/bin/python + takopi package |
| Node + pi | @mariozechner/pi-coding-agent CLI on PATH (nvm is fine) |
| pi-cursor-sdk | npm extension that adds cursor/composer-2.5 to pi |
| CURSOR_API_KEY | From Cursor dashboard → Integrations |
| Telegram bot token | Existing Takopi bot config or @BotFather |
# Example: nvm node 22
npm install -g @mariozechner/pi-coding-agent
npm install -g pi-cursor-sdk # or: pi install pi-cursor-sdk
pi list | grep pi-cursor-sdk # should show the extensionAuth (pick one):
# Option A: env var (what we use for systemd)
export CURSOR_API_KEY="crsr_..."
# Option B: pi auth store (~/.pi/agent/auth.json)
mkdir -p ~/.pi/agent
cat > ~/.pi/agent/auth.json <<'EOF'
{
"cursor": { "type": "api_key", "key": "crsr_..." }
}
EOF
chmod 600 ~/.pi/agent/auth.jsonSmoke test pi alone:
export PI_STDIN_CLOSE=1
export PI_CURSOR_SETTING_SOURCES=none
export PI_CURSOR_NATIVE_TOOL_DISPLAY=1
pi --no-session --provider cursor --model cursor/composer-2.5 --print \
-p "Reply with exactly: ok" </dev/nullMinimal ~/.config/takopi-standalone/linux/takopi.toml:
default_engine = "pi"
transport = "telegram"
[transports.telegram]
bot_token = "YOUR_BOT_TOKEN"
chat_id = YOUR_CHAT_ID
voice_transcription = true
[pi]
model = "cursor/composer-2.5"
provider = "cursor"
extra_args = []If migrating from control-plane bots, a small sync script can patch legacy configs:
PI_BLOCK = {
"model": "cursor/composer-2.5",
"provider": "cursor",
"extra_args": [],
}
# set default_engine = "pi", keep existing telegram tokenSecrets file ~/.config/takopi-standalone/secrets.env (mode 600):
CURSOR_API_KEY=crsr_...We reuse the key from ~/.hermes/.env when Hermes is already set up.
Stock Takopi takopi.runners.pi does not handle Cursor’s thinking/tool JSONL well enough for Telegram. Use a wrapper that:
- Sources
secrets.env - Puts nvm
pionPATH - Sets pi-cursor env vars (see Step 4)
- Preloads a patched
takopi.runners.pibeforetakopi.cli.main()
#!/usr/bin/env bash
set -euo pipefail
CONFIG_PATH="$1"
ENGINE="$2"
shift 2
source ~/.config/takopi-standalone/secrets.env
export PATH="$HOME/.nvm/versions/node/v22.12.0/bin:/opt/takopi-runtime/bin:$PATH"
export PI_STDIN_CLOSE=1
export PI_CURSOR_PI_TOOL_BRIDGE=0
export PI_CURSOR_SETTING_SOURCES=none
export PI_CURSOR_NATIVE_TOOL_DISPLAY=1
exec /opt/takopi-runtime/bin/python - "$CONFIG_PATH" "$ENGINE" "$@" <<'PY'
from pathlib import Path
import importlib.util, sys
config_path = Path(sys.argv[1]).expanduser().resolve()
engine = sys.argv[2]
argv = ["takopi", engine, *sys.argv[3:]]
patch_path = Path("/path/to/takopi_standalone/patches/takopi-runners-pi.py")
if patch_path.is_file():
spec = importlib.util.spec_from_file_location("takopi.runners.pi", patch_path)
pi_mod = importlib.util.module_from_spec(spec)
sys.modules["takopi.runners.pi"] = pi_mod
spec.loader.exec_module(pi_mod)
import takopi.cli as cli, takopi.config as config, takopi.settings as settings
config.HOME_CONFIG_PATH = config_path
settings.HOME_CONFIG_PATH = config_path
cli.HOME_CONFIG_PATH = config_path
sys.argv = argv
cli.main()
PYRun manually:
./takopi-run-standalone.sh ~/.config/takopi-standalone/linux/takopi.toml piSet in the wrapper (systemd inherits them):
| Variable | Value | Why |
|---|---|---|
PI_STDIN_CLOSE |
1 |
pi must not block on stdin when launched from systemd |
PI_CURSOR_SETTING_SOURCES |
none |
lean headless bots; no ambient Cursor project settings |
PI_CURSOR_PI_TOOL_BRIDGE |
0 |
disable MCP pi-tool bridge for simpler Telegram runs |
PI_CURSOR_NATIVE_TOOL_DISPLAY |
1 |
Required. With 0, tool activity is batched into thinking traces after long commands finish → Telegram looks frozen for minutes |
Optional:
PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS=3600 # long MCP/shell toolsPatch file replaces stock takopi/runners/pi.py at runtime. It adds:
- Cursor trace parsing — turns
thinking_deltalines like$ git status,read path,glob **into TakopiActionEvents toolcall_start/ToolExecutionStarthandling — shows tool steps when pi-cursor emits them- 12s heartbeat — while pi is quiet (Composer running a long shell/SSH), emit:
so Telegram keeps updating and you know it’s alivestill running… (36s quiet, after tool: read: foo.py)
Without the heartbeat, headless --print --mode json often emits no JSONL for minutes during a single long tool — pi is working, but Takopi has nothing to render.
Patch highlights:
_HEARTBEAT_IDLE_S = 12.0
PI_CURSOR_NATIVE_TOOL_DISPLAY=1 # in shell, not Python
# In PiRunner._iter_jsonl_events:
# - wait up to 12s for next JSONL line
# - on timeout → yield heartbeat ActionEvent with quiet seconds + last tool nameOptional: install patch into site-packages instead of preload:
sudo install -m 0644 patches/takopi-runners-pi.py \
/opt/takopi-runtime/lib/python3.14/site-packages/takopi/runners/pi.pyPreload is nicer for iteration — no sudo.
[Unit]
Description=Takopi bot (standalone, pi + Cursor)
After=network-online.target
[Service]
Type=simple
WorkingDirectory=/path/to/your/cwd
Environment=HOME=/home/you
Environment=TAKOPI_STANDALONE_SECRETS=/home/you/.config/takopi-standalone/secrets.env
ExecStart=/path/to/takopi-run-standalone.sh /home/you/.config/takopi-standalone/linux/takopi.toml pi
Restart=always
RestartSec=5
[Install]
WantedBy=default.targetsystemctl --user daemon-reload
systemctl --user enable --now takopi-standalone-linux.service
journalctl --user -u takopi-standalone-linux.service -f# pi + extension present
command -v pi && pi list | grep pi-cursor-sdk
# secrets
test -f ~/.config/takopi-standalone/secrets.env
# config uses pi + composer
grep -E 'default_engine|"pi"' ~/.config/takopi-standalone/linux/takopi.toml
# direct pi smoke (no secrets printed)
source ~/.config/takopi-standalone/secrets.env
pi --no-session --model cursor/composer-2.5 --print -p "Reply with exactly: ok" </dev/null
# bot running
systemctl --user is-active takopi-standalone-linux.serviceProgress path test (optional Python script): run patched PiRunner with a glob+read prompt and assert Telegram-style progress shows tool lines.
Resume in Telegram with the token pi prints:
pi --session 019e5588
Or send a message while Takopi has the session token in context.
That is you hitting /cancel or the cancel button — not an auto-timeout. Takopi has no runner duration limit. If progress looked dead, pi was likely still running.
Check:
journalctl --user -u takopi-standalone-linux.service --since "10 min ago" | rg "subprocess.spawn|subprocess.exit|cancel"
ps aux | rg " pi "Expected during quiet gaps. Composer may run SSH, tests, or git for minutes without new JSONL. The heartbeat line should update every ~12s:
▸ still running… (48s quiet, after ssh -o ConnectTimeout=10 …)
If (Ns quiet) keeps climbing → still working. If it stops climbing for many minutes with no new tool lines → maybe stuck; cancel is reasonable.
Running linux and polymarket bots against the same Telegram chat is confusing (parallel sessions, duplicate progress). Prefer one active bot per chat.
| Hermes + Discord | Takopi + pi + Telegram | |
|---|---|---|
| Main model | Kimi (config) | Composer 2.5 via pi |
| Composer | via coding_fleet tools |
direct engine |
| Bridge | cursor_sdk in agent-fleet | pi-cursor-sdk subprocess |
Same CURSOR_API_KEY can feed both.
| Symptom | Likely cause | Fix |
|---|---|---|
| Progress frozen at step N for minutes | PI_CURSOR_NATIVE_TOOL_DISPLAY=0 or no heartbeat patch |
Set display=1, use patched runner |
cancelled after long wait |
User cancel while pi was still running | Wait for heartbeat; check ps |
| pi works in shell, not in bot | PATH missing nvm pi | Fix PATH in wrapper |
CURSOR_API_KEY is not set |
secrets not sourced | Check secrets.env + systemd env |
| No tool lines at all | patch not loaded | Confirm preload path in wrapper |
| AttributeError on pi runner import | patch / takopi version mismatch | Re-sync patch with installed takopi |
takopi_standalone/
├── patches/
│ └── takopi-runners-pi.py # Cursor-aware runner + heartbeat
├── scripts/
│ ├── install-standalone.sh # migrate configs, secrets, systemd
│ ├── takopi-run-standalone.sh # wrapper (preload patch + env)
│ ├── sync-bot-config.py # legacy toml → pi+cursor
│ └── verify-standalone.sh # smoke checks
└── systemd/
└── takopi-standalone-*.service
npm i -g @mariozechner/pi-coding-agent pi-cursor-sdk- Put
CURSOR_API_KEYin~/.config/takopi-standalone/secrets.env - Set
default_engine = "pi",[pi] model = "cursor/composer-2.5" - Wrapper: PATH to pi,
PI_CURSOR_NATIVE_TOOL_DISPLAY=1, preload patched runner - systemd user service calling wrapper
- Expect heartbeat during long tool gaps — pi is usually still working
Written from a working setup on Pop!_OS, May 2026. Adjust paths (/opt/takopi-runtime, nvm node version, config dirs) for your machine.