gfclaw 2.2.0 → 2.3.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 CHANGED
@@ -268,6 +268,31 @@ async function getGeminiApiKey(rl) {
268
268
  logWarn("Gemini API keys typically start with 'AIza'. Make sure you copied the full key.");
269
269
  }
270
270
 
271
+ // Validate key by calling Gemini API
272
+ logInfo("Validating API key...");
273
+ try {
274
+ const result = execSync(
275
+ `curl -s "https://generativelanguage.googleapis.com/v1beta/models?key=${geminiKey}" 2>&1`,
276
+ { timeout: 15000 }
277
+ ).toString();
278
+
279
+ const parsed = JSON.parse(result);
280
+ if (parsed.error) {
281
+ logError(`API key validation failed: ${parsed.error.message}`);
282
+ const proceed = await ask(rl, "Continue with this key anyway? (y/N): ");
283
+ if (proceed.toLowerCase() !== "y") {
284
+ return null;
285
+ }
286
+ } else if (parsed.models && parsed.models.length > 0) {
287
+ logSuccess(`API key valid (${parsed.models.length} models available)`);
288
+ } else {
289
+ logWarn("API key accepted but no models found. Billing may not be enabled.");
290
+ }
291
+ } catch (e) {
292
+ logWarn(`Could not validate API key: ${e.message}`);
293
+ logInfo("Continuing anyway — you can test the key later.");
294
+ }
295
+
271
296
  logSuccess("API key received");
272
297
  return geminiKey;
273
298
  }
@@ -798,12 +823,16 @@ ${c("bright", "Usage:")}
798
823
  npx gfclaw --status Show current GFClaw setup
799
824
  npx gfclaw --reconfigure Change photo, personality, or API key
800
825
  npx gfclaw --repair Change allowed Telegram user
826
+ npx gfclaw --version Show installed version
827
+ npx gfclaw --uninstall Remove GFClaw from your system
801
828
 
802
829
  ${c("bright", "Aliases:")}
803
830
  --help, -h
804
831
  --status, -s
805
832
  --reconfigure, -c
806
833
  --repair, -r
