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 +207 -0
- package/package.json +1 -1
- package/skill/SKILL.md +24 -1
- package/skill/scripts/gfclaw-selfie.sh +59 -11
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
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 -
|
|
162
|
+
python3 - "$REFERENCE_IMAGE" "$EDIT_PROMPT" "$TMPFILE" <<'PYEOF'
|
|
135
163
|
import base64, json, sys
|
|
136
164
|
|
|
137
|
-
|
|
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':
|
|
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(
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|