Chat-channel ↔ ACP agent gateway with scheduler.
cli-gateway runs as a standalone service and lets you talk to ACP-compatible coding agents (Codex/Claude/Gemini, via ACP adapters) from:
- Discord
- Telegram
- Feishu (webhook mode, MVP)
It uses one ACP stdio agent process per conversation binding to avoid cross-talk and support concurrency.
It implements ACP stdio transport (JSON-RPC 2.0 over newline-delimited JSON) and supports the Client-side tool surface:
session/updatestreamingsession/request_permissionfs/read_text_file,fs/write_text_fileterminal/*
ACP refs:
- Overview: https://agentclientprotocol.com/protocol/overview
- Initialization: https://agentclientprotocol.com/protocol/initialization
- Transports: https://agentclientprotocol.com/protocol/transports
- Schema: https://agentclientprotocol.com/protocol/schema
Requirements:
- Node.js >= 18
- Install
Option A (global):
npm i -g cli-gatewayOption B (no global install):
npx -y cli-gateway- Configure
On first run, if config is missing, cli-gateway opens an interactive setup wizard and writes:
~/.cli-gateway/config.json
You can edit that file any time to update tokens / agent command / defaults. See skills.md.
- Run
If installed globally:
cli-gatewayIf using npx, use the same command each time:
npx -y cli-gatewaynpm i
npm run devFor crash protection in long-running deployments, run-guard.sh now runs as a background daemon with nohup, supports lifecycle commands, and automatically updates/builds on startup.
Start guard (default app command: node dist/main.js):
npm run start:guardRestart/stop/status/logs:
bash scripts/run-guard.sh request-restart
bash scripts/run-guard.sh stop
bash scripts/run-guard.sh status
bash scripts/run-guard.sh logsCustom command is supported:
bash scripts/run-guard.sh start -- npm run dev
bash scripts/run-guard.sh request-restart -- npm run devstart/request-restart automatically runs:
npm i
npm run buildThen guard keeps restarting the app on abnormal exit with exponential backoff.
Before each launch attempt, guard also checks gateway.lock under CLI_GATEWAY_HOME (or ~/.cli-gateway), terminates the lock PID if still alive, and removes stale lock files.
Sandbox-friendly restart bridge:
- Run
scripts/restart-watcher.shon the host (outside sandbox). It watches.run-guard/restart.requestand callsrun-guard.sh restart. - From sandbox, only send a restart request marker:
bash scripts/run-guard.sh request-restart- Host watcher startup example:
nohup bash scripts/restart-watcher.sh >> .run-guard/restart-watcher.log 2>&1 &Useful env vars:
RESTART_BASE_DELAY_SECONDS(default2)RESTART_MAX_DELAY_SECONDS(default30)RESTART_MAX_ATTEMPTS(default0, unlimited)RESTART_ON_EXIT_0(default0)STOP_TIMEOUT_SECONDS(default20)SKIP_UPDATE=1to skipnpm i+npm run buildGUARD_STATE_DIRto override pid/log directory (default./.run-guard)RESTART_REQUEST_SOURCEpayload source forrequest-restart(defaultmanual)RESTART_REQUEST_COOLDOWN_SECONDSwatcher debounce window (default10)
Feishu currently runs in webhook event-subscription mode:
- Listener:
http(s)://<host>:<feishuListenPort>/feishu/events - Config file keys:
feishuAppId,feishuAppSecret,feishuVerificationToken,feishuListenPort - Assumption: event payloads are not encrypted (no encrypt key)
/helpshow available commands/newstart a fresh ACP session for this conversation/allow <n>select a pending permission option by index (fallback)/denyreject a pending permission request (fallback)/whitelist list|add|del|clearmanage per-conversation permission whitelist bytool_kind(optional prefix scope)/cron help|list|add|del|enable|disablemanage scheduled prompts/lastshow last run output for this session/replay [runId]replay storedsession/updateoutput for a run (best-effort)/ui verbose|summaryset UI verbosity for this conversation/cli show|codex|claudeshow/switch ACP CLI preset for this conversation- Claude preset uses
@zed-industries/claude-code-acp; make sure Claude auth is available (for exampleANTHROPIC_API_KEYor Claude/login). - ACP startup failures now fail fast (exit/timeout) and return an explicit error instead of hanging the conversation.
/workspace show|~|~/...|/abs/pathshow/set per-conversation workspace root (alias:/ws)/helpalso includes ACPavailable_commands_updateentries ascli-inlinecommands (best-effort)
Telegram note:
- Chat-scoped command menu is synced best-effort from
cli-inlinecommands. Commands with-are mapped to_in Telegram UI.
Discord note:
- Built-in commands are available as slash commands (
/help,/ui,/cli,/workspace,/new,/last,/replay,/allow,/deny,/whitelist,/cron). - Slash commands are synced at startup (global + per-guild best-effort). Global command propagation may take time on Discord side.
- ACP
cli-inlinedynamic commands are not yet exposed as Discord slash commands. - Inbound message processing uses reaction acks (
🤔while running, then🕊on success or😢on error), aligned with Telegram behavior. - On fresh ACP sessions, the channel topic/description is injected as a global context block before the user prompt.
- File system and terminal tool calls are restricted to the active workspace root (per conversation; see
/workspace). - Tool execution is deny-by-default; the user must approve via ACP permission flow.
- You can pre-allow specific
tool_kindvalues per conversation via/whitelist add <tool_kind>(read|edit|delete|move|search|execute|think|fetch|switch_mode|other). - You can also scope allow rules by prefix:
/whitelist add read /abs/path/prefix(path kinds) or/whitelist add execute npm run(argument prefix). Non-matching calls still require approval. - If an agent calls a tool directly without first sending
session/request_permission, the gateway synthesizes an interactive permission prompt and blocks the tool call until approved/denied. - Approvals are interactive on Discord/Telegram (buttons). Discord permission cards also add reaction shortcuts (
👍allow,👎deny;✅/❌still accepted);/allow//denyremain as fallback. - You can persist policy choices (e.g.
allow_always/reject_always) per conversation.
summary(default): quieter.verbose: show structured messages for tool execution + plan/task updates.
Set per conversation: /ui verbose|summary.
Tool-call UI is lifecycle-based (started/running/completed) and updates by tool-call id when supported by the channel sink.
Agent text is streamed by editing one message while output is text-only; when a tool call starts, the next agent text segment resumes in a new message.
- Discord:
- DM: isolated per user (DM channel)
- Guild channel: isolated per channel (shared across members in that channel)
- Telegram:
- Private chat: isolated per user
- Group/supergroup/topic: isolated per chat (and topic thread when present)
- Workspace root (
/workspace) and run history follow the same binding scope. /newstarts a fresh ACP session but keeps conversation-scoped preferences (UI mode, workspace root, CLI preset, permission policies).
ACP sessions are process-local; if the gateway restarts (or an idle runtime is GC'ed), the new ACP session would otherwise start "blank".
To reduce this, cli-gateway can replay recent conversation runs from the DB into the first prompt of a fresh ACP session:
- Config keys:
contextReplayEnabled,contextReplayRuns,contextReplayMaxChars - Default: enabled, last 8 runs, max 12k chars (used only on fresh ACP sessions)
- Discord-only: fresh sessions also include channel topic/description as global context.
This repository is in active build-out; expect breaking changes.