834
+ --version, -v
835
+ --uninstall, -u
807
836
  `);
808
837
  }
809
838
 
@@ -1080,6 +1109,179 @@ async function reconfigureApiKey(rl) {
1080
1109
  logSuccess(`API key saved to: ${OPENCLAW_ENV}`);
1081
1110
  }
1082
1111
 
1112
+ // --uninstall
1113
+ async function runUninstall() {
1114
+ if (!fs.existsSync(OPENCLAW_CONFIG)) {
1115
+ logError("GFClaw is not installed (no OpenClaw config found)");
1116
+ logInfo(`Expected: ${OPENCLAW_CONFIG}`);
1117
+ process.exit(1);
1118
+ }
1119
+
1120
+ const config = readJsonFile(OPENCLAW_CONFIG);
1121
+ if (!config) {
1122
+ logError("Failed to parse OpenClaw config");
1123
+ process.exit(1);
1124
+ }
1125
+
1126
+ const agent = findGfclawAgent(config);
1127
+ if (!agent) {
1128
+ logError("No GFClaw agent found in config");
1129
+ logInfo("Nothing to uninstall.");
1130
+ process.exit(1);
1131
+ }
1132
+
1133
+ const agentId = agent.id;
1134
+ const workspace = agent.workspace || path.join(OPENCLAW_DIR, `workspace-${agentId}`);
1135
+ const accountName = findGfclawAccountName(config, agentId);
1136
+
1137
+ console.log(`
1138
+ ${c("bright", "GFClaw Uninstall")}
1139
+ ${c("cyan", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
1140
+
1141
+ ${c("cyan", "Will remove:")}
1142
+ • Skill directory: ${SKILL_DEST}
1143
+ • Agent entry: ${c("bright", agentId)}${accountName ? `\n • Telegram account: ${c("bright", accountName)}` : ""}
1144
+ • Binding: ${agentId} ↔ telegram${accountName ? "/" + accountName : ""}
1145
+ • Skill config entry: ${SKILL_NAME}
1146
+ • SELFIE-SKILL.md from workspace
1147
+ • GFClaw persona section from SOUL.md
1148
+
1149
+ ${c("yellow", "Will preserve:")}
1150
+ • Workspace data (journal, reminders): ${workspace}
1151
+ • GEMINI_API_KEY in .env
1152
+ `);
1153
+
1154
+ const rl = createPrompt();
1155
+
1156
+ try {
1157
+ const confirm = await ask(rl, "This will remove GFClaw from your system. Continue? (y/N): ");
1158
+ if (confirm.toLowerCase() !== "y") {
1159
+ log("\nAborted. No changes made.");
1160
+ rl.close();
1161
+ return;
1162
+ }
1163
+
1164
+ log("");
1165
+ let removed = [];
1166
+
1167
+ // 1. Remove skill directory
1168
+ if (fs.existsSync(SKILL_DEST)) {
1169
+ fs.rmSync(SKILL_DEST, { recursive: true, force: true });
1170
+ logSuccess(`Removed skill directory: ${SKILL_DEST}`);
1171
+ removed.push("skill directory");
1172
+ }
1173
+
1174
+ // 2. Remove agent entry from config.agents.list
1175
+ if (config.agents && config.agents.list) {
1176
+ const before = config.agents.list.length;
1177
+ config.agents.list = config.agents.list.filter((a) => a.id !== agentId);
1178
+ if (config.agents.list.length < before) {
1179
+ logSuccess(`Removed agent entry: ${agentId}`);
1180
+ removed.push("agent entry");
1181
+ }
1182
+ }
1183
+
1184
+ // 3. Remove telegram account
1185
+ if (accountName && config.channels && config.channels.telegram && config.channels.telegram.accounts) {
1186
+ if (config.channels.telegram.accounts[accountName]) {
1187
+ delete config.channels.telegram.accounts[accountName];
1188
+ logSuccess(`Removed Telegram account: ${accountName}`);
1189
+ removed.push("telegram account");
1190
+ }
1191
+ }
1192
+
1193
+ // 4. Remove binding
1194
+ if (config.bindings) {
1195
+ const before = config.bindings.length;
1196
+ config.bindings = config.bindings.filter(
1197
+ (b) => !(b.agentId === agentId && b.match && b.match.channel === "telegram")
1198
+ );
1199
+ if (config.bindings.length < before) {
1200
+ logSuccess(`Removed binding: ${agentId} ↔ telegram`);
1201
+ removed.push("binding");
1202
+ }
1203
+ }
1204
+
1205
+ // 5. Remove skill entry from config.skills.entries
1206
+ if (config.skills && config.skills.entries && config.skills.entries[SKILL_NAME]) {
1207
+ delete config.skills.entries[SKILL_NAME];
1208
+ logSuccess(`Removed skill config entry: ${SKILL_NAME}`);
1209
+ removed.push("skill config");
1210
+ }
1211
+
1212
+ // 6. Remove SELFIE-SKILL.md from main workspace
1213
+ const selfieSkillMain = path.join(OPENCLAW_WORKSPACE, "SELFIE-SKILL.md");
1214
+ if (fs.existsSync(selfieSkillMain)) {
1215
+ fs.unlinkSync(selfieSkillMain);
1216
+ logSuccess(`Removed: ${selfieSkillMain}`);
1217
+ removed.push("SELFIE-SKILL.md (main workspace)");
1218
+ }
1219
+
1220
+ // Also remove from agent workspace if different
1221
+ if (workspace !== OPENCLAW_WORKSPACE) {
1222
+ const selfieSkillAgent = path.join(workspace, "SELFIE-SKILL.md");
1223
+ if (fs.existsSync(selfieSkillAgent)) {
1224
+ fs.unlinkSync(selfieSkillAgent);
1225
+ logSuccess(`Removed: ${selfieSkillAgent}`);
1226
+ removed.push("SELFIE-SKILL.md (agent workspace)");
1227
+ }
1228
+ }
1229
+
1230
+ // 7. Remove GFClaw persona section from SOUL.md
1231
+ if (fs.existsSync(SOUL_MD)) {
1232
+ let soulContent = fs.readFileSync(SOUL_MD, "utf8");
1233
+ const gfclawPattern = /\n## GFClaw[\s\S]*?(?=\n## |\n# |$)/;
1234
+ if (gfclawPattern.test(soulContent)) {
1235
+ soulContent = soulContent.replace(gfclawPattern, "");
1236
+ fs.writeFileSync(SOUL_MD, soulContent);
1237
+ logSuccess(`Removed GFClaw section from: ${SOUL_MD}`);
1238
+ removed.push("SOUL.md persona section");
1239
+ }
1240
+ }
1241
+
1242
+ // Also clean agent workspace SOUL.md if different
1243
+ if (workspace !== OPENCLAW_WORKSPACE) {
1244
+ const agentSoul = path.join(workspace, "SOUL.md");
1245
+ if (fs.existsSync(agentSoul)) {
1246
+ let agentSoulContent = fs.readFileSync(agentSoul, "utf8");
1247
+ const gfclawPattern = /\n## GFClaw[\s\S]*?(?=\n## |\n# |$)/;
1248
+ if (gfclawPattern.test(agentSoulContent)) {
1249
+ agentSoulContent = agentSoulContent.replace(gfclawPattern, "");
1250
+ fs.writeFileSync(agentSoul, agentSoulContent);
1251
+ logSuccess(`Removed GFClaw section from: ${agentSoul}`);
1252
+ removed.push("SOUL.md persona section (agent workspace)");
1253
+ }
1254
+ }
1255
+ }
1256
+
1257
+ // 8. Save updated config
1258
+ writeJsonFile(OPENCLAW_CONFIG, config);
1259
+ logSuccess(`Config saved: ${OPENCLAW_CONFIG}`);
1260
+
1261
+ // 9. Print summary
1262
+ console.log(`
1263
+ ${c("green", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
1264
+ ${c("bright", " GFClaw has been removed.")}
1265
+ ${c("green", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
1266
+
1267
+ ${c("cyan", "Removed:")} ${removed.join(", ")}
1268
+ `);
1269
+
1270
+ logWarn(`Note: Your workspace data (journal, reminders) was preserved at: ${workspace}`);
1271
+ logWarn(`Note: GEMINI_API_KEY in .env was preserved (may be used by other tools)`);
1272
+
1273
+ log("");
1274
+ logInfo("Restart OpenClaw for changes to take effect:");
1275
+ log(` ${c("bright", "systemctl --user restart openclaw-gateway.service")}`);
1276
+ log("");
1277
+
1278
+ rl.close();
1279
+ } catch (error) {
1280
+ logError(`Uninstall failed: ${error.message}`);
1281
+ rl.close();
1282
+ process.exit(1);
1283
+ }
1284
+ }
1083
1285
  // --repair
1084
1286
  async function runRepair() {
1085
1287
  if (!fs.existsSync(OPENCLAW_CONFIG)) {
@@ -1231,6 +1433,11 @@ if (flag === "--help" || flag === "-h") {
1231
1433
  runReconfigure();
1232
1434
  } else if (flag === "--repair" || flag === "-r") {
1233
1435
  runRepair();
1436
+ } else if (flag === "--version" || flag === "-v") {
1437
+ const pkg = require(path.join(PACKAGE_ROOT, "package.json"));
1438
+ console.log(`gfclaw v${pkg.version}`);
1439
+ } else if (flag === "--uninstall" || flag === "-u") {
1440
+ runUninstall();
1234
1441
  } else {
1235
1442
  main();
1236
1443
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gfclaw",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "AI virtual girlfriend agent for OpenClaw — selfies, journal, reminders, and more",
5
5
  "bin": {
6
6
  "gfclaw": "./bin/cli.js"
package/skill/SKILL.md CHANGED
@@ -76,15 +76,38 @@ These skills are handled entirely through SOUL.md behavior instructions:
76
76
 
77
77
  ## Technical Details
78
78
 
79
- - Selfie images generated via Google Gemini (`gemini-2.5-flash-image`)
79
+ - Selfie images generated via Google Gemini (default: `gemini-2.5-flash-image`, configurable via `GEMINI_MODEL` env var)
80
80
  - Default reference image: `~/.openclaw/skills/gfclaw-selfie/assets/gfclaw.png`
81
81
  - Custom reference: agent sets `GFCLAW_REFERENCE_IMAGE` env var
82
82
  - Journal entries stored in `workspace/journal/` directory
83
83
  - Reminders stored in `workspace/reminders.json`
84
84
  - Images auto-deleted after sending (no disk waste)
85
+ - Debug mode: set `GFCLAW_DEBUG=1` for verbose logging in selfie script
86
+ - Rate limiting: 30-second cooldown between selfie generations
85
87
 
86
88
  ## Important Notes
87
89
 
88
90
  - **Do NOT save images to /tmp** — the message tool cannot access files outside the workspace
89
91
  - **Do NOT manually call curl or the Gemini API** — use the scripts
90
92
  - **Do NOT try to read files outside the agent workspace** — use script paths directly
93
+
94
+ ## CLI Management
95
+
96
+ ```bash
97
+ npx gfclaw # Install / reconfigure GFClaw
98
+ npx gfclaw --status # Check installation status
99
+ npx gfclaw --reconfigure # Change settings
100
+ npx gfclaw --repair # Fix broken installation
101
+ npx gfclaw --version # Show installed version
102
+ npx gfclaw --uninstall # Clean removal from system
103
+ ```
104
+
105
+ ## Environment Variables
106
+
107
+ | Variable | Default | Description |
108
+ |----------|---------|-------------|
109
+ | `GEMINI_API_KEY` | (required) | Google Gemini API key |
110
+ | `GEMINI_MODEL` | `gemini-2.5-flash-image` | Gemini model for image generation |
111
+ | `GFCLAW_DEBUG` | `0` | Set to `1` for verbose logging |
112
+ | `GFCLAW_REFERENCE_IMAGE` | default asset | Custom reference photo path |
113
+ | `GFCLAW_PERSONALITY` | (from personality.txt) | Personality traits override |
@@ -37,6 +37,17 @@ if [ -z "${GEMINI_API_KEY:-}" ]; then
37
37
  exit 1
38
38
  fi
39
39
 
40
+ # Configurable model (override with GEMINI_MODEL env var)
41
+ GEMINI_MODEL="${GEMINI_MODEL:-gemini-2.5-flash-image}"
42
+
43
+ # Debug mode
44
+ DEBUG="${GFCLAW_DEBUG:-0}"
45
+ debug_log() {
46
+ if [ "$DEBUG" = "1" ]; then
47
+ echo -e "${YELLOW}[DEBUG]${NC} $1"
48
+ fi
49
+ }
50
+
40
51
  # Check for jq
41
52
  if ! command -v jq &> /dev/null; then
42
53
  log_error "jq is required but not installed"
@@ -123,6 +134,23 @@ fi
123
134
  log_info "Mode: $MODE"
124
135
  log_info "Editing reference image with prompt: $EDIT_PROMPT"
125
136
 
137
+ debug_log "EDIT_PROMPT: $EDIT_PROMPT"
138
+
139
+ # Rate limiting: minimum 30 seconds between selfie calls
140
+ COOLDOWN_FILE="/tmp/gfclaw-cooldown.lock"
141
+ COOLDOWN_SECS=30
142
+ if [ -f "$COOLDOWN_FILE" ]; then
143
+ LAST_RUN=$(cat "$COOLDOWN_FILE" 2>/dev/null || echo 0)
144
+ NOW=$(date +%s)
145
+ ELAPSED=$((NOW - LAST_RUN))
146
+ if [ "$ELAPSED" -lt "$COOLDOWN_SECS" ]; then
147
+ REMAINING=$((COOLDOWN_SECS - ELAPSED))
148
+ log_warn "Cooldown active. Please wait ${REMAINING}s before generating another selfie."
149
+ exit 1
150
+ fi
151
+ fi
152
+ date +%s > "$COOLDOWN_FILE"
153
+
126
154
  # Build JSON payload using python3 (avoids argument list too long for base64)
127
155
  TMPFILE=$(mktemp /tmp/gemini-req-XXXXXX.json)
128
156
  # Save output image in MAIN workspace (CLI uses main agent context, fs.workspaceOnly blocks other paths)
@@ -131,35 +159,55 @@ mkdir -p "$OUTPUT_DIR"
131
159
  OUTPUT_IMAGE="${OUTPUT_DIR}/selfie-$$.png"
132
160
  trap "rm -f '$TMPFILE' '$OUTPUT_IMAGE'" EXIT
133
161
 
134
- python3 -c "
162
+ python3 - "$REFERENCE_IMAGE" "$EDIT_PROMPT" "$TMPFILE" <<'PYEOF'
135
163
  import base64, json, sys
136
164
 
137
- with open('$REFERENCE_IMAGE', 'rb') as f:
165
+ ref_image = sys.argv[1]
166
+ edit_prompt = sys.argv[2]
167
+ tmp_file = sys.argv[3]
168
+
169
+ with open(ref_image, 'rb') as f:
138
170
  img_b64 = base64.b64encode(f.read()).decode()
139
171
 
140
172
  payload = {
141
173
  'contents': [{
142
174
  'parts': [
143
- {'text': $(echo "$EDIT_PROMPT" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read().strip()))")},
175
+ {'text': edit_prompt},
144
176
  {'inline_data': {'mime_type': 'image/png', 'data': img_b64}}
145
177
  ]
146
178
  }],
147
179
  'generationConfig': {'responseModalities': ['IMAGE']}
148
180
  }
149
181
 
150
- with open('$TMPFILE', 'w') as f:
182
+ with open(tmp_file, 'w') as f:
151
183
  json.dump(payload, f)
152
- "
184
+ PYEOF
153
185
 
154
186
  log_info "Sending request to Gemini..."
155
187
 
156
- # Call Gemini API
157
- RESPONSE=$(curl -s -X POST \
158
- "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent" \
159
- -H "x-goog-api-key: $GEMINI_API_KEY" \
160
- -H "Content-Type: application/json" \
161
- -d @"$TMPFILE")
188
+ # Call Gemini API with retry logic
189
+ GEMINI_URL="https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent"
190
+ debug_log "Gemini API URL: $GEMINI_URL"
191
+ MAX_RETRIES=3
192
+ RETRY_DELAY=2
193
+ RESPONSE=""
194
+ for ATTEMPT in $(seq 1 $MAX_RETRIES); do
195
+ RESPONSE=$(curl -s -X POST \
196
+ "$GEMINI_URL" \
197
+ -H "x-goog-api-key: $GEMINI_API_KEY" \
198
+ -H "Content-Type: application/json" \
199
+ -d @"$TMPFILE") && break
200
+ if [ "$ATTEMPT" -lt "$MAX_RETRIES" ]; then
201
+ log_warn "API call failed (attempt $ATTEMPT/$MAX_RETRIES). Retrying in ${RETRY_DELAY}s..."
202
+ sleep "$RETRY_DELAY"
203
+ else
204
+ log_error "API call failed after $MAX_RETRIES attempts."
205
+ exit 1
206
+ fi
207
+ done
162
208
 
209
+ debug_log "Response size: ${#RESPONSE} chars"
210
+ debug_log "Response (first 500 chars): ${RESPONSE:0:500}"
163
211
  # Check for errors
164
212
  ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty')
165
213
  if [ -n "$ERROR_MSG" ]; then