diff --git a/.changeset/openclaw-integration.md b/.changeset/openclaw-integration.md new file mode 100644 index 0000000000000..ed5774432e1ae --- /dev/null +++ b/.changeset/openclaw-integration.md @@ -0,0 +1,17 @@ +--- +"@rocket.chat/meteor": minor +--- + +feat: Add OpenClaw AI agent integration + +Adds a new OpenClaw integration module that allows Rocket.Chat users to interact with OpenClaw autonomous AI agents directly from chat channels. + +**Features:** +- `/openclaw` slash command to send prompts to the AI agent +- Incoming webhook endpoint (`/api/v1/openclaw.webhook`) for receiving AI responses +- Admin settings for configuring API URL, auth token, default model, bot username, and thread behavior +- Bot message loop prevention and proper error handling + +**Configuration:** +- Navigate to Admin → Settings → OpenClaw to enable and configure the integration +- Requires a running OpenClaw instance with a valid authentication token diff --git a/apps/meteor/app/openclaw/README.md b/apps/meteor/app/openclaw/README.md new file mode 100644 index 0000000000000..5dad911dd8bfd --- /dev/null +++ b/apps/meteor/app/openclaw/README.md @@ -0,0 +1,54 @@ +# OpenClaw AI Agent Integration + +This module integrates [OpenClaw](https://openclaw.ai), an open-source autonomous AI agent platform, into Rocket.Chat. + +## Features + +- **`/openclaw` slash command**: Send prompts directly to your OpenClaw AI agent from any channel +- **Webhook endpoint**: OpenClaw can post responses back to Rocket.Chat channels via `/api/v1/openclaw.webhook` +- **Admin settings**: Configure API URL, authentication token, default LLM model, and bot behavior + +## Setup + +1. **Enable the integration**: Go to **Admin → Settings → OpenClaw** and enable it +2. **Set the API URL**: Enter your OpenClaw instance URL (e.g., `http://localhost:3080`) +3. **Set the authentication token**: Enter the shared secret token for webhook authentication +4. **Optional**: Configure the default LLM model, bot username, and thread response behavior + +## Usage + +### Slash command + +```bash +/openclaw What is the weather in Berlin today? +``` + +The AI agent will process your prompt and respond in the channel. + +### Webhook (for OpenClaw → Rocket.Chat) + +OpenClaw can POST responses to: + +```json +POST /api/v1/openclaw.webhook +Content-Type: application/json + +{ + "token": "", + "channel_id": "", + "text": "Hello from OpenClaw!", + "thread_id": "" +} +``` + +## Architecture + +```text +Rocket.Chat ──► OpenClaw /hooks/agent (outbound: user messages/commands) +OpenClaw ──► Rocket.Chat /api/v1/openclaw.webhook (inbound: AI responses) +``` + +## Requirements + +- A running OpenClaw instance (self-hosted or cloud) +- A valid authentication token configured on both sides diff --git a/apps/meteor/app/openclaw/server/api/webhook.ts b/apps/meteor/app/openclaw/server/api/webhook.ts new file mode 100644 index 0000000000000..633c10491ff9d --- /dev/null +++ b/apps/meteor/app/openclaw/server/api/webhook.ts @@ -0,0 +1,114 @@ +import { timingSafeEqual } from 'crypto'; + +import { isPlainObject } from '../../../../lib/utils/isPlainObject'; +import { API } from '../../../api/server/api'; +import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; +import { settings } from '../../../settings/server'; +import { openclawLogger } from '../logger'; +import { getOpenClawBotUser, getRoomById } from '../lib/messageHandler'; + +function safeCompare(a: unknown, b: string): boolean { + if (typeof a !== 'string') { + return false; + } + const aBuffer = Buffer.from(a, 'utf8'); + const bBuffer = Buffer.from(b, 'utf8'); + if (aBuffer.length !== bBuffer.length) { + return false; + } + return timingSafeEqual(aBuffer, bBuffer); +} + +API.v1.addRoute( + 'openclaw.webhook', + { authRequired: false }, + { + async post() { + openclawLogger.info({ msg: 'Received OpenClaw webhook callback' }); + + if (settings.get('OpenClaw_Enabled') !== true) { + openclawLogger.warn({ msg: 'OpenClaw webhook received but integration is disabled' }); + return API.v1.failure('OpenClaw integration is disabled'); + } + + const body = this.bodyParams; + if (!isPlainObject(body)) { + return API.v1.failure('Invalid request body'); + } + + const expectedToken = settings.get('OpenClaw_Auth_Token'); + const receivedToken = body.token; + + if (!expectedToken || !safeCompare(receivedToken, expectedToken)) { + openclawLogger.warn({ msg: 'OpenClaw webhook token validation failed' }); + return API.v1.unauthorized('Invalid webhook token'); + } + + const channelId = typeof body.channel_id === 'string' ? body.channel_id : undefined; + const text = typeof body.text === 'string' ? body.text : undefined; + + if (!channelId) { + return API.v1.failure('Missing required field: channel_id'); + } + + if (!text?.trim()) { + return API.v1.failure('Missing required field: text'); + } + + const room = await getRoomById(channelId); + if (!room) { + openclawLogger.error({ msg: 'Target room not found for OpenClaw webhook', channelId }); + return API.v1.failure('Target room not found'); + } + + const botUser = await getOpenClawBotUser(); + if (!botUser || !botUser.username) { + openclawLogger.error({ msg: 'OpenClaw bot user not found' }); + return API.v1.failure('Bot user not found'); + } + + const threadId = typeof body.thread_id === 'string' ? body.thread_id : undefined; + const alias = typeof body.alias === 'string' ? body.alias : 'OpenClaw AI'; + const avatar = typeof body.avatar === 'string' ? body.avatar : ''; + const emoji = typeof body.emoji === 'string' ? body.emoji : ':robot:'; + + const messagePayload = { + text, + channel: `#${room._id}`, + ...(threadId && { tmid: threadId }), + }; + + const defaultValues = { + alias, + avatar, + emoji, + channel: `#${room._id}`, + }; + + try { + const result = await processWebhookMessage( + messagePayload, + { ...botUser, username: botUser.username }, + defaultValues, + ); + + if (!result || result.length === 0) { + openclawLogger.error({ msg: 'Failed to process OpenClaw webhook message' }); + return API.v1.failure('Failed to deliver message'); + } + + openclawLogger.info({ + msg: 'OpenClaw webhook message delivered successfully', + roomId: channelId, + messageCount: result.length, + }); + + return API.v1.success({ message: 'Message delivered' }); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + openclawLogger.error({ msg: 'Error processing OpenClaw webhook', error: errorMsg }); + return API.v1.failure(`Error delivering message: ${errorMsg}`); + } + }, + }, +); diff --git a/apps/meteor/app/openclaw/server/index.ts b/apps/meteor/app/openclaw/server/index.ts new file mode 100644 index 0000000000000..e1c362895bbf4 --- /dev/null +++ b/apps/meteor/app/openclaw/server/index.ts @@ -0,0 +1,3 @@ +import './settings'; +import './slashCommand'; +import './api/webhook'; diff --git a/apps/meteor/app/openclaw/server/lib/messageHandler.ts b/apps/meteor/app/openclaw/server/lib/messageHandler.ts new file mode 100644 index 0000000000000..f01c0907aded6 --- /dev/null +++ b/apps/meteor/app/openclaw/server/lib/messageHandler.ts @@ -0,0 +1,102 @@ +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import { Rooms, Users } from '@rocket.chat/models'; + +import type { OpenClawAgentPayload } from './openclawClient'; +import { sendToAgent, isEnabled } from './openclawClient'; +import { settings } from '../../../settings/server'; +import { openclawLogger } from '../logger'; + +export function shouldProcessMessage(message: IMessage): boolean { + if (!isEnabled()) { + return false; + } + + if (message.t) { + return false; + } + + if (!message.msg || message.msg.trim().length === 0) { + return false; + } + + if (message.bot) { + return false; + } + + const botUsername = settings.get('OpenClaw_Bot_Username'); + if (botUsername && message.u?.username === botUsername) { + return false; + } + + return true; +} + +export function formatMessagePayload( + message: IMessage, + room: IRoom, + callbackUrl?: string, +): OpenClawAgentPayload { + const respondInThread = settings.get('OpenClaw_Respond_In_Thread'); + + return { + message: message.msg || '', + channel_id: room._id, + channel_name: room.name || room._id, + user_id: message.u._id, + user_name: message.u.username || '', + ...(callbackUrl && { callback_url: callbackUrl }), + ...(respondInThread && message._id && { thread_id: message.tmid || message._id }), + }; +} + +export async function forwardMessageToAgent( + message: IMessage, + room: IRoom, + callbackUrl?: string, +): Promise { + if (!shouldProcessMessage(message)) { + return null; + } + + const payload = formatMessagePayload(message, room, callbackUrl); + + openclawLogger.info({ + msg: 'Forwarding message to OpenClaw agent', + messageId: message._id, + roomId: room._id, + }); + + const result = await sendToAgent(payload); + + if (!result.success) { + openclawLogger.error({ + msg: 'Failed to forward message to OpenClaw', + error: result.error, + }); + return null; + } + + return result.response || result.message || null; +} + +export async function getOpenClawBotUser() { + const botUsername = settings.get('OpenClaw_Bot_Username') || 'openclaw.bot'; + + const user = await Users.findOneByUsername(botUsername); + if (user) { + return user; + } + + openclawLogger.debug({ + msg: 'OpenClaw bot user not found, falling back to rocket.cat', + botUsername, + fallback: 'rocket.cat', + }); + + return Users.findOneByUsername('rocket.cat'); +} + +export async function getRoomById(roomId: string): Promise { + const room = await Rooms.findOneById(roomId); + return room || null; +} diff --git a/apps/meteor/app/openclaw/server/lib/openclawClient.ts b/apps/meteor/app/openclaw/server/lib/openclawClient.ts new file mode 100644 index 0000000000000..95731002c2194 --- /dev/null +++ b/apps/meteor/app/openclaw/server/lib/openclawClient.ts @@ -0,0 +1,168 @@ +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; + +import { settings } from '../../../settings/server'; +import { openclawLogger } from '../logger'; + +export interface OpenClawAgentPayload { + message: string; + channel_id: string; + channel_name?: string; + user_id: string; + user_name: string; + callback_url?: string; + model?: string; + thread_id?: string; +} + +export interface OpenClawWakePayload { + event: string; + data: Record; +} + +export interface OpenClawResponse { + success: boolean; + message?: string; + response?: string; + error?: string; +} + +export type OpenClawConfigError = 'NOT_ENABLED' | 'MISSING_API_URL' | 'MISSING_AUTH_TOKEN'; + +function getConfig(): { apiUrl: string; authToken: string; model: string } { + const apiUrl = settings.get('OpenClaw_API_URL'); + const authToken = settings.get('OpenClaw_Auth_Token'); + const model = settings.get('OpenClaw_Default_Model'); + + return { apiUrl, authToken, model }; +} + +function isEnabled(): boolean { + return settings.get('OpenClaw_Enabled') === true; +} + +function validateConfig(): { valid: boolean; error?: OpenClawConfigError } { + if (!isEnabled()) { + return { valid: false, error: 'NOT_ENABLED' }; + } + + const { apiUrl, authToken } = getConfig(); + + if (!apiUrl) { + return { valid: false, error: 'MISSING_API_URL' }; + } + + if (!authToken) { + return { valid: false, error: 'MISSING_AUTH_TOKEN' }; + } + + return { valid: true }; +} + +export async function sendToAgent(payload: OpenClawAgentPayload): Promise { + const validation = validateConfig(); + if (!validation.valid) { + return { success: false, error: validation.error }; + } + + const { apiUrl, authToken, model } = getConfig(); + const url = `${apiUrl.replace(/\/+$/, '')}/hooks/agent`; + + const finalPayload = model && !payload.model ? { ...payload, model } : payload; + + openclawLogger.info({ msg: 'Sending message to OpenClaw agent', url, channel: finalPayload.channel_id }); + openclawLogger.debug({ + msg: 'OpenClaw agent request metadata', + channel: finalPayload.channel_id, + threadId: finalPayload.thread_id, + model: finalPayload.model, + hasCallbackUrl: Boolean(finalPayload.callback_url), + }); + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify(finalPayload), + timeout: 30000, + }); + + const contentType = response.headers.get('content-type') || ''; + let data: Record | null = null; + let malformedJson = false; + + if (contentType.includes('application/json')) { + try { + data = (await response.json()) as Record; + } catch (error) { + malformedJson = true; + openclawLogger.error({ + msg: 'OpenClaw agent returned malformed JSON', + status: response.status, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + if (malformedJson) { + return { success: false, error: 'OpenClaw agent returned malformed JSON' }; + } + + if (!response.ok) { + const errorMsg = data?.error || data?.message || `HTTP ${response.status}: ${response.statusText}`; + openclawLogger.error({ msg: 'OpenClaw agent request failed', status: response.status, error: errorMsg }); + return { success: false, error: String(errorMsg) }; + } + + openclawLogger.info({ msg: 'OpenClaw agent response received', status: response.status }); + + return { + success: true, + response: data?.response as string | undefined, + message: data?.message as string | undefined, + }; + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + openclawLogger.error({ msg: 'OpenClaw agent request error', error: errorMsg }); + return { success: false, error: `Failed to connect to OpenClaw: ${errorMsg}` }; + } +} + +export async function wakeAgent(payload: OpenClawWakePayload): Promise { + const validation = validateConfig(); + if (!validation.valid) { + return { success: false, error: validation.error }; + } + + const { apiUrl, authToken } = getConfig(); + const url = `${apiUrl.replace(/\/+$/, '')}/hooks/wake`; + + openclawLogger.info({ msg: 'Sending wake event to OpenClaw', url, event: payload.event }); + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify(payload), + timeout: 10000, + }); + + if (!response.ok) { + openclawLogger.error({ msg: 'OpenClaw wake request failed', status: response.status }); + return { success: false, error: `HTTP ${response.status}: ${response.statusText}` }; + } + + return { success: true }; + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + openclawLogger.error({ msg: 'OpenClaw wake request error', error: errorMsg }); + return { success: false, error: `Failed to connect to OpenClaw: ${errorMsg}` }; + } +} + +export { isEnabled, validateConfig }; diff --git a/apps/meteor/app/openclaw/server/logger.ts b/apps/meteor/app/openclaw/server/logger.ts new file mode 100644 index 0000000000000..e26852e928a29 --- /dev/null +++ b/apps/meteor/app/openclaw/server/logger.ts @@ -0,0 +1,3 @@ +import { Logger } from '@rocket.chat/logger'; + +export const openclawLogger = new Logger('OpenClaw'); diff --git a/apps/meteor/app/openclaw/server/settings.ts b/apps/meteor/app/openclaw/server/settings.ts new file mode 100644 index 0000000000000..a1a15ff321276 --- /dev/null +++ b/apps/meteor/app/openclaw/server/settings.ts @@ -0,0 +1,65 @@ +import { settingsRegistry } from '../../settings/server'; + +export const createOpenClawSettings = () => + settingsRegistry.addGroup('OpenClaw', async function () { + await this.with( + { + section: 'General', + i18nLabel: 'OpenClaw_General', + }, + async function () { + await this.add('OpenClaw_Enabled', false, { + type: 'boolean', + i18nLabel: 'OpenClaw_Enabled', + i18nDescription: 'OpenClaw_Enabled_Description', + public: true, + }); + + await this.add('OpenClaw_API_URL', '', { + type: 'string', + i18nLabel: 'OpenClaw_API_URL', + i18nDescription: 'OpenClaw_API_URL_Description', + enableQuery: { _id: 'OpenClaw_Enabled', value: true }, + }); + + await this.add('OpenClaw_Auth_Token', '', { + type: 'string', + i18nLabel: 'OpenClaw_Auth_Token', + i18nDescription: 'OpenClaw_Auth_Token_Description', + enableQuery: { _id: 'OpenClaw_Enabled', value: true }, + secret: true, + }); + }, + ); + + await this.with( + { + section: 'Agent', + i18nLabel: 'OpenClaw_Agent', + }, + async function () { + await this.add('OpenClaw_Default_Model', '', { + type: 'string', + i18nLabel: 'OpenClaw_Default_Model', + i18nDescription: 'OpenClaw_Default_Model_Description', + enableQuery: { _id: 'OpenClaw_Enabled', value: true }, + }); + + await this.add('OpenClaw_Bot_Username', 'openclaw.bot', { + type: 'string', + i18nLabel: 'OpenClaw_Bot_Username', + i18nDescription: 'OpenClaw_Bot_Username_Description', + enableQuery: { _id: 'OpenClaw_Enabled', value: true }, + }); + + await this.add('OpenClaw_Respond_In_Thread', true, { + type: 'boolean', + i18nLabel: 'OpenClaw_Respond_In_Thread', + i18nDescription: 'OpenClaw_Respond_In_Thread_Description', + enableQuery: { _id: 'OpenClaw_Enabled', value: true }, + }); + }, + ); + }); + +void createOpenClawSettings(); diff --git a/apps/meteor/app/openclaw/server/slashCommand.ts b/apps/meteor/app/openclaw/server/slashCommand.ts new file mode 100644 index 0000000000000..cce895a184cf9 --- /dev/null +++ b/apps/meteor/app/openclaw/server/slashCommand.ts @@ -0,0 +1,113 @@ +import { api } from '@rocket.chat/core-services'; +import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +import { sendMessage } from '../../lib/server/functions/sendMessage'; +import { forwardMessageToAgent, getRoomById, getOpenClawBotUser } from './lib/messageHandler'; +import { validateConfig } from './lib/openclawClient'; +import { openclawLogger } from './logger'; +import { i18n } from '../../../server/lib/i18n'; +import { settings } from '../../settings/server'; +import { slashCommands } from '../../utils/server/slashCommand'; + +const CONFIG_ERROR_KEY_MAP = { + NOT_ENABLED: 'OpenClaw_Error_Not_Enabled', + MISSING_API_URL: 'OpenClaw_Error_Missing_API_URL', + MISSING_AUTH_TOKEN: 'OpenClaw_Error_Missing_Auth_Token', +} as const; + +slashCommands.add({ + command: 'openclaw', + callback: async function OpenClaw({ params, message, userId }: SlashCommandCallbackParams<'openclaw'>): Promise { + const user = await Users.findOneById(userId); + if (!user) { + return; + } + + const userLanguage = user.language || settings.get('language') || 'en'; + + const validation = validateConfig(); + if (!validation.valid && validation.error) { + const i18nKey = CONFIG_ERROR_KEY_MAP[validation.error] ?? 'OpenClaw_Error_Not_Enabled'; + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t(i18nKey, { lng: userLanguage }), + ...(message.tmid && { tmid: message.tmid }), + }); + return; + } + + const prompt = params?.trim(); + if (!prompt) { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t('OpenClaw_Usage_Hint', { lng: userLanguage }), + ...(message.tmid && { tmid: message.tmid }), + }); + return; + } + + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: `:hourglass_flowing_sand: ${i18n.t('OpenClaw_Processing', { lng: userLanguage })}`, + ...(message.tmid && { tmid: message.tmid }), + }); + + const room = await getRoomById(message.rid); + if (!room) { + openclawLogger.error({ msg: 'Room not found for slash command', roomId: message.rid }); + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: `:x: ${i18n.t('OpenClaw_Room_Not_Found', { lng: userLanguage })}`, + ...(message.tmid && { tmid: message.tmid }), + }); + return; + } + + const agentMessage = { + ...message, + msg: prompt, + u: { + _id: user._id, + username: user.username || '', + name: user.name || '', + }, + }; + + const responseText = await forwardMessageToAgent(agentMessage, room); + + if (!responseText) { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: `:x: ${i18n.t('OpenClaw_No_Response', { lng: userLanguage })}`, + ...(message.tmid && { tmid: message.tmid }), + }); + return; + } + + const respondInThread = settings.get('OpenClaw_Respond_In_Thread'); + + const botUser = await getOpenClawBotUser(); + if (botUser) { + await sendMessage( + botUser, + { + msg: responseText, + rid: message.rid, + ...(respondInThread && message.tmid && { tmid: message.tmid }), + }, + room, + ); + } else { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: responseText, + ...(respondInThread && message.tmid && { tmid: message.tmid }), + }); + } + + openclawLogger.info({ + msg: 'OpenClaw slash command response delivered', + userId, + roomId: message.rid, + }); + }, + options: { + description: 'OpenClaw_Slash_Description', + params: 'OpenClaw_Slash_Params', + }, +}); diff --git a/apps/meteor/server/importPackages.ts b/apps/meteor/server/importPackages.ts index 645ff19932e0a..dd4c52ddb665d 100644 --- a/apps/meteor/server/importPackages.ts +++ b/apps/meteor/server/importPackages.ts @@ -41,6 +41,7 @@ import '../app/message-mark-as-unread/server'; import '../app/message-pin/server'; import '../app/message-star/server'; import '../app/nextcloud/server'; +import '../app/openclaw/server'; import '../app/oauth2-server-config/server'; import '../app/push-notifications/server'; import '../app/retention-policy/server'; diff --git a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/config.php b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/config.php index ed9c74b2609f4..b85a79e6b4175 100644 --- a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/config.php +++ b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/config.php @@ -16,7 +16,7 @@ 'technicalcontact_name' => 'Administrator', 'technicalcontact_email' => 'na@example.org', 'timezone' => null, - 'logging.level' => SimpleSAML_Logger::DEBUG, + 'logging.level' => SimpleSAML\Logger::DEBUG, 'logging.handler' => 'errorlog', //'logging.format' => '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg', 'logging.facility' => defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER, diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 52f55dc1708db..b0e97a2d7612d 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -4004,6 +4004,30 @@ "Open_settings": "Open settings", "Open_sidebar": "Open sidebar", "Open_thread": "Open Thread", + "OpenClaw": "OpenClaw", + "OpenClaw_Agent": "Agent Configuration", + "OpenClaw_Auth_Token": "Authentication Token", + "OpenClaw_Auth_Token_Description": "Shared secret token for authenticating with the OpenClaw instance. Must match the token configured in OpenClaw.", + "OpenClaw_API_URL": "API URL", + "OpenClaw_API_URL_Description": "The base URL of your OpenClaw instance (e.g., http://localhost:3080).", + "OpenClaw_Bot_Username": "Bot Username", + "OpenClaw_Bot_Username_Description": "The username that the OpenClaw bot will use to post messages. This user must exist in Rocket.Chat.", + "OpenClaw_Default_Model": "Default LLM Model", + "OpenClaw_Default_Model_Description": "The default language model to use for AI responses (e.g., gpt-4, claude-3, deepseek). Leave empty to use OpenClaw's default.", + "OpenClaw_Enabled": "Enable OpenClaw Integration", + "OpenClaw_Enabled_Description": "Enable the OpenClaw AI agent integration to allow users to interact with AI agents from Rocket.Chat.", + "OpenClaw_General": "General Settings", + "OpenClaw_No_Response": "**OpenClaw:** No response received from the AI agent. Please try again.", + "OpenClaw_Processing": "**OpenClaw:** Processing your request...", + "OpenClaw_Respond_In_Thread": "Respond in Thread", + "OpenClaw_Respond_In_Thread_Description": "When enabled, OpenClaw responses will be posted as thread replies instead of channel messages.", + "OpenClaw_Slash_Description": "Send a prompt to the OpenClaw AI agent", + "OpenClaw_Slash_Params": "your_prompt_here", + "OpenClaw_Usage_Hint": "Usage: `/openclaw ` — Send a prompt to the OpenClaw AI agent.", + "OpenClaw_Room_Not_Found": "**OpenClaw:** Room not found.", + "OpenClaw_Error_Not_Enabled": ":warning: **OpenClaw:** The integration is not enabled. Enable it in Admin → Settings → OpenClaw.", + "OpenClaw_Error_Missing_API_URL": ":warning: **OpenClaw:** The API URL is not configured. Set it in Admin → Settings → OpenClaw.", + "OpenClaw_Error_Missing_Auth_Token": ":warning: **OpenClaw:** The authentication token is not configured. Set it in Admin → Settings → OpenClaw.", "Opened": "Opened", "Opened_in_a_new_window": "Opened in a new window.", "Opens_a_channel_group_or_direct_message": "Opens a channel, group or direct message",