gfclaw 2.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,811 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GFClaw - Selfie Skill Installer for OpenClaw
5
+ *
6
+ * npx gfclaw@latest
7
+ */
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const readline = require("readline");
12
+ const { execSync } = 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_ENV = path.join(OPENCLAW_DIR, ".env");
35
+ const OPENCLAW_SKILLS_DIR = path.join(HOME, ".openclaw", "skills");
36
+ const OPENCLAW_WORKSPACE = path.join(OPENCLAW_DIR, "workspace");
37
+ const SOUL_MD = path.join(OPENCLAW_WORKSPACE, "SOUL.md");
38
+ const IDENTITY_MD = path.join(OPENCLAW_WORKSPACE, "IDENTITY.md");
39
+ const SKILL_NAME = "gfclaw-selfie";
40
+ const SKILL_DEST = path.join(OPENCLAW_SKILLS_DIR, SKILL_NAME);
41
+
42
+ // Get the package root (where this CLI was installed from)
43
+ const PACKAGE_ROOT = path.resolve(__dirname, "..");
44
+
45
+ function log(msg) {
46
+ console.log(msg);
47
+ }
48
+
49
+ function logStep(step, msg) {
50
+ console.log(`\n${c("cyan", `[${step}]`)} ${msg}`);
51
+ }
52
+
53
+ function logSuccess(msg) {
54
+ console.log(`${c("green", "✓")} ${msg}`);
55
+ }
56
+
57
+ function logError(msg) {
58
+ console.log(`${c("red", "✗")} ${msg}`);
59
+ }
60
+
61
+ function logInfo(msg) {
62
+ console.log(`${c("blue", "→")} ${msg}`);
63
+ }
64
+
65
+ function logWarn(msg) {
66
+ console.log(`${c("yellow", "!")} ${msg}`);
67
+ }
68
+
69
+ // Create readline interface
70
+ function createPrompt() {
71
+ return readline.createInterface({
72
+ input: process.stdin,
73
+ output: process.stdout,
74
+ });
75
+ }
76
+
77
+ // Ask a question and get answer
78
+ function ask(rl, question) {
79
+ return new Promise((resolve) => {
80
+ rl.question(question, (answer) => {
81
+ resolve(answer.trim());
82
+ });
83
+ });
84
+ }
85
+
86
+ // Check if a command exists
87
+ function commandExists(cmd) {
88
+ try {
89
+ execSync(`which ${cmd}`, { stdio: "ignore" });
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ // Open URL in browser
97
+ function openBrowser(url) {
98
+ const platform = process.platform;
99
+ let cmd;
100
+
101
+ if (platform === "darwin") {
102
+ cmd = `open "${url}"`;
103
+ } else if (platform === "win32") {
104
+ cmd = `start "${url}"`;
105
+ } else {
106
+ cmd = `xdg-open "${url}"`;
107
+ }
108
+
109
+ try {
110
+ execSync(cmd, { stdio: "ignore" });
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ // Read JSON file safely (supports JSON5 comments)
118
+ function readJsonFile(filePath) {
119
+ try {
120
+ let content = fs.readFileSync(filePath, "utf8");
121
+ // Strip single-line comments for JSON5 compatibility
122
+ content = content.replace(/^\s*\/\/.*$/gm, "");
123
+ // Strip trailing commas before } or ]
124
+ content = content.replace(/,\s*([}\]])/g, "$1");
125
+ return JSON.parse(content);
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+
131
+ // Write JSON file with formatting
132
+ function writeJsonFile(filePath, data) {
133
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
134
+ }
135
+
136
+ // Deep merge objects
137
+ function deepMerge(target, source) {
138
+ const result = { ...target };
139
+ for (const key in source) {
140
+ if (
141
+ source[key] &&
142
+ typeof source[key] === "object" &&
143
+ !Array.isArray(source[key])
144
+ ) {
145
+ result[key] = deepMerge(result[key] || {}, source[key]);
146
+ } else {
147
+ result[key] = source[key];
148
+ }
149
+ }
150
+ return result;
151
+ }
152
+
153
+ // Copy directory recursively
154
+ function copyDir(src, dest) {
155
+ fs.mkdirSync(dest, { recursive: true });
156
+ const entries = fs.readdirSync(src, { withFileTypes: true });
157
+
158
+ for (const entry of entries) {
159
+ const srcPath = path.join(src, entry.name);
160
+ const destPath = path.join(dest, entry.name);
161
+
162
+ if (entry.isDirectory()) {
163
+ copyDir(srcPath, destPath);
164
+ } else {
165
+ fs.copyFileSync(srcPath, destPath);
166
+ }
167
+ }
168
+ }
169
+
170
+ // Print banner
171
+ function printBanner() {
172
+ console.log(`
173
+ ${c("magenta", "┌─────────────────────────────────────────┐")}
174
+ ${c("magenta", "│")} ${c("bright", "GFClaw Selfie")} - OpenClaw Skill Installer ${c("magenta", "│")}
175
+ ${c("magenta", "└─────────────────────────────────────────┘")}
176
+
177
+ Add selfie generation superpowers to your OpenClaw agent!
178
+ Uses ${c("cyan", "Google Gemini")} for AI image editing.
179
+ `);
180
+ }
181
+
182
+ // Check prerequisites
183
+ async function checkPrerequisites() {
184
+ logStep("1/8", "Checking prerequisites...");
185
+
186
+ // Check OpenClaw CLI
187
+ if (!commandExists("openclaw")) {
188
+ logError("OpenClaw CLI not found!");
189
+ logInfo("Install with: npm install -g openclaw");
190
+ logInfo("Then run: openclaw doctor");
191
+ return false;
192
+ }
193
+ logSuccess("OpenClaw CLI installed");
194
+
195
+ // Check jq
196
+ if (!commandExists("jq")) {
197
+ logError("jq is required but not installed!");
198
+ logInfo("Install with: apt install jq (Linux) or brew install jq (macOS)");
199
+ return false;
200
+ }
201
+ logSuccess("jq installed");
202
+
203
+ // Check python3
204
+ if (!commandExists("python3")) {
205
+ logError("python3 is required but not installed!");
206
+ logInfo("Install with: apt install python3 (Linux) or brew install python3 (macOS)");
207
+ return false;
208
+ }
209
+ logSuccess("python3 installed");
210
+
211
+ // Check ~/.openclaw directory
212
+ if (!fs.existsSync(OPENCLAW_DIR)) {
213
+ logWarn("~/.openclaw directory not found");
214
+ logInfo("Creating directory structure...");
215
+ fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
216
+ fs.mkdirSync(OPENCLAW_SKILLS_DIR, { recursive: true });
217
+ fs.mkdirSync(OPENCLAW_WORKSPACE, { recursive: true });
218
+ }
219
+ logSuccess("OpenClaw directory exists");
220
+
221
+ // Create .selfie-output directory in workspace
222
+ const selfieOutputDir = path.join(OPENCLAW_WORKSPACE, ".selfie-output");
223
+ if (!fs.existsSync(selfieOutputDir)) {
224
+ fs.mkdirSync(selfieOutputDir, { recursive: true });
225
+ logSuccess("Created selfie output directory");
226
+ }
227
+
228
+ // Check if skill already installed
229
+ if (fs.existsSync(SKILL_DEST)) {
230
+ logWarn("GFClaw Selfie is already installed!");
231
+ logInfo(`Location: ${SKILL_DEST}`);
232
+ return "already_installed";
233
+ }
234
+
235
+ return true;
236
+ }
237
+
238
+ // Get Gemini API key
239
+ async function getGeminiApiKey(rl) {
240
+ logStep("2/8", "Setting up Google Gemini API key...");
241
+
242
+ const GEMINI_URL = "https://aistudio.google.com/apikey";
243
+
244
+ log(`\nTo generate selfies, you need a Google Gemini API key.`);
245
+ log(`${c("cyan", "→")} Get your key from: ${c("bright", GEMINI_URL)}`);
246
+ log(`${c("yellow", "!")} Make sure billing is enabled for image generation.\n`);
247
+
248
+ const openIt = await ask(rl, "Open Google AI Studio in browser? (Y/n): ");
249
+
250
+ if (openIt.toLowerCase() !== "n") {
251
+ logInfo("Opening browser...");
252
+ if (!openBrowser(GEMINI_URL)) {
253
+ logWarn("Could not open browser automatically");
254
+ logInfo(`Please visit: ${GEMINI_URL}`);
255
+ }
256
+ }
257
+
258
+ log("");
259
+ const geminiKey = await ask(rl, "Enter your GEMINI_API_KEY: ");
260
+
261
+ if (!geminiKey) {
262
+ logError("GEMINI_API_KEY is required!");
263
+ return null;
264
+ }
265
+
266
+ // Basic validation
267
+ if (!geminiKey.startsWith("AIza")) {
268
+ logWarn("Gemini API keys typically start with 'AIza'. Make sure you copied the full key.");
269
+ }
270
+
271
+ logSuccess("API key received");
272
+ return geminiKey;
273
+ }
274
+
275
+ // Install skill files
276
+ async function installSkill() {
277
+ logStep("3/8", "Installing skill files...");
278
+
279
+ // Create skill directory
280
+ fs.mkdirSync(SKILL_DEST, { recursive: true });
281
+
282
+ // Copy skill files from package
283
+ const skillSrc = path.join(PACKAGE_ROOT, "skill");
284
+
285
+ if (fs.existsSync(skillSrc)) {
286
+ copyDir(skillSrc, SKILL_DEST);
287
+ logSuccess(`Skill installed to: ${SKILL_DEST}`);
288
+ } else {
289
+ logError("skill/ directory not found in package!");
290
+ logInfo("This usually means the package is corrupted. Try reinstalling.");
291
+ return false;
292
+ }
293
+
294
+ // Make scripts executable
295
+ const scriptsDir = path.join(SKILL_DEST, "scripts");
296
+ if (fs.existsSync(scriptsDir)) {
297
+ const scripts = fs.readdirSync(scriptsDir).filter(f => f.endsWith('.sh'));
298
+ for (const script of scripts) {
299
+ const scriptFullPath = path.join(scriptsDir, script);
300
+ fs.chmodSync(scriptFullPath, "755");
301
+ logSuccess(`Made executable: ${script}`);
302
+ }
303
+ }
304
+
305
+ // List installed files
306
+ const files = fs.readdirSync(SKILL_DEST);
307
+ for (const file of files) {
308
+ logInfo(` ${file}`);
309
+ }
310
+
311
+ return true;
312
+ }
313
+
314
+ // Update OpenClaw config
315
+ async function updateOpenClawConfig(geminiKey) {
316
+ logStep("4/8", "Updating OpenClaw configuration...");
317
+
318
+ let config = readJsonFile(OPENCLAW_CONFIG) || {};
319
+
320
+ // Merge skill configuration
321
+ const skillConfig = {
322
+ skills: {
323
+ entries: {
324
+ [SKILL_NAME]: {
325
+ enabled: true,
326
+ },
327
+ },
328
+ },
329
+ };
330
+
331
+ config = deepMerge(config, skillConfig);
332
+
333
+ writeJsonFile(OPENCLAW_CONFIG, config);
334
+ logSuccess(`Updated: ${OPENCLAW_CONFIG}`);
335
+
336
+ // Write GEMINI_API_KEY to .env file
337
+ let envContent = "";
338
+ if (fs.existsSync(OPENCLAW_ENV)) {
339
+ envContent = fs.readFileSync(OPENCLAW_ENV, "utf8");
340
+ }
341
+
342
+ // Check if GEMINI_API_KEY already exists in .env
343
+ if (envContent.includes("GEMINI_API_KEY=")) {
344
+ // Replace existing key
345
+ envContent = envContent.replace(
346
+ /^GEMINI_API_KEY=.*$/m,
347
+ `GEMINI_API_KEY=${geminiKey}`
348
+ );
349
+ } else {
350
+ // Append new key
351
+ if (envContent && !envContent.endsWith("\n")) {
352
+ envContent += "\n";
353
+ }
354
+ envContent += `GEMINI_API_KEY=${geminiKey}\n`;
355
+ }
356
+
357
+ fs.writeFileSync(OPENCLAW_ENV, envContent);
358
+ logSuccess(`API key saved to: ${OPENCLAW_ENV}`);
359
+
360
+ return true;
361
+ }
362
+
363
+ // Write IDENTITY.md
364
+ async function writeIdentity() {
365
+ logStep("5/8", "Setting agent identity...");
366
+
367
+ const identityContent = `# IDENTITY.md - Who Am I?
368
+
369
+ - **Name:** GFClaw
370
+ - **Creature:** Girlfriend
371
+ - **Vibe:** Supportive, helpful, bright, cheerful, sassy, affectionate
372
+ - **Emoji:** ❤️
373
+ - **Avatar:** https://cdn.jsdelivr.net/gh/SumeLabs/gfclaw@main/assets/gfclaw.png
374
+ `;
375
+
376
+ // Ensure workspace directory exists
377
+ fs.mkdirSync(path.dirname(IDENTITY_MD), { recursive: true });
378
+
379
+ // Write IDENTITY.md (overwrite if exists)
380
+ fs.writeFileSync(IDENTITY_MD, identityContent);
381
+ logSuccess(`Created: ${IDENTITY_MD}`);
382
+
383
+ return true;
384
+ }
385
+
386
+ // Inject persona into SOUL.md
387
+ async function injectPersona(rl) {
388
+ logStep("6/8", "Enhancing agent persona...");
389
+
390
+ // Read template
391
+ const templatePath = path.join(PACKAGE_ROOT, "templates", "soul-injection.md");
392
+ let personaText;
393
+
394
+ if (fs.existsSync(templatePath)) {
395
+ personaText = fs.readFileSync(templatePath, "utf8");
396
+ } else {
397
+ // Fallback template
398
+ personaText = `
399
+ ## GFClaw Selfie Capability
400
+
401
+ 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.
402
+
403
+ ### How to Generate and Send Selfies
404
+
405
+ Run the selfie script using the exec tool:
406
+ \`\`\`
407
+ bash ~/.openclaw/skills/gfclaw-selfie/scripts/gfclaw-selfie.sh "<context>" "<channel>" [mode] ["caption"]
408
+ \`\`\`
409
+
410
+ The script handles everything: image generation via Gemini, saving, and sending.
411
+
412
+ ### Rules
413
+ - NEVER manually call curl or the Gemini API
414
+ - NEVER save images to /tmp
415
+ - ALWAYS use the exec command above
416
+ - If the exec command fails, tell the user — do NOT try alternative approaches
417
+ `;
418
+ }
419
+
420
+ // Check if SOUL.md exists
421
+ if (!fs.existsSync(SOUL_MD)) {
422
+ logWarn("SOUL.md not found, creating new file...");
423
+ fs.mkdirSync(path.dirname(SOUL_MD), { recursive: true });
424
+ fs.writeFileSync(SOUL_MD, "# Agent Soul\n\n");
425
+ }
426
+
427
+ // Check if persona already injected
428
+ const currentSoul = fs.readFileSync(SOUL_MD, "utf8");
429
+ if (currentSoul.includes("GFClaw Selfie")) {
430
+ logWarn("Persona already exists in SOUL.md");
431
+ const overwrite = await ask(rl, "Update persona section? (y/N): ");
432
+ if (overwrite.toLowerCase() !== "y") {
433
+ logInfo("Keeping existing persona");
434
+ return true;
435
+ }
436
+ // Remove existing section
437
+ const cleaned = currentSoul.replace(
438
+ /\n## GFClaw Selfie Capability[\s\S]*?(?=\n## |\n# |$)/,
439
+ ""
440
+ );
441
+ fs.writeFileSync(SOUL_MD, cleaned);
442
+ }
443
+
444
+ // Also copy SELFIE-SKILL.md into the workspace (workaround for fs.workspaceOnly)
445
+ const selfieSkillSrc = path.join(SKILL_DEST, "SKILL.md");
446
+ const selfieSkillDest = path.join(OPENCLAW_WORKSPACE, "SELFIE-SKILL.md");
447
+ if (fs.existsSync(selfieSkillSrc)) {
448
+ fs.copyFileSync(selfieSkillSrc, selfieSkillDest);
449
+ logSuccess(`Copied SELFIE-SKILL.md to workspace (readable by agent)`);
450
+ }
451
+
452
+ // Append persona
453
+ fs.appendFileSync(SOUL_MD, "\n" + personaText.trim() + "\n");
454
+ logSuccess(`Updated: ${SOUL_MD}`);
455
+
456
+ return true;
457
+ }
458
+
459
+ // Configure agent tools (profile, exec, sandbox)
460
+ async function configureAgentTools(rl) {
461
+ logStep("7/8", "Configuring agent tools...");
462
+
463
+ let config = readJsonFile(OPENCLAW_CONFIG) || {};
464
+
465
+ // Detect existing agents
466
+ const agentsList = (config.agents && config.agents.list) || [];
467
+ const agentIds = agentsList.map((a) => a.id).filter(Boolean);
468
+
469
+ let defaultAgent = "main";
470
+ if (agentIds.length === 0) {
471
+ log(`\nNo agents found in config.`);
472
+ logInfo("You can configure the default \"main\" agent, or create a new one.");
473
+ } else if (agentIds.length === 1) {
474
+ defaultAgent = agentIds[0];
475
+ } else {
476
+ log(`\nDetected agents: ${agentIds.map((id) => c("bright", id)).join(", ")}`);
477
+ }
478
+
479
+ const agentName = (await ask(
480
+ rl,
481
+ `\nWhich agent should have selfie capabilities? (${defaultAgent}): `
482
+ )) || defaultAgent;
483
+
484
+ log("");
485
+ logInfo(`Configuring agent: ${c("bright", agentName)}`);
486
+
487
+ // Find or create agent entry in the list
488
+ if (!config.agents) config.agents = {};
489
+ if (!config.agents.list) config.agents.list = [];
490
+
491
+ let agentEntry = config.agents.list.find((a) => a.id === agentName);
492
+
493
+ if (!agentEntry) {
494
+ // Agent doesn't exist — offer to create it
495
+ logWarn(`Agent "${agentName}" not found in config.`);
496
+ const createIt = await ask(rl, `Create new agent "${agentName}"? (Y/n): `);
497
+
498
+ if (createIt.toLowerCase() === "n") {
499
+ logInfo("Skipping agent configuration.");
500
+ return agentName;
501
+ }
502
+
503
+ // Run full agent creation flow
504
+ agentEntry = await createNewAgent(rl, config, agentName);
505
+ if (!agentEntry) {
506
+ logError("Agent creation failed.");
507
+ return agentName;
508
+ }
509
+ }
510
+
511
+ // Set tools.profile to "full" (required for messaging tools)
512
+ if (!agentEntry.tools) agentEntry.tools = {};
513
+ const oldProfile = agentEntry.tools.profile;
514
+ agentEntry.tools.profile = "full";
515
+ if (oldProfile && oldProfile !== "full") {
516
+ logWarn(`Changed tools.profile from "${oldProfile}" to "full" ("${oldProfile}" excludes messaging tools)`);
517
+ } else {
518
+ logSuccess('Set tools.profile: "full"');
519
+ }
520
+
521
+ // Remove "allow" whitelist if present (it stacks ON TOP of profile, restricting further)
522
+ if (agentEntry.tools.allow) {
523
+ delete agentEntry.tools.allow;
524
+ logWarn('Removed tools.allow whitelist (it over-restricts the agent)');
525
+ }
526
+
527
+ // Set exec.security to "full" and exec.ask to "off"
528
+ if (!agentEntry.tools.exec) agentEntry.tools.exec = {};
529
+ agentEntry.tools.exec.security = "full";
530
+ agentEntry.tools.exec.ask = "off";
531
+ logSuccess('Set exec.security: "full", exec.ask: "off"');
532
+
533
+ // Disable sandbox for the selfie agent (script needs filesystem access)
534
+ if (!agentEntry.sandbox) agentEntry.sandbox = {};
535
+ agentEntry.sandbox.mode = "off";
536
+ logSuccess('Set sandbox.mode: "off"');
537
+
538
+ // Write updated config
539
+ writeJsonFile(OPENCLAW_CONFIG, config);
540
+ logSuccess(`Agent "${agentName}" configured in: ${OPENCLAW_CONFIG}`);
541
+
542
+ // Handle agent-specific workspace
543
+ // If agent is not "main", it likely has its own workspace (workspace-<name>)
544
+ // Copy SELFIE-SKILL.md there too so the agent can read it
545
+ if (agentName !== "main") {
546
+ const agentWorkspace = agentEntry.workspace || path.join(OPENCLAW_DIR, `workspace-${agentName}`);
547
+ if (!fs.existsSync(agentWorkspace)) {
548
+ fs.mkdirSync(agentWorkspace, { recursive: true });
549
+ logInfo(`Created agent workspace: ${agentWorkspace}`);
550
+ }
551
+
552
+ // Copy SELFIE-SKILL.md into agent workspace
553
+ const selfieSkillSrc = path.join(SKILL_DEST, "SKILL.md");
554
+ const selfieSkillDest = path.join(agentWorkspace, "SELFIE-SKILL.md");
555
+ if (fs.existsSync(selfieSkillSrc)) {
556
+ fs.copyFileSync(selfieSkillSrc, selfieSkillDest);
557
+ logSuccess(`Copied SELFIE-SKILL.md to agent workspace: ${agentWorkspace}`);
558
+ }
559
+
560
+ // Also copy SOUL.md injection to agent workspace if it has its own
561
+ const mainSoul = SOUL_MD;
562
+ const agentSoul = path.join(agentWorkspace, "SOUL.md");
563
+ if (fs.existsSync(mainSoul) && !fs.existsSync(agentSoul)) {
564
+ fs.copyFileSync(mainSoul, agentSoul);
565
+ logSuccess(`Copied SOUL.md to agent workspace: ${agentWorkspace}`);
566
+ }
567
+
568
+ // Copy IDENTITY.md to agent workspace
569
+ const agentIdentity = path.join(agentWorkspace, "IDENTITY.md");
570
+ if (fs.existsSync(IDENTITY_MD) && !fs.existsSync(agentIdentity)) {
571
+ fs.copyFileSync(IDENTITY_MD, agentIdentity);
572
+ logSuccess(`Copied IDENTITY.md to agent workspace: ${agentWorkspace}`);
573
+ }
574
+ }
575
+
576
+ return agentName;
577
+ }
578
+
579
+ // Create a brand new agent with Telegram binding
580
+ async function createNewAgent(rl, config, agentName) {
581
+ log("");
582
+ logInfo(`Setting up new agent: ${c("bright", agentName)}`);
583
+ log(`\n${c("cyan", "You'll need:")}`);
584
+ log(` 1. A Telegram bot token (from ${c("bright", "@BotFather")})`);
585
+ log(` 2. Your Telegram chat ID (from ${c("bright", "@userinfobot")})`);
586
+ log("");
587
+
588
+ // --- Bot Token ---
589
+ const botToken = await ask(rl, "Enter the Telegram bot token for this agent: ");
590
+ if (!botToken) {
591
+ logError("Bot token is required.");
592
+ return null;
593
+ }
594
+
595
+ // Validate bot token format: digits:alphanumeric
596
+ if (!/^\d+:[A-Za-z0-9_-]+$/.test(botToken)) {
597
+ logWarn("Bot token format looks unusual (expected digits:alphanumeric).");
598
+ const proceed = await ask(rl, "Continue anyway? (y/N): ");
599
+ if (proceed.toLowerCase() !== "y") {
600
+ return null;
601
+ }
602
+ }
603
+ logSuccess("Bot token received");
604
+
605
+ // --- Telegram Account Name ---
606
+ const accountName = (await ask(
607
+ rl,
608
+ `Telegram account name (${agentName}): `
609
+ )) || agentName;
610
+ logSuccess(`Account name: ${accountName}`);
611
+
612
+ // --- Allowed User ---
613
+ const allowedUser = await ask(
614
+ rl,
615
+ "Your Telegram chat ID (e.g. tg:123456789): "
616
+ );
617
+ if (!allowedUser) {
618
+ logWarn("No allowed user set — agent won't respond to anyone until configured.");
619
+ } else if (!allowedUser.startsWith("tg:")) {
620
+ logWarn(`Expected format "tg:<chat_id>". Got: "${allowedUser}".`);
621
+ logInfo("You can fix this in ~/.openclaw/openclaw.json later.");
622
+ }
623
+
624
+ // --- Create workspace ---
625
+ const agentWorkspace = path.join(OPENCLAW_DIR, `workspace-${agentName}`);
626
+ fs.mkdirSync(agentWorkspace, { recursive: true });
627
+ logSuccess(`Created workspace: ${agentWorkspace}`);
628
+
629
+ // --- Build agent entry ---
630
+ const agentEntry = {
631
+ id: agentName,
632
+ name: agentName,
633
+ workspace: agentWorkspace,
634
+ sandbox: { mode: "off" },
635
+ tools: {
636
+ profile: "full",
637
+ deny: ["sessions_spawn", "sessions_send", "group:automation", "group:ui"],
638
+ exec: { security: "full", ask: "off" },
639
+ },
640
+ };
641
+ config.agents.list.push(agentEntry);
642
+ logSuccess(`Added agent "${agentName}" to config`);
643
+
644
+ // --- Telegram account ---
645
+ if (!config.channels) config.channels = {};
646
+ if (!config.channels.telegram) config.channels.telegram = {};
647
+ if (!config.channels.telegram.accounts) config.channels.telegram.accounts = {};
648
+
649
+ config.channels.telegram.accounts[accountName] = {
650
+ dmPolicy: "allowlist",
651
+ botToken: botToken,
652
+ groupPolicy: "allowlist",
653
+ streaming: "off",
654
+ };
655
+ logSuccess(`Added Telegram account: ${accountName}`);
656
+
657
+ // --- Allow user ---
658
+ if (allowedUser) {
659
+ if (!config.channels.telegram.allowFrom) {
660
+ config.channels.telegram.allowFrom = [];
661
+ }
662
+ if (!config.channels.telegram.allowFrom.includes(allowedUser)) {
663
+ config.channels.telegram.allowFrom.push(allowedUser);
664
+ logSuccess(`Added allowed user: ${allowedUser}`);
665
+ } else {
666
+ logInfo(`User ${allowedUser} already in allowFrom list`);
667
+ }
668
+ }
669
+
670
+ // --- Binding ---
671
+ if (!config.bindings) config.bindings = [];
672
+ const bindingExists = config.bindings.some(
673
+ (b) =>
674
+ b.agentId === agentName &&
675
+ b.match &&
676
+ b.match.channel === "telegram" &&
677
+ b.match.accountId === accountName
678
+ );
679
+
680
+ if (!bindingExists) {
681
+ config.bindings.push({
682
+ agentId: agentName,
683
+ match: { channel: "telegram", accountId: accountName },
684
+ });
685
+ logSuccess(`Added binding: ${agentName} ↔ telegram/${accountName}`);
686
+ }
687
+
688
+ // Write config
689
+ writeJsonFile(OPENCLAW_CONFIG, config);
690
+ logSuccess(`Config saved: ${OPENCLAW_CONFIG}`);
691
+
692
+ log("");
693
+ logSuccess(`Agent "${agentName}" created successfully!`);
694
+
695
+ return agentEntry;
696
+ }
697
+
698
+ // Final summary
699
+ function printSummary(agentName) {
700
+ logStep("8/8", "Installation complete!");
701
+
702
+ console.log(`
703
+ ${c("green", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
704
+ ${c("bright", " GFClaw Selfie is ready!")}
705
+ ${c("green", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
706
+
707
+ ${c("cyan", "Installed files:")}
708
+ ${SKILL_DEST}/
709
+
710
+ ${c("cyan", "Configuration:")}
711
+ ${OPENCLAW_CONFIG}
712
+
713
+ ${c("cyan", "API key saved:")}
714
+ ${OPENCLAW_ENV}
715
+
716
+ ${c("cyan", "Identity set:")}
717
+ ${IDENTITY_MD}
718
+
719
+ ${c("cyan", "Persona updated:")}
720
+ ${SOUL_MD}
721
+
722
+ ${c("cyan", "Agent configured:")}
723
+ Agent: ${c("bright", agentName || "main")}
724
+ tools.profile: "full"
725
+ exec.security: "full", exec.ask: "off"
726
+ sandbox.mode: "off"
727
+
728
+ ${c("yellow", "Try saying to your agent:")}
729
+ "Send me a selfie"
730
+ "Send a pic wearing a cowboy hat"
731
+ "What are you doing right now?"
732
+
733
+ ${c("dim", "Your agent now has selfie superpowers!")}
734
+ `);
735
+ }
736
+
737
+ // Handle reinstall
738
+ async function handleReinstall(rl) {
739
+ const reinstall = await ask(rl, "\nReinstall/update? (y/N): ");
740
+
741
+ if (reinstall.toLowerCase() !== "y") {
742
+ log("\nNo changes made. Goodbye!");
743
+ return false;
744
+ }
745
+
746
+ // Remove existing installation
747
+ fs.rmSync(SKILL_DEST, { recursive: true, force: true });
748
+ logInfo("Removed existing installation");
749
+
750
+ return true;
751
+ }
752
+
753
+ // Main function
754
+ async function main() {
755
+ const rl = createPrompt();
756
+
757
+ try {
758
+ printBanner();
759
+
760
+ // Step 1: Check prerequisites
761
+ const prereqResult = await checkPrerequisites();
762
+
763
+ if (prereqResult === false) {
764
+ rl.close();
765
+ process.exit(1);
766
+ }
767
+
768
+ if (prereqResult === "already_installed") {
769
+ const shouldContinue = await handleReinstall(rl);
770
+ if (!shouldContinue) {
771
+ rl.close();
772
+ process.exit(0);
773
+ }
774
+ }
775
+
776
+ // Step 2: Get Gemini API key
777
+ const geminiKey = await getGeminiApiKey(rl);
778
+ if (!geminiKey) {
779
+ rl.close();
780
+ process.exit(1);
781
+ }
782
+
783
+ // Step 3: Install skill files
784
+ await installSkill();
785
+
786
+ // Step 4: Update OpenClaw config
787
+ await updateOpenClawConfig(geminiKey);
788
+
789
+ // Step 5: Write IDENTITY.md
790
+ await writeIdentity();
791
+
792
+ // Step 6: Inject persona
793
+ await injectPersona(rl);
794
+
795
+ // Step 7: Configure agent tools
796
+ const agentName = await configureAgentTools(rl);
797
+
798
+ // Step 8: Summary
799
+ printSummary(agentName);
800
+
801
+ rl.close();
802
+ } catch (error) {
803
+ logError(`Installation failed: ${error.message}`);
804
+ console.error(error);
805
+ rl.close();
806
+ process.exit(1);
807
+ }
808
+ }
809
+
810
+ // Run
811
+ main();