create-openclaw-bot 5.6.1 → 5.6.3

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/dist/cli.js CHANGED
@@ -9,34 +9,35 @@ import { spawn, execSync, execFileSync } from 'child_process';
9
9
  import { createRequire } from 'module';
10
10
 
11
11
  // ─── Shared generators (dual-mode IIFE + CJS) ────────────────────────────────
12
- // These modules export via module.exports when required from Node.js
13
- const _require = createRequire(import.meta.url);
14
-
15
- function loadSharedModule(modulePath, globalName) {
16
- const loaded = _require(modulePath);
17
- if (loaded && Object.keys(loaded).length > 0) {
18
- return loaded;
19
- }
20
- return globalThis[globalName] || loaded || {};
21
- }
22
-
23
- const {
24
- OPENCLAW_NPM_SPEC,
25
- OPENCLAW_RUNTIME_PACKAGES,
26
- TELEGRAM_RELAY_PLUGIN_SPEC,
27
- buildRelayPluginInstallCommand,
28
- buildTelegramPostInstallChecklist,
29
- } = loadSharedModule('./setup/shared/common-gen.js', '__openclawCommon');
30
-
31
- const {
32
- buildDockerArtifacts,
33
- } = loadSharedModule('./setup/shared/docker-gen.js', '__openclawDockerGen');
34
-
35
- const {
36
- buildWorkspaceFileMap,
37
- } = loadSharedModule('./setup/shared/workspace-gen.js', '__openclawWorkspace');
38
-
39
- const dataExport = loadSharedModule('./setup/data/index.js', '__openclawData');
12
+ // These modules export via module.exports when required from Node.js
13
+ const _require = createRequire(import.meta.url);
14
+
15
+ function loadSharedModule(modulePath, globalName) {
16
+ const loaded = _require(modulePath);
17
+ if (loaded && Object.keys(loaded).length > 0) {
18
+ return loaded;
19
+ }
20
+ return globalThis[globalName] || loaded || {};
21
+ }
22
+
23
+ const {
24
+ OPENCLAW_NPM_SPEC,
25
+ OPENCLAW_RUNTIME_PACKAGES,
26
+ TELEGRAM_RELAY_PLUGIN_SPEC,
27
+ TELEGRAM_SETUP_GUIDE_FILENAME,
28
+ buildRelayPluginInstallCommand,
29
+ buildTelegramPostInstallChecklist,
30
+ } = loadSharedModule('./setup/shared/common-gen.js', '__openclawCommon');
31
+
32
+ const {
33
+ buildDockerArtifacts,
34
+ } = loadSharedModule('./setup/shared/docker-gen.js', '__openclawDockerGen');
35
+
36
+ const {
37
+ buildWorkspaceFileMap,
38
+ } = loadSharedModule('./setup/shared/workspace-gen.js', '__openclawWorkspace');
39
+
40
+ const dataExport = loadSharedModule('./setup/data/index.js', '__openclawData');
40
41
 
41
42
  const {
42
43
  PROVIDERS: _PROVIDERS,
@@ -45,14 +46,14 @@ const {
45
46
  OLLAMA_MODELS,
46
47
  } = dataExport;
47
48
 
48
- const {
49
- buildCliChromeDebugArtifacts,
50
- buildCliUninstallArtifacts,
51
- buildCliUpgradeArtifacts,
52
- buildCliStartBotArtifacts,
53
- } = loadSharedModule('./setup/shared/install-gen.js', '__openclawInstall');
49
+ const {
50
+ buildCliChromeDebugArtifacts,
51
+ buildCliUninstallArtifacts,
52
+ buildCliUpgradeArtifacts,
53
+ buildCliStartBotArtifacts,
54
+ } = loadSharedModule('./setup/shared/install-gen.js', '__openclawInstall');
54
55
 
55
- function installRelayPluginForProject(projectDir, isVi) {
56
+ function installRelayPluginForProject(projectDir, isVi) {
56
57
  try {
57
58
  execSync(`openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`, { cwd: projectDir, stdio: 'ignore' });
58
59
  return true;
@@ -393,16 +394,16 @@ function installLatestOpenClaw({ isVi, osChoice }) {
393
394
  }
394
395
 
395
396
  console.log(chalk.cyan(isVi
396
- ? '\n📦 Dang cai/cap nhat openclaw@latest...'
397
- : '\n📦 Installing/updating openclaw@latest...'));
397
+ ? `\n📦 Dang cai/cap nhat ${OPENCLAW_NPM_SPEC}...`
398
+ : `\n📦 Installing/updating ${OPENCLAW_NPM_SPEC}...`));
398
399
 
399
400
  if (!installGlobalPackage(OPENCLAW_NPM_SPEC, { isVi, osChoice, displayName: 'openclaw' })) {
400
401
  process.exit(1);
401
402
  }
402
403
 
403
404
  console.log(chalk.green(isVi
404
- ? '✅ openclaw da duoc cap nhat ban moi nhat!'
405
- : '✅ openclaw is now on the latest version!'));
405
+ ? `✅ openclaw da duoc ghim dung ban ${OPENCLAW_NPM_SPEC}!`
406
+ : `✅ openclaw is now pinned to ${OPENCLAW_NPM_SPEC}!`));
406
407
  }
407
408
 
408
409
  // ─── Shared from docker-gen.js ──────────────────────────────────────────────
@@ -557,28 +558,28 @@ function printZaloPersonalLoginInfo({ isVi, deployMode, projectDir }) {
557
558
  : ` → If needed, copy the QR into the project folder with: ${copyCmd}`));
558
559
  }
559
560
 
560
- async function waitForFile(filePath, timeoutMs = 15000, intervalMs = 500) {
561
- const deadline = Date.now() + timeoutMs;
562
- while (Date.now() < deadline) {
563
- if (await fs.pathExists(filePath)) {
564
- return true;
561
+ async function waitForFile(filePath, timeoutMs = 15000, intervalMs = 500) {
562
+ const deadline = Date.now() + timeoutMs;
563
+ while (Date.now() < deadline) {
564
+ if (await fs.pathExists(filePath)) {
565
+ return true;
565
566
  }
566
567
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
567
- }
568
- return fs.pathExists(filePath);
569
- }
570
-
571
- async function writeGeneratedArtifacts(targetDir, artifacts = []) {
572
- for (const artifact of artifacts.filter(Boolean)) {
573
- const artifactPath = path.join(targetDir, artifact.name);
574
- await fs.writeFile(artifactPath, artifact.content, 'utf8');
575
- if (artifact.executable || artifact.name.endsWith('.sh')) {
576
- try { await fs.chmod(artifactPath, 0o755); } catch (_) {}
577
- }
578
- }
579
- }
580
-
581
- function extractZaloPairingCode(text) {
568
+ }
569
+ return fs.pathExists(filePath);
570
+ }
571
+
572
+ async function writeGeneratedArtifacts(targetDir, artifacts = []) {
573
+ for (const artifact of artifacts.filter(Boolean)) {
574
+ const artifactPath = path.join(targetDir, artifact.name);
575
+ await fs.writeFile(artifactPath, artifact.content, 'utf8');
576
+ if (artifact.executable || artifact.name.endsWith('.sh')) {
577
+ try { await fs.chmod(artifactPath, 0o755); } catch (_) {}
578
+ }
579
+ }
580
+ }
581
+
582
+ function extractZaloPairingCode(text) {
582
583
  const value = String(text || '');
583
584
  const explicitCommandMatch = value.match(/openclaw pairing approve zalouser\s+([A-Z0-9-]+)/i);
584
585
  if (explicitCommandMatch) {
@@ -698,10 +699,10 @@ async function runNativeZaloPersonalLoginFlow({ isVi, projectDir }) {
698
699
  }
699
700
  }
700
701
 
701
- function runPm2Save({ projectDir, isVi }) {
702
- try {
703
- execSync('pm2 save', {
704
- cwd: projectDir,
702
+ function runPm2Save({ projectDir, isVi }) {
703
+ try {
704
+ execSync('pm2 save', {
705
+ cwd: projectDir,
705
706
  stdio: 'inherit',
706
707
  shell: true,
707
708
  env: process.env
@@ -710,138 +711,138 @@ function runPm2Save({ projectDir, isVi }) {
710
711
  console.log(chalk.yellow(isVi
711
712
  ? '⚠️ PM2 save khong hoan tat. Bot van co the dang chay, nhung hay thu chay lai `pm2 save` sau.'
712
713
  : '⚠️ PM2 save did not complete. The app may still be running, but try `pm2 save` again afterwards.'));
713
- }
714
- }
715
-
716
- function getDetectedOsChoice() {
717
- const detectedPlatform = process.platform;
718
- return detectedPlatform === 'win32' ? 'windows'
719
- : detectedPlatform === 'darwin' ? 'macos'
720
- : 'vps';
721
- }
722
-
723
- function getCliSubcommand() {
724
- return String(process.argv[2] || '').trim().toLowerCase();
725
- }
726
-
727
- function findProjectDir(startDir = process.cwd()) {
728
- let currentDir = path.resolve(startDir);
729
-
730
- while (true) {
731
- if (
732
- fs.existsSync(path.join(currentDir, '.openclaw'))
733
- || fs.existsSync(path.join(currentDir, 'docker', 'openclaw'))
734
- ) {
735
- return currentDir;
736
- }
737
-
738
- const isDockerOpenClawDir =
739
- path.basename(currentDir).toLowerCase() === 'openclaw'
740
- && path.basename(path.dirname(currentDir)).toLowerCase() === 'docker';
741
- if (isDockerOpenClawDir) {
742
- const projectDir = path.dirname(path.dirname(currentDir));
743
- if (
744
- fs.existsSync(path.join(projectDir, '.openclaw'))
745
- || fs.existsSync(path.join(currentDir, 'docker-compose.yml'))
746
- ) {
747
- return projectDir;
748
- }
749
- }
750
-
751
- const parentDir = path.dirname(currentDir);
752
- if (parentDir === currentDir) {
753
- return null;
754
- }
755
- currentDir = parentDir;
756
- }
757
- }
758
-
759
- function detectProjectDeployMode(projectDir) {
760
- const dockerDir = path.join(projectDir, 'docker', 'openclaw');
761
- if (
762
- fs.existsSync(path.join(dockerDir, 'docker-compose.yml'))
763
- || fs.existsSync(path.join(dockerDir, '.env'))
764
- ) {
765
- return 'docker';
766
- }
767
- return 'native';
768
- }
769
-
770
- function detectProjectBotName(projectDir) {
771
- try {
772
- const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
773
- if (fs.existsSync(configPath)) {
774
- const config = fs.readJsonSync(configPath);
775
- const firstAgentId = config?.agents?.list?.[0]?.id;
776
- if (firstAgentId) {
777
- return firstAgentId;
778
- }
779
- }
780
- } catch {
781
- // fallback below
782
- }
783
- return path.basename(projectDir);
784
- }
785
-
786
- function detectProjectUses9Router(projectDir) {
787
- try {
788
- const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
789
- if (fs.existsSync(configPath)) {
790
- const config = fs.readJsonSync(configPath);
791
- if (config?.models?.providers?.['9router']) {
792
- return true;
793
- }
794
- }
795
- } catch {
796
- // fallback below
797
- }
798
- return fs.existsSync(path.join(projectDir, '.9router'));
799
- }
800
-
801
- async function runUpgradeCommand() {
802
- const projectDir = findProjectDir();
803
- if (!projectDir) {
804
- console.error(chalk.red('Error: no OpenClaw project found in the current directory tree.'));
805
- console.error(chalk.yellow('Run this inside the bot project folder that contains .openclaw or docker/openclaw.'));
806
- process.exit(1);
807
- }
808
-
809
- const deployMode = detectProjectDeployMode(projectDir);
810
- const osChoice = getDetectedOsChoice();
811
- const botName = detectProjectBotName(projectDir);
812
- const is9Router = detectProjectUses9Router(projectDir);
813
-
814
- console.log(chalk.cyan('\nRefreshing generated OpenClaw project artifacts...'));
815
- console.log(chalk.gray(` Project: ${projectDir}`));
816
- console.log(chalk.gray(` Mode: ${deployMode}`));
817
-
818
- await writeGeneratedArtifacts(projectDir, buildCliChromeDebugArtifacts());
819
- await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
820
- deployMode,
821
- osChoice,
822
- projectDir,
823
- botName,
824
- }));
825
- await writeGeneratedArtifacts(projectDir, buildCliUpgradeArtifacts());
826
-
827
- if (deployMode !== 'docker') {
828
- await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
829
- projectDir,
830
- openclawHome: path.join(projectDir, '.openclaw'),
831
- is9Router,
832
- isVi: false,
833
- }));
834
- }
835
-
836
- console.log(chalk.green('\nUpgrade artifacts refreshed successfully.'));
837
- if (deployMode === 'docker') {
838
- console.log(chalk.white(` Next: cd ${path.join(projectDir, 'docker', 'openclaw')} && docker compose up -d --build`));
839
- } else {
840
- console.log(chalk.white(` Next: run ${process.platform === 'win32' ? '.\\start-bot.bat' : './start-bot.sh'} from ${projectDir}`));
841
- }
842
- }
843
-
844
- function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
714
+ }
715
+ }
716
+
717
+ function getDetectedOsChoice() {
718
+ const detectedPlatform = process.platform;
719
+ return detectedPlatform === 'win32' ? 'windows'
720
+ : detectedPlatform === 'darwin' ? 'macos'
721
+ : 'vps';
722
+ }
723
+
724
+ function getCliSubcommand() {
725
+ return String(process.argv[2] || '').trim().toLowerCase();
726
+ }
727
+
728
+ function findProjectDir(startDir = process.cwd()) {
729
+ let currentDir = path.resolve(startDir);
730
+
731
+ while (true) {
732
+ if (
733
+ fs.existsSync(path.join(currentDir, '.openclaw'))
734
+ || fs.existsSync(path.join(currentDir, 'docker', 'openclaw'))
735
+ ) {
736
+ return currentDir;
737
+ }
738
+
739
+ const isDockerOpenClawDir =
740
+ path.basename(currentDir).toLowerCase() === 'openclaw'
741
+ && path.basename(path.dirname(currentDir)).toLowerCase() === 'docker';
742
+ if (isDockerOpenClawDir) {
743
+ const projectDir = path.dirname(path.dirname(currentDir));
744
+ if (
745
+ fs.existsSync(path.join(projectDir, '.openclaw'))
746
+ || fs.existsSync(path.join(currentDir, 'docker-compose.yml'))
747
+ ) {
748
+ return projectDir;
749
+ }
750
+ }
751
+
752
+ const parentDir = path.dirname(currentDir);
753
+ if (parentDir === currentDir) {
754
+ return null;
755
+ }
756
+ currentDir = parentDir;
757
+ }
758
+ }
759
+
760
+ function detectProjectDeployMode(projectDir) {
761
+ const dockerDir = path.join(projectDir, 'docker', 'openclaw');
762
+ if (
763
+ fs.existsSync(path.join(dockerDir, 'docker-compose.yml'))
764
+ || fs.existsSync(path.join(dockerDir, '.env'))
765
+ ) {
766
+ return 'docker';
767
+ }
768
+ return 'native';
769
+ }
770
+
771
+ function detectProjectBotName(projectDir) {
772
+ try {
773
+ const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
774
+ if (fs.existsSync(configPath)) {
775
+ const config = fs.readJsonSync(configPath);
776
+ const firstAgentId = config?.agents?.list?.[0]?.id;
777
+ if (firstAgentId) {
778
+ return firstAgentId;
779
+ }
780
+ }
781
+ } catch {
782
+ // fallback below
783
+ }
784
+ return path.basename(projectDir);
785
+ }
786
+
787
+ function detectProjectUses9Router(projectDir) {
788
+ try {
789
+ const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
790
+ if (fs.existsSync(configPath)) {
791
+ const config = fs.readJsonSync(configPath);
792
+ if (config?.models?.providers?.['9router']) {
793
+ return true;
794
+ }
795
+ }
796
+ } catch {
797
+ // fallback below
798
+ }
799
+ return fs.existsSync(path.join(projectDir, '.9router'));
800
+ }
801
+
802
+ async function runUpgradeCommand() {
803
+ const projectDir = findProjectDir();
804
+ if (!projectDir) {
805
+ console.error(chalk.red('Error: no OpenClaw project found in the current directory tree.'));
806
+ console.error(chalk.yellow('Run this inside the bot project folder that contains .openclaw or docker/openclaw.'));
807
+ process.exit(1);
808
+ }
809
+
810
+ const deployMode = detectProjectDeployMode(projectDir);
811
+ const osChoice = getDetectedOsChoice();
812
+ const botName = detectProjectBotName(projectDir);
813
+ const is9Router = detectProjectUses9Router(projectDir);
814
+
815
+ console.log(chalk.cyan('\nRefreshing generated OpenClaw project artifacts...'));
816
+ console.log(chalk.gray(` Project: ${projectDir}`));
817
+ console.log(chalk.gray(` Mode: ${deployMode}`));
818
+
819
+ await writeGeneratedArtifacts(projectDir, buildCliChromeDebugArtifacts());
820
+ await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
821
+ deployMode,
822
+ osChoice,
823
+ projectDir,
824
+ botName,
825
+ }));
826
+ await writeGeneratedArtifacts(projectDir, buildCliUpgradeArtifacts());
827
+
828
+ if (deployMode !== 'docker') {
829
+ await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
830
+ projectDir,
831
+ openclawHome: path.join(projectDir, '.openclaw'),
832
+ is9Router,
833
+ isVi: false,
834
+ }));
835
+ }
836
+
837
+ console.log(chalk.green('\nUpgrade artifacts refreshed successfully.'));
838
+ if (deployMode === 'docker') {
839
+ console.log(chalk.white(` Next: cd ${path.join(projectDir, 'docker', 'openclaw')} && docker compose up -d --build`));
840
+ } else {
841
+ console.log(chalk.white(` Next: run ${process.platform === 'win32' ? '.\\start-bot.bat' : './start-bot.sh'} from ${projectDir}`));
842
+ }
843
+ }
844
+
845
+ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
845
846
  const routerAppName = `${appName}-9router`;
