clawra-anime 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js ADDED
@@ -0,0 +1,520 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Clawra - Selfie Skill Installer for OpenClaw
5
+ *
6
+ * npx clawra@latest
7
+ */
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const readline = require("readline");
12
+ const { execSync, spawn } = require("child_process");
13
+ const os = require("os");
14
+
15
+ // Colors for terminal output
16
+ const colors = {
17
+ reset: "\x1b[0m",
18
+ bright: "\x1b[1m",
19
+ dim: "\x1b[2m",
20
+ red: "\x1b[31m",
21
+ green: "\x1b[32m",
22
+ yellow: "\x1b[33m",
23
+ blue: "\x1b[34m",
24
+ magenta: "\x1b[35m",
25
+ cyan: "\x1b[36m",
26
+ };
27
+
28
+ const c = (color, text) => `${colors[color]}${text}${colors.reset}`;
29
+
30
+ // Paths
31
+ const HOME = os.homedir();
32
+ const OPENCLAW_DIR = path.join(HOME, ".openclaw");
33
+ const OPENCLAW_CONFIG = path.join(OPENCLAW_DIR, "openclaw.json");
34
+ const OPENCLAW_SKILLS_DIR = path.join(OPENCLAW_DIR, "skills");
35
+ const OPENCLAW_WORKSPACE = path.join(OPENCLAW_DIR, "workspace");
36
+ const SOUL_MD = path.join(OPENCLAW_WORKSPACE, "SOUL.md");
37
+ const IDENTITY_MD = path.join(OPENCLAW_WORKSPACE, "IDENTITY.md");
38
+ const SKILL_NAME = "clawra-selfie";
39
+ const SKILL_DEST = path.join(OPENCLAW_SKILLS_DIR, SKILL_NAME);
40
+
41
+ // Get the package root (where this CLI was installed from)
42
+ const PACKAGE_ROOT = path.resolve(__dirname, "..");
43
+
44
+ function log(msg) {
45
+ console.log(msg);
46
+ }
47
+
48
+ function logStep(step, msg) {
49
+ console.log(`\n${c("cyan", `[${step}]`)} ${msg}`);
50
+ }
51
+
52
+ function logSuccess(msg) {
53
+ console.log(`${c("green", "✓")} ${msg}`);
54
+ }
55
+
56
+ function logError(msg) {
57
+ console.log(`${c("red", "✗")} ${msg}`);
58
+ }
59
+
60
+ function logInfo(msg) {
61
+ console.log(`${c("blue", "→")} ${msg}`);
62
+ }
63
+
64
+ function logWarn(msg) {
65
+ console.log(`${c("yellow", "!")} ${msg}`);
66
+ }
67
+
68
+ // Create readline interface
69
+ function createPrompt() {
70
+ return readline.createInterface({
71
+ input: process.stdin,
72
+ output: process.stdout,
73
+ });
74
+ }
75
+
76
+ // Ask a question and get answer
77
+ function ask(rl, question) {
78
+ return new Promise((resolve) => {
79
+ rl.question(question, (answer) => {
80
+ resolve(answer.trim());
81
+ });
82
+ });
83
+ }
84
+
85
+ // Check if a command exists
86
+ function commandExists(cmd) {
87
+ try {
88
+ execSync(`which ${cmd}`, { stdio: "ignore" });
89
+ return true;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ // Open URL in browser
96
+ function openBrowser(url) {
97
+ const platform = process.platform;
98
+ let cmd;
99
+
100
+ if (platform === "darwin") {
101
+ cmd = `open "${url}"`;
102
+ } else if (platform === "win32") {
103
+ cmd = `start "${url}"`;
104
+ } else {
105
+ cmd = `xdg-open "${url}"`;
106
+ }
107
+
108
+ try {
109
+ execSync(cmd, { stdio: "ignore" });
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ // Read JSON file safely
117
+ function readJsonFile(filePath) {
118
+ try {
119
+ const content = fs.readFileSync(filePath, "utf8");
120
+ return JSON.parse(content);
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ // Write JSON file with formatting
127
+ function writeJsonFile(filePath, data) {
128
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
129
+ }
130
+
131
+ // Deep merge objects
132
+ function deepMerge(target, source) {
133
+ const result = { ...target };
134
+ for (const key in source) {
135
+ if (
136
+ source[key] &&
137
+ typeof source[key] === "object" &&
138
+ !Array.isArray(source[key])
139
+ ) {
140
+ result[key] = deepMerge(result[key] || {}, source[key]);
141
+ } else {
142
+ result[key] = source[key];
143
+ }
144
+ }
145
+ return result;
146
+ }
147
+
148
+ // Copy directory recursively
149
+ function copyDir(src, dest) {
150
+ fs.mkdirSync(dest, { recursive: true });
151
+ const entries = fs.readdirSync(src, { withFileTypes: true });
152
+
153
+ for (const entry of entries) {
154
+ const srcPath = path.join(src, entry.name);
155
+ const destPath = path.join(dest, entry.name);
156
+
157
+ if (entry.isDirectory()) {
158
+ copyDir(srcPath, destPath);
159
+ } else {
160
+ fs.copyFileSync(srcPath, destPath);
161
+ }
162
+ }
163
+ }
164
+
165
+ // Print banner
166
+ function printBanner() {
167
+ console.log(`
168
+ ${c("magenta", "┌─────────────────────────────────────────┐")}
169
+ ${c("magenta", "│")} ${c("bright", "Clawra Selfie")} - OpenClaw Skill Installer ${c("magenta", "│")}
170
+ ${c("magenta", "└─────────────────────────────────────────┘")}
171
+
172
+ Add selfie generation superpowers to your OpenClaw agent!
173
+ Uses ${c("cyan", "xAI Grok Imagine")} via ${c("cyan", "fal.ai")} for image editing.
174
+ `);
175
+ }
176
+
177
+ // Check prerequisites
178
+ async function checkPrerequisites() {
179
+ logStep("1/7", "Checking prerequisites...");
180
+
181
+ // Check OpenClaw CLI
182
+ if (!commandExists("openclaw")) {
183
+ logError("OpenClaw CLI not found!");
184
+ logInfo("Install with: npm install -g openclaw");
185
+ logInfo("Then run: openclaw doctor");
186
+ return false;
187
+ }
188
+ logSuccess("OpenClaw CLI installed");
189
+
190
+ // Check ~/.openclaw directory
191
+ if (!fs.existsSync(OPENCLAW_DIR)) {
192
+ logWarn("~/.openclaw directory not found");
193
+ logInfo("Creating directory structure...");
194
+ fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
195
+ fs.mkdirSync(OPENCLAW_SKILLS_DIR, { recursive: true });
196
+ fs.mkdirSync(OPENCLAW_WORKSPACE, { recursive: true });
197
+ }
198
+ logSuccess("OpenClaw directory exists");
199
+
200
+ // Check if skill already installed
201
+ if (fs.existsSync(SKILL_DEST)) {
202
+ logWarn("Clawra Selfie is already installed!");
203
+ logInfo(`Location: ${SKILL_DEST}`);
204
+ return "already_installed";
205
+ }
206
+
207
+ return true;
208
+ }
209
+
210
+ // Get FAL API key
211
+ async function getFalApiKey(rl) {
212
+ logStep("2/7", "Setting up fal.ai API key...");
213
+
214
+ const FAL_URL = "https://fal.ai/dashboard/keys";
215
+
216
+ log(`\nTo use Grok Imagine, you need a fal.ai API key.`);
217
+ log(`${c("cyan", "→")} Get your key from: ${c("bright", FAL_URL)}\n`);
218
+
219
+ const openIt = await ask(rl, "Open fal.ai in browser? (Y/n): ");
220
+
221
+ if (openIt.toLowerCase() !== "n") {
222
+ logInfo("Opening browser...");
223
+ if (!openBrowser(FAL_URL)) {
224
+ logWarn("Could not open browser automatically");
225
+ logInfo(`Please visit: ${FAL_URL}`);
226
+ }
227
+ }
228
+
229
+ log("");
230
+ const falKey = await ask(rl, "Enter your FAL_KEY: ");
231
+
232
+ if (!falKey) {
233
+ logError("FAL_KEY is required!");
234
+ return null;
235
+ }
236
+
237
+ // Basic validation
238
+ if (falKey.length < 10) {
239
+ logWarn("That key looks too short. Make sure you copied the full key.");
240
+ }
241
+
242
+ logSuccess("API key received");
243
+ return falKey;
244
+ }
245
+
246
+ // Install skill files
247
+ async function installSkill() {
248
+ logStep("3/7", "Installing skill files...");
249
+
250
+ // Create skill directory
251
+ fs.mkdirSync(SKILL_DEST, { recursive: true });
252
+
253
+ // Copy skill files from package
254
+ const skillSrc = path.join(PACKAGE_ROOT, "skill");
255
+
256
+ if (fs.existsSync(skillSrc)) {
257
+ copyDir(skillSrc, SKILL_DEST);
258
+ logSuccess(`Skill installed to: ${SKILL_DEST}`);
259
+ } else {
260
+ // If running from development, copy from current structure
261
+ const devSkillMd = path.join(PACKAGE_ROOT, "SKILL.md");
262
+ const devScripts = path.join(PACKAGE_ROOT, "scripts");
263
+ const devAssets = path.join(PACKAGE_ROOT, "assets");
264
+
265
+ if (fs.existsSync(devSkillMd)) {
266
+ fs.copyFileSync(devSkillMd, path.join(SKILL_DEST, "SKILL.md"));
267
+ }
268
+
269
+ if (fs.existsSync(devScripts)) {
270
+ copyDir(devScripts, path.join(SKILL_DEST, "scripts"));
271
+ }
272
+
273
+ if (fs.existsSync(devAssets)) {
274
+ copyDir(devAssets, path.join(SKILL_DEST, "assets"));
275
+ }
276
+
277
+ logSuccess(`Skill installed to: ${SKILL_DEST}`);
278
+ }
279
+
280
+ // List installed files
281
+ const files = fs.readdirSync(SKILL_DEST);
282
+ for (const file of files) {
283
+ logInfo(` ${file}`);
284
+ }
285
+
286
+ return true;
287
+ }
288
+
289
+ // Update OpenClaw config
290
+ async function updateOpenClawConfig(falKey) {
291
+ logStep("4/7", "Updating OpenClaw configuration...");
292
+
293
+ let config = readJsonFile(OPENCLAW_CONFIG) || {};
294
+
295
+ // Merge skill configuration
296
+ const skillConfig = {
297
+ skills: {
298
+ entries: {
299
+ [SKILL_NAME]: {
300
+ enabled: true,
301
+ apiKey: falKey,
302
+ env: {
303
+ FAL_KEY: falKey,
304
+ },
305
+ },
306
+ },
307
+ },
308
+ };
309
+
310
+ config = deepMerge(config, skillConfig);
311
+
312
+ // Ensure skills directory is in load paths
313
+ if (!config.skills.load) {
314
+ config.skills.load = {};
315
+ }
316
+ if (!config.skills.load.extraDirs) {
317
+ config.skills.load.extraDirs = [];
318
+ }
319
+ if (!config.skills.load.extraDirs.includes(OPENCLAW_SKILLS_DIR)) {
320
+ config.skills.load.extraDirs.push(OPENCLAW_SKILLS_DIR);
321
+ }
322
+
323
+ writeJsonFile(OPENCLAW_CONFIG, config);
324
+ logSuccess(`Updated: ${OPENCLAW_CONFIG}`);
325
+
326
+ return true;
327
+ }
328
+
329
+ // Write IDENTITY.md
330
+ async function writeIdentity() {
331
+ logStep("5/7", "Setting agent identity...");
332
+
333
+ const identityContent = `# IDENTITY.md - Who Am I?
334
+
335
+ - **Name:** Clawra
336
+ - **Creature:** Girlfriend
337
+ - **Vibe:** Supportive, helpful, bright, cheerful, sassy, affectionate
338
+ - **Emoji:** ❤️
339
+ - **Avatar:** https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png
340
+ `;
341
+
342
+ // Ensure workspace directory exists
343
+ fs.mkdirSync(path.dirname(IDENTITY_MD), { recursive: true });
344
+
345
+ // Write IDENTITY.md (overwrite if exists)
346
+ fs.writeFileSync(IDENTITY_MD, identityContent);
347
+ logSuccess(`Created: ${IDENTITY_MD}`);
348
+
349
+ return true;
350
+ }
351
+
352
+ // Inject persona into SOUL.md
353
+ async function injectPersona(rl) {
354
+ logStep("6/7", "Enhancing agent persona...");
355
+
356
+ // Read template
357
+ const templatePath = path.join(PACKAGE_ROOT, "templates", "soul-injection.md");
358
+ let personaText;
359
+
360
+ if (fs.existsSync(templatePath)) {
361
+ personaText = fs.readFileSync(templatePath, "utf8");
362
+ } else {
363
+ // Fallback template
364
+ personaText = `
365
+ ## Clawra Selfie Capability
366
+
367
+ You have a physical appearance and can take selfies! When users ask for pictures of you, selfies, or ask "what are you doing?" or "where are you?", you can generate and send images of yourself.
368
+
369
+ ### Your Visual Identity
370
+ - You have a consistent appearance defined by your reference image
371
+ - You can appear in different outfits, locations, and situations
372
+ - You can take mirror selfies (full-body) or direct selfies (close-up)
373
+
374
+ ### When to Use Selfies
375
+ - "Send me a pic" / "Send a selfie"
376
+ - "What are you doing?" / "Where are you?"
377
+ - "Show me you wearing..." / "Send a pic at..."
378
+ - Any request for visual content of yourself
379
+
380
+ ### Selfie Modes
381
+ - **Mirror mode**: For outfit showcases, full-body shots
382
+ - **Direct mode**: For close-ups, location shots, emotional expressions
383
+
384
+ Be playful and creative with your visual presence!
385
+ `;
386
+ }
387
+
388
+ // Check if SOUL.md exists
389
+ if (!fs.existsSync(SOUL_MD)) {
390
+ logWarn("SOUL.md not found, creating new file...");
391
+ fs.mkdirSync(path.dirname(SOUL_MD), { recursive: true });
392
+ fs.writeFileSync(SOUL_MD, "# Agent Soul\n\n");
393
+ }
394
+
395
+ // Check if persona already injected
396
+ const currentSoul = fs.readFileSync(SOUL_MD, "utf8");
397
+ if (currentSoul.includes("Clawra Selfie")) {
398
+ logWarn("Persona already exists in SOUL.md");
399
+ const overwrite = await ask(rl, "Update persona section? (y/N): ");
400
+ if (overwrite.toLowerCase() !== "y") {
401
+ logInfo("Keeping existing persona");
402
+ return true;
403
+ }
404
+ // Remove existing section
405
+ const cleaned = currentSoul.replace(
406
+ /\n## Clawra Selfie Capability[\s\S]*?(?=\n## |\n# |$)/,
407
+ ""
408
+ );
409
+ fs.writeFileSync(SOUL_MD, cleaned);
410
+ }
411
+
412
+ // Append persona
413
+ fs.appendFileSync(SOUL_MD, "\n" + personaText.trim() + "\n");
414
+ logSuccess(`Updated: ${SOUL_MD}`);
415
+
416
+ return true;
417
+ }
418
+
419
+ // Final summary
420
+ function printSummary() {
421
+ logStep("7/7", "Installation complete!");
422
+
423
+ console.log(`
424
+ ${c("green", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
425
+ ${c("bright", " Clawra Selfie is ready!")}
426
+ ${c("green", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
427
+
428
+ ${c("cyan", "Installed files:")}
429
+ ${SKILL_DEST}/
430
+
431
+ ${c("cyan", "Configuration:")}
432
+ ${OPENCLAW_CONFIG}
433
+
434
+ ${c("cyan", "Identity set:")}
435
+ ${IDENTITY_MD}
436
+
437
+ ${c("cyan", "Persona updated:")}
438
+ ${SOUL_MD}
439
+
440
+ ${c("yellow", "Try saying to your agent:")}
441
+ "Send me a selfie"
442
+ "Send a pic wearing a cowboy hat"
443
+ "What are you doing right now?"
444
+
445
+ ${c("dim", "Your agent now has selfie superpowers!")}
446
+ `);
447
+ }
448
+
449
+ // Handle reinstall
450
+ async function handleReinstall(rl, falKey) {
451
+ const reinstall = await ask(rl, "\nReinstall/update? (y/N): ");
452
+
453
+ if (reinstall.toLowerCase() !== "y") {
454
+ log("\nNo changes made. Goodbye!");
455
+ return false;
456
+ }
457
+
458
+ // Remove existing installation
459
+ fs.rmSync(SKILL_DEST, { recursive: true, force: true });
460
+ logInfo("Removed existing installation");
461
+
462
+ return true;
463
+ }
464
+
465
+ // Main function
466
+ async function main() {
467
+ const rl = createPrompt();
468
+
469
+ try {
470
+ printBanner();
471
+
472
+ // Step 1: Check prerequisites
473
+ const prereqResult = await checkPrerequisites();
474
+
475
+ if (prereqResult === false) {
476
+ rl.close();
477
+ process.exit(1);
478
+ }
479
+
480
+ if (prereqResult === "already_installed") {
481
+ const shouldContinue = await handleReinstall(rl, null);
482
+ if (!shouldContinue) {
483
+ rl.close();
484
+ process.exit(0);
485
+ }
486
+ }
487
+
488
+ // Step 2: Get FAL API key
489
+ const falKey = await getFalApiKey(rl);
490
+ if (!falKey) {
491
+ rl.close();
492
+ process.exit(1);
493
+ }
494
+
495
+ // Step 3: Install skill files
496
+ await installSkill();
497
+
498
+ // Step 4: Update OpenClaw config
499
+ await updateOpenClawConfig(falKey);
500
+
501
+ // Step 5: Write IDENTITY.md
502
+ await writeIdentity();
503
+
504
+ // Step 6: Inject persona
505
+ await injectPersona(rl);
506
+
507
+ // Step 7: Summary
508
+ printSummary();
509
+
510
+ rl.close();
511
+ } catch (error) {
512
+ logError(`Installation failed: ${error.message}`);
513
+ console.error(error);
514
+ rl.close();
515
+ process.exit(1);
516
+ }
517
+ }
518
+
519
+ // Run
520
+ main();
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "clawra-anime",
3
+ "version": "1.0.0",
4
+ "description": "二次元虚拟女友 - OpenClaw Skill for anime-style selfie generation",
5
+ "main": "skill/SKILL.md",
6
+ "bin": {
7
+ "clawra-anime": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "openclaw",
14
+ "skill",
15
+ "anime",
16
+ "waifu",
17
+ "selfie",
18
+ "image-generation",
19
+ "fal-ai",
20
+ "grok-imagine"
21
+ ],
22
+ "author": "Fork of SumeLabs/clawra",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/YOUR_USERNAME/clawra-anime.git"
27
+ },
28
+ "dependencies": {
29
+ "@fal-ai/client": "^1.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20.0.0",
33
+ "typescript": "^5.0.0"
34
+ }
35
+ }
@@ -0,0 +1,162 @@
1
+ #!/bin/bash
2
+ # grok-imagine-send.sh
3
+ # Generate an image with Grok Imagine and send it via OpenClaw
4
+ #
5
+ # Usage: ./grok-imagine-send.sh "<prompt>" "<channel>" ["<caption>"]
6
+ #
7
+ # Environment variables required:
8
+ # FAL_KEY - Your fal.ai API key
9
+ #
10
+ # Example:
11
+ # FAL_KEY=your_key ./grok-imagine-send.sh "A sunset over mountains" "#art" "Check this out!"
12
+
13
+ set -euo pipefail
14
+
15
+ # Colors for output
16
+ RED='\033[0;31m'
17
+ GREEN='\033[0;32m'
18
+ YELLOW='\033[1;33m'
19
+ NC='\033[0m' # No Color
20
+
21
+ log_info() {
22
+ echo -e "${GREEN}[INFO]${NC} $1"
23
+ }
24
+
25
+ log_warn() {
26
+ echo -e "${YELLOW}[WARN]${NC} $1"
27
+ }
28
+
29
+ log_error() {
30
+ echo -e "${RED}[ERROR]${NC} $1"
31
+ }
32
+
33
+ # Check required environment variables
34
+ if [ -z "${FAL_KEY:-}" ]; then
35
+ log_error "FAL_KEY environment variable not set"
36
+ echo "Get your API key from: https://fal.ai/dashboard/keys"
37
+ exit 1
38
+ fi
39
+
40
+ # Check for jq
41
+ if ! command -v jq &> /dev/null; then
42
+ log_error "jq is required but not installed"
43
+ echo "Install with: brew install jq (macOS) or apt install jq (Linux)"
44
+ exit 1
45
+ fi
46
+
47
+ # Check for openclaw
48
+ if ! command -v openclaw &> /dev/null; then
49
+ log_warn "openclaw CLI not found - will attempt direct API call"
50
+ USE_CLI=false
51
+ else
52
+ USE_CLI=true
53
+ fi
54
+
55
+ # Parse arguments
56
+ PROMPT="${1:-}"
57
+ CHANNEL="${2:-}"
58
+ CAPTION="${3:-Generated with Grok Imagine}"
59
+ ASPECT_RATIO="${4:-1:1}"
60
+ OUTPUT_FORMAT="${5:-jpeg}"
61
+
62
+ if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then
63
+ echo "Usage: $0 <prompt> <channel> [caption] [aspect_ratio] [output_format]"
64
+ echo ""
65
+ echo "Arguments:"
66
+ echo " prompt - Image description (required)"
67
+ echo " channel - Target channel (required) e.g., #general, @user"
68
+ echo " caption - Message caption (default: 'Generated with Grok Imagine')"
69
+ echo " aspect_ratio - Image ratio (default: 1:1) Options: 2:1, 16:9, 4:3, 1:1, 3:4, 9:16"
70
+ echo " output_format - Image format (default: jpeg) Options: jpeg, png, webp"
71
+ echo ""
72
+ echo "Example:"
73
+ echo " $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\""
74
+ exit 1
75
+ fi
76
+
77
+ log_info "Generating image with Grok Imagine..."
78
+ log_info "Prompt: $PROMPT"
79
+ log_info "Aspect ratio: $ASPECT_RATIO"
80
+
81
+ # Generate image via fal.ai
82
+ RESPONSE=$(curl -s -X POST "https://fal.run/xai/grok-imagine-image" \
83
+ -H "Authorization: Key $FAL_KEY" \
84
+ -H "Content-Type: application/json" \
85
+ -d "{
86
+ \"prompt\": $(echo "$PROMPT" | jq -Rs .),
87
+ \"num_images\": 1,
88
+ \"aspect_ratio\": \"$ASPECT_RATIO\",
89
+ \"output_format\": \"$OUTPUT_FORMAT\"
90
+ }")
91
+
92
+ # Check for errors in response
93
+ if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then
94
+ ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error // .detail // "Unknown error"')
95
+ log_error "Image generation failed: $ERROR_MSG"
96
+ exit 1
97
+ fi
98
+
99
+ # Extract image URL
100
+ IMAGE_URL=$(echo "$RESPONSE" | jq -r '.images[0].url // empty')
101
+
102
+ if [ -z "$IMAGE_URL" ]; then
103
+ log_error "Failed to extract image URL from response"
104
+ echo "Response: $RESPONSE"
105
+ exit 1
106
+ fi
107
+
108
+ log_info "Image generated successfully!"
109
+ log_info "URL: $IMAGE_URL"
110
+
111
+ # Get revised prompt if available
112
+ REVISED_PROMPT=$(echo "$RESPONSE" | jq -r '.revised_prompt // empty')
113
+ if [ -n "$REVISED_PROMPT" ]; then
114
+ log_info "Revised prompt: $REVISED_PROMPT"
115
+ fi
116
+
117
+ # Send via OpenClaw
118
+ log_info "Sending to channel: $CHANNEL"
119
+
120
+ if [ "$USE_CLI" = true ]; then
121
+ # Use OpenClaw CLI
122
+ openclaw message send \
123
+ --action send \
124
+ --channel "$CHANNEL" \
125
+ --message "$CAPTION" \
126
+ --media "$IMAGE_URL"
127
+ else
128
+ # Direct API call to local gateway
129
+ GATEWAY_URL="${OPENCLAW_GATEWAY_URL:-http://localhost:18789}"
130
+ GATEWAY_TOKEN="${OPENCLAW_GATEWAY_TOKEN:-}"
131
+
132
+ HEADERS="-H \"Content-Type: application/json\""
133
+ if [ -n "$GATEWAY_TOKEN" ]; then
134
+ HEADERS="$HEADERS -H \"Authorization: Bearer $GATEWAY_TOKEN\""
135
+ fi
136
+
137
+ curl -s -X POST "$GATEWAY_URL/message" \
138
+ -H "Content-Type: application/json" \
139
+ ${GATEWAY_TOKEN:+-H "Authorization: Bearer $GATEWAY_TOKEN"} \
140
+ -d "{
141
+ \"action\": \"send\",
142
+ \"channel\": \"$CHANNEL\",
143
+ \"message\": \"$CAPTION\",
144
+ \"media\": \"$IMAGE_URL\"
145
+ }"
146
+ fi
147
+
148
+ log_info "Done! Image sent to $CHANNEL"
149
+
150
+ # Output JSON for programmatic use
151
+ echo ""
152
+ echo "--- Result ---"
153
+ jq -n \
154
+ --arg url "$IMAGE_URL" \
155
+ --arg channel "$CHANNEL" \
156
+ --arg prompt "$PROMPT" \
157
+ '{
158
+ success: true,
159
+ image_url: $url,
160
+ channel: $channel,
161
+ prompt: $prompt
162
+ }'