Raven ScottandCursor a638118caf feat(agentctl): joinFirstStage step for stage channel scenarios (v0.8.385)
Headless and attached run paths support join-stage IPC; used by
agentctl-stage-join.json in test:agentctl-phase422-voice.

Co-authored-by: Cursor <[email protected]>
2026-05-23 15:53:40 -04:00

pearcord-agentctl

Agent control plane for Pearcord — send the same IPC messages the UI sends, read view snapshots, and run scripted journeys against a live worker or an in-process headless session.

Mission

Enable autonomous agents (and developers) to drive Pearcord like a real user without clicking the Electron UI: register, create guilds, select channels, send messages, and assert on platform state.

Modes

Mode When How
Attached Desktop pear run with PEARCORD_AGENTCTL=1 TCP JSON-lines on 127.0.0.1:39482 (configurable)
Headless CI, smokes, agent scripts HeadlessSession + pearcord-platform + pearcord-ui-flow in-process

Both modes use pearcord-ui-flow dispatchUiMessage — the same handler as apps/pearcord/index.js.

AgentClient resolves RPC replies before treating { event: "state" | "sidecar" } push lines as events (fixes wait-event responses that include an event field).

Quick start

1. Start Pearcord with the control server

cd apps/pearcord
PEARCORD_AGENTCTL=1 pear run

Worker logs: agentctl listening {"host":"127.0.0.1","port":39482}.

2. Ping from another terminal

cd apps/pearcord
npm run agentctl:ping
# or
bare ../../modules/pearcord-agentctl/bin/agentctl.cjs ping

3. Drive IPC

npm run agentctl:ipc -- '{"type":"register","username":"agent1","displayName":"Agent One"}'
npm run agentctl:wait -- '{"onboarded":true}' --timeout 30000
npm run agentctl:ipc -- '{"type":"create-guild","name":"Agent Guild"}'
npm run agentctl:wait -- '{"mode":"guild"}'
npm run agentctl:view

4. Headless scenario (no GUI)

npm run agentctl:headless -- scenarios/agentctl-onboard.json

Protocol (TCP)

One JSON object per line (newline-delimited).

Requests

op Fields Description
ping Health check
ipc payload UI IPC object (type, …)
view light? Current platform view
wait match, timeoutMs?, light? Block until view matches partial spec
wait-event match, timeoutMs?, since? Block until sidecar error / log event
run scenario Run scenario JSON on attached worker
log-tail lines? Last N lines of pearcord.log
shutdown Close agentctl server

Responses

{ "id": 1, "ok": true, "view": { ... } }
{ "id": 2, "ok": false, "error": "wait timeout", "view": { ... } }

Events (server → client, no id)

{ "event": "state", "view": { ... } }
{ "event": "sidecar", "payload": { "type": "error", "message": "..." }, "at": 1734567890123 }

Set PEARCORD_AGENTCTL_TOKEN to require the same secret on every request when the server is enabled.

Environment

Variable Default Role
PEARCORD_AGENTCTL off 1 enables TCP server in worker
PEARCORD_AGENTCTL_HOST 127.0.0.1 Bind/connect host
PEARCORD_AGENTCTL_PORT 39482 TCP port
PEARCORD_STORAGE ~/.config/pearcord Log tail path
PEARCORD_LOG_LEVEL info Headless session logging

API

const { AgentServer, AgentClient, HeadlessSession, matchView } = require('pearcord-agentctl')

See docs/AGENTCTL.md for full reference, security notes, and scenario format.

Security

  • Binds localhost only — not exposed to the LAN.
  • Optional PEARCORD_AGENTCTL_TOKEN shared secret; still localhost-only.
  • Can execute any UI IPC type (moderation, guild admin, etc.) — same power as the logged-in user session.

Smoke tests

cd apps/pearcord
npm run test:agentctl-headless
npm run test:agentctl-slash
npm run test:agentctl-dm-voice
npm run test:agentctl-wait-event
npm run test:agentctl-server-wire
npm run test:agentctl-leave-voice-noop
npm run test:agentctl-settings-deep-link
npm run test:agentctl-settings-navigate
npm run test:agentctl-settings-guild-deep-link
npm run test:agentctl-mesh-channel-mention
npm run test:agentctl-composer-channel-mention

Bundled scenarios under apps/pearcord/scenarios/:

