gfclaw 2.0.0 → 2.1.1

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
@@ -651,8 +651,18 @@ async function createNewAgent(rl, config, agentName) {
651
651
  botToken: botToken,
652
652
  groupPolicy: "allowlist",
653
653
  streaming: "off",
654
+ commands: {
655
+ native: false,
656
+ nativeSkills: false,
657
+ },
658
+ customCommands: [
659
+ { command: "newphoto", description: "Change my reference photo" },
660
+ { command: "personality", description: "Change my personality" },
661
+ { command: "mystatus", description: "Show current setup" },
662
+ ],
654
663
  };
655
664
  logSuccess(`Added Telegram account: ${accountName}`);
665
+ logSuccess("Configured Telegram command menu (gfclaw commands only)");
656
666
 
657
667
  // --- Allow user ---
658
668
  if (allowedUser) {
@@ -750,6 +760,408 @@ async function handleReinstall(rl) {
750
760
  return true;
751
761
  }
752
762
 
763
+ // ─────────────────────────────────────────────
764
+ // CLI Management Commands
765
+ // ─────────────────────────────────────────────
766
+
767
+ // Find the gfclaw agent entry in config
768
+ function findGfclawAgent(config) {
769
+ const agents = (config.agents && config.agents.list) || [];
770
+ // Try exact match first
771
+ let agent = agents.find((a) => a.id === "gfclaw");
772
+ if (agent) return agent;
773
+ // Try workspace path containing 'gfclaw'
774
+ agent = agents.find((a) => a.workspace && a.workspace.includes("gfclaw"));
775
+ if (agent) return agent;
776
+ // Fallback: first non-main agent
777
+ agent = agents.find((a) => a.id !== "main");
778
+ return agent || null;
779
+ }
780
+
781
+ // Find the gfclaw telegram account name from bindings
782
+ function findGfclawAccountName(config, agentId) {
783
+ const bindings = config.bindings || [];
784
+ const binding = bindings.find(
785
+ (b) => b.agentId === agentId && b.match && b.match.channel === "telegram"
786
+ );
787
+ return binding ? binding.match.accountId : null;
788
+ }
789
+
790
+ // --help
791
+ function showHelp() {
792
+ console.log(`
793
+ ${c("magenta", "GFClaw")} - Selfie Skill for OpenClaw
794
+
795
+ ${c("bright", "Usage:")}
796
+ npx gfclaw Install GFClaw (first-time setup)
797
+ npx gfclaw --help Show this help message
798
+ npx gfclaw --status Show current GFClaw setup
799
+ npx gfclaw --reconfigure Change photo, personality, or API key
800
+ npx gfclaw --repair Change allowed Telegram user
801
+
802
+ ${c("bright", "Aliases:")}
803
+ --help, -h
804
+ --status, -s
805
+ --reconfigure, -c
806
+ --repair, -r
807
+ `);
808
+ }
809
+
810
+ // --status
811
+ async function showStatus() {
812
+ if (!fs.existsSync(OPENCLAW_CONFIG)) {
813
+ logError("GFClaw is not installed (no OpenClaw config found)");
814
+ logInfo(`Expected: ${OPENCLAW_CONFIG}`);
815
+ process.exit(1);
816
+ }
817
+
818
+ const config = readJsonFile(OPENCLAW_CONFIG);
819
+ if (!config) {
820
+ logError("Failed to parse OpenClaw config");
821
+ process.exit(1);
822
+ }
823
+
824
+ const agent = findGfclawAgent(config);
825
+ if (!agent) {
826
+ logError("No GFClaw agent found in config");
827
+ logInfo("Run 'npx gfclaw' to install.");
828
+ process.exit(1);
829
+ }
830
+
831
+ const workspace = agent.workspace || path.join(OPENCLAW_DIR, `workspace-${agent.id}`);
832
+ const accountName = findGfclawAccountName(config, agent.id);
833
+
834
+ // Read workspace files
835
+ let onboarding = null;
836
+ const onboardingPath = path.join(workspace, "onboarding.json");
837
+ if (fs.existsSync(onboardingPath)) {
838
+ try { onboarding = JSON.parse(fs.readFileSync(onboardingPath, "utf8")); } catch {}
839
+ }
840
+
841
+ let personality = null;
842
+ const personalityPath = path.join(workspace, "personality.txt");
843
+ if (fs.existsSync(personalityPath)) {
844
+ personality = fs.readFileSync(personalityPath, "utf8").trim();
845
+ }
846
+
847
+ const hasCustomPhoto = fs.existsSync(path.join(workspace, "my-reference.png"));
848
+
849
+ // Get bot info
850
+ let botToken = null;
851
+ if (accountName && config.channels && config.channels.telegram && config.channels.telegram.accounts) {
852
+ const acct = config.channels.telegram.accounts[accountName];
853
+ if (acct) botToken = acct.botToken;
854
+ }
855
+
856
+ // Get allowed users
857
+ const allowFrom = (config.channels && config.channels.telegram && config.channels.telegram.allowFrom) || [];
858
+
859
+ // Check API key
860
+ let hasApiKey = false;
861
+ if (fs.existsSync(OPENCLAW_ENV)) {
862
+ const envContent = fs.readFileSync(OPENCLAW_ENV, "utf8");
863
+ hasApiKey = /^GEMINI_API_KEY=.+$/m.test(envContent);
864
+ }
865
+
866
+ // Print status
867
+ console.log(`
868
+ ${c("bright", "GFClaw Status")}
869
+ ${c("cyan", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
870
+
871
+ ${c("cyan", "Agent:")} ${c("bright", agent.id)}
872
+ ${c("cyan", "Workspace:")} ${workspace}
873
+ `);
874
+
875
+ // Onboarding
876
+ if (onboarding && onboarding.completed) {
877
+ const date = onboarding.completedAt ? ` (${onboarding.completedAt.split("T")[0]})` : "";
878
+ logSuccess(`Onboarding: Completed${date}`);
879
+ } else {
880
+ logError("Onboarding: Not completed");
881
+ }
882
+
883
+ // Photo
884
+ if (hasCustomPhoto) {
885
+ logSuccess("Custom Photo: Set");
886
+ } else {
887
+ logWarn("Custom Photo: Using default");
888
+ }
889
+
890
+ // Personality
891
+ if (personality) {
892
+ logSuccess(`Personality: \"${personality}\"`);
893
+ } else {
894
+ logWarn("Personality: Not set (using default)");
895
+ }
896
+
897
+ log("");
898
+
899
+ // Telegram
900
+ if (botToken) {
901
+ const botId = botToken.split(":")[0];
902
+ logInfo(`Telegram Bot: token ${botId}:***`);
903
+ }
904
+ if (allowFrom.length > 0) {
905
+ logInfo(`Allowed Users: ${allowFrom.join(", ")}`);
906
+ } else {
907
+ logWarn("Allowed Users: None configured");
908
+ }
909
+
910
+ // API Key
911
+ if (hasApiKey) {
912
+ logSuccess("Gemini API Key: Configured");
913
+ } else {
914
+ logError("Gemini API Key: Not found");
915
+ }
916
+
917
+ log("");
918
+ }
919
+
920
+ // --reconfigure
921
+ async function runReconfigure() {
922
+ if (!fs.existsSync(OPENCLAW_CONFIG)) {
923
+ logError("GFClaw is not installed (no OpenClaw config found)");
924
+ process.exit(1);
925
+ }
926
+
927
+ const config = readJsonFile(OPENCLAW_CONFIG);
928
+ if (!config) {
929
+ logError("Failed to parse OpenClaw config");
930
+ process.exit(1);
931
+ }
932
+
933
+ const agent = findGfclawAgent(config);
934
+ if (!agent) {
935
+ logError("No GFClaw agent found in config");
936
+ logInfo("Run 'npx gfclaw' to install.");
937
+ process.exit(1);
938
+ }
939
+
940
+ const workspace = agent.workspace || path.join(OPENCLAW_DIR, `workspace-${agent.id}`);
941
+
942
+ console.log(`
943
+ ${c("bright", "GFClaw Reconfigure")}
944
+ ${c("cyan", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
945
+
946
+ What would you like to change?
947
+ ${c("bright", "1.")} Reference photo
948
+ ${c("bright", "2.")} Personality
949
+ ${c("bright", "3.")} Gemini API key
950
+ ${c("bright", "4.")} All of the above
951
+ `);
952
+
953
+ const rl = createPrompt();
954
+
955
+ try {
956
+ const choice = await ask(rl, "Choose (1-4): ");
957
+ log("");
958
+
959
+ if (choice === "1" || choice === "4") {
960
+ await reconfigurePhoto(rl, workspace);
961
+ }
962
+ if (choice === "2" || choice === "4") {
963
+ await reconfigurePersonality(rl, workspace);
964
+ }
965
+ if (choice === "3" || choice === "4") {
966
+ await reconfigureApiKey(rl);
967
+ }
968
+
969
+ if (!["1", "2", "3", "4"].includes(choice)) {
970
+ logError("Invalid choice. Please enter 1, 2, 3, or 4.");
971
+ rl.close();
972
+ process.exit(1);
973
+ }
974
+
975
+ log("");
976
+ logSuccess("Changes saved!");
977
+ logInfo("Restart OpenClaw for changes to take effect:");
978
+ log(` ${c("bright", "systemctl --user restart openclaw-gateway.service")}`);
979
+ log("");
980
+
981
+ rl.close();
982
+ } catch (error) {
983
+ logError(`Reconfigure failed: ${error.message}`);
984
+ rl.close();
985
+ process.exit(1);
986
+ }
987
+ }
988
+
989
+ async function reconfigurePhoto(rl, workspace) {
990
+ logStep("Photo", "Changing reference photo...");
991
+ const photoPath = await ask(rl, "Enter path to new reference photo: ");
992
+
993
+ if (!photoPath) {
994
+ logWarn("Skipped (no path entered)");
995
+ return;
996
+ }
997
+
998
+ // Expand ~ to home dir
999
+ const resolvedPath = photoPath.replace(/^~/, HOME);
1000
+
1001
+ if (!fs.existsSync(resolvedPath)) {
1002
+ logError(`File not found: ${resolvedPath}`);
1003
+ return;
1004
+ }
1005
+
1006
+ const ext = path.extname(resolvedPath).toLowerCase();
1007
+ if (![".png", ".jpg", ".jpeg", ".webp"].includes(ext)) {
1008
+ logWarn(`Unexpected file type: ${ext}. Expected .png, .jpg, .jpeg, or .webp`);
1009
+ const proceed = await ask(rl, "Continue anyway? (y/N): ");
1010
+ if (proceed.toLowerCase() !== "y") return;
1011
+ }
1012
+
1013
+ const destPath = path.join(workspace, "my-reference.png");
1014
+ fs.copyFileSync(resolvedPath, destPath);
1015
+ logSuccess(`Reference photo updated: ${destPath}`);
1016
+
1017
+ // Update onboarding.json
1018
+ const onboardingPath = path.join(workspace, "onboarding.json");
1019
+ let onboarding = {};
1020
+ if (fs.existsSync(onboardingPath)) {
1021
+ try { onboarding = JSON.parse(fs.readFileSync(onboardingPath, "utf8")); } catch {}
1022
+ }
1023
+ onboarding.hasCustomImage = true;
1024
+ fs.writeFileSync(onboardingPath, JSON.stringify(onboarding, null, 2) + "\n");
1025
+ }
1026
+
1027
+ async function reconfigurePersonality(rl, workspace) {
1028
+ logStep("Personality", "Changing personality...");
1029
+
1030
+ // Show current personality if exists
1031
+ const personalityPath = path.join(workspace, "personality.txt");
1032
+ if (fs.existsSync(personalityPath)) {
1033
+ const current = fs.readFileSync(personalityPath, "utf8").trim();
1034
+ logInfo(`Current: \"${current}\"`);
1035
+ }
1036
+
1037
+ const personality = await ask(rl, "Enter new personality description: ");
1038
+
1039
+ if (!personality) {
1040
+ logWarn("Skipped (no input)");
1041
+ return;
1042
+ }
1043
+
1044
+ fs.writeFileSync(personalityPath, personality + "\n");
1045
+ logSuccess(`Personality updated: ${personalityPath}`);
1046
+
1047
+ // Update onboarding.json
1048
+ const onboardingPath = path.join(workspace, "onboarding.json");
1049
+ let onboarding = {};
1050
+ if (fs.existsSync(onboardingPath)) {
1051
+ try { onboarding = JSON.parse(fs.readFileSync(onboardingPath, "utf8")); } catch {}
1052
+ }
1053
+ onboarding.personality = personality;
1054
+ fs.writeFileSync(onboardingPath, JSON.stringify(onboarding, null, 2) + "\n");
1055
+ }
1056
+
1057
+ async function reconfigureApiKey(rl) {
1058
+ logStep("API Key", "Changing Gemini API key...");
1059
+
1060
+ const geminiKey = await getGeminiApiKey(rl);
1061
+ if (!geminiKey) {
1062
+ logWarn("Skipped (no key entered)");
1063
+ return;
1064
+ }
1065
+
1066
+ // Update .env file
1067
+ let envContent = "";
1068
+ if (fs.existsSync(OPENCLAW_ENV)) {
1069
+ envContent = fs.readFileSync(OPENCLAW_ENV, "utf8");
1070
+ }
1071
+
1072
+ if (envContent.includes("GEMINI_API_KEY=")) {
1073
+ envContent = envContent.replace(/^GEMINI_API_KEY=.*$/m, `GEMINI_API_KEY=${geminiKey}`);
1074
+ } else {
1075
+ if (envContent && !envContent.endsWith("\n")) envContent += "\n";
1076
+ envContent += `GEMINI_API_KEY=${geminiKey}\n`;
1077
+ }
1078
+
1079
+ fs.writeFileSync(OPENCLAW_ENV, envContent);
1080
+ logSuccess(`API key saved to: ${OPENCLAW_ENV}`);
1081
+ }
1082
+
1083
+ // --repair
1084
+ async function runRepair() {
1085
+ if (!fs.existsSync(OPENCLAW_CONFIG)) {
1086
+ logError("GFClaw is not installed (no OpenClaw config found)");
1087
+ process.exit(1);
1088
+ }
1089
+
1090
+ const config = readJsonFile(OPENCLAW_CONFIG);
1091
+ if (!config) {
1092
+ logError("Failed to parse OpenClaw config");
1093
+ process.exit(1);
1094
+ }
1095
+
1096
+ console.log(`
1097
+ ${c("bright", "GFClaw Repair")} — Change Allowed Telegram User
1098
+ ${c("cyan", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
1099
+ `);
1100
+
1101
+ // Show current allowed users
1102
+ const allowFrom = (config.channels && config.channels.telegram && config.channels.telegram.allowFrom) || [];
1103
+ if (allowFrom.length > 0) {
1104
+ logInfo(`Current allowed users: ${allowFrom.join(", ")}`);
1105
+ } else {
1106
+ logWarn("No allowed users currently configured");
1107
+ }
1108
+
1109
+ const rl = createPrompt();
1110
+
1111
+ try {
1112
+ log("");
1113
+ const newUser = await ask(rl, "Enter new Telegram chat ID (e.g. tg:123456789): ");
1114
+
1115
+ if (!newUser) {
1116
+ logWarn("No changes made.");
1117
+ rl.close();
1118
+ return;
1119
+ }
1120
+
1121
+ if (!newUser.startsWith("tg:")) {
1122
+ logWarn(`Expected format \"tg:<chat_id>\". Got: \"${newUser}\"`);
1123
+ const proceed = await ask(rl, "Continue anyway? (y/N): ");
1124
+ if (proceed.toLowerCase() !== "y") {
1125
+ rl.close();
1126
+ return;
1127
+ }
1128
+ }
1129
+
1130
+ log("");
1131
+ const mode = await ask(rl, "Replace all existing users, or add this one? (replace/add): ");
1132
+
1133
+ if (!config.channels) config.channels = {};
1134
+ if (!config.channels.telegram) config.channels.telegram = {};
1135
+
1136
+ if (mode.toLowerCase() === "replace" || mode.toLowerCase() === "r") {
1137
+ config.channels.telegram.allowFrom = [newUser];
1138
+ logSuccess(`Replaced allowed users with: ${newUser}`);
1139
+ } else {
1140
+ if (!config.channels.telegram.allowFrom) config.channels.telegram.allowFrom = [];
1141
+ if (!config.channels.telegram.allowFrom.includes(newUser)) {
1142
+ config.channels.telegram.allowFrom.push(newUser);
1143
+ logSuccess(`Added allowed user: ${newUser}`);
1144
+ } else {
1145
+ logInfo(`User ${newUser} is already in the list`);
1146
+ }
1147
+ }
1148
+
1149
+ writeJsonFile(OPENCLAW_CONFIG, config);
1150
+ logSuccess(`Config saved: ${OPENCLAW_CONFIG}`);
1151
+
1152
+ log("");
1153
+ logInfo("Restart OpenClaw for changes to take effect:");
1154
+ log(` ${c("bright", "systemctl --user restart openclaw-gateway.service")}`);
1155
+ log("");
1156
+
1157
+ rl.close();
1158
+ } catch (error) {
1159
+ logError(`Repair failed: ${error.message}`);
1160
+ rl.close();
1161
+ process.exit(1);
1162
+ }
1163
+ }
1164
+
753
1165
  // Main function
754
1166
  async function main() {
755
1167
  const rl = createPrompt();
@@ -807,5 +1219,18 @@ async function main() {
807
1219
  }
808
1220
  }
809
1221
 
810
- // Run
811
- main();
1222
+ // Parse CLI arguments and dispatch
1223
+ const args = process.argv.slice(2);
1224
+ const flag = args[0];
1225
+
1226
+ if (flag === "--help" || flag === "-h") {
1227
+ showHelp();
1228
+ } else if (flag === "--status" || flag === "-s") {
1229
+ showStatus();
1230
+ } else if (flag === "--reconfigure" || flag === "-c") {
1231
+ runReconfigure();
1232
+ } else if (flag === "--repair" || flag === "-r") {
1233
+ runRepair();
1234
+ } else {
1235
+ main();
1236
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gfclaw",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "Add selfie superpowers to your OpenClaw agent using Google Gemini image editing",
5
5
  "bin": {
6
6
  "gfclaw": "./bin/cli.js"
@@ -81,7 +81,7 @@ Wait for the user to reply.
81
81
  - Also update the `personality` field in `onboarding.json`.
82
82
  - Confirm: "Done! New me, who dis? 😏"
83
83
 
84
- **`/status`** — Show current setup
84
+ **`/mystatus`** — Show current setup
85
85
  - Read `onboarding.json` and `personality.txt` from workspace.
86
86
  - Tell user: whether custom image is set, current personality, when they set it up.
87
87