846
847
  const routerLaunch = resolveNative9RouterDesktopLaunch();
847
848
  const normalizedProjectDir = projectDir.replace(/\\/g, '/');
@@ -934,9 +935,9 @@ function providerSupportsMemoryEmbeddings(providerKey) {
934
935
  return !!PROVIDERS[providerKey]?.supportsEmbeddings;
935
936
  }
936
937
 
937
- function getCliSkillChoices({ providerKey, isVi }) {
938
- const memoryRecommended = providerSupportsMemoryEmbeddings(providerKey);
939
- return SKILLS
938
+ function getCliSkillChoices({ providerKey, isVi }) {
939
+ const memoryRecommended = providerSupportsMemoryEmbeddings(providerKey);
940
+ return SKILLS
940
941
  .filter((skill) => skill.value !== 'memory' || providerSupportsMemoryEmbeddings(providerKey) || skill.id === 'memory')
941
942
  .map((skill) => {
942
943
  const value = skill.value || skill.id;
@@ -951,8 +952,677 @@ function getCliSkillChoices({ providerKey, isVi }) {
951
952
  value,
952
953
  checked: value === 'browser' || value === 'scheduler' || (value === 'memory' && memoryRecommended),
953
954
  };
954
- });
955
- }
955
+ });
956
+ }
957
+
958
+ const CLI_BACK = '__openclaw_cli_back__';
959
+
960
+ function getBackChoice(isVi) {
961
+ return {
962
+ name: isVi ? '← Quay lại' : '← Back',
963
+ value: CLI_BACK,
964
+ };
965
+ }
966
+
967
+ function withBackHint(message, isVi) {
968
+ return `${message} ${isVi ? '(gõ "back" để quay lại)' : '(type "back" to go back)'}`;
969
+ }
970
+
971
+ async function selectWithBack({ message, choices, defaultValue, allowBack = false, isVi = true }) {
972
+ const finalChoices = allowBack ? [...choices, getBackChoice(isVi)] : choices;
973
+ return select({
974
+ message,
975
+ choices: finalChoices,
976
+ ...(defaultValue ? { default: defaultValue } : {}),
977
+ });
978
+ }
979
+
980
+ async function inputWithBack({ message, defaultValue = '', required = false, allowBack = false, isVi = true }) {
981
+ const value = await input({
982
+ message: allowBack ? withBackHint(message, isVi) : message,
983
+ default: defaultValue,
984
+ required,
985
+ });
986
+ if (allowBack && String(value || '').trim().toLowerCase() === 'back') {
987
+ return CLI_BACK;
988
+ }
989
+ return value;
990
+ }
991
+
992
+ async function checkboxWithBack({ message, choices, isVi = true, allowBack = false }) {
993
+ const finalChoices = allowBack ? [...choices, getBackChoice(isVi)] : choices;
994
+ const value = await checkbox({
995
+ message,
996
+ choices: finalChoices,
997
+ });
998
+ return allowBack && value.includes(CLI_BACK) ? CLI_BACK : value;
999
+ }
1000
+
1001
+ async function collectBotSetupStep({
1002
+ isVi,
1003
+ channelKey,
1004
+ channel,
1005
+ existingBots = [],
1006
+ existingBotCount = 1,
1007
+ existingGroupId = '',
1008
+ }) {
1009
+ let botCount = channelKey === 'telegram' ? existingBotCount : 1;
1010
+ let groupId = existingGroupId;
1011
+ const bots = [];
1012
+
1013
+ if (channelKey === 'telegram') {
1014
+ const botCountValue = await selectWithBack({
1015
+ message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1016
+ choices: [
1017
+ { name: '1 bot (single)', value: '1' },
1018
+ { name: '2 bots (Department Room)', value: '2' },
1019
+ { name: '3 bots', value: '3' },
1020
+ { name: '4 bots', value: '4' },
1021
+ { name: '5 bots', value: '5' },
1022
+ ],
1023
+ defaultValue: String(existingBotCount || 1),
1024
+ allowBack: true,
1025
+ isVi,
1026
+ });
1027
+ if (botCountValue === CLI_BACK) {
1028
+ return { back: true };
1029
+ }
1030
+ botCount = parseInt(botCountValue, 10);
1031
+
1032
+ if (botCount > 1) {
1033
+ const groupOption = await selectWithBack({
1034
+ message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1035
+ choices: [
1036
+ { name: isVi ? '✨ Tôi sẽ tạo sau (nhập Group ID vào .env sau khi setup)' : '✨ I\'ll create later (add Group ID to .env after setup)', value: 'create' },
1037
+ { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1038
+ ],
1039
+ defaultValue: groupId ? 'existing' : 'create',
1040
+ allowBack: true,
1041
+ isVi,
1042
+ });
1043
+ if (groupOption === CLI_BACK) {
1044
+ return { back: true };
1045
+ }
1046
+
1047
+ if (groupOption === 'existing') {
1048
+ console.log(chalk.dim(isVi
1049
+ ? '\n 📌 Cách lấy Group ID:\n 1. Mở Telegram → tìm @userinfobot\n 2. Bấm Start để bắt đầu → chọn nút "Group" trên màn hình → chọn Group bạn muốn thêm bot vào\n 3. Bot sẽ trả về "Chat ID" — đó là Group ID (bắt đầu bằng -100)\n 👉 https://t.me/userinfobot\n'
1050
+ : '\n 📌 How to get Group ID:\n 1. Open Telegram → find @userinfobot\n 2. Click Start → select "Group" button on the screen → select the group you want to add the bot to\n 3. The bot replies with "Chat ID" — that is your Group ID (starts with -100)\n 👉 https://t.me/userinfobot\n'));
1051
+ const nextGroupId = await inputWithBack({
1052
+ message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1053
+ defaultValue: groupId,
1054
+ allowBack: true,
1055
+ isVi,
1056
+ });
1057
+ if (nextGroupId === CLI_BACK) {
1058
+ return { back: true };
1059
+ }
1060
+ groupId = nextGroupId;
1061
+ } else {
1062
+ groupId = '';
1063
+ }
1064
+ } else {
1065
+ groupId = '';
1066
+ }
1067
+
1068
+ for (let i = 0; i < botCount; i++) {
1069
+ console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1070
+ const defaults = existingBots[i] || {};
1071
+ const fields = [
1072
+ {
1073
+ key: 'name',
1074
+ message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1075
+ defaultValue: defaults.name || `Bot ${i + 1}`,
1076
+ required: true,
1077
+ },
1078
+ {
1079
+ key: 'slashCmd',
1080
+ message: isVi ? `Slash command (VD: /bot${i + 1}):` : `Slash command (e.g. /bot${i + 1}):`,
1081
+ defaultValue: defaults.slashCmd || `/bot${i + 1}`,
1082
+ required: true,
1083
+ },
1084
+ {
1085
+ key: 'desc',
1086
+ message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1087
+ defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1088
+ required: true,
1089
+ },
1090
+ {
1091
+ key: 'persona',
1092
+ message: isVi ? `Tính cách & quy tắc Bot ${i + 1} (VD: siêu thân thiện, nhiều emoji):` : `Bot ${i + 1} persona & rules (e.g. friendly, uses emojis):`,
1093
+ defaultValue: defaults.persona || '',
1094
+ required: false,
1095
+ },
1096
+ {
1097
+ key: 'token',
1098
+ message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1099
+ defaultValue: defaults.token || '',
1100
+ required: true,
1101
+ },
1102
+ ];
1103
+
1104
+ const draft = { ...defaults };
1105
+ let fieldIndex = 0;
1106
+ while (fieldIndex < fields.length) {
1107
+ const field = fields[fieldIndex];
1108
+ const value = await inputWithBack({
1109
+ message: field.message,
1110
+ defaultValue: draft[field.key] || field.defaultValue,
1111
+ required: field.required,
1112
+ allowBack: true,
1113
+ isVi,
1114
+ });
1115
+ if (value === CLI_BACK) {
1116
+ if (fieldIndex > 0) {
1117
+ fieldIndex--;
1118
+ continue;
1119
+ }
1120
+ return { back: true };
1121
+ }
1122
+ draft[field.key] = value;
1123
+ fieldIndex++;
1124
+ }
1125
+ bots.push(draft);
1126
+ }
1127
+ } else if (channelKey !== 'zalo-personal') {
1128
+ const defaults = existingBots[0] || {};
1129
+ const fields = [
1130
+ {
1131
+ key: 'name',
1132
+ message: isVi ? 'Tên Bot:' : 'Bot Name:',
1133
+ defaultValue: defaults.name || 'Chat Bot',
1134
+ required: true,
1135
+ },
1136
+ {
1137
+ key: 'desc',
1138
+ message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
1139
+ defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1140
+ required: true,
1141
+ },
1142
+ {
1143
+ key: 'persona',
1144
+ message: isVi ? 'Tính cách & quy tắc (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1145
+ defaultValue: defaults.persona || '',
1146
+ required: false,
1147
+ },
1148
+ {
1149
+ key: 'token',
1150
+ message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1151
+ defaultValue: defaults.token || '',
1152
+ required: true,
1153
+ },
1154
+ ];
1155
+ const draft = { ...defaults, slashCmd: '' };
1156
+ let fieldIndex = 0;
1157
+ while (fieldIndex < fields.length) {
1158
+ const field = fields[fieldIndex];
1159
+ const value = await inputWithBack({
1160
+ message: field.message,
1161
+ defaultValue: draft[field.key] || field.defaultValue,
1162
+ required: field.required,
1163
+ allowBack: true,
1164
+ isVi,
1165
+ });
1166
+ if (value === CLI_BACK) {
1167
+ if (fieldIndex > 0) {
1168
+ fieldIndex--;
1169
+ continue;
1170
+ }
1171
+ return { back: true };
1172
+ }
1173
+ draft[field.key] = value;
1174
+ fieldIndex++;
1175
+ }
1176
+ bots.push(draft);
1177
+ } else {
1178
+ bots.push({ name: 'Bot', slashCmd: '', desc: '', persona: '', token: '' });
1179
+ }
1180
+
1181
+ return {
1182
+ back: false,
1183
+ botCount,
1184
+ groupId,
1185
+ bots,
1186
+ botToken: bots[0]?.token || '',
1187
+ };
1188
+ }
1189
+
1190
+ async function collectBotSetupStepWithGroupBack(options) {
1191
+ const {
1192
+ isVi,
1193
+ channelKey,
1194
+ channel,
1195
+ existingBots = [],
1196
+ existingBotCount = 1,
1197
+ existingGroupId = '',
1198
+ } = options;
1199
+
1200
+ let botCount = channelKey === 'telegram' ? existingBotCount : 1;
1201
+ let groupId = existingGroupId;
1202
+ const bots = [];
1203
+
1204
+ if (channelKey === 'telegram') {
1205
+ const botCountValue = await selectWithBack({
1206
+ message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1207
+ choices: [
1208
+ { name: '1 bot (single)', value: '1' },
1209
+ { name: '2 bots (Department Room)', value: '2' },
1210
+ { name: '3 bots', value: '3' },
1211
+ { name: '4 bots', value: '4' },
1212
+ { name: '5 bots', value: '5' },
1213
+ ],
1214
+ defaultValue: String(existingBotCount || 1),
1215
+ allowBack: true,
1216
+ isVi,
1217
+ });
1218
+ if (botCountValue === CLI_BACK) {
1219
+ return { back: true };
1220
+ }
1221
+ botCount = parseInt(botCountValue, 10);
1222
+
1223
+ if (botCount > 1) {
1224
+ while (true) {
1225
+ const groupOption = await selectWithBack({
1226
+ message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1227
+ choices: [
1228
+ { name: isVi ? '✨ Tôi sẽ tạo sau (nhập Group ID vào .env sau khi setup)' : '✨ I\'ll create later (add Group ID to .env after setup)', value: 'create' },
1229
+ { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1230
+ ],
1231
+ defaultValue: groupId ? 'existing' : 'create',
1232
+ allowBack: true,
1233
+ isVi,
1234
+ });
1235
+ if (groupOption === CLI_BACK) {
1236
+ return { back: true };
1237
+ }
1238
+
1239
+ if (groupOption === 'existing') {
1240
+ console.log(chalk.dim(isVi
1241
+ ? '\n 📌 Cách lấy Group ID:\n 1. Mở Telegram → tìm @userinfobot\n 2. Bấm Start để bắt đầu → chọn nút "Group" trên màn hình → chọn Group bạn muốn thêm bot vào\n 3. Bot sẽ trả về "Chat ID" — đó là Group ID (bắt đầu bằng -100)\n 👉 https://t.me/userinfobot\n'
1242
+ : '\n 📌 How to get Group ID:\n 1. Open Telegram → find @userinfobot\n 2. Click Start → select "Group" button on the screen → select the group you want to add the bot to\n 3. The bot replies with "Chat ID" — that is your Group ID (starts with -100)\n 👉 https://t.me/userinfobot\n'));
1243
+ const nextGroupId = await inputWithBack({
1244
+ message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1245
+ defaultValue: groupId,
1246
+ allowBack: true,
1247
+ isVi,
1248
+ });
1249
+ if (nextGroupId === CLI_BACK) {
1250
+ continue;
1251
+ }
1252
+ groupId = nextGroupId;
1253
+ break;
1254
+ }
1255
+
1256
+ groupId = '';
1257
+ break;
1258
+ }
1259
+ } else {
1260
+ groupId = '';
1261
+ }
1262
+
1263
+ for (let i = 0; i < botCount; i++) {
1264
+ console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1265
+ const defaults = existingBots[i] || {};
1266
+ const fields = [
1267
+ {
1268
+ key: 'name',
1269
+ message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1270
+ defaultValue: defaults.name || `Bot ${i + 1}`,
1271
+ required: true,
1272
+ },
1273
+ {
1274
+ key: 'slashCmd',
1275
+ message: isVi ? `Slash command (VD: /bot${i + 1}):` : `Slash command (e.g. /bot${i + 1}):`,
1276
+ defaultValue: defaults.slashCmd || `/bot${i + 1}`,
1277
+ required: true,
1278
+ },
1279
+ {
1280
+ key: 'desc',
1281
+ message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1282
+ defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1283
+ required: true,
1284
+ },
1285
+ {
1286
+ key: 'persona',
1287
+ message: isVi ? `Tính cách & quy tắc Bot ${i + 1} (VD: siêu thân thiện, nhiều emoji):` : `Bot ${i + 1} persona & rules (e.g. friendly, uses emojis):`,
1288
+ defaultValue: defaults.persona || '',
1289
+ required: false,
1290
+ },
1291
+ {
1292
+ key: 'token',
1293
+ message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1294
+ defaultValue: defaults.token || '',
1295
+ required: true,
1296
+ },
1297
+ ];
1298
+
1299
+ const draft = { ...defaults };
1300
+ let fieldIndex = 0;
1301
+ while (fieldIndex < fields.length) {
1302
+ const field = fields[fieldIndex];
1303
+ const value = await inputWithBack({
1304
+ message: field.message,
1305
+ defaultValue: draft[field.key] || field.defaultValue,
1306
+ required: field.required,
1307
+ allowBack: true,
1308
+ isVi,
1309
+ });
1310
+ if (value === CLI_BACK) {
1311
+ if (fieldIndex > 0) {
1312
+ fieldIndex--;
1313
+ continue;
1314
+ }
1315
+ return { back: true };
1316
+ }
1317
+ draft[field.key] = value;
1318
+ fieldIndex++;
1319
+ }
1320
+ bots.push(draft);
1321
+ }
1322
+ } else if (channelKey !== 'zalo-personal') {
1323
+ const defaults = existingBots[0] || {};
1324
+ const fields = [
1325
+ {
1326
+ key: 'name',
1327
+ message: isVi ? 'Tên Bot:' : 'Bot Name:',
1328
+ defaultValue: defaults.name || 'Chat Bot',
1329
+ required: true,
1330
+ },
1331
+ {
1332
+ key: 'desc',
1333
+ message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
1334
+ defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1335
+ required: true,
1336
+ },
1337
+ {
1338
+ key: 'persona',
1339
+ message: isVi ? 'Tính cách & quy tắc (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1340
+ defaultValue: defaults.persona || '',
1341
+ required: false,
1342
+ },
1343
+ {
1344
+ key: 'token',
1345
+ message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1346
+ defaultValue: defaults.token || '',
1347
+ required: true,
1348
+ },
1349
+ ];
1350
+ const draft = { ...defaults, slashCmd: '' };
1351
+ let fieldIndex = 0;
1352
+ while (fieldIndex < fields.length) {
1353
+ const field = fields[fieldIndex];
1354
+ const value = await inputWithBack({
1355
+ message: field.message,
1356
+ defaultValue: draft[field.key] || field.defaultValue,
1357
+ required: field.required,
1358
+ allowBack: true,
1359
+ isVi,
1360
+ });
1361
+ if (value === CLI_BACK) {
1362
+ if (fieldIndex > 0) {
1363
+ fieldIndex--;
1364
+ continue;
1365
+ }
1366
+ return { back: true };
1367
+ }
1368
+ draft[field.key] = value;
1369
+ fieldIndex++;
1370
+ }
1371
+ bots.push(draft);
1372
+ } else {
1373
+ bots.push({ name: 'Bot', slashCmd: '', desc: '', persona: '', token: '' });
1374
+ }
1375
+
1376
+ return {
1377
+ back: false,
1378
+ botCount,
1379
+ groupId,
1380
+ bots,
1381
+ botToken: bots[0]?.token || '',
1382
+ };
1383
+ }
1384
+
1385
+ async function collectProviderStep({
1386
+ isVi,
1387
+ existingProviderKey = '',
1388
+ existingProviderKeyVal = '',
1389
+ existingOllamaModel = 'gemma4:e2b',
1390
+ }) {
1391
+ const providerKey = await selectWithBack({
1392
+ message: isVi ? 'Chọn AI Provider:' : 'Select AI Provider:',
1393
+ choices: Object.entries(PROVIDERS).map(([k, v]) => ({ name: `${v.icon} ${v.name}`, value: k })),
1394
+ defaultValue: existingProviderKey || undefined,
1395
+ allowBack: true,
1396
+ isVi,
1397
+ });
1398
+ if (providerKey === CLI_BACK) {
1399
+ return { back: true };
1400
+ }
1401
+ const provider = PROVIDERS[providerKey];
1402
+
1403
+ let providerKeyVal = existingProviderKey === providerKey ? existingProviderKeyVal : '';
1404
+ if (!provider.isProxy && !provider.isLocal) {
1405
+ const keyValue = await inputWithBack({
1406
+ message: isVi ? `Nhập ${provider.envKey}:` : `Enter ${provider.envKey}:`,
1407
+ defaultValue: providerKeyVal,
1408
+ required: true,
1409
+ allowBack: true,
1410
+ isVi,
1411
+ });
1412
+ if (keyValue === CLI_BACK) {
1413
+ return { back: true };
1414
+ }
1415
+ providerKeyVal = keyValue;
1416
+ }
1417
+
1418
+ let selectedOllamaModel = existingProviderKey === 'ollama' ? existingOllamaModel : 'gemma4:e2b';
1419
+ if (providerKey === 'ollama') {
1420
+ console.log(chalk.yellow(isVi
1421
+ ? '\n💡 Gemma 4 (02/04/2026) — chọn kích thước phù hợp với RAM máy bạn:'
1422
+ : '\n💡 Gemma 4 (April 2, 2026) — pick a size that fits your RAM:'));
1423
+ const modelValue = await selectWithBack({
1424
+ message: isVi ? 'Chọn model Ollama:' : 'Select Ollama model:',
1425
+ choices: [
1426
+ {
1427
+ name: isVi
1428
+ ? '🟢 gemma4:e2b — Nhẹ nhất (~4-6 GB RAM) — Laptop / test nhanh ★ Khuyên dùng'
1429
+ : '🟢 gemma4:e2b — Lightest (~4-6 GB RAM) — Laptop / fastest test ★ Recommended',
1430
+ value: 'gemma4:e2b'
1431
+ },
1432
+ {
1433
+ name: isVi
1434
+ ? '🟡 gemma4:e4b — Cân bằng (~8-10 GB RAM) — Dùng hằng ngày'
1435
+ : '🟡 gemma4:e4b — Balanced (~8-10 GB RAM) — Daily use',
1436
+ value: 'gemma4:e4b'
1437
+ },
1438
+ {
1439
+ name: isVi
1440
+ ? '🟠 gemma4:26b — Mạnh (~18-24 GB RAM/VRAM) — Máy mạnh'
1441
+ : '🟠 gemma4:26b — Powerful (~18-24 GB RAM/VRAM) — High-end machine',
1442
+ value: 'gemma4:26b'
1443
+ },
1444
+ {
1445
+ name: isVi
1446
+ ? '🔴 gemma4:31b — Mạnh nhất (~24+ GB RAM/VRAM) — GPU workstation'
1447
+ : '🔴 gemma4:31b — Most powerful (~24+ GB RAM/VRAM) — GPU workstation',
1448
+ value: 'gemma4:31b'
1449
+ },
1450
+ ],
1451
+ defaultValue: selectedOllamaModel,
1452
+ allowBack: true,
1453
+ isVi,
1454
+ });
1455
+ if (modelValue === CLI_BACK) {
1456
+ return { back: true };
1457
+ }
1458
+ selectedOllamaModel = modelValue;
1459
+ }
1460
+
1461
+ return {
1462
+ back: false,
1463
+ providerKey,
1464
+ provider,
1465
+ providerKeyVal,
1466
+ selectedOllamaModel,
1467
+ };
1468
+ }
1469
+
1470
+ async function collectSkillsStep({
1471
+ isVi,
1472
+ providerKey,
1473
+ existingSelectedSkills = [],
1474
+ existingBrowserMode = 'server',
1475
+ existingTtsOpenaiKey = '',
1476
+ existingTtsElevenKey = '',
1477
+ existingSmtpHost = 'smtp.gmail.com',
1478
+ existingSmtpPort = '587',
1479
+ existingSmtpUser = '',
1480
+ existingSmtpPass = '',
1481
+ }) {
1482
+ const selectedSkills = await checkboxWithBack({
1483
+ message: isVi ? 'Bật tính năng bổ sung (Space để chọn):' : 'Enable extra skills (Space to select):',
1484
+ choices: getCliSkillChoices({ providerKey, isVi }).map((choice) => ({
1485
+ ...choice,
1486
+ checked: existingSelectedSkills.includes(choice.value),
1487
+ })),
1488
+ allowBack: true,
1489
+ isVi,
1490
+ });
1491
+ if (selectedSkills === CLI_BACK) {
1492
+ return { back: true };
1493
+ }
1494
+
1495
+ let browserMode = existingBrowserMode;
1496
+ if (selectedSkills.includes('browser')) {
1497
+ const isLinux = process.platform === 'linux';
1498
+ const browserValue = await selectWithBack({
1499
+ message: isVi ? 'Chế độ Browser Automation:' : 'Browser Automation mode:',
1500
+ choices: [
1501
+ {
1502
+ name: isVi
1503
+ ? '🖥️ Dùng Chrome trên máy tính (Windows/Mac — Bypass Cloudflare tốt hơn)'
1504
+ : '🖥️ Use Host Chrome (Windows/Mac — Better Cloudflare bypass)',
1505
+ value: 'desktop'
1506
+ },
1507
+ {
1508
+ name: isVi
1509
+ ? '🐳 Headless Chromium trong Docker (Ubuntu Server / VPS — không cần GUI)'
1510
+ : '🐳 Headless Chromium inside Docker (Ubuntu Server / VPS — No GUI)',
1511
+ value: 'server'
1512
+ }
1513
+ ],
1514
+ defaultValue: browserMode || (isLinux ? 'server' : 'desktop'),
1515
+ allowBack: true,
1516
+ isVi,
1517
+ });
1518
+ if (browserValue === CLI_BACK) {
1519
+ return { back: true };
1520
+ }
1521
+ browserMode = browserValue;
1522
+ } else {
1523
+ browserMode = 'server';
1524
+ }
1525
+
1526
+ let ttsOpenaiKey = existingTtsOpenaiKey;
1527
+ let ttsElevenKey = existingTtsElevenKey;
1528
+ if (selectedSkills.includes('tts')) {
1529
+ const openaiKey = await inputWithBack({
1530
+ message: isVi ? 'Nhập OPENAI_API_KEY (cho TTS, bỏ trống nếu dùng ElevenLabs):' : 'Enter OPENAI_API_KEY (for TTS, leave empty for ElevenLabs):',
1531
+ defaultValue: ttsOpenaiKey,
1532
+ allowBack: true,
1533
+ isVi,
1534
+ });
1535
+ if (openaiKey === CLI_BACK) {
1536
+ return { back: true };
1537
+ }
1538
+ ttsOpenaiKey = openaiKey;
1539
+
1540
+ const elevenKey = await inputWithBack({
1541
+ message: isVi ? 'Nhập ELEVENLABS_API_KEY (hoặc bỏ trống):' : 'Enter ELEVENLABS_API_KEY (or leave empty):',
1542
+ defaultValue: ttsElevenKey,
1543
+ allowBack: true,
1544
+ isVi,
1545
+ });
1546
+ if (elevenKey === CLI_BACK) {
1547
+ return { back: true };
1548
+ }
1549
+ ttsElevenKey = elevenKey;
1550
+ } else {
1551
+ ttsOpenaiKey = '';
1552
+ ttsElevenKey = '';
1553
+ }
1554
+
1555
+ let smtpHost = existingSmtpHost;
1556
+ let smtpPort = existingSmtpPort;
1557
+ let smtpUser = existingSmtpUser;
1558
+ let smtpPass = existingSmtpPass;
1559
+ if (selectedSkills.includes('email')) {
1560
+ const smtpHostValue = await inputWithBack({
1561
+ message: isVi ? 'SMTP Host (VD: smtp.gmail.com):' : 'SMTP Host (e.g. smtp.gmail.com):',
1562
+ defaultValue: smtpHost,
1563
+ required: true,
1564
+ allowBack: true,
1565
+ isVi,
1566
+ });
1567
+ if (smtpHostValue === CLI_BACK) {
1568
+ return { back: true };
1569
+ }
1570
+ smtpHost = smtpHostValue;
1571
+
1572
+ const smtpPortValue = await inputWithBack({
1573
+ message: 'SMTP Port:',
1574
+ defaultValue: smtpPort,
1575
+ required: true,
1576
+ allowBack: true,
1577
+ isVi,
1578
+ });
1579
+ if (smtpPortValue === CLI_BACK) {
1580
+ return { back: true };
1581
+ }
1582
+ smtpPort = smtpPortValue;
1583
+
1584
+ const smtpUserValue = await inputWithBack({
1585
+ message: isVi ? 'SMTP Email:' : 'SMTP Email:',
1586
+ defaultValue: smtpUser,
1587
+ required: true,
1588
+ allowBack: true,
1589
+ isVi,
1590
+ });
1591
+ if (smtpUserValue === CLI_BACK) {
1592
+ return { back: true };
1593
+ }
1594
+ smtpUser = smtpUserValue;
1595
+
1596
+ const smtpPassValue = await inputWithBack({
1597
+ message: isVi ? 'SMTP App Password:' : 'SMTP App Password:',
1598
+ defaultValue: smtpPass,
1599
+ required: true,
1600
+ allowBack: true,
1601
+ isVi,
1602
+ });
1603
+ if (smtpPassValue === CLI_BACK) {
1604
+ return { back: true };
1605
+ }
1606
+ smtpPass = smtpPassValue;
1607
+ } else {
1608
+ smtpHost = 'smtp.gmail.com';
1609
+ smtpPort = '587';
1610
+ smtpUser = '';
1611
+ smtpPass = '';
1612
+ }
1613
+
1614
+ return {
1615
+ back: false,
1616
+ selectedSkills,
1617
+ browserMode,
1618
+ ttsOpenaiKey,
1619
+ ttsElevenKey,
1620
+ smtpHost,
1621
+ smtpPort,
1622
+ smtpUser,
1623
+ smtpPass,
1624
+ };
1625
+ }
956
1626
 
957
1627
 
958
1628
  // ─── Shared workspace file writer ─────────────────────────────────────────────
@@ -971,11 +1641,11 @@ async function writeWorkspaceFiles({
971
1641
  ownAliases = [],
972
1642
  otherAgents = [], // [{ name, agentId }]
973
1643
  teamRoster = [],
974
- userInfo = '',
975
- agentWorkspaceDir = 'workspace',
976
- isRelayBot = false,
977
- replyToDirectMessages = true,
978
- }) {
1644
+ userInfo = '',
1645
+ agentWorkspaceDir = 'workspace',
1646
+ isRelayBot = false,
1647
+ replyToDirectMessages = true,
1648
+ }) {
979
1649
  const skillListStr = SKILLS
980
1650
  .filter((s) => selectedSkills.includes(s.value))
981
1651
  .map((s) => {
@@ -986,46 +1656,46 @@ async function writeWorkspaceFiles({
986
1656
  })
987
1657
  .join('\n') || (isVi ? '- _(Chưa có skill nào)_' : '- _(No skills installed)_');
988
1658
 
989
- const workspacePath = `.openclaw/${agentWorkspaceDir}/`;
990
- const teamRosterFormatted = teamRoster
991
- .map((peer, idx) => {
992
- const agentId = peer.agentId || String(peer.name || `Bot ${idx + 1}`).toLowerCase().replace(/[^a-z0-9]+/g, '-');
993
- const desc = peer.desc || (isVi ? 'Tro ly AI ca nhan' : 'Personal AI assistant');
994
- const accountId = peer.accountId ? `, accountId: ${peer.accountId}` : '';
995
- const slashCmd = peer.slashCmd ? `, slash: ${peer.slashCmd}` : '';
996
- return `- \`${agentId}\`: ${peer.name || `Bot ${idx + 1}`} - ${desc}${accountId}${slashCmd}`;
997
- })
998
- .join('\n');
999
-
1000
- const files = buildWorkspaceFileMap({
1001
- isVi,
1002
- variant: isRelayBot ? 'relay' : 'single',
1003
- botName,
1004
- botDesc,
1005
- ownAliases,
1006
- otherAgents,
1007
- replyToDirectMessages,
1008
- skillListStr,
1009
- workspacePath,
1010
- agentWorkspaceDir,
1011
- persona,
1012
- userInfo,
1013
- hasBrowser: isDesktop || isServer,
1014
- soulVariant: isRelayBot ? 'cli-simple' : 'cli-rich',
1015
- userVariant: isRelayBot ? 'cli-multi' : 'cli-single',
1016
- memoryVariant: isRelayBot ? 'cli-multi' : 'cli-single',
1017
- browserDocVariant: isServer ? 'cli-server' : 'cli-desktop',
1018
- browserToolVariant: 'cli',
1019
- includeBrowserTool: isDesktop,
1020
- teamRosterFormatted,
1021
- hasScheduler: selectedSkills.includes('scheduler'),
1022
- });
1023
-
1024
- await fs.ensureDir(workspaceDir);
1025
- for (const [name, content] of Object.entries(files)) {
1026
- await fs.writeFile(path.join(workspaceDir, name), content, 'utf8');
1027
- }
1028
- }
1659
+ const workspacePath = `.openclaw/${agentWorkspaceDir}/`;
1660
+ const teamRosterFormatted = teamRoster
1661
+ .map((peer, idx) => {
1662
+ const agentId = peer.agentId || String(peer.name || `Bot ${idx + 1}`).toLowerCase().replace(/[^a-z0-9]+/g, '-');
1663
+ const desc = peer.desc || (isVi ? 'Tro ly AI ca nhan' : 'Personal AI assistant');
1664
+ const accountId = peer.accountId ? `, accountId: ${peer.accountId}` : '';
1665
+ const slashCmd = peer.slashCmd ? `, slash: ${peer.slashCmd}` : '';
1666
+ return `- \`${agentId}\`: ${peer.name || `Bot ${idx + 1}`} - ${desc}${accountId}${slashCmd}`;
1667
+ })
1668
+ .join('\n');
1669
+
1670
+ const files = buildWorkspaceFileMap({
1671
+ isVi,
1672
+ variant: isRelayBot ? 'relay' : 'single',
1673
+ botName,
1674
+ botDesc,
1675
+ ownAliases,
1676
+ otherAgents,
1677
+ replyToDirectMessages,
1678
+ skillListStr,
1679
+ workspacePath,
1680
+ agentWorkspaceDir,
1681
+ persona,
1682
+ userInfo,
1683
+ hasBrowser: isDesktop || isServer,
1684
+ soulVariant: isRelayBot ? 'cli-simple' : 'cli-rich',
1685
+ userVariant: isRelayBot ? 'cli-multi' : 'cli-single',
1686
+ memoryVariant: isRelayBot ? 'cli-multi' : 'cli-single',
1687
+ browserDocVariant: isServer ? 'cli-server' : 'cli-desktop',
1688
+ browserToolVariant: 'cli',
1689
+ includeBrowserTool: isDesktop,
1690
+ teamRosterFormatted,
1691
+ hasScheduler: selectedSkills.includes('scheduler'),
1692
+ });
1693
+
1694
+ await fs.ensureDir(workspaceDir);
1695
+ for (const [name, content] of Object.entries(files)) {
1696
+ await fs.writeFile(path.join(workspaceDir, name), content, 'utf8');
1697
+ }
1698
+ }
1029
1699
 
1030
1700
 
1031
1701
  async function main() {
@@ -1038,316 +1708,218 @@ async function main() {
1038
1708
  console.log(chalk.red('\n=================================='));
1039
1709
  console.log(chalk.redBright(LOGO));
1040
1710
  console.log(chalk.greenBright(' OpenClaw Auto Setup CLI '));
1041
- console.log(chalk.red('==================================\n'));
1042
-
1043
- // 1. Language
1044
- const lang = await select({
1045
- message: 'Select language / Chọn ngôn ngữ:',
1046
- choices: [
1047
- { name: 'Tiếng Việt', value: 'vi' },
1048
- { name: 'English', value: 'en' }
1049
- ]
1050
- });
1051
- const isVi = lang === 'vi';
1052
-
1053
- // 1b. OS Selection
1054
- const detectedPlatform = process.platform; // 'win32' | 'darwin' | 'linux'
1055
- const detectedOS = detectedPlatform === 'win32' ? 'windows'
1056
- : detectedPlatform === 'darwin' ? 'macos'
1057
- : 'linux';
1058
-
1059
- const osChoice = await select({
1060
- message: isVi ? 'Bạn đang chạy trên hệ điều hành nào?' : 'What OS are you running on?',
1061
- choices: [
1062
- { name: isVi ? '🪟 Windows' : '🪟 Windows', value: 'windows' },
1063
- { name: isVi ? '🍎 macOS' : '🍎 macOS', value: 'macos' },
1064
- { name: isVi ? '🐧 Ubuntu Desktop' : '🐧 Ubuntu Desktop', value: 'ubuntu' },
1065
- { name: isVi ? '🖥️ VPS / Ubuntu Server' : '🖥️ VPS / Ubuntu Server', value: 'vps' },
1066
- ],
1067
- default: detectedOS === 'linux' ? 'vps' : detectedOS
1068
- });
1069
-
1070
- // 1c. Deploy mode — Ubuntu/VPS default native, Windows/macOS default docker
1071
- // User always gets to choose; if they pick Docker and it's missing we auto-install
1072
- const deployModeDefault = (osChoice === 'ubuntu' || osChoice === 'vps') ? 'native' : 'docker';
1073
- let deployMode = await select({
1074
- message: isVi ? 'Chọn cách chạy bot:' : 'How do you want to run the bot?',
1075
- choices: [
1076
- {
1077
- name: isVi
1078
- ? '🐳 Docker (Khuyên dùng cho Windows / macOS dễ cài, chạy ngay)'
1079
- : '🐳 Docker (Recommended for Windows / macOS — easy setup, runs immediately)',
1080
- value: 'docker'
1081
- },
1082
- {
1083
- name: isVi
1084
- ? '⚡ Native / PM2 (Khuyên dùng cho Ubuntu / VPS — ít RAM, ổn định hơn)'
1085
- : '⚡ Native / PM2 (Recommended for Ubuntu / VPS — less RAM, more stable)',
1086
- value: 'native'
1087
- }
1088
- ],
1089
- default: deployModeDefault
1090
- });
1091
-
1092
- // 1d. Docker selected auto-install Engine + Compose v2 plugin if not present (no extra prompts)
1093
- if (deployMode === 'docker' && !isDockerInstalled()) {
1094
- console.log(chalk.cyan(isVi
1095
- ? '\n🐳 Docker chưa được cài đang tự động cài Docker Engine + Compose plugin...'
1096
- : '\n🐳 Docker not found auto-installing Docker Engine + Compose plugin...'));
1097
- try {
1098
- const platform = process.platform;
1099
- if (platform === 'win32') {
1100
- execSync('winget install -e --id Docker.DockerDesktop --accept-source-agreements --accept-package-agreements', { stdio: 'inherit' });
1101
- console.log(chalk.green(isVi
1102
- ? '✅ Docker Desktop đã cài xong. Vui lòng mở Docker Desktop, đợi khởi động (icon tray chuyển xanh) rồi chạy lại lệnh này.'
1103
- : '✅ Docker Desktop installed. Open Docker Desktop, wait for it to start (tray icon turns green), then re-run this command.'));
1104
- process.exit(0);
1105
- } else if (platform === 'darwin') {
1106
- execSync('brew install --cask docker', { stdio: 'inherit' });
1107
- console.log(chalk.green(isVi
1108
- ? '✅ Docker Desktop cài xong qua Homebrew. Mở Docker Desktop, đợi khởi động rồi chạy lại lệnh này.'
1109
- : '✅ Docker Desktop installed via Homebrew. Open Docker Desktop, wait for it to start, then re-run this command.'));
1110
- process.exit(0);
1111
- } else {
1112
- // Linux Docker Engine + Compose v2 plugin
1113
- execSync('curl -fsSL https://get.docker.com | sh', { stdio: 'inherit', shell: true });
1114
- try { execSync('apt-get install -y docker-compose-plugin', { stdio: 'ignore', shell: true }); } catch { /* best-effort */ }
1115
- console.log(chalk.green(isVi
1116
- ? '✅ Docker Engine + Compose plugin đã cài xong.'
1117
- : ' Docker Engine + Compose plugin installed.'));
1118
- }
1119
- } catch {
1120
- console.log(chalk.red(isVi
1121
- ? '❌ Không thể tự cài Docker. Tải thủ công: https://www.docker.com/products/docker-desktop/'
1122
- : '❌ Could not auto-install Docker. Download manually: https://www.docker.com/products/docker-desktop/'));
1123
- process.exit(1);
1124
- }
1125
- }
1126
-
1127
-
1128
- // 2. Channel
1129
- const channelKey = await select({
1130
- message: isVi ? 'Chọn nền tảng bot:' : 'Select bot platform:',
1131
- choices: Object.entries(CHANNELS).map(([k, v]) => ({ name: `${v.icon} ${v.name}`, value: k }))
1132
- });
1133
- const channel = CHANNELS[channelKey];
1134
-
1135
- if (channelKey === 'zalo-bot') {
1136
- console.log(chalk.yellow(`\n⚠️ ${isVi ? 'LƯU Ý: Zalo OA Bot yêu cầu phải thiết lập Webhook Public (qua VPS/ngrok có HTTPS). Hãy dùng Zalo Personal nếu bạn chưa có Webhook.' : 'NOTE: Zalo OA requires a Public Webhook (via VPS/ngrok with HTTPS). Use Zalo Personal if you do not have one.'}`));
1137
- }
1138
-
1139
- // ── Multi-bot: only Telegram supports multiple bots for now ──────────────
1140
- let botToken = ''; // single-bot compat
1141
- let botCount = 1; // total bots
1142
- let bots = []; // [{name, slashCmd, token}]
1143
- let groupId = '';
1144
-
1145
- if (channelKey === 'telegram') {
1146
- botCount = parseInt(await select({
1147
- message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1148
- choices: [
1149
- { name: '1 bot (single)', value: '1' },
1150
- { name: '2 bots (Department Room)', value: '2' },
1151
- { name: '3 bots', value: '3' },
1152
- { name: '4 bots', value: '4' },
1153
- { name: '5 bots', value: '5' },
1154
- ],
1155
- default: '1'
1156
- }), 10);
1157
-
1158
- if (botCount > 1) {
1159
- // Ask if user already has a group or will create later
1160
- const groupOption = await select({
1161
- message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1162
- choices: [
1163
- { name: isVi ? '✨ Tôi sẽ tạo sau (nhập Group ID vào .env sau khi setup)' : '✨ I\'ll create later (add Group ID to .env after setup)', value: 'create' },
1164
- { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1165
- ],
1166
- default: 'create'
1167
- });
1168
-
1169
- if (groupOption === 'existing') {
1170
- console.log(chalk.dim(isVi
1171
- ? '\n 📌 Cách lấy Group ID:\n 1. Mở Telegram → tìm @userinfobot\n 2. Bấm Start để bắt đầu → chọn nút "Group" trên màn hình → chọn Group bạn muốn thêm bot vào\n 3. Bot sẽ trả về "Chat ID" — đó là Group ID (bắt đầu bằng -100)\n 👉 https://t.me/userinfobot\n'
1172
- : '\n 📌 How to get Group ID:\n 1. Open Telegram → find @userinfobot\n 2. Click Start → select "Group" button on the screen → select the group you want to add the bot to\n 3. The bot replies with "Chat ID" — that is your Group ID (starts with -100)\n 👉 https://t.me/userinfobot\n'));
1173
- groupId = await input({
1174
- message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1175
- default: ''
1176
- });
1177
- }
1178
- }
1179
-
1180
-
1181
- for (let i = 0; i < botCount; i++) {
1182
- console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`))
1183
- const bName = await input({
1184
- message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1185
- default: `Bot ${i + 1}`
1186
- });
1187
- const bSlash = await input({
1188
- message: isVi ? `Slash command (VD: /bot${i+1}):` : `Slash command (e.g. /bot${i+1}):`,
1189
- default: `/bot${i + 1}`
1190
- });
1191
- const bDesc = await input({
1192
- message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ AI nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1193
- default: isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'
1194
- });
1195
- const bPersona = await input({
1196
- message: isVi ? `Tính cách & quy tắc Bot ${i + 1} (VD: siêu thân thiện, nhiều emoji):` : `Bot ${i + 1} persona & rules (e.g. friendly, uses emojis):`,
1197
- default: ''
1198
- });
1199
- const bToken = await input({
1200
- message: isVi ? `Bot Token (từ @BotFather):` : `Bot Token (from @BotFather):`,
1201
- required: true
1202
- });
1203
- bots.push({ name: bName, slashCmd: bSlash, desc: bDesc, persona: bPersona, token: bToken });
1204
- }
1205
- botToken = bots[0].token;
1206
-
1207
- } else if (channelKey !== 'zalo-personal') {
1208
- const bName = await input({ message: isVi ? 'Tên Bot:' : 'Bot Name:', default: 'Chat Bot' });
1209
- const bDesc = await input({ message: isVi ? 'Mô tả Bot:' : 'Bot Description:', default: isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant' });
1210
- const bPersona = await input({ message: isVi ? 'Tính cách & quy tắc (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):', default: '' });
1211
- botToken = await input({
1212
- message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1213
- required: true
1214
- });
1215
- bots.push({ name: bName, slashCmd: '', desc: bDesc, persona: bPersona, token: botToken });
1216
- } else {
1217
- bots.push({ name: 'Bot', slashCmd: '', desc: '', persona: '', token: '' });
1218
- }
1219
-
1220
- const isMultiBot = botCount > 1 && channelKey === 'telegram';
1221
-
1222
- // 3. User Info
1223
- console.log(chalk.bold(`\n${isVi ? '─── Thông tin của bạn ───' : '─── About You ───'}`));
1224
- const userInfo = await input({
1225
- message: isVi ? '👤 Thông tin về bạn (tên bạn, ngôn ngữ, múi giờ, sở thích...):' : '👤 About you (your name, language, timezone, interests...):',
1226
- default: '',
1227
- required: true
1228
- });
1229
-
1230
- const botName = bots[0].name;
1231
- const botDesc = bots[0].desc;
1232
- const botPersona = bots[0].persona;
1233
-
1234
-
1235
- // 3. Provider
1236
- const providerKey = await select({
1237
- message: isVi ? 'Chọn AI Provider:' : 'Select AI Provider:',
1238
- choices: Object.entries(PROVIDERS).map(([k, v]) => ({ name: `${v.icon} ${v.name}`, value: k }))
1239
- });
1240
- const provider = PROVIDERS[providerKey];
1241
-
1242
- let providerKeyVal = '';
1243
- if (!provider.isProxy && !provider.isLocal) {
1244
- providerKeyVal = await input({
1245
- message: isVi ? `Nhập ${provider.envKey}:` : `Enter ${provider.envKey}:`,
1246
- required: true
1247
- });
1248
- }
1249
-
1250
- // 3b. Ollama model help user pick the right size for their hardware
1251
- let selectedOllamaModel = 'gemma4:e2b';
1252
- if (providerKey === 'ollama') {
1253
- console.log(chalk.yellow(isVi
1254
- ? '\n💡 Gemma 4 (02/04/2026) — chọn kích thước phù hợp với RAM máy bạn:'
1255
- : '\n💡 Gemma 4 (April 2, 2026) — pick a size that fits your RAM:'));
1256
- selectedOllamaModel = await select({
1257
- message: isVi ? 'Chọn model Ollama:' : 'Select Ollama model:',
1258
- choices: [
1259
- {
1260
- name: isVi
1261
- ? '🟢 gemma4:e2b — Nhẹ nhất (~4-6 GB RAM) — Laptop / test nhanh ★ Khuyên dùng'
1262
- : '🟢 gemma4:e2b — Lightest (~4-6 GB RAM) — Laptop / fastest test ★ Recommended',
1263
- value: 'gemma4:e2b'
1264
- },
1265
- {
1266
- name: isVi
1267
- ? '🟡 gemma4:e4b — Cân bằng (~8-10 GB RAM) — Dùng hằng ngày'
1268
- : '🟡 gemma4:e4b — Balanced (~8-10 GB RAM) — Daily use',
1269
- value: 'gemma4:e4b'
1270
- },
1271
- {
1272
- name: isVi
1273
- ? '🟠 gemma4:26b — Mạnh (~18-24 GB RAM/VRAM) — Máy mạnh'
1274
- : '🟠 gemma4:26b — Powerful (~18-24 GB RAM/VRAM) — High-end machine',
1275
- value: 'gemma4:26b'
1276
- },
1277
- {
1278
- name: isVi
1279
- ? '🔴 gemma4:31b — Mạnh nhất (~24+ GB RAM/VRAM) — GPU workstation'
1280
- : '🔴 gemma4:31b — Most powerful (~24+ GB RAM/VRAM) — GPU workstation',
1281
- value: 'gemma4:31b'
1282
- },
1283
- ],
1284
- default: 'gemma4:e2b'
1285
- });
1286
- }
1287
-
1288
- // 4. Skills
1289
- const selectedSkills = await checkbox({
1290
- message: isVi ? 'Bật tính năng bổ sung (Space để chọn):' : 'Enable extra skills (Space to select):',
1291
- choices: getCliSkillChoices({ providerKey, isVi })
1292
- });
1293
-
1294
- let tavilyKey = '';
1295
- // (web-search removed — native search built-in)
1296
-
1297
- // Browser mode: Desktop (host Chrome via CDP) vs Server (headless Chromium inside Docker)
1298
- let browserMode = 'server';
1299
- if (selectedSkills.includes('browser')) {
1300
- const isLinux = process.platform === 'linux';
1301
- browserMode = await select({
1302
- message: isVi ? 'Chế độ Browser Automation:' : 'Browser Automation mode:',
1303
- choices: [
1304
- {
1305
- name: isVi
1306
- ? '🖥️ Dùng Chrome trên máy tính (Windows/Mac — Bypass Cloudflare tốt hơn)'
1307
- : '🖥️ Use Host Chrome (Windows/Mac — Better Cloudflare bypass)',
1308
- value: 'desktop'
1309
- },
1310
- {
1311
- name: isVi
1312
- ? '🐳 Headless Chromium trong Docker (Ubuntu Server / VPS — không cần GUI)'
1313
- : '🐳 Headless Chromium inside Docker (Ubuntu Server / VPS — No GUI)',
1314
- value: 'server'
1315
- }
1316
- ],
1317
- default: isLinux ? 'server' : 'desktop'
1318
- });
1319
- }
1320
- const hasBrowserDesktop = selectedSkills.includes('browser') && browserMode === 'desktop';
1321
- const hasBrowserServer = selectedSkills.includes('browser') && browserMode === 'server';
1322
-
1323
- let ttsOpenaiKey = '';
1324
- let ttsElevenKey = '';
1325
- if (selectedSkills.includes('tts')) {
1326
- ttsOpenaiKey = await input({ message: isVi ? 'Nhập OPENAI_API_KEY (cho TTS, bỏ trống nếu dùng ElevenLabs):' : 'Enter OPENAI_API_KEY (for TTS, leave empty for ElevenLabs):' });
1327
- ttsElevenKey = await input({ message: isVi ? 'Nhập ELEVENLABS_API_KEY (hoặc bỏ trống):' : 'Enter ELEVENLABS_API_KEY (or leave empty):', default: '' });
1328
- }
1329
-
1330
- let smtpHost = 'smtp.gmail.com', smtpPort = '587', smtpUser = '', smtpPass = '';
1331
- if (selectedSkills.includes('email')) {
1332
- smtpHost = await input({ message: isVi ? 'SMTP Host (VD: smtp.gmail.com):' : 'SMTP Host (e.g. smtp.gmail.com):', default: 'smtp.gmail.com' });
1333
- smtpPort = await input({ message: 'SMTP Port:', default: '587' });
1334
- smtpUser = await input({ message: isVi ? 'SMTP Email:' : 'SMTP Email:' });
1335
- smtpPass = await input({ message: isVi ? 'SMTP App Password:' : 'SMTP App Password:' });
1336
- }
1337
-
1338
-
1339
-
1340
-
1341
- // 6. Project Dir
1342
- let defaultDir = process.cwd();
1343
- if (!defaultDir.endsWith('openclaw-setup') && !defaultDir.endsWith('openclaw')) {
1344
- defaultDir = path.join(defaultDir, 'openclaw-setup');
1345
- }
1346
- const projectDir = await input({
1347
- message: isVi ? 'Thư mục cài đặt project:' : 'Project install directory:',
1348
- default: defaultDir
1349
- });
1350
-
1711
+ console.log(chalk.red('==================================\n'));
1712
+
1713
+ let lang = 'vi';
1714
+ let isVi = true;
1715
+ const detectedPlatform = process.platform;
1716
+ const detectedOS = detectedPlatform === 'win32' ? 'windows'
1717
+ : detectedPlatform === 'darwin' ? 'macos'
1718
+ : 'linux';
1719
+ let osChoice = detectedOS === 'linux' ? 'vps' : detectedOS;
1720
+ let deployMode = 'docker';
1721
+ let channelKey = 'telegram';
1722
+ let channel = CHANNELS[channelKey];
1723
+ let botToken = '';
1724
+ let botCount = 1;
1725
+ let bots = [];
1726
+ let groupId = '';
1727
+ let userInfo = '';
1728
+ let providerKey = '9router';
1729
+ let provider = PROVIDERS[providerKey];
1730
+ let providerKeyVal = '';
1731
+ let selectedOllamaModel = 'gemma4:e2b';
1732
+ let selectedSkills = [];
1733
+ let tavilyKey = '';
1734
+ let browserMode = 'server';
1735
+ let ttsOpenaiKey = '';
1736
+ let ttsElevenKey = '';
1737
+ let smtpHost = 'smtp.gmail.com', smtpPort = '587', smtpUser = '', smtpPass = '';
1738
+ let defaultDir = process.cwd();
1739
+ if (!defaultDir.endsWith('openclaw-setup') && !defaultDir.endsWith('openclaw')) {
1740
+ defaultDir = path.join(defaultDir, 'openclaw-setup');
1741
+ }
1742
+ let projectDir = defaultDir;
1743
+
1744
+ let setupStep = 'language';
1745
+ while (true) {
1746
+ if (setupStep === 'language') {
1747
+ lang = await select({
1748
+ message: 'Select language / Chọn ngôn ngữ:',
1749
+ choices: [
1750
+ { name: 'Tiếng Việt', value: 'vi' },
1751
+ { name: 'English', value: 'en' }
1752
+ ],
1753
+ default: lang
1754
+ });
1755
+ isVi = lang === 'vi';
1756
+ setupStep = 'os';
1757
+ continue;
1758
+ }
1759
+
1760
+ if (setupStep === 'os') {
1761
+ const nextOsChoice = await selectWithBack({
1762
+ message: isVi ? 'Bạn đang chạy trên hệ điều hành nào?' : 'What OS are you running on?',
1763
+ choices: [
1764
+ { name: isVi ? '🪟 Windows' : '🪟 Windows', value: 'windows' },
1765
+ { name: isVi ? '🍎 macOS' : '🍎 macOS', value: 'macos' },
1766
+ { name: isVi ? '🐧 Ubuntu Desktop' : '🐧 Ubuntu Desktop', value: 'ubuntu' },
1767
+ { name: isVi ? '🖥️ VPS / Ubuntu Server' : '🖥️ VPS / Ubuntu Server', value: 'vps' },
1768
+ ],
1769
+ defaultValue: osChoice,
1770
+ allowBack: true,
1771
+ isVi,
1772
+ });
1773
+ if (nextOsChoice === CLI_BACK) {
1774
+ setupStep = 'language';
1775
+ continue;
1776
+ }
1777
+ osChoice = nextOsChoice;
1778
+ setupStep = 'deploy';
1779
+ continue;
1780
+ }
1781
+
1782
+ if (setupStep === 'deploy') {
1783
+ const deployModeDefault = (osChoice === 'ubuntu' || osChoice === 'vps') ? 'native' : 'docker';
1784
+ const nextDeployMode = await selectWithBack({
1785
+ message: isVi ? 'Chọn cách chạy bot:' : 'How do you want to run the bot?',
1786
+ choices: [
1787
+ { name: isVi ? '🐳 Docker (Khuyên dùng cho Windows / macOS — dễ cài, chạy ngay)' : '🐳 Docker (Recommended for Windows / macOS — easy setup, runs immediately)', value: 'docker' },
1788
+ { name: isVi ? '⚡ Native / PM2 (Khuyên dùng cho Ubuntu / VPS — ít RAM, ổn định hơn)' : '⚡ Native / PM2 (Recommended for Ubuntu / VPS — less RAM, more stable)', value: 'native' }
1789
+ ],
1790
+ defaultValue: deployMode || deployModeDefault,
1791
+ allowBack: true,
1792
+ isVi,
1793
+ });
1794
+ if (nextDeployMode === CLI_BACK) {
1795
+ setupStep = 'os';
1796
+ continue;
1797
+ }
1798
+ deployMode = nextDeployMode;
1799
+ if (deployMode === 'docker' && !isDockerInstalled()) {
1800
+ console.log(chalk.cyan(isVi ? '\n🐳 Docker chưa được cài — đang tự động cài Docker Engine + Compose plugin...' : '\n🐳 Docker not found — auto-installing Docker Engine + Compose plugin...'));
1801
+ try {
1802
+ const platform = process.platform;
1803
+ if (platform === 'win32') {
1804
+ execSync('winget install -e --id Docker.DockerDesktop --accept-source-agreements --accept-package-agreements', { stdio: 'inherit' });
1805
+ console.log(chalk.green(isVi ? '✅ Docker Desktop đã cài xong. Vui lòng mở Docker Desktop, đợi khởi động (icon tray chuyển xanh) rồi chạy lại lệnh này.' : '✅ Docker Desktop installed. Open Docker Desktop, wait for it to start (tray icon turns green), then re-run this command.'));
1806
+ process.exit(0);
1807
+ } else if (platform === 'darwin') {
1808
+ execSync('brew install --cask docker', { stdio: 'inherit' });
1809
+ console.log(chalk.green(isVi ? '✅ Docker Desktop cài xong qua Homebrew. Mở Docker Desktop, đợi khởi động rồi chạy lại lệnh này.' : '✅ Docker Desktop installed via Homebrew. Open Docker Desktop, wait for it to start, then re-run this command.'));
1810
+ process.exit(0);
1811
+ } else {
1812
+ execSync('curl -fsSL https://get.docker.com | sh', { stdio: 'inherit', shell: true });
1813
+ try { execSync('apt-get install -y docker-compose-plugin', { stdio: 'ignore', shell: true }); } catch {}
1814
+ console.log(chalk.green(isVi ? '✅ Docker Engine + Compose plugin đã cài xong.' : '✅ Docker Engine + Compose plugin installed.'));
1815
+ }
1816
+ } catch {
1817
+ console.log(chalk.red(isVi ? ' Không thể tự cài Docker. Tải thủ công: https://www.docker.com/products/docker-desktop/' : ' Could not auto-install Docker. Download manually: https://www.docker.com/products/docker-desktop/'));
1818
+ process.exit(1);
1819
+ }
1820
+ }
1821
+ setupStep = 'channel';
1822
+ continue;
1823
+ }
1824
+
1825
+ if (setupStep === 'channel') {
1826
+ const nextChannelKey = await selectWithBack({
1827
+ message: isVi ? 'Chọn nền tảng bot:' : 'Select bot platform:',
1828
+ choices: Object.entries(CHANNELS).map(([k, v]) => ({ name: v.icon + ' ' + v.name, value: k })),
1829
+ defaultValue: channelKey,
1830
+ allowBack: true,
1831
+ isVi,
1832
+ });
1833
+ if (nextChannelKey === CLI_BACK) {
1834
+ setupStep = 'deploy';
1835
+ continue;
1836
+ }
1837
+ channelKey = nextChannelKey;
1838
+ channel = CHANNELS[channelKey];
1839
+ if (channelKey === 'zalo-bot') {
1840
+ console.log(chalk.yellow('\n⚠️ ' + (isVi ? 'LƯU Ý: Zalo OA Bot yêu cầu phải thiết lập Webhook Public (qua VPS/ngrok có HTTPS). Hãy dùng Zalo Personal nếu bạn chưa có Webhook.' : 'NOTE: Zalo OA requires a Public Webhook (via VPS/ngrok with HTTPS). Use Zalo Personal if you do not have one.')));
1841
+ }
1842
+ setupStep = 'botSetup';
1843
+ continue;
1844
+ }
1845
+
1846
+ if (setupStep === 'botSetup') {
1847
+ const botSetup = await collectBotSetupStepWithGroupBack({ isVi, channelKey, channel, existingBots: bots, existingBotCount: botCount, existingGroupId: groupId });
1848
+ if (botSetup.back) {
1849
+ setupStep = 'channel';
1850
+ continue;
1851
+ }
1852
+ botCount = botSetup.botCount;
1853
+ groupId = botSetup.groupId;
1854
+ bots = botSetup.bots;
1855
+ botToken = botSetup.botToken;
1856
+ setupStep = 'userInfo';
1857
+ continue;
1858
+ }
1859
+
1860
+ if (setupStep === 'userInfo') {
1861
+ console.log(chalk.bold('\n' + (isVi ? '👤 Thông tin của bạn 👤' : '👤 About You 👤')));
1862
+ const nextUserInfo = await inputWithBack({ message: isVi ? '👤 Thông tin về bạn (tên bạn, ngôn ngữ, múi giờ, sở thích...):' : '👤 About you (your name, language, timezone, interests...):', defaultValue: userInfo, required: true, allowBack: true, isVi });
1863
+ if (nextUserInfo === CLI_BACK) {
1864
+ setupStep = 'botSetup';
1865
+ continue;
1866
+ }
1867
+ userInfo = nextUserInfo;
1868
+ setupStep = 'provider';
1869
+ continue;
1870
+ }
1871
+
1872
+ if (setupStep === 'provider') {
1873
+ const providerSetup = await collectProviderStep({ isVi, existingProviderKey: providerKey, existingProviderKeyVal: providerKeyVal, existingOllamaModel: selectedOllamaModel });
1874
+ if (providerSetup.back) {
1875
+ setupStep = 'userInfo';
1876
+ continue;
1877
+ }
1878
+ providerKey = providerSetup.providerKey;
1879
+ provider = providerSetup.provider;
1880
+ providerKeyVal = providerSetup.providerKeyVal;
1881
+ selectedOllamaModel = providerSetup.selectedOllamaModel;
1882
+ setupStep = 'skills';
1883
+ continue;
1884
+ }
1885
+
1886
+ if (setupStep === 'skills') {
1887
+ const skillSetup = await collectSkillsStep({ isVi, providerKey, existingSelectedSkills: selectedSkills, existingBrowserMode: browserMode, existingTtsOpenaiKey: ttsOpenaiKey, existingTtsElevenKey: ttsElevenKey, existingSmtpHost: smtpHost, existingSmtpPort: smtpPort, existingSmtpUser: smtpUser, existingSmtpPass: smtpPass });
1888
+ if (skillSetup.back) {
1889
+ setupStep = 'provider';
1890
+ continue;
1891
+ }
1892
+ selectedSkills = skillSetup.selectedSkills;
1893
+ browserMode = skillSetup.browserMode;
1894
+ ttsOpenaiKey = skillSetup.ttsOpenaiKey;
1895
+ ttsElevenKey = skillSetup.ttsElevenKey;
1896
+ smtpHost = skillSetup.smtpHost;
1897
+ smtpPort = skillSetup.smtpPort;
1898
+ smtpUser = skillSetup.smtpUser;
1899
+ smtpPass = skillSetup.smtpPass;
1900
+ setupStep = 'projectDir';
1901
+ continue;
1902
+ }
1903
+
1904
+ if (setupStep === 'projectDir') {
1905
+ const nextProjectDir = await inputWithBack({ message: isVi ? 'Thư mục cài đặt project:' : 'Project install directory:', defaultValue: projectDir, allowBack: true, isVi });
1906
+ if (nextProjectDir === CLI_BACK) {
1907
+ setupStep = 'skills';
1908
+ continue;
1909
+ }
1910
+ projectDir = nextProjectDir;
1911
+ break;
1912
+ }
1913
+ }
1914
+
1915
+ const isMultiBot = botCount > 1 && channelKey === 'telegram';
1916
+ const botName = bots[0].name;
1917
+ const botDesc = bots[0].desc;
1918
+ const botPersona = bots[0].persona;
1919
+ const agentId = String(botName || 'chat').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'chat';
1920
+ const modelsPrimary = providerKey === 'ollama' ? selectedOllamaModel : providerKey === '9router' ? 'smart-route' : provider.models?.[0]?.id || 'gpt-4o-mini';
1921
+ const hasBrowserDesktop = selectedSkills.includes('browser') && browserMode === 'desktop';
1922
+ const hasBrowserServer = selectedSkills.includes('browser') && browserMode === 'server';
1351
1923
  console.log(chalk.cyan(`\n🚀 ${isVi ? 'Đang tạo thư mục và file cấu hình...' : 'Generating directories and configurations...'}`));
1352
1924
 
1353
1925
  await fs.ensureDir(projectDir);
@@ -1430,7 +2002,7 @@ async function main() {
1430
2002
  ? buildRelayPluginInstallCommand('openclaw')
1431
2003
  : '';
1432
2004
  const socatBridge = hasBrowserDesktop ? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 &' : '';
1433
- const deviceApproveLoop = '(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) &';
2005
+ const deviceApproveLoop = 'while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done >/dev/null 2>&1 &';
1434
2006
 
1435
2007
  // buildDockerArtifacts joins runtimeCommandParts with spaces, then appends 'openclaw gateway run'
1436
2008
  // Each part should be a standalone command fragment (no trailing &&)
@@ -1449,7 +2021,7 @@ async function main() {
1449
2021
  socatBridge,
1450
2022
  deviceApproveLoop,
1451
2023
  ].filter(Boolean),
1452
- volumeMount: '../../.openclaw:/root/.openclaw',
2024
+ volumeMount: '../../.openclaw:/root/project/.openclaw',
1453
2025
  singleComposeName: `oc-${agentId}`,
1454
2026
  multiComposeName: 'oc-multibot',
1455
2027
  singleAppContainerName: `openclaw-${agentId}`,
@@ -1541,10 +2113,9 @@ async function main() {
1541
2113
  workspaceDir: `workspace-${agentSlug}`,
1542
2114
  };
1543
2115
  });
1544
- const telegramAccounts = Object.fromEntries(agentMetas.map((meta) => [meta.accountId, {
1545
- botToken: meta.token,
1546
- ackReaction: '👍',
1547
- }]));
2116
+ const telegramAccounts = Object.fromEntries(agentMetas.map((meta) => [meta.accountId, {
2117
+ botToken: meta.token,
2118
+ }]));
1548
2119
  const telegramChannelConfig = {
1549
2120
  enabled: true,
1550
2121
  defaultAccount: 'default',
@@ -1579,13 +2150,13 @@ async function main() {
1579
2150
  timeoutSeconds: provider.isLocal ? 900 : 120,
1580
2151
  ...(provider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
1581
2152
  },
1582
- list: agentMetas.map((meta) => ({
1583
- id: meta.agentId,
1584
- name: meta.name,
1585
- workspace: `.openclaw/${meta.workspaceDir}`,
1586
- agentDir: `agents/${meta.agentId}/agent`,
1587
- model: { primary: modelsPrimary, fallbacks: [] },
1588
- })),
2153
+ list: agentMetas.map((meta) => ({
2154
+ id: meta.agentId,
2155
+ name: meta.name,
2156
+ workspace: `.openclaw/${meta.workspaceDir}`,
2157
+ agentDir: `agents/${meta.agentId}/agent`,
2158
+ model: { primary: modelsPrimary, fallbacks: [] },
2159
+ })),
1589
2160
  },
1590
2161
  ...(providerKey === '9router' ? {
1591
2162
  models: {
@@ -1641,7 +2212,7 @@ async function main() {
1641
2212
  auth: { mode: 'token', token: 'cli-dummy-token-xyz123' },
1642
2213
  },
1643
2214
  };
1644
- sharedConfig.plugins = { entries: {} };
2215
+ sharedConfig.plugins = { entries: {} };
1645
2216
 
1646
2217
  if (hasBrowserDesktop) {
1647
2218
  sharedConfig.browser = {
@@ -1658,7 +2229,7 @@ async function main() {
1658
2229
 
1659
2230
  await fs.writeJson(path.join(rootClawDir, 'openclaw.json'), sharedConfig, { spaces: 2 });
1660
2231
  await fs.writeFile(
1661
- path.join(projectDir, 'TELEGRAM-POST-INSTALL.md'),
2232
+ path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME),
1662
2233
  buildTelegramPostInstallChecklist({ isVi, bots, groupId }),
1663
2234
  'utf8',
1664
2235
  );
@@ -1727,11 +2298,11 @@ async function main() {
1727
2298
  selectedSkills, deployMode,
1728
2299
  isDesktop: hasBrowserDesktop, isServer: hasBrowserServer,
1729
2300
  isMultiBot: true, ownAliases, otherAgents,
1730
- teamRoster: teamMdRoster, userInfo,
1731
- agentWorkspaceDir: meta.workspaceDir,
1732
- isRelayBot: true,
1733
- replyToDirectMessages: true,
1734
- });
2301
+ teamRoster: teamMdRoster, userInfo,
2302
+ agentWorkspaceDir: meta.workspaceDir,
2303
+ isRelayBot: true,
2304
+ replyToDirectMessages: true,
2305
+ });
1735
2306
  }
1736
2307
  } else {
1737
2308
  const numBotsToConfigure = 1;
@@ -1748,9 +2319,9 @@ async function main() {
1748
2319
  }));
1749
2320
  const ownAliases = [loopBotName, bots[bIndex]?.slashCmd || '', `bot ${bIndex + 1}`].filter(Boolean);
1750
2321
  const otherBotNames = teamRoster.filter((peer) => peer.idx !== bIndex).map((peer) => peer.name);
1751
- const loopAgentId = loopBotName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `bot${bIndex + 1}`;
1752
- const loopWorkspaceDir = `workspace-${loopAgentId}`;
1753
- const loopBotDir = isMultiBot ? path.join(projectDir, `bot${bIndex+1}`) : projectDir;
2322
+ const loopAgentId = loopBotName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `bot${bIndex + 1}`;
2323
+ const loopWorkspaceDir = `workspace-${loopAgentId}`;
2324
+ const loopBotDir = isMultiBot ? path.join(projectDir, `bot${bIndex+1}`) : projectDir;
1754
2325
 
1755
2326
  await fs.ensureDir(path.join(loopBotDir, '.openclaw', 'agents', loopAgentId, 'agent'));
1756
2327
  if (Object.keys(authProfilesJson).length > 0) {
@@ -1767,13 +2338,13 @@ async function main() {
1767
2338
  compaction: { mode: 'safeguard' },
1768
2339
  timeoutSeconds: provider.isLocal ? 900 : 120,
1769
2340
  ...(provider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
1770
- },
1771
- list: [{
1772
- id: loopAgentId,
1773
- workspace: `.openclaw/${loopWorkspaceDir}`,
1774
- agentDir: `agents/${loopAgentId}/agent`,
1775
- model: { primary: modelsPrimary, fallbacks: [] }
1776
- }]
2341
+ },
2342
+ list: [{
2343
+ id: loopAgentId,
2344
+ workspace: `.openclaw/${loopWorkspaceDir}`,
2345
+ agentDir: `agents/${loopAgentId}/agent`,
2346
+ model: { primary: modelsPrimary, fallbacks: [] }
2347
+ }]
1777
2348
  },
1778
2349
  ...(providerKey === '9router' ? {
1779
2350
  models: {
@@ -1834,13 +2405,29 @@ async function main() {
1834
2405
  botConfig.skills = { entries: skillEntries };
1835
2406
  }
1836
2407
 
1837
- if (channelKey === 'telegram') {
1838
- const telegramConfig = { enabled: true, dmPolicy: 'open', allowFrom: ['*'] };
1839
- if (isMultiBot) {
1840
- telegramConfig.groupPolicy = groupId ? 'allowlist' : 'open';
1841
- telegramConfig.groupAllowFrom = ['*'];
1842
- telegramConfig.groups = {
1843
- [groupId || '*']: { enabled: true, requireMention: false }
2408
+ if (channelKey === 'telegram') {
2409
+ const telegramConfig = {
2410
+ enabled: true,
2411
+ dmPolicy: 'open',
2412
+ allowFrom: ['*'],
2413
+ defaultAccount: 'default',
2414
+ replyToMode: 'first',
2415
+ reactionLevel: 'minimal',
2416
+ actions: {
2417
+ sendMessage: true,
2418
+ reactions: true,
2419
+ },
2420
+ accounts: {
2421
+ default: {
2422
+ botToken: loopBotToken || '<your_bot_token>',
2423
+ },
2424
+ },
2425
+ };
2426
+ if (isMultiBot) {
2427
+ telegramConfig.groupPolicy = groupId ? 'allowlist' : 'open';
2428
+ telegramConfig.groupAllowFrom = ['*'];
2429
+ telegramConfig.groups = {
2430
+ [groupId || '*']: { enabled: true, requireMention: false }
1844
2431
  };
1845
2432
  }
1846
2433
  botConfig.channels['telegram'] = telegramConfig;
@@ -1857,7 +2444,7 @@ async function main() {
1857
2444
  await fs.writeJson(path.join(loopBotDir, '.openclaw', 'openclaw.json'), botConfig, { spaces: 2 });
1858
2445
 
1859
2446
  // ── Workspace files: use shared writeWorkspaceFiles() ──────────────────────
1860
- const dockerWorkspaceDir = path.join(loopBotDir, '.openclaw', loopWorkspaceDir);
2447
+ const dockerWorkspaceDir = path.join(loopBotDir, '.openclaw', loopWorkspaceDir);
1861
2448
  const dockerOwnAliases = [loopBotName, bots[bIndex]?.slashCmd || '', `bot ${bIndex + 1}`].filter(Boolean);
1862
2449
  const dockerOtherAgents = teamRoster
1863
2450
  .filter((peer) => peer.idx !== bIndex)
@@ -1876,44 +2463,44 @@ async function main() {
1876
2463
  isMultiBot,
1877
2464
  ownAliases: dockerOwnAliases,
1878
2465
  otherAgents: dockerOtherAgents,
1879
- teamRoster,
1880
- userInfo,
1881
- agentWorkspaceDir: loopWorkspaceDir,
1882
- isRelayBot: isMultiBot,
1883
- replyToDirectMessages: true,
1884
- });
2466
+ teamRoster,
2467
+ userInfo,
2468
+ agentWorkspaceDir: loopWorkspaceDir,
2469
+ isRelayBot: isMultiBot,
2470
+ replyToDirectMessages: true,
2471
+ });
1885
2472
 
1886
2473
  if (isMultiBot) {
1887
2474
  // Append per-bot reply rules to AGENTS.md
1888
2475
  const otherBotNames = teamRoster.filter((p) => p.idx !== bIndex).map((p) => p.name);
1889
2476
  const extraAgentsMd = isVi
1890
- ? `\n\n## Khi nao nen tra loi\n- Neu metadata khong noi ro day la group/supergroup, mac dinh xem la chat rieng/DM va tra loi binh thuong.\n- Trong group, chi tra loi khi tin nhan co alias cua ban: ${dockerOwnAliases.map((a) => `\`${a}\``).join(', ')} hoac username Telegram cua ban.\n- Quy tac im lang khi tin nhan khong goi ban chi ap dung cho group chat, khong ap dung cho DM/chat rieng.\n- Neu group message chi goi ro bot khac ${otherBotNames.length ? otherBotNames.map((n) => `\`${n}\``).join(', ') : '`bot khac`'} thi khong cuop loi.\n- Gateway tu dong tha ack reaction; khong goi action react thu cong tru khi user yeu cau.\n- Khi can phoi hop noi bo, dung dung agent id ky thuat trong \`AGENTS.md\`, khong dung ten hien thi.\n- Khi hoi ve vai tro cac bot, dung \`AGENTS.md\` lam nguon su that.`
1891
- : `\n\n## When To Reply\n- If metadata does not clearly say this is a group/supergroup, treat it as a private DM and reply normally.\n- In group chats, only reply when the message contains one of your aliases: ${dockerOwnAliases.map((a) => `\`${a}\``).join(', ')} or your Telegram username.\n- The stay-silent rule for messages not addressed to you applies only to group chats, never to DMs/private chats.\n- If a group message is clearly calling another bot such as ${otherBotNames.length ? otherBotNames.map((n) => `\`${n}\``).join(', ') : '`another bot`'}, do not hijack it.\n- The gateway automatically sends ack reactions; do not call react manually unless the user asks.\n- When you need internal coordination, use the exact technical agent id from \`AGENTS.md\`, not the display name.\n- Use \`AGENTS.md\` as the source of truth for team roles.`;
2477
+ ? `\n\n## Khi nao nen tra loi\n- Neu metadata khong noi ro day la group/supergroup, mac dinh xem la chat rieng/DM va tra loi binh thuong.\n- Trong group, chi tra loi khi tin nhan co alias cua ban: ${dockerOwnAliases.map((a) => `\`${a}\``).join(', ')} hoac username Telegram cua ban.\n- Quy tac im lang khi tin nhan khong goi ban chi ap dung cho group chat, khong ap dung cho DM/chat rieng.\n- Neu group message chi goi ro bot khac ${otherBotNames.length ? otherBotNames.map((n) => `\`${n}\``).join(', ') : '`bot khac`'} thi khong cuop loi.\n- Trước MỖI phản hồi Telegram nhìn thấy bởi user, nếu turn hiện tại có tool/action reaction native thì ưu tiên dùng nó để thả \`👍\` trước, trừ khi \`👍\` đã có sẵn.\n- Khong duoc noi la minh da tha like neu turn hien tai chua goi duoc tool reaction hoac reaction khong hien that tren chat.\n- Khi can phoi hop noi bo, dung dung agent id ky thuat trong \`AGENTS.md\`, khong dung ten hien thi.\n- Khi hoi ve vai tro cac bot, dung \`AGENTS.md\` lam nguon su that.`
2478
+ : `\n\n## When To Reply\n- If metadata does not clearly say this is a group/supergroup, treat it as a private DM and reply normally.\n- In group chats, only reply when the message contains one of your aliases: ${dockerOwnAliases.map((a) => `\`${a}\``).join(', ')} or your Telegram username.\n- The stay-silent rule for messages not addressed to you applies only to group chats, never to DMs/private chats.\n- If a group message is clearly calling another bot such as ${otherBotNames.length ? otherBotNames.map((n) => `\`${n}\``).join(', ') : '`another bot`'}, do not hijack it.\n- Before EVERY user-visible Telegram reply, if the current turn exposes a native reaction tool/action, use it to add \`👍\` first unless \`👍\` is already present.\n- Never claim that you already reacted unless the current turn actually executed the reaction and it is visible on the chat.\n- When you need internal coordination, use the exact technical agent id from \`AGENTS.md\`, not the display name.\n- Use \`AGENTS.md\` as the source of truth for team roles.`;
1892
2479
  await fs.appendFile(path.join(dockerWorkspaceDir, 'AGENTS.md'), extraAgentsMd);
1893
2480
  }
