// 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 type { FabricClient } from './fabric-client.js'; import { resolveCommandsSyncKey } from './accounts.js'; type Logger = { info: (m: string) => void; warn: (m: string) => void }; type FabricArg = { name: string; description: string; type: string; required: boolean; captureRemaining: boolean; preferAutocomplete: boolean; choices: Array<{ value: string; label: string }> | null; }; type FabricCommand = { name: string; nativeName: string; description: string; acceptsArgs: boolean; args: FabricArg[]; argsParsing: string; }; function normChoice(c: unknown): { value: string; label: string } { if (typeof c === 'string') return { value: c, label: c }; const o = c as { value?: string; label?: string }; return { value: String(o.value ?? ''), label: String(o.label ?? o.value ?? '') }; } export function buildFabricCommandSpecs(cfg: unknown): FabricCommand[] { const specs = listNativeCommandSpecsForConfig(cfg as never, { provider: 'fabric', }) as Array<{ name: string; description: string; acceptsArgs?: boolean; args?: Array>; }>; return specs.map((s) => { // ChatCommandDefinition (for argsParsing + dynamic choices provider) const def = findCommandByNativeName(s.name, 'fabric') as | { argsParsing?: string; args?: Array> } | undefined; const args: FabricArg[] = (s.args ?? []).map((a) => { const raw = a.choices; let choices: Array<{ value: string; label: string }> | null = null; if (Array.isArray(raw)) { choices = raw.map(normChoice); } else if (typeof raw === 'function' && def) { try { const r = resolveCommandArgChoices({ command: def as never, arg: a as never, cfg: cfg as never, provider: 'fabric', }) as Array<{ value: string; label: string }>; 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: FabricClient, cfg: unknown, accounts: Array<{ agentId: string; fabricApiKey: string }>, log: Logger, ): Promise { // 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 as never); 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: FabricCommand[]; 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)}`); } } } }