pi-clickclack is a private local bridge between ClickClack conversations and persistent Pi coding-agent sessions.
ClickClack
⇅ HTTP, realtime WebSocket, agent progress
pi-clickclack
⇅ AgentSession SDK
Pi
The bridge is built for one trusted owner in one ClickClack workspace. Each bound channel or direct conversation keeps one persistent Pi session, pinned to an approved project directory. It streams visible commentary and tool activity, uploads referenced output files, and carries Pi confirmation, selection, input, and editor requests back through ClickClack.
See docs/SCOPE.md for the locked v1 boundary.
- Linux with systemd user services
- Node 24 or newer
- pnpm 11.20.0
- a running ClickClack server
- a ClickClack bot token for the target workspace
- the configured Pi provider credentials in the service environment
The bot token needs the standard bot:write scope bundle plus the explicit agent_activity:write scope. agent_activity:write is not included in any bot bundle. The bridge uses realtime read, channel and DM message read/write, uploads, command publication, workspace/profile reads, ephemeral progress, and durable agent activity.
Install dependencies and build the bridge:
pnpm install --frozen-lockfile
pnpm typecheck
pnpm buildKeep credentials outside the repository. The installer defaults to ~/.config/pi-clickclack/env:
mkdir -p ~/.config/pi-clickclack
cp .env.example ~/.config/pi-clickclack/env
chmod 600 ~/.config/pi-clickclack/env
$EDITOR ~/.config/pi-clickclack/envThe environment file uses ordinary KEY=value lines. JSON arrays must remain on one line.
| Variable | Purpose |
|---|---|
CLICKCLACK_URL |
ClickClack server base URL. |
CLICKCLACK_WORKSPACE_ID |
Workspace the bot token is bound to. |
CLICKCLACK_BOT_TOKEN |
Bot token with bot:write,agent_activity:write. |
CLICKCLACK_OWNER_IDS |
Comma-separated user IDs allowed to invoke Pi or answer prompts. |
CLICKCLACK_PI_PROJECTS |
JSON array of approved { "alias", "cwd" } project bindings. Every path must be absolute and already exist. |
CLICKCLACK_PI_INVOCATIONS |
Optional JSON array of preconfigured channel or direct-conversation invocation modes. |
CLICKCLACK_PI_MODEL |
Pi model in provider/model-id form. |
CLICKCLACK_PI_THINKING_LEVEL |
off, minimal, low, medium, high, xhigh, or max. |
CLICKCLACK_GIT_ACTIVITY_CHANNEL_ID |
Optional channel for durable commit, push, and merge cards. |
CLICKCLACK_PI_STATE_PATH |
SQLite state path. Defaults to ~/.local/state/pi-clickclack/state.sqlite. |
CLICKCLACK_PI_AGENT_DIR |
Pi agent directory. Defaults to ~/.pi/agent. |
Example project and invocation configuration:
CLICKCLACK_PI_PROJECTS=[{"alias":"clickclack","cwd":"/home/user/dev/clickclack"},{"alias":"bridge","cwd":"/home/user/dev/pi-clickclack"}]
CLICKCLACK_PI_INVOCATIONS=[{"conversationType":"channel","conversationId":"chn_replace_me","mode":"mention"},{"conversationType":"direct","conversationId":"dcn_replace_me","mode":"auto"}]Channels support mention or always. Direct conversations use auto.
Run the interactive helper:
pnpm persona:addIt asks for the project alias, display name, handle, and directory, then creates the user-owned ClickClack bot, writes its isolated 0600 environment file, builds the bridge, installs a named systemd service, starts it, and verifies that it is active. Existing ~/.config/pi-clickclack/env values are used as defaults for the ClickClack URL, workspace, owner, Pi model, thinking level, and agent directory.
For example, utmco, утмсо, utmco, and /home/user/dev/utmco create @utmco backed by pi-clickclack-utmco.service and locked to that directory.
Preview without creating anything:
pnpm persona:add -- --dry-runEvery prompt also has a flag for automation. Run pnpm persona:add -- --help for the complete list. Each channel or DM with the bot receives its own persistent Pi session, while every session is pinned to the persona's single project directory.
The lower-level service:install -- --persona <alias> command remains available for existing external environment files. It rejects multi-project configuration and implicit shared state paths. Legacy single-service mode remains available for one bridge that can switch between approved projects.
Install, enable, and start the legacy user service:
pnpm service:install -- --repo "$PWD" --env "$HOME/.config/pi-clickclack/env" --startThe installer writes ~/.config/systemd/user/pi-clickclack.service, pins the current Node executable and checkout, sets the environment file to mode 0600, and verifies that the service became active. It refuses environment files inside the repository. If an older installation has systemd drop-ins, review them and rerun with --replace-overrides to archive them before installing the new unit.
Preview the generated unit without changing systemd:
pnpm service:install -- --repo "$PWD" --env "$HOME/.config/pi-clickclack/env" --dry-runThe bridge publishes its supported commands to ClickClack:
/project <alias>binds the conversation to an approved project./invoke [mention|always]shows or changes a channel's invocation mode./continueresumes the latest recoverable Pi session./compact [instructions]compacts the current Pi session./newarchives the current session and starts a replacement./name [name]shows or sets the session name./sessionshows session and context statistics./model [provider/model]shows or changes the session model./thinking [level]shows or changes the thinking level./fast [on|off]toggles or sets native priority service for the current model family. the desktop fast toggle uses this command, and its status comes from omp's active request tier rather than extension status text./reasoning [stream|off]shows or changes whether Pi's intermediate working commentary streams before tool batches. Streaming is the default;offdrops that commentary for this conversation until the bridge restarts. Provider reasoning summaries stay hidden./reloadreloads Pi extensions, skills, prompts, and context files./copyposts the latest assistant answer as a new ClickClack message.
Compatible extension and prompt-template commands are added after a project runtime loads. Skill commands contain a colon and must be typed directly because ClickClack command names do not accept colons.
SQLite stores the realtime cursor, source-message claims, conversation bindings, Pi session references, active turns, pending interactive requests, steering receipts, and outbound nonce reconciliation state.
- Replayed or duplicate realtime events cannot start a second turn for the same source message.
- Reconnect catches up from the committed cursor before opening a new WebSocket.
- Durable messages use deterministic nonces. A create whose response was lost is checked through ClickClack nonce lookup before another create is attempted.
- A process restart archives the interrupted Pi runtime, removes its pending interaction, and clears its ephemeral progress frame. The owner must explicitly use
/continueto reopen recoverable session history. - Ambiguous steering is never replayed automatically. The bridge posts a nonce-protected uncertainty notice when the original authorization and binding still match.
- Generated uploads use deterministic upload nonces and reconcile a lost create response through upload lookup.
Pi remains the source of truth for session history. ClickClack remains the source of truth for chat messages. The bridge does not keep a third transcript.
Run the deterministic local checks with Bun 1.4.2:
bun run typecheck
bun run build
bun run testThe standard suite covers duplicate events, reconnect catch-up, restart cleanup, abort and steering races, nonce reconciliation, interaction timeout/cancellation, progress throttling, generated files, and SQLite migration rollback.
See docs/smoke-suite.md for the opt-in installed-injector and isolated live read-only probes.
Legacy service:
systemctl --user status pi-clickclack.service
journalctl --user -u pi-clickclack.service -f
systemctl --user restart pi-clickclack.service
systemctl --user stop pi-clickclack.serviceProject persona service:
systemctl --user status pi-clickclack-clickclack.service
journalctl --user -u pi-clickclack-clickclack.service -f
systemctl --user restart pi-clickclack-clickclack.service
systemctl --user stop pi-clickclack-clickclack.serviceA healthy start logs bridge service started with the bot, workspace, configured aliases, command count, and runtime kind. Structured logs redact configured tokens and provider credentials.
Back up SQLite before changing binaries:
state=${CLICKCLACK_PI_STATE_PATH:-$HOME/.local/state/pi-clickclack/state.sqlite}
mkdir -p "$HOME/.local/state/pi-clickclack/backups"
sqlite3 "$state" ".backup '$HOME/.local/state/pi-clickclack/backups/state-$(date +%Y%m%d-%H%M%S).sqlite'"Then update and cut over:
systemctl --user stop pi-clickclack.service
pnpm install --frozen-lockfile
pnpm typecheck
pnpm build
pnpm test
pnpm service:install -- --repo "$PWD" --env "$HOME/.config/pi-clickclack/env" --startConfirm systemctl --user is-active pi-clickclack.service and inspect the startup journal before deleting the backup. To roll back, stop the service, restore the previous checkout and build, restore the matching SQLite backup if that release cannot read the migrated schema, rerun the installer, and start the service.
Run the entrypoint with the external environment file to see the configuration error directly:
node --env-file="$HOME/.config/pi-clickclack/env" dist/index.jsThe bridge rejects missing owners, malformed JSON, duplicate aliases, relative project paths, nonexistent project or agent directories, invalid invocation modes, non-bot tokens, and the wrong workspace before opening realtime ingestion.
Check that the sender is in CLICKCLACK_OWNER_IDS. Bind the conversation with /project <alias>. Channels default to mention-only, so mention the bot or use /invoke always in a dedicated channel.
Mint a token with bot:write,agent_activity:write. Confirm the bot can write to the target channel or DM and that its workspace matches CLICKCLACK_WORKSPACE_ID. A token without the explicit activity scope can send ordinary replies but cannot publish commentary or tool rows.
Check the ClickClack server and the service journal. The bridge resumes from its durable cursor after the WebSocket closes. Do not delete the SQLite file to fix a network outage because it also holds replay protection and conversation bindings.
The old progress frame is cleared and the stale runtime is archived. Send /continue to restore the latest recoverable Pi session. Pending confirmations and inputs fail closed and are not answered on the owner's behalf.