diff --git a/CHANGELOG.md b/CHANGELOG.md
index 73d0b4d3..f04b25fe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
## [Unreleased]
### Added
+- **AI caption generation**: Generate platform-optimized cross-post captions from article content using Claude or OpenAI (#161)
+- **AI provider config**: New "AI Caption Generation" fieldset in component options with provider, API key, model, and tone settings
+- **AI Generate button**: One-click AI generation button in the Share Content panel that fills all caption fields
- **X/Twitter threads**: Auto-split messages exceeding 280 chars into reply chains at sentence boundaries
- **X/Twitter cost-optimized posting**: Optional mode to post text-only tweet first ($0.015) with URL as separate reply ($0.20)
- **X/Twitter cost warning**: Language string documenting X API pricing for text vs URL posts
diff --git a/source/packages/com_mokosuitecross/config.xml b/source/packages/com_mokosuitecross/config.xml
index b20dd5b1..2df31bfe 100644
--- a/source/packages/com_mokosuitecross/config.xml
+++ b/source/packages/com_mokosuitecross/config.xml
@@ -227,6 +227,45 @@
/>
+
+
+ COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_NONE
+ COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_CLAUDE
+ COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_OPENAI
+
+
+
+
+ COM_MOKOSUITECROSS_CONFIG_AI_TONE_PROFESSIONAL
+ COM_MOKOSUITECROSS_CONFIG_AI_TONE_FRIENDLY
+ COM_MOKOSUITECROSS_CONFIG_AI_TONE_CASUAL
+
+
+
+ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+namespace Joomla\Component\MokoSuiteCross\Administrator\Controller;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\CMS\MVC\Controller\BaseController;
+use Joomla\CMS\Session\Session;
+use Joomla\Component\MokoSuiteCross\Administrator\Helper\AiGeneratorHelper;
+
+class AiController extends BaseController
+{
+ public function generate(): void
+ {
+ if (!Session::checkToken('get')) {
+ echo json_encode(['success' => false, 'error' => 'Invalid token']);
+ $this->app->close();
+
+ return;
+ }
+
+ $user = $this->app->getIdentity();
+
+ if (!$user->authorise('core.edit', 'com_mokosuitecross')) {
+ echo json_encode(['success' => false, 'error' => 'Permission denied']);
+ $this->app->close();
+
+ return;
+ }
+
+ $articleId = $this->input->getInt('article_id', 0);
+
+ if ($articleId < 1) {
+ echo json_encode(['success' => false, 'error' => 'Missing article ID']);
+ $this->app->close();
+
+ return;
+ }
+
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select($db->quoteName(['id', 'title', 'introtext', 'catid']))
+ ->from($db->quoteName('#__content'))
+ ->where($db->quoteName('id') . ' = ' . $articleId);
+ $db->setQuery($query);
+ $article = $db->loadObject();
+
+ if (!$article) {
+ echo json_encode(['success' => false, 'error' => 'Article not found']);
+ $this->app->close();
+
+ return;
+ }
+
+ $category = '';
+ $catQuery = $db->getQuery(true)
+ ->select($db->quoteName('title'))
+ ->from($db->quoteName('#__categories'))
+ ->where($db->quoteName('id') . ' = ' . (int) $article->catid);
+ $db->setQuery($catQuery);
+ $category = $db->loadResult() ?: '';
+
+ $tagQuery = $db->getQuery(true)
+ ->select($db->quoteName('t.title'))
+ ->from($db->quoteName('#__tags', 't'))
+ ->join('INNER', $db->quoteName('#__contentitem_tag_map', 'm') . ' ON ' . $db->quoteName('m.tag_id') . ' = ' . $db->quoteName('t.id'))
+ ->where($db->quoteName('m.content_item_id') . ' = ' . $articleId)
+ ->where($db->quoteName('m.type_alias') . ' = ' . $db->quote('com_content.article'));
+ $db->setQuery($tagQuery);
+ $tags = $db->loadColumn() ?: [];
+
+ $introtext = strip_tags($article->introtext ?? '');
+ $introtext = mb_substr($introtext, 0, 500);
+
+ $params = \Joomla\CMS\Component\ComponentHelper::getParams('com_mokosuitecross');
+
+ $config = [
+ 'ai_provider' => $params->get('ai_provider', 'none'),
+ 'ai_api_key' => $params->get('ai_api_key', ''),
+ 'ai_model' => $params->get('ai_model', ''),
+ 'ai_tone' => $params->get('ai_tone', 'professional'),
+ ];
+
+ $result = AiGeneratorHelper::generate($article->title, $introtext, $category, $tags, $config);
+
+ $this->app->setHeader('Content-Type', 'application/json; charset=utf-8');
+ echo json_encode($result);
+ $this->app->close();
+ }
+}
diff --git a/source/packages/com_mokosuitecross/src/Helper/AiGeneratorHelper.php b/source/packages/com_mokosuitecross/src/Helper/AiGeneratorHelper.php
new file mode 100644
index 00000000..c5201d51
--- /dev/null
+++ b/source/packages/com_mokosuitecross/src/Helper/AiGeneratorHelper.php
@@ -0,0 +1,196 @@
+
+ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+namespace Joomla\Component\MokoSuiteCross\Administrator\Helper;
+
+defined('_JEXEC') or die;
+
+class AiGeneratorHelper
+{
+ public static function generate(string $title, string $introtext, string $category, array $tags, array $config): array
+ {
+ $provider = $config['ai_provider'] ?? 'none';
+ $apiKey = $config['ai_api_key'] ?? '';
+ $model = $config['ai_model'] ?? '';
+ $tone = $config['ai_tone'] ?? 'professional';
+
+ if ($provider === 'none' || $apiKey === '') {
+ return ['success' => false, 'error' => 'AI provider not configured or API key missing.'];
+ }
+
+ $prompt = self::buildPrompt($title, $introtext, $category, $tags, $tone);
+
+ $response = match ($provider) {
+ 'claude' => self::callClaude($prompt, $apiKey, $model ?: 'claude-haiku-4-5'),
+ 'openai' => self::callOpenAI($prompt, $apiKey, $model ?: 'gpt-4o-mini'),
+ default => '',
+ };
+
+ if ($response === '') {
+ return ['success' => false, 'error' => 'AI provider returned an empty response.'];
+ }
+
+ $parsed = self::parseResponse($response);
+
+ if ($parsed === null) {
+ return ['success' => false, 'error' => 'Could not parse AI response as JSON.'];
+ }
+
+ return ['success' => true, 'data' => $parsed];
+ }
+
+ private static function callClaude(string $prompt, string $apiKey, string $model): string
+ {
+ $payload = json_encode([
+ 'model' => $model,
+ 'max_tokens' => 500,
+ 'messages' => [
+ ['role' => 'user', 'content' => $prompt],
+ ],
+ ]);
+
+ $ch = curl_init('https://api.anthropic.com/v1/messages');
+ curl_setopt_array($ch, [
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $payload,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 30,
+ CURLOPT_HTTPHEADER => [
+ 'Content-Type: application/json',
+ 'x-api-key: ' . $apiKey,
+ 'anthropic-version: 2023-06-01',
+ ],
+ ]);
+
+ $response = curl_exec($ch);
+
+ if ($response === false) {
+ curl_close($ch);
+ return '';
+ }
+
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode < 200 || $httpCode >= 300) {
+ return '';
+ }
+
+ $data = json_decode($response, true);
+
+ return $data['content'][0]['text'] ?? '';
+ }
+
+ private static function callOpenAI(string $prompt, string $apiKey, string $model): string
+ {
+ $payload = json_encode([
+ 'model' => $model,
+ 'max_tokens' => 500,
+ 'messages' => [
+ ['role' => 'user', 'content' => $prompt],
+ ],
+ ]);
+
+ $ch = curl_init('https://api.openai.com/v1/chat/completions');
+ curl_setopt_array($ch, [
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $payload,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 30,
+ CURLOPT_HTTPHEADER => [
+ 'Content-Type: application/json',
+ 'Authorization: Bearer ' . $apiKey,
+ ],
+ ]);
+
+ $response = curl_exec($ch);
+
+ if ($response === false) {
+ curl_close($ch);
+ return '';
+ }
+
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode < 200 || $httpCode >= 300) {
+ return '';
+ }
+
+ $data = json_decode($response, true);
+
+ return $data['choices'][0]['message']['content'] ?? '';
+ }
+
+ private static function buildPrompt(string $title, string $introtext, string $category, array $tags, string $tone): string
+ {
+ $tagList = !empty($tags) ? implode(', ', $tags) : 'none';
+
+ $toneGuide = match ($tone) {
+ 'casual' => 'Use a relaxed, conversational tone.',
+ 'friendly' => 'Use a warm, approachable tone with enthusiasm.',
+ default => 'Use a professional, polished tone.',
+ };
+
+ return << mb_substr($data['social'], 0, 500),
+ 'short' => mb_substr($data['short'], 0, 280),
+ 'chat' => mb_substr($data['chat'], 0, 500),
+ 'email_subject' => mb_substr($data['email_subject'], 0, 120),
+ ];
+ }
+}
diff --git a/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php
index 376b4003..57d8eb15 100644
--- a/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php
+++ b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php
@@ -212,8 +212,53 @@ XML;
$form->load($xml);
- // Cross-post history panel for existing articles
+ // AI Generate button for the Share Content panel
$articleId = Factory::getApplication()->input->getInt('id', 0);
+ $aiParams = ComponentHelper::getParams('com_mokosuitecross');
+ $aiEnabled = \in_array($aiParams->get('ai_provider', 'none'), ['claude', 'openai'], true);
+
+ if ($aiEnabled && $articleId > 0) {
+ $aiToken = Session::getFormToken();
+ $aiUrl = Uri::base() . 'index.php?option=com_mokosuitecross&task=ai.generate&format=raw&article_id=' . $articleId . '&' . $aiToken . '=1';
+
+ $aiButtonHtml = ''
+ . ''
+ . ' '
+ . \Joomla\CMS\Language\Text::_('COM_MOKOSUITECROSS_AI_GENERATE')
+ . ' '
+ . ' '
+ . '
'
+ . '';
+
+ $aiXml = '
+';
+ $form->load($aiXml);
+ $form->setFieldAttribute('mokosuitecross_ai_generate', 'description', $aiButtonHtml, 'attribs');
+ }
+
+ // Cross-post history panel for existing articles
if ($articleId > 0) {
$query = $db->getQuery(true)