1894
2481
  } // END FOR LOOP
1895
2482
  }
1896
2483
 
1897
2484
  // ── Chrome Debug scripts — via shared builder (same content as wizard ZIP) ─
1898
- await writeGeneratedArtifacts(projectDir, buildCliChromeDebugArtifacts());
2485
+ await writeGeneratedArtifacts(projectDir, buildCliChromeDebugArtifacts());
1899
2486
 
1900
- // ── Uninstall scripts ───────────────────────────────────────────────────────
1901
- await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
1902
- deployMode, osChoice: detectedOS, projectDir, botName,
1903
- }));
1904
-
1905
- // ── Upgrade scripts ─────────────────────────────────────────────────────────
1906
- await writeGeneratedArtifacts(projectDir, buildCliUpgradeArtifacts());
2487
+ // ── Uninstall scripts ───────────────────────────────────────────────────────
2488
+ await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
2489
+ deployMode, osChoice: detectedOS, projectDir, botName,
2490
+ }));
2491
+
2492
+ // ── Upgrade scripts ─────────────────────────────────────────────────────────
2493
+ await writeGeneratedArtifacts(projectDir, buildCliUpgradeArtifacts());
1907
2494
 
1908
2495
  // ── start-bot.bat / start-bot.sh — one-click restart scripts ─────────────