File Purpose
agentctl-onboard.json Register + onboard
agentctl-slash.json Slash command roundtrip
agentctl-dm.json DM open
agentctl-voice.json Join first voice channel
agentctl-deep-link.json pearcord://explore navigate
agentctl-composer-mention.json @everyone message
agentctl-leave-voice-noop.json leave-voice when disconnected (v0.8.257)
agentctl-settings-deep-link.json format-settings-deep-link IPC (v0.8.257)
agentctl-settings-navigate.json navigate-deep-linkpearcord://settings (v0.8.258)
agentctl-settings-guild-deep-link.json Register + guild + pearcord://settings?scope=guild&guildId=… (v0.8.259)
agentctl-composer-channel-mention.json Send @channel message (v0.8.258)
agentctl-guild-search.json Guild-wide update-search hit (v0.8.260)
agentctl-thread-mesh.json Host thread + reply; guest mesh sync (v0.8.260)
agentctl-forum-search.json Forum channel + post + forum-scoped search (v0.8.261)
agentctl-mark-channel-read.json markActiveChannelRead on guild text channel (v0.8.261)
agentctl-forum-search-mesh.json Forum search with includeMesh: true (v0.8.262)
agentctl-thread-notif-mark-read.json Thread from message + markThreadAndParentRead (v0.8.262)
agentctl-guild-search-mesh.json Guild-wide search with mesh enabled (v0.8.263)
agentctl-guild-load-supersede.json Second create-guild + beta search not served from cache (v0.8.263)
agentctl-channel-search.json Channel-scoped update-search hit (v0.8.264)
agentctl-mark-read-mention-alerts.json Seed mention alert + markActiveChannelRead clears mentionAlerts (v0.8.264)
agentctl-forum-channel-cache.json Forum search then switch forum → empty hits (v0.8.265)
agentctl-thread-parent-perms.json Thread exposes parentChannelPermissions in view (v0.8.265)
agentctl-channel-search-cache.json Channel search empty after switching text channels (v0.8.266)
agentctl-search-unchanged.json Duplicate update-searchsearchUnchanged + cached (v0.8.270)
agentctl-notif-thread-place.json Thread notification place includes parent channel (v0.8.270)
agentctl-search-recent-dedupe.json Guild + channel recent rows for same query (v0.8.271)
agentctl-mark-all-notifications.json Seed inbox + mark-all-notifications-read (v0.8.272)
agentctl-forum-search-refresh.json Forum refresh-search with mesh off (v0.8.272)
agentctl-guild-search-refresh.json Guild refresh-search with mesh off (v0.8.273)
agentctl-thread-inbox-mark-read.json Thread inbox seed + markLatestNotificationRead (v0.8.273)
agentctl-channel-search-refresh.json Channel refresh-search with mesh off (v0.8.274)
agentctl-thread-parent-mark-read.json Thread parent select + markActiveChannelRead (v0.8.274)
agentctl-thread-mark-read-parent-roundtrip.json Thread mark-read, parent nav, return + markThreadAndParentRead (v0.8.275)
agentctl-guild-search-mesh-toggle.json Guild search mesh-off then mesh-on (v0.8.276)
agentctl-thread-inbox-mark-channel-read.json Thread inbox + parent markActiveChannelRead (v0.8.276)
agentctl-channel-search-mesh-toggle.json Channel search mesh-off then mesh-on (v0.8.277)
agentctl-forum-thread-parent-mark-read.json Forum post thread + parent mark-read (v0.8.277)
agentctl-forum-search-mesh-toggle.json Forum search mesh-off then mesh-on (v0.8.278)
agentctl-thread-inbox-mark-thread-parent.json Thread inbox + markThreadAndParentRead (v0.8.278)
agentctl-forum-search-refresh-cache-hit.json Forum search refresh with warm cache (v0.8.279)
agentctl-thread-mesh-mark-read.json Thread mesh + markThreadAndParentRead (v0.8.279; live mesh)
agentctl-thread-search-mesh-mark-read.json Thread search mesh toggle + mark-read fallback (v0.8.279)
agentctl-forum-thread-parent-navigate-mark-read.json Forum thread inbox navigate + parent mark-read (v0.8.280)
agentctl-thread-inbox-navigate-mark-thread-parent.json Thread inbox open-notification + mark-thread-parent (v0.8.281)
agentctl-thread-mesh-navigate-mark-thread-parent.json Mesh thread sync + inbox navigate + mark-thread-parent (v0.8.282)
agentctl-channel-search-mesh-stale-refresh.json Channel mesh-off/on + refresh-search (v0.8.283)
agentctl-guild-search-mesh-stale-refresh.json Guild mesh-off/on + refresh-search (v0.8.284)
agentctl-forum-search-mesh-stale-refresh.json Forum mesh-off/on + refresh-search (v0.8.285)
agentctl-voice-leave-roundtrip.json Join first voice channel then leave-voice clears occupancy (v0.8.333)
agentctl-attachment-send.json Fixture stage + send message with attachmentIds (v0.8.384)
agentctl-attachment-preview.json PNG stage + preview IPC event (v0.8.384)
agentctl-stage-join.json Create stage channel + join-stage (v0.8.385)

mesh.connectGuildMeshThread(scenarioPath) — optional markThreadAndParentRead: true in scenario JSON (v0.8.279). host steps + invite + create-thread + reply; guest must receive threadReply token.

Scenario step extras (headless + attached run): joinFirstVoice, joinFirstStage, openDmPeer, selectFirstForumChannel, selectSecondForumChannel, selectFirstTextChannel, selectSecondTextChannel, selectChannelNamed, selectChannelOtherThanActive, selectParentOfActiveThread, selectActiveThread, threadName with selectActiveThread, markActiveChannelRead, seedInboxNotification, markLatestNotificationRead, openLatestNotification, seedMentionAlertForActiveChannel, createThreadFromLastMessage, markThreadAndParentRead, toggleReactionOnLastMessage (emoji string or { emoji }), stageFixtureAttachment ({ image: true } for PNG), sendMessageWithStagedAttachments, requestPreviewForStagedAttachment, messagesMin, messageContentIncludes, searchResultsMin, searchResultsMax, searchResultsCached, searchUnchanged, searchRecentIncludes, mentionAlertCountMax, notificationUnreadMin, notificationUnreadMax, reactionCountMin, reactionsInclude, attachmentsOnLastMessageMin, activeChannelName, parentChannelPermissions on wait. waitEvent supports payloadType (e.g. attachment-preview).

Dependencies

  • pearcord-platform, pearcord-ui-flow, pearcord-log
  • bare-tcp for attached mode
S
Description
Pearcord module: agent TCP/headless control plane for live app + CI scenarios
Readme
26 MiB
Languages
JavaScript 100%