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/README.md +238 -0
- package/bin/cli.js +811 -0
- package/package.json +37 -0
- package/skill/SKILL.md +85 -0
- package/skill/assets/gfclaw.png +0 -0
- package/skill/scripts/gfclaw-selfie.sh +218 -0
- package/skill/scripts/save-reference.sh +45 -0
- package/templates/soul-injection.md +144 -0
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();
|