Add createGuildCustomRole, deleteGuildCustomRole, and upsertChannelPermissionOverwrite for roles automation. Co-authored-by: Cursor <[email protected]>
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).
v0.8.486 (Phase 523): Headless sendGuildEmbedMessage(content) and resolveGuildEmbed(content) for link-preview automation. Bundle: npm run test:agentctl-phase523-embeds (app repo; chains test:embed + test:agentctl-embed-message).
v0.8.485 (Phase 522): Headless stageGuildAttachment(opts) and sendGuildMessageWithAttachments(content, attachmentIds, opts) for attachment automation. Bundle: npm run test:agentctl-phase522-attachments (app repo; chains test:attachments + test:agentctl-phase481-attachments).
v0.8.484 (Phase 521): Headless toggleGuildReaction(messageId, emoji) for reaction automation. Bundle: npm run test:agentctl-phase521-reactions (app repo; chains test:reactions + test:agentctl-reaction-toggle).
v0.8.483 (Phase 520): Headless sendGuildMessage(content, opts), editGuildMessage(messageId, content), and deleteGuildMessage(messageId) for messaging automation. Bundle: npm run test:agentctl-phase520-messaging (app repo; chains test:agentctl-phase482-messaging).
v0.8.482 (Phase 519): Headless searchGuildMessages(query, opts) and setSearchScope(scope) for guild search automation. Bundle: npm run test:agentctl-phase519-search (app repo; chains test:parse-search-query + test:search-ranking).
v0.8.391 (Phase 428): Assert keys auditLogMin, auditLogExportFilter, auditLogIncludesAction; sidecar auditExportCountMin, bodyIncludes. Bundle: npm run test:agentctl-phase428-audit.
v0.8.390 (Phase 427): expectIpcError step, wait.banForUserId (DB ban row), waitEvent with automodBlocked, assert keys automodEnabled, automodBlockedKeywordsMin, memberTimeoutFor. Bundle: npm run test:agentctl-phase427-moderation.
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_TOKENshared secret; still localhost-only. - Can execute any UI IPC type (moderation, guild admin, etc.) — same power as the logged-in user session.
v0.8.497 (Phase 534): Headless helpers createGuildCustomRole(name, opts), deleteGuildCustomRole(roleId), and upsertChannelPermissionOverwrite(channelId, payload) for roles automation. Bundle: npm run test:phase534-roles (app repo).
v0.8.496 (Phase 533): Headless helpers createGuildChannel(name, opts), deleteGuildChannel(channelId), and reorderGuildChannels(parentId, channelIds) for guild-admin automation. Bundle: npm run test:phase533-guild-admin (app repo).
v0.8.495 (Phase 532): Headless helpers createGuildInvite(), joinGuildInvite(code), and revokeGuildInvite(code) for invite automation. Bundle: npm run test:phase532-invites (app repo).
v0.8.494 (Phase 531): Headless helpers openDmWithPeer(peerUserId, peerDisplayName), createGroupDm(peerUserIds, name), and sendDmMessage(content, opts) for DM automation. Bundle: npm run test:phase531-dms (app repo).
v0.8.493 (Phase 530): Headless helpers sendFriendRequest(opts), acceptFriendRequest(peerUserId), and declineFriendRequest(peerUserId) for contacts automation. Bundle: npm run test:phase530-contacts (app repo).
v0.8.492 (Phase 529): Headless helpers refreshPublicDiscovery() and setDiscoveryFilter(filter) for explore mesh refresh and filter automation (plus existing listPublicDiscovery, publishDiscoveryListing, joinPublicDiscovery). Bundle: npm run test:phase529-discovery (app repo).
v0.8.491 (Phase 528): Headless helpers setGuildActivity(activity) and clearGuildActivity() for rich-presence automation (plus existing setPresenceStatus, setCustomStatus). Bundle: npm run test:phase528-presence (app repo).
v0.8.490 (Phase 527): Headless helpers playGuildSoundboardSound, upsertGuildSound, removeGuildSound for soundboard roundtrip automation. Bundle: npm run test:phase527-soundboard (app repo).
v0.8.489 (Phase 526): Headless helpers startGuildScreenShare, stopGuildScreenShare for screen share roundtrip automation. Bundle: npm run test:phase526-screen (app repo).
v0.8.488 (Phase 525): Headless helpers joinGuildStageChannel, requestGuildStageSpeak, approveGuildStageSpeaker for stage roundtrip automation. Bundle: npm run test:phase525-stage (app repo).
v0.8.487 (Phase 524): Headless helpers joinGuildVoiceChannel, leaveGuildVoiceChannel, setGuildVoiceMute for voice roundtrip automation. Bundle: npm run test:phase524-voice (app repo).
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-link → pearcord://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-search → searchUnchanged + 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) |
agentctl-role-create.json |
Create custom guild role (v0.8.389) |
agentctl-channel-deny-send.json |
Channel overwrite deny send for @everyone (v0.8.389) |
agentctl-presence-idle-custom.json |
Set idle + custom status (v0.8.388) |
agentctl-activity-playing.json |
Set playing activity (v0.8.388) |
agentctl-emoji-unicode.json |
Send message with unicode 🎉 (v0.8.386) |
agentctl-sticker-send.json |
stageFixtureSticker + sticker message (v0.8.386) |
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, stageFixtureSticker, sendMessageWithStagedStickers, 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, stickersOnLastMessageMin, activeChannelName, parentChannelPermissions on wait. waitEvent supports payloadType (e.g. attachment-preview).
Dependencies
pearcord-platform,pearcord-ui-flow,pearcord-logbare-tcpfor attached mode