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/.serena/project.yml +112 -0
- package/INSTALL.md +149 -0
- package/README-CN.md +188 -0
- package/README.md +125 -0
- package/SKILL.md +412 -0
- package/assets/clawra-original.png +0 -0
- package/assets/clawra.png +0 -0
- package/bin/cli.js +520 -0
- package/package.json +35 -0
- package/scripts/clawra-selfie.sh +162 -0
- package/scripts/clawra-selfie.ts +299 -0
- package/skill/SKILL.md +412 -0
- package/skill/assets/clawra.png +0 -0
- package/skill/scripts/clawra-anime-selfie.sh +177 -0
- package/skill/scripts/clawra-selfie.sh +162 -0
- package/skill/scripts/clawra-selfie.ts +299 -0
- package/templates/soul-anime-girlfriend.md +105 -0
- package/templates/soul-injection.md +44 -0
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
|
+
}'
|