1909
- // Generated for native deployments only (docker has docker compose up)
1910
- if (deployMode !== 'docker') {
1911
- await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
1912
- projectDir,
1913
- openclawHome: path.join(projectDir, '.openclaw'),
1914
- is9Router: providerKey === '9router',
1915
- isVi,
1916
- }));
2496
+ // Generated for native deployments only (docker has docker compose up)
2497
+ if (deployMode !== 'docker') {
2498
+ await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
2499
+ projectDir,
2500
+ openclawHome: path.join(projectDir, '.openclaw'),
2501
+ is9Router: providerKey === '9router',
2502
+ isVi,
2503
+ }));
1917
2504
 
1918
2505
  console.log(chalk.cyan(
1919
2506
  isVi
@@ -1922,10 +2509,12 @@ async function main() {
1922
2509
  ));
1923
2510
  }
1924
2511
 
1925
- console.log(chalk.green(`✅ ${isVi ? 'Tạo cấu hình thành công!' : 'Configs created successfully!'}`));
1926
-
1927
- // 7. Auto Run
1928
- const autoRun = deployMode === 'docker' ? await confirm({
2512
+ console.log(chalk.green(`✅ ${isVi ? 'Tạo cấu hình thành công!' : 'Configs created successfully!'}`));
2513
+
2514
+ installLatestOpenClaw({ isVi, osChoice });
2515
+
2516
+ // 7. Auto Run
2517
+ const autoRun = deployMode === 'docker' ? await confirm({
1929
2518
  message: isVi ? 'Bạn có muốn tự động build Docker và khởi động Bot luôn không?' : 'Do you want to run Docker compose and start the bot now?',
1930
2519
  default: true
1931
2520
  }) : false;
@@ -1988,7 +2577,7 @@ async function main() {
1988
2577
  ? 'Nhắn tin cho bot trên Telegram là dùng được ngay!'
1989
2578
  : 'Just message your bot on Telegram to start chatting!'}`));
1990
2579
  if (isMultiBot) {
1991
- console.log(chalk.yellow(`\n${isVi ? '📋 Bắt buộc:' : '📋 Required:'} TELEGRAM-POST-INSTALL.md`));
2580
+ console.log(chalk.yellow(`\n${isVi ? '📋 Đọc hướng dẫn setup bot trong Group:' : '📋 Read setup guide in Group:'} ${TELEGRAM_SETUP_GUIDE_FILENAME}`));
1992
2581
  console.log(chalk.gray(isVi
1993
2582
  ? ' → Chạy scripts/telegram-post-install-check.mjs để lấy link thật, kiểm tra group/privacy, rồi mới add bot và Disable privacy mode.'
1994
2583
  : ' → Run scripts/telegram-post-install-check.mjs to get the real links, verify group/privacy, then add the bots and disable privacy mode.'));
@@ -2005,38 +2594,32 @@ async function main() {
2005
2594
  });
2006
2595
 
2007
2596
  }
2597
+ if (deployMode === 'docker') {
2008
2598
 
2009
- installLatestOpenClaw({ isVi, osChoice });
2010
-
2011
- if (deployMode === 'docker') {
2012
-
2013
- if (isMultiBot && channelKey === 'telegram') {
2014
- console.log(chalk.yellow(`\n${isVi ? '📋 Xem hướng dẫn sau cài:' : '📋 Read post-install guide:'} ${path.join(projectDir, 'TELEGRAM-POST-INSTALL.md')}`));
2015
- }
2016
2599
  // ── Auto-install openclaw binary if not present ──────────────────────────
2017
2600
  const isOpenClawInstalled = () => { try { execSync('openclaw --version', { stdio: 'ignore' }); return true; } catch { return false; } };
2018
2601
  if (!isOpenClawInstalled()) {
2019
2602
  console.log(chalk.cyan(isVi
2020
- ? '\n📦 Đang cài openclaw binary (npm install -g openclaw)...'
2021
- : '\n📦 Installing openclaw binary (npm install -g openclaw)...'));
2603
+ ? `\n📦 Đang cài openclaw binary (${OPENCLAW_NPM_SPEC})...`
2604
+ : `\n📦 Installing openclaw binary (${OPENCLAW_NPM_SPEC})...`));
2022
2605
  try {
2023
- execSync('npm install -g openclaw', { stdio: 'inherit' });
2606
+ execSync(`npm install -g ${OPENCLAW_NPM_SPEC}`, { stdio: 'inherit' });
2024
2607
  console.log(chalk.green(isVi ? '✅ openclaw đã cài xong!' : '✅ openclaw installed!'));
2025
2608
  } catch {
2026
2609
  console.log(chalk.yellow(isVi
2027
- ? '⚠️ Không tự cài được. Chạy thủ công: sudo npm install -g openclaw'
2028
- : '⚠️ Could not auto-install. Run manually: sudo npm install -g openclaw'));
2610
+ ? `⚠️ Không tự cài được. Chạy thủ công: sudo npm install -g ${OPENCLAW_NPM_SPEC}`
2611
+ : `⚠️ Could not auto-install. Run manually: sudo npm install -g ${OPENCLAW_NPM_SPEC}`));
2029
2612
  }
2030
2613
  }
2031
2614
 
2032
2615
  if (isMultiBot && channelKey === 'telegram') {
2033
- console.log(chalk.yellow(`\n${isVi ? '📋 Xem hướng dẫn sau cài:' : '📋 Read post-install guide:'} ${path.join(projectDir, 'TELEGRAM-POST-INSTALL.md')}`));
2616
+ console.log(chalk.yellow(`\n${isVi ? '📋 Xem hướng dẫn sau cài:' : '📋 Read post-install guide:'} ${path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME)}`));
2034
2617
  }
2035
2618
  } else {
2036
2619
  if (!isOpenClawInstalled()) {
2037
2620
  console.log(chalk.cyan(isVi
2038
- ? '\n📦 Dang cai openclaw binary (npm install -g openclaw)...'
2039
- : '\n📦 Installing openclaw binary (npm install -g openclaw)...'));
2621
+ ? `\n📦 Dang cai openclaw binary (${OPENCLAW_NPM_SPEC})...`
2622
+ : `\n📦 Installing openclaw binary (${OPENCLAW_NPM_SPEC})...`));
2040
2623
  if (!installGlobalPackage(OPENCLAW_NPM_SPEC, { isVi, osChoice, displayName: 'openclaw' })) {
2041
2624
  process.exit(1);
2042
2625
  }
@@ -2178,10 +2761,12 @@ async function main() {
2178
2761
 
2179
2762
  console.log(chalk.cyan(`\n👉 ${isVi ? 'Native runtime da duoc cai san va khoi dong.' : 'Native runtime is installed and started.'}`));
2180
2763
  if (isMultiBot && channelKey === 'telegram') {
2181
- console.log(chalk.yellow(`\n📋 ${isVi ? 'Xem huong dan sau cai:' : 'Read post-install guide:'} ${path.join(projectDir, 'TELEGRAM-POST-INSTALL.md')}`));
2764
+ console.log(chalk.yellow(`\n📋 ${isVi ? 'Xem huong dan sau cai:' : 'Read post-install guide:'} ${path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME)}`));
2182
2765
  }
2183
2766
  }
2184
- }
2767
+ }
2768
+
2769
+
2185
2770
 
2186
2771
  main().catch(err => {
2187
2772
  console.error(chalk.red('Error:'), err);