From simulator
Sends messages and manages p2p/group chats in Simulator.Company. Chats are actors with participants as access-rule members and messages as reactions. Handles deterministic p2p dedup.
How this skill is triggered — by the user, by Claude, or both
Slash command
/simulator:simulator-chatThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> **Curated tool names (v2 server):** `searchUsers`, `getUsers`, `getSystemActor`,
Curated tool names (v2 server):
searchUsers,getUsers,getSystemActor,getForms,filterActors,createActor,getActorByRef,saveAccessRules,createReaction,getReactions. Call them by these exact names. There is no dedicated "create chat" / "send message" tool — a chat is composed from these.
In Simulator.Company a chat is an actor of the Events system form:
data.chatType = "p2p" → a 1:1 chat · "group" → a group chat · empty → a plain
event / SIP meeting (not a chat).userId) — not
a data field. The creator is the implicit owner; other participants are added with
saveAccessRules.comment reactions posted under the chat actor.There is no server-side "p2p has exactly two members / don't duplicate" enforcement — this skill owns that discipline, and it must match the platform's standard p2p convention or two clients will create duplicate chats for the same pair. The convention:
ref = p2pConversation_<minUserId>_<maxUserId> — both
participants' raw workspace userIds sorted ascending, joined with _. This is the
p2p dedup key; the UI finds/reuses a chat only by this ref.title = <nickA>:p2p:<nickB> — nicks ordered by the same ascending userId sort.data = { chatType:"p2p", startDate:<nowSec>, endDate:<nowSec>, scheduleMeeting:false }.saveAccessRules call (never inline in
the create body); the creator is owner implicitly.Read
$CLAUDE_PLUGIN_ROOT/docs/entities/chats.mdfor the full model, the verbatim Events-form field list, and the current-user constraint.
Reply to the user in their own language.
This is the canonical flow. Each step names the exact tool.
searchUsers(query="N") # accId defaults to the active workspace
Take N's userId from the result (e.g. 4210). If several match, ask the user which
one (or show name + email). getUsers lists everyone if you need to browse.
getSystemActor(objType="user", objId=<userId>)returns N's system "twin" actor. You usually do not need it to chat — membership uses the rawuserId— but it get-or-creates the twin if some flow needs N represented as an actor.
getForms(formTypes="system") # find the row whose title == "Events" → its id
The Events form id differs per workspace, so don't hardcode it. (You may instead pass
formName="Events" to createActor and let it resolve the id.)
The p2p dedup key needs both userIds. The recipient's came from step 1; resolve the
sender's userId too (see the box below), then:
ids = sortAscending(senderUserId, N_userId) # numeric ascending
ref = "p2pConversation_" + ids[0] + "_" + ids[1] # e.g. p2pConversation_4210_4310
title = nick(ids[0]) + ":p2p:" + nick(ids[1]) # e.g. olena:p2p:petro
Getting the sender's userId. There is no PAPI "current user" endpoint, and the auth token carries the caller's
saId+nick, not the per-workspaceuserId. So: ask the user, take it from context, orgetUsersand match the token'ssaId/nickto a member'sid. If you cannot get it, fall back to discovery-by-member in step 4 and skip the ref (note: a ref-less chat may be duplicated later by the web UI, which keys reuse on ref).
Canonical — by the deterministic ref, exactly as the web UI does:
getActorByRef(formId=<EventsFormId>, ref=<ref>) # found → reuse its id, skip to step 6
Fallback when the sender's userId is unknown — discover by participant (still finds UI-created chats, since the recipient is an access member):
filterActors(formId=<EventsFormId>, q="chatType=p2p", members="<N_userId>:view")
createActor(
formName="Events", # or formId=<EventsFormId>
title=<title>, # "<nickA>:p2p:<nickB>"
ref=<ref>, # p2pConversation_<minId>_<maxId> — REQUIRED for UI reuse
data={ "chatType": "p2p",
"startDate": <nowUnixSeconds>,
"endDate": <nowUnixSeconds>,
"scheduleMeeting": false }) # plain unix SECONDS on Events — not a calendar object
startDate/endDate are required by the Events form. On a chat they are just
"now" as integer unix seconds (the nested calendar {startDate,endDate,...} object used
elsewhere does not apply here).
Then add N as the second participant (the creator is owner implicitly — do not add a self access-rule):
saveAccessRules(
objType="actor", objId="<newChatId>",
rules=[{ "action":"create",
"data":{ "userId": <N_userId>,
"privs":{ "view":true, "modify":true, "remove":true } } }])
saveAccessRules is applied asynchronously and returns a taskId; posting the message
next still works.
comment reaction)createReaction(type="comment", actorId="<chatId>", description="<the message text>")
That comment reaction under the chat actor is the chat message. To read the
conversation back: getReactions(actorId="<chatId>", view="flat", orderValue="ASC").
data.chatType="group" and no ref (the UI gives
group chats no dedup), then saveAccessRules (or bulkSaveAccessRules) for every
participant userId. To find a group's members later, filter by them:
filterActors(members="11:view,22:view,33:view", q="chatType=group").parentId=<reactionId> to createReaction (see
simulator-reactions).uploadBase64 → attachId), then
createReaction(..., attachments=[{attachId:<id>}]) — see simulator-attachments.createReaction(..., notify=false).createReaction(..., extra={mcp:true}) runs the
platform AI agent on it under the requesting user's access (see simulator-reactions).ref does.ref on create (p2pConversation_<minId>_<maxId>) so the
web UI reuses the same chat instead of making its own duplicate.data fields. Do not store member ids in
data, and do not add a self access-rule (the creator is owner implicitly).chatType set; an Events actor without it is a plain event/meeting, and
comments on it are ordinary discussion, not chat messages.| Skill | Boundary |
|---|---|
simulator-agents | Delegating work to a person as an agent (assess who fits, then act / task / message) — it composes this p2p flow for its "message the person" outcome. |
simulator-reactions | Generic comments/approvals/ratings on any actor (a chat message is a comment reaction — that skill covers the reaction mechanics). |
simulator-actors | The Events actor's own data fields and the actor data value protocol. |
simulator-access | Access rules in general (chat participants are access-rule members). |
simulator-attachments | Files on a message/actor. |
simulator-init | Login + workspace selection (needed before any of this). |
| Path | When to read |
|---|---|
$CLAUDE_PLUGIN_ROOT/docs/entities/chats.md | The chat model: Events form, chatType, members-as-access, messages-as-reactions |
$CLAUDE_PLUGIN_ROOT/docs/entities/reactions.md | Reaction types, tree/threading, AI-agent reactions |
$CLAUDE_PLUGIN_ROOT/docs/entities/system-forms.md | The Events system form among the built-ins |
accId/workspace defaults to the active one — make sure simulator-init ran first.getForms/formName), never hardcode.startDate/endDate are plain unix seconds, both "now" for a chat.getActorByRef(ref="p2pConversation_<minId>_<maxId>") (matches the web UI); it needs both userIds — the sender's isn't in the token (saId/nick only, no PAPI /me), so derive it or fall back to filterActors(members=…, q="chatType=p2p").createReaction(type="comment", actorId=<chatId>, description=...).npx claudepluginhub corezoid/simulator-ai-plugin --plugin simulatorManages comments, reactions, and threaded discussions attached to Simulator.Company actors. Use to create, update, delete, pin, and read reaction trees.
Interacts with Channel Talk workspaces via CLI - read/send messages, search chats, manage groups, and auto-extract credentials from desktop app or browser.
Read WeCom contacts, send app/group messages, manage WeDoc smart sheets, schedules, and meetings via the WeCom server-side API as a self-built app. Triggered when users mention 企业微信 or WeCom operations.