// Build the Fabric slash-command catalog from OpenClaw's native-command // specs (the same source Discord uses to register slash commands) and push // it to each connected guild. Fabric is a TEXT-command surface: a / // message is delivered normally and OpenClaw's command system executes it — // this catalog only drives the frontend `/` autocomplete, so we resolve any // dynamic arg `choices` to a static snapshot here (like Discord does at // registration time). import { listNativeCommandSpecsForConfig, findCommandByNativeName, resolveCommandArgChoices, } from 'openclaw/plugin-sdk/native-command-registry'; import { resolveCommandsSyncKey } from './accounts.js'; function normChoice(c) { if (typeof c === 'string') return { value: c, label: c }; const o = c; return { value: String(o.value ?? ''), label: String(o.label ?? o.value ?? '') }; } export function buildFabricCommandSpecs(cfg) { const specs = listNativeCommandSpecsForConfig(cfg, { provider: 'fabric', }); return specs.map((s) => { // ChatCommandDefinition (for argsParsing + dynamic choices provider) const def = findCommandByNativeName(s.name, 'fabric'); const args = (s.args ?? []).map((a) => { const raw = a.choices; let choices = null; if (Array.isArray(raw)) { choices = raw.map(normChoice); } else if (typeof raw === 'function' && def) { try { const r = resolveCommandArgChoices({ command: def, arg: a, cfg: cfg, provider: 'fabric', }); choices = r.map((x) => ({ value: x.value, label: x.label })); } catch { choices = null; } } return { name: String(a.name ?? ''), description: String(a.description ?? ''), type: String(a.type ?? 'string'), required: !!a.required, captureRemaining: !!a.captureRemaining, preferAutocomplete: !!a.preferAutocomplete, choices, }; }); return { name: s.name, nativeName: s.name, description: s.description, acceptsArgs: !!s.acceptsArgs, args, argsParsing: def?.argsParsing ?? 'positional', }; }); } // Push the catalog to every guild the known agents belong to (idempotent; // the catalog is OpenClaw-global, so one PUT per guild is enough). export async function syncFabricCommands(client, cfg, accounts, log) { // Guild C-2: the sync key comes from the channel config only (schema // marks it required). Without it the guild rejects the catalog write. const syncKey = resolveCommandsSyncKey(cfg); if (!syncKey) { log.warn('fabric: channels.fabric.commandsSyncKey is not set — skipping ' + 'slash-command sync (set it to the guild FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY)'); return; } let specs; try { specs = buildFabricCommandSpecs(cfg); } catch (err) { log.warn(`fabric: build command specs failed: ${String(err)}`); return; } if (!specs.length) return; const done = new Set(); for (const a of accounts) { let session; try { session = await client.agentLogin(a.fabricApiKey); } catch { continue; } for (const g of session.guilds) { if (done.has(g.nodeId)) continue; const tok = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token; if (!tok) continue; try { await client.syncCommands(g.endpoint, tok, specs, syncKey); done.add(g.nodeId); log.info(`fabric: synced ${specs.length} slash command(s) -> ${g.nodeId}`); } catch (err) { log.warn(`fabric: command sync failed ${g.nodeId}: ${String(err)}`); } } } }