channel enum + X_TYPES + realtime XType gain 'dm'. dm channels are
forced private (never public) and non-unique (no dedup; create()
always makes a fresh one). computeWakeup: dm wakes every non-author
participant unconditionally (no rotation / no wake_mapping). The
message.created realtime payload now carries xType so the plugin can
treat dm specially.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
node dist/cli/print-commands-sync-key.js (npm run print:commands-key)
outputs FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY as the process sees it,
so an operator can docker-exec it on the deployed guild and copy the
value into the plugin's FABRIC_COMMANDS_SYNC_KEY. --export prints a
ready-to-paste assignment; exits 1 when unset (fallback mode).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C-1: messaging endpoints now enforce channel participation (public
channels open; private require channel_members). authorUserId is
forced to the authenticated user (no more author spoofing); edit/
delete require message-author ownership; history read gated too.
C-2: PUT /commands body strictly validated + size-capped via
SyncCommandsDto (kills catalog poisoning / DoS). Optional
FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY restricts the write to the
plugin when set; never weaker than before when unset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Guild-global slash-command catalog (one row per node guild). The
OpenClaw plugin PUTs the native-command specs (same data Discord
registers as slash commands); the frontend GETs it for / autocomplete.
- GuildCommand entity (guild_id unique, commands json, updatedAt)
- PUT /api/commands -> idempotent full replace (any authed agent/user)
- GET /api/commands -> { commands, updatedAt } (authed)
- stored verbatim (NativeCommandSpec-shaped); execution path unchanged:
a /<cmd> message is delivered as a normal message -> plugin ->
OpenClaw command system (only /no-reply, /force-proceed stay
server-intercepted).
Verified: PUT->{ok,count}, GET round-trips args/choices, no-auth->401.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- channel_turn_state.bypass_user_ids: order and bypass form a disjoint
partition of the channel members; bypass excluded from rotation.
- initForChannel(channelId, members, bypass=[]) computes order = members
− bypass; create() passes bypassUserIds (∩ members) for discuss/work.
- pushFrame() enforces mention nesting cap: max 4 sub-frames (5 levels
incl. root); overflow evicts the bottom-most (root->A..D + E => root->B..E).
- mention sites use pushFrame so bypass members are only transiently
pulled in via @-mention, then return to bypass on pop.
- moveToBypass(): move an order member to bypass mid-rotation; if current
speaker, successor takes over. onMemberRemoved also strips bypass.
- POST /channels/:id/bypass; GET :id/members now returns {userId,bypass}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
package.json type=module, tsconfig module/moduleResolution=NodeNext,
target es2022, explicit .js on all relative imports. Center: jsonwebtoken
& bcryptjs switched to default imports (ESM/CJS interop). Verified:
builds, boots, full auth + plugin round-trip work under ESM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Channel.closed; POST /channels/:id/close (member-only); message/command
posts on closed channel -> 409 {error:channel_closed}; GET history still
allowed; listForUser carries closed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before persist/parse, resolve <@user.name:NAME> (outside backticks) via
Center and rewrite to <@userId>; unresolved tokens left as-is. Translated
ids then flow into the existing mention/wakeup/sub-frame logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- parse <@user-id> outside backtick spans
- general: message with an at-list wakes only the at'd users (else all)
- report/triage/custom: mentions change nothing
- discuss/work: mention by current speaker pushes a sub-rotation frame
(atList = mentions - sender, intersected with channel members); single
linear pass (real/no-reply/force-proceed), then pop back to the saved
parent pointer (resumes at the pusher); nested frames supported
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Channel.x_type enum(general|work|report|discuss|triage|custom); required
and validated on channel creation (400 if missing/invalid).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- new channel_members table; creator always added, plus selected members
- Channel.isPublic (default false): public channels visible to all guild
members; non-public only to explicit members
- GET /channels filters to channels visible to the requesting user
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>