create-openclaw-bot 5.1.14 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -5,11 +5,13 @@ import fs from 'fs-extra';
5
5
  import path from 'path';
6
6
  import os from 'os';
7
7
  import chalk from 'chalk';
8
- import { spawn, execSync, execFileSync } from 'child_process';
9
- const TELEGRAM_RELAY_PLUGIN_ID = 'openclaw-telegram-multibot-relay';
8
+ import { spawn, execSync, execFileSync } from 'child_process';
9
+ const TELEGRAM_RELAY_PLUGIN_RUNTIME_ID = 'telegram-multibot-relay';
10
+ const TELEGRAM_RELAY_PLUGIN_PACKAGE = 'openclaw-telegram-multibot-relay';
10
11
  const OPENCLAW_NPM_SPEC = 'openclaw@2026.4.5';
11
- // Use plain npm package name clawhub: protocol not supported in all OpenClaw versions
12
- const TELEGRAM_RELAY_PLUGIN_SPEC = TELEGRAM_RELAY_PLUGIN_ID;
12
+ const OPENCLAW_RUNTIME_PACKAGES = 'grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api';
13
+ // Use plain npm package name — clawhub: protocol not supported in all OpenClaw versions
14
+ const TELEGRAM_RELAY_PLUGIN_SPEC = TELEGRAM_RELAY_PLUGIN_PACKAGE;
13
15
 
14
16
  // Install command: only use clawhub: spec (published to ClawHub)
15
17
  function buildRelayPluginInstallCommand(prefix = 'openclaw') {
@@ -20,13 +22,17 @@ function buildRelayPluginInstallCommandWin(prefix = 'openclaw') {
20
22
  return `${prefix} plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC} || exit /b 0`;
21
23
  }
22
24
 
23
- function installRelayPluginForProject(projectDir, isVi) {
24
- try {
25
- execSync(`openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`, { cwd: projectDir, stdio: 'ignore' });
26
- return true;
27
- } catch {
28
- // silent fallback
29
- }
25
+ function installRelayPluginForProject(projectDir, isVi) {
26
+ try {
27
+ execSync(`openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`, {
28
+ cwd: projectDir,
29
+ stdio: 'ignore',
30
+ env: getProjectRuntimeEnv(projectDir),
31
+ });
32
+ return true;
33
+ } catch {
34
+ // silent fallback
35
+ }
30
36
  console.log(chalk.yellow(isVi
31
37
  ? `\n⚠️ Chua the tu dong cai plugin. Sau khi bot chay, chay thu cong:\n openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`
32
38
  : `\n⚠️ Could not auto-install plugin. After the bot starts, run manually:\n openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`));
@@ -114,19 +120,23 @@ function resolveWindowsCommand(command) {
114
120
  }
115
121
  }
116
122
 
117
- function spawnBackgroundProcess(command, args, options = {}) {
118
- const { cwd, env = {} } = options;
119
- const mergedEnv = { ...process.env, ...env };
120
-
121
- if (process.platform === 'win32') {
122
- const resolvedCommand = resolveWindowsCommand(command);
123
- const argList = args.map((arg) => quotePowerShellSingle(arg)).join(', ');
124
- const startProcessScript = [
125
- `$filePath = ${quotePowerShellSingle(resolvedCommand)}`,
126
- `$workingDir = ${quotePowerShellSingle(cwd || process.cwd())}`,
127
- `$argList = @(${argList})`,
128
- "Start-Process -WindowStyle Hidden -FilePath $filePath -WorkingDirectory $workingDir -ArgumentList $argList"
129
- ].join('; ');
123
+ function spawnBackgroundProcess(command, args, options = {}) {
124
+ const { cwd, env = {} } = options;
125
+ const mergedEnv = { ...process.env, ...env };
126
+
127
+ if (process.platform === 'win32') {
128
+ const resolvedCommand = resolveWindowsCommand(command);
129
+ const argList = args.map((arg) => quotePowerShellSingle(arg)).join(', ');
130
+ const envAssignments = Object.entries(env)
131
+ .map(([key, value]) => `$env:${key}=${quotePowerShellSingle(String(value))}`)
132
+ .join('; ');
133
+ const startProcessScript = [
134
+ `$filePath = ${quotePowerShellSingle(resolvedCommand)}`,
135
+ `$workingDir = ${quotePowerShellSingle(cwd || process.cwd())}`,
136
+ `$argList = @(${argList})`,
137
+ ...(envAssignments ? [envAssignments] : []),
138
+ "Start-Process -WindowStyle Hidden -FilePath $filePath -WorkingDirectory $workingDir -ArgumentList $argList"
139
+ ].join('; ');
130
140
 
131
141
  return spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', startProcessScript], {
132
142
  cwd,
@@ -146,26 +156,38 @@ function spawnBackgroundProcess(command, args, options = {}) {
146
156
  });
147
157
  }
148
158
 
149
- function resolveNative9RouterDesktopLaunch() {
150
- return {
151
- command: process.execPath,
152
- args: [path.join(getGlobalNpmRoot(), '9router', 'app', 'server.js')],
153
- env: {
154
- PORT: '20128',
155
- HOSTNAME: '0.0.0.0'
156
- }
157
- };
158
- }
159
-
160
- function getNative9RouterDataDir() {
161
- if (process.platform === 'win32') {
162
- return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), '9router');
163
- }
164
-
165
- return path.join(os.homedir(), '.9router');
166
- }
167
-
168
- function getGatewayAllowedOrigins(port) {
159
+ function resolveNative9RouterDesktopLaunch() {
160
+ const serverEntry = get9RouterServerEntryCandidates().find((candidate) => fs.existsSync(candidate))
161
+ || get9RouterServerEntryCandidates()[0];
162
+ return {
163
+ command: process.execPath,
164
+ args: [serverEntry],
165
+ env: {
166
+ PORT: '20128',
167
+ HOSTNAME: '0.0.0.0'
168
+ }
169
+ };
170
+ }
171
+
172
+ function getProjectOpenClawHome(projectDir) {
173
+ return path.join(projectDir, '.openclaw');
174
+ }
175
+
176
+ function getProject9RouterDataDir(projectDir) {
177
+ return path.join(projectDir, '.9router');
178
+ }
179
+
180
+ function getProjectRuntimeEnv(projectDir, extraEnv = {}) {
181
+ return {
182
+ ...process.env,
183
+ OPENCLAW_HOME: getProjectOpenClawHome(projectDir),
184
+ OPENCLAW_STATE_DIR: getProjectOpenClawHome(projectDir),
185
+ DATA_DIR: getProject9RouterDataDir(projectDir),
186
+ ...extraEnv,
187
+ };
188
+ }
189
+
190
+ function getGatewayAllowedOrigins(port) {
169
191
  const normalizedPort = Number(port) || 18791;
170
192
  const origins = new Set([
171
193
  `http://localhost:${normalizedPort}`,
@@ -427,7 +449,7 @@ function resolveCommandOnPath(command) {
427
449
  }
428
450
  }
429
451
 
430
- function getGlobalNpmRoot() {
452
+ function getGlobalNpmRoot() {
431
453
  try {
432
454
  return execSync('npm root -g', {
433
455
  stdio: ['ignore', 'pipe', 'ignore'],
@@ -435,13 +457,57 @@ function getGlobalNpmRoot() {
435
457
  shell: true,
436
458
  env: process.env
437
459
  }).trim();
438
- } catch {
439
- if (process.platform === 'win32') {
440
- return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules');
441
- }
442
- return path.join(os.homedir(), '.local', 'lib', 'node_modules');
443
- }
444
- }
460
+ } catch {
461
+ if (process.platform === 'win32') {
462
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules');
463
+ }
464
+ return path.join(os.homedir(), '.local', 'share', 'npm', 'lib', 'node_modules');
465
+ }
466
+ }
467
+
468
+ function getNativeOpenClawRootDir(projectDir = '.') {
469
+ return path.join(projectDir, '.openclaw').replace(/\\/g, '/');
470
+ }
471
+
472
+ function getGeneratedWorkspaceRoot(deployMode, projectDir = '.') {
473
+ return deployMode === 'native' ? getNativeOpenClawRootDir(projectDir) : '/root/.openclaw';
474
+ }
475
+
476
+ function get9RouterServerEntryCandidates() {
477
+ const homeDir = os.homedir();
478
+ const npmRoots = [];
479
+
480
+ try {
481
+ const root = execSync('npm root -g', {
482
+ stdio: ['ignore', 'pipe', 'ignore'],
483
+ encoding: 'utf8',
484
+ shell: true,
485
+ env: process.env
486
+ }).trim();
487
+ if (root) npmRoots.push(root);
488
+ } catch {
489
+ // handled by fallback candidates below
490
+ }
491
+
492
+ const prefixes = [
493
+ process.env.npm_config_prefix,
494
+ process.env.NPM_CONFIG_PREFIX,
495
+ process.env.PREFIX,
496
+ process.env.NPM_PREFIX,
497
+ path.join(homeDir, '.local'),
498
+ path.join(homeDir, '.npm-global'),
499
+ path.join(homeDir, '.local', 'share', 'npm')
500
+ ].filter(Boolean);
501
+
502
+ for (const prefix of prefixes) {
503
+ npmRoots.push(path.join(prefix, 'lib', 'node_modules'));
504
+ }
505
+
506
+ npmRoots.push(path.join(homeDir, '.local', 'lib', 'node_modules'));
507
+ npmRoots.push(getGlobalNpmRoot());
508
+
509
+ return [...new Set(npmRoots.map((root) => path.join(root, '9router', 'app', 'server.js')))];
510
+ }
445
511
 
446
512
  function indentBlock(text, spaces) {
447
513
  const prefix = ' '.repeat(spaces);
@@ -460,26 +526,26 @@ function build9RouterComposeEntrypointScript(syncScriptBase64) {
460
526
  ].join('\n');
461
527
  }
462
528
 
463
- async function writeNative9RouterSyncScript(projectDir) {
464
- const syncScriptPath = path.join(projectDir, '.openclaw', '9router-smart-route-sync.js');
465
- await fs.ensureDir(path.dirname(syncScriptPath));
466
- await fs.writeFile(syncScriptPath, build9RouterSmartRouteSyncScript(path.join(getNative9RouterDataDir(), 'db.json')));
467
- return syncScriptPath;
468
- }
529
+ async function writeNative9RouterSyncScript(projectDir) {
530
+ const syncScriptPath = path.join(projectDir, '.openclaw', '9router-smart-route-sync.js');
531
+ await fs.ensureDir(path.dirname(syncScriptPath));
532
+ await fs.writeFile(syncScriptPath, build9RouterSmartRouteSyncScript(path.join(getProject9RouterDataDir(projectDir), 'db.json')));
533
+ return syncScriptPath;
534
+ }
469
535
 
470
536
  function extractFirstHttpUrl(text) {
471
537
  const match = String(text || '').match(/https?:\/\/[^\s"'`]+/);
472
538
  return match ? match[0] : null;
473
539
  }
474
540
 
475
- function getTokenizedDashboardUrl(projectDir) {
476
- try {
477
- const output = execSync('openclaw dashboard', {
478
- cwd: projectDir,
479
- env: process.env,
480
- encoding: 'utf8',
481
- shell: true,
482
- stdio: ['ignore', 'pipe', 'pipe'],
541
+ function getTokenizedDashboardUrl(projectDir) {
542
+ try {
543
+ const output = execSync('openclaw dashboard', {
544
+ cwd: projectDir,
545
+ env: getProjectRuntimeEnv(projectDir),
546
+ encoding: 'utf8',
547
+ shell: true,
548
+ stdio: ['ignore', 'pipe', 'pipe'],
483
549
  timeout: 15000
484
550
  });
485
551
  return extractFirstHttpUrl(output);
@@ -607,14 +673,14 @@ function extractZaloPairingCode(text) {
607
673
  return null;
608
674
  }
609
675
 
610
- function approveZaloPairingCode({ pairingCode, projectDir, isVi }) {
611
- try {
612
- execSync(`openclaw pairing approve zalouser ${pairingCode}`, {
613
- cwd: projectDir,
614
- stdio: 'inherit',
615
- shell: true,
616
- env: process.env
617
- });
676
+ function approveZaloPairingCode({ pairingCode, projectDir, isVi }) {
677
+ try {
678
+ execSync(`openclaw pairing approve zalouser ${pairingCode}`, {
679
+ cwd: projectDir,
680
+ stdio: 'inherit',
681
+ shell: true,
682
+ env: getProjectRuntimeEnv(projectDir)
683
+ });
618
684
  console.log(chalk.green(isVi
619
685
  ? `✅ Da tu dong approve pairing code Zalo: ${pairingCode}`
620
686
  : `✅ Automatically approved the Zalo pairing code: ${pairingCode}`));
@@ -645,11 +711,12 @@ async function runNativeZaloPersonalLoginFlow({ isVi, projectDir }) {
645
711
  // ignore stale project QR cleanup failures
646
712
  }
647
713
 
648
- const child = spawn('openclaw', ['channels', 'login', '--channel', 'zalouser', '--verbose'], {
649
- cwd: projectDir,
650
- stdio: ['inherit', 'pipe', 'pipe'],
651
- shell: process.platform === 'win32'
652
- });
714
+ const child = spawn('openclaw', ['channels', 'login', '--channel', 'zalouser', '--verbose'], {
715
+ cwd: projectDir,
716
+ stdio: ['inherit', 'pipe', 'pipe'],
717
+ shell: process.platform === 'win32',
718
+ env: getProjectRuntimeEnv(projectDir),
719
+ });
653
720
 
654
721
  let loginSucceeded = false;
655
722
  let approvedPairingCode = null;
@@ -712,14 +779,14 @@ async function runNativeZaloPersonalLoginFlow({ isVi, projectDir }) {
712
779
  }
713
780
  }
714
781
 
715
- function runPm2Save({ projectDir, isVi }) {
716
- try {
717
- execSync('pm2 save', {
718
- cwd: projectDir,
719
- stdio: 'inherit',
720
- shell: true,
721
- env: process.env
722
- });
782
+ function runPm2Save({ projectDir, isVi }) {
783
+ try {
784
+ execSync('pm2 save', {
785
+ cwd: projectDir,
786
+ stdio: 'inherit',
787
+ shell: true,
788
+ env: getProjectRuntimeEnv(projectDir)
789
+ });
723
790
  } catch {
724
791
  console.log(chalk.yellow(isVi
725
792
  ? '⚠️ PM2 save khong hoan tat. Bot van co the dang chay, nhung hay thu chay lai `pm2 save` sau.'
@@ -727,9 +794,9 @@ function runPm2Save({ projectDir, isVi }) {
727
794
  }
728
795
  }
729
796
 
730
- function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
731
- const routerAppName = `${appName}-9router`;
732
- const routerLaunch = resolveNative9RouterDesktopLaunch();
797
+ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
798
+ const routerAppName = `${appName}-9router`;
799
+ const routerLaunch = resolveNative9RouterDesktopLaunch();
733
800
  execFileSync('pm2', [
734
801
  'start',
735
802
  routerLaunch.command,
@@ -741,11 +808,11 @@ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
741
808
  'none',
742
809
  '--',
743
810
  ...routerLaunch.args
744
- ], {
745
- cwd: projectDir,
746
- stdio: 'inherit',
747
- env: { ...process.env, ...routerLaunch.env }
748
- });
811
+ ], {
812
+ cwd: projectDir,
813
+ stdio: 'inherit',
814
+ env: getProjectRuntimeEnv(projectDir, routerLaunch.env)
815
+ });
749
816
  if (syncScriptPath) {
750
817
  const syncAppName = `${appName}-9router-sync`;
751
818
  const normalizedSyncScriptPath = syncScriptPath.replace(/\\/g, '/');
@@ -759,19 +826,19 @@ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
759
826
  projectDir.replace(/\\/g, '/'),
760
827
  '--interpreter',
761
828
  process.execPath
762
- ], {
763
- cwd: projectDir,
764
- stdio: 'inherit',
765
- env: process.env
766
- });
829
+ ], {
830
+ cwd: projectDir,
831
+ stdio: 'inherit',
832
+ env: getProjectRuntimeEnv(projectDir)
833
+ });
767
834
  } catch {
768
835
  try {
769
- execSync(`nohup "${process.execPath}" "${normalizedSyncScriptPath}" >/tmp/${syncAppName}.log 2>&1 &`, {
770
- cwd: projectDir,
771
- stdio: 'ignore',
772
- shell: true,
773
- env: process.env
774
- });
836
+ execSync(`nohup "${process.execPath}" "${normalizedSyncScriptPath}" >/tmp/${syncAppName}.log 2>&1 &`, {
837
+ cwd: projectDir,
838
+ stdio: 'ignore',
839
+ shell: true,
840
+ env: getProjectRuntimeEnv(projectDir)
841
+ });
775
842
  console.log(chalk.yellow(isVi
776
843
  ? `⚠️ PM2 khong khoi dong duoc sync helper. Da fallback sang background node: /tmp/${syncAppName}.log`
777
844
  : `⚠️ PM2 could not start the sync helper. Fell back to a background node process: /tmp/${syncAppName}.log`));
@@ -787,24 +854,21 @@ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
787
854
  console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${routerAppName}` : ` View logs: pm2 logs ${routerAppName}`));
788
855
  }
789
856
 
790
- async function syncLocalConfigToHome(projectDir, isVi) {
791
- const homedir = os.homedir();
792
- const globalClawDir = path.join(homedir, '.openclaw');
793
- const localClawDir = path.join(projectDir, '.openclaw');
794
- try {
795
- await fs.ensureDir(globalClawDir);
796
- await fs.copy(localClawDir, globalClawDir, { overwrite: true });
797
- console.log(chalk.green(`\n✅ ${isVi
798
- ? 'Config đã được sync vào ~/.openclaw/ — openclaw sẵn sàng!'
799
- : 'Config synced to ~/.openclaw/ — openclaw is ready!'}`));
800
- return true;
801
- } catch {
802
- console.log(chalk.yellow(`\n⚠️ ${isVi
803
- ? `Không thể tự sync config. Chạy thủ công:\n cp -rn ${localClawDir}/. ${globalClawDir}/`
804
- : `Could not auto-sync config. Run manually:\n cp -rn ${localClawDir}/. ${globalClawDir}/`}`));
805
- return false;
806
- }
807
- }
857
+ async function ensureProjectRuntimeDirs(projectDir, isVi) {
858
+ try {
859
+ await fs.ensureDir(getProjectOpenClawHome(projectDir));
860
+ await fs.ensureDir(getProject9RouterDataDir(projectDir));
861
+ console.log(chalk.green(`\n✅ ${isVi
862
+ ? 'Runtime project đã sẵn sàng trong thư mục đã chọn.'
863
+ : 'The project runtime directories are ready inside the chosen folder.'}`));
864
+ return true;
865
+ } catch {
866
+ console.log(chalk.yellow(`\n⚠️ ${isVi
867
+ ? `Không thể tạo runtime folders trong project. Hãy tự kiểm tra: ${projectDir}`
868
+ : `Could not create the runtime folders inside the project. Check: ${projectDir}`}`));
869
+ return false;
870
+ }
871
+ }
808
872
 
809
873
  function buildTelegramPostInstallChecklist({ isVi, bots, groupId }) {
810
874
  const botList = bots.map((bot, idx) => `- **${bot?.name || `Bot ${idx + 1}`}** — token: ${String(bot?.token || '').slice(0, 10)}...`).join('\n');
@@ -963,17 +1027,17 @@ const CHANNELS = {
963
1027
  'zalo-personal': { name: 'Zalo Personal (Quét QR)', type: 'zalo-personal', icon: '📱' }
964
1028
  };
965
1029
 
966
- const PROVIDERS = {
967
- '9router': { name: '9Router Proxy (Khuyên dùng)', icon: '🔀', isProxy: true },
968
- 'openai': { name: 'OpenAI (ChatGPT)', icon: '🧠', envKey: 'OPENAI_API_KEY' },
969
- 'ollama': { name: 'Local Ollama', icon: '🏠', isLocal: true },
970
- 'google': { name: 'Google (Gemini)', icon: '⚡', envKey: 'GEMINI_API_KEY' },
971
- 'anthropic': { name: 'Anthropic (Claude)', icon: '🦄', envKey: 'ANTHROPIC_API_KEY' },
972
- 'xai': { name: 'xAI (Grok)', icon: '✖️', envKey: 'XAI_API_KEY' },
973
- 'groq': { name: 'Groq (LPU)', icon: '🏎️', envKey: 'GROQ_API_KEY' }
974
- };
975
-
976
- const SKILLS = [
1030
+ const PROVIDERS = {
1031
+ '9router': { name: '9Router Proxy (Khuyên dùng)', icon: '🔀', isProxy: true, supportsEmbeddings: false },
1032
+ 'openai': { name: 'OpenAI (ChatGPT)', icon: '🧠', envKey: 'OPENAI_API_KEY', supportsEmbeddings: true },
1033
+ 'ollama': { name: 'Local Ollama', icon: '🏠', isLocal: true, supportsEmbeddings: true },
1034
+ 'google': { name: 'Google (Gemini)', icon: '⚡', envKey: 'GEMINI_API_KEY', supportsEmbeddings: true },
1035
+ 'anthropic': { name: 'Anthropic (Claude)', icon: '🦄', envKey: 'ANTHROPIC_API_KEY', supportsEmbeddings: false },
1036
+ 'xai': { name: 'xAI (Grok)', icon: '✖️', envKey: 'XAI_API_KEY', supportsEmbeddings: false },
1037
+ 'groq': { name: 'Groq (LPU)', icon: '🏎️', envKey: 'GROQ_API_KEY', supportsEmbeddings: false }
1038
+ };
1039
+
1040
+ const SKILLS = [
977
1041
  // Web Search removed — OpenClaw has native search built-in
978
1042
  { value: 'browser', name: '🌐 Browser Automation (Playwright) (⭐ Khuyên dùng)', checked: false, slug: null },
979
1043
  { value: 'memory', name: '🧠 Long-term Memory (⭐ Khuyên dùng)', checked: false, slug: 'memory' },
@@ -983,10 +1047,124 @@ const SKILLS = [
983
1047
  { value: 'code-interpreter', name: '💻 Code Interpreter (Python/JS)', checked: false, slug: 'code-interpreter' },
984
1048
  { value: 'email', name: '📧 Email Assistant', checked: false, slug: 'email-assistant' },
985
1049
  { value: 'tts', name: '🔊 Text-To-Speech (OpenAI/ElevenLabs)', checked: false, slug: 'tts' },
986
- ];
1050
+ ];
1051
+
1052
+ function providerSupportsMemoryEmbeddings(providerKey) {
1053
+ return !!PROVIDERS[providerKey]?.supportsEmbeddings;
1054
+ }
1055
+
1056
+ function getCliSkillChoices({ providerKey, isVi }) {
1057
+ return SKILLS.map((skill) => {
1058
+ if (skill.value !== 'memory') return { ...skill };
1059
+ const recommended = providerSupportsMemoryEmbeddings(providerKey);
1060
+ return {
1061
+ ...skill,
1062
+ name: recommended
1063
+ ? (isVi ? '🧠 Long-term Memory (⭐ Khuyên dùng)' : '🧠 Long-term Memory (⭐ Recommended)')
1064
+ : '🧠 Long-term Memory'
1065
+ };
1066
+ });
1067
+ }
1068
+
987
1069
 
1070
+
1071
+ // ─── Upgrade Mode ──────────────────────────────────────────────────────────
1072
+ // Usage: npx create-openclaw-bot@latest upgrade
1073
+ // Auto-detects Docker vs Native, updates OpenClaw, rebuilds/restarts.
1074
+ // Does NOT touch .env, memory, sessions, credentials.
1075
+ async function runUpgrade() {
1076
+ console.log(chalk.red('\n=================================='));
1077
+ console.log(chalk.redBright(LOGO));
1078
+ console.log(chalk.cyan(' 🔄 OpenClaw Upgrade Mode '));
1079
+ console.log(chalk.red('==================================\n'));
1080
+
1081
+ const projectDir = process.cwd();
1082
+ const dockerComposePath = path.join(projectDir, 'docker', 'openclaw', 'docker-compose.yml');
1083
+ const dockerfilePath = path.join(projectDir, 'docker', 'openclaw', 'Dockerfile');
1084
+ const dockerDir = path.join(projectDir, 'docker', 'openclaw');
1085
+ const nativeOpenClawDir = path.join(projectDir, '.openclaw');
1086
+ const dotEnvPath = path.join(projectDir, '.env');
1087
+
1088
+ const isDockerSetup = fs.existsSync(dockerComposePath);
1089
+ const isNativeSetup = fs.existsSync(nativeOpenClawDir) &&
1090
+ (fs.existsSync(dotEnvPath) || fs.existsSync(path.join(nativeOpenClawDir, 'openclaw.json')));
1091
+
1092
+ if (!isDockerSetup && !isNativeSetup) {
1093
+ console.log(chalk.red('\n❌ Khong tim thay project OpenClaw trong thu muc hien tai.'));
1094
+ console.log(chalk.gray(' Da kiem tra: ' + projectDir));
1095
+ console.log(chalk.yellow('\n💡 Di chuyen vao thu muc bot roi chay lai:'));
1096
+ console.log(chalk.white(' npx create-openclaw-bot@latest upgrade'));
1097
+ console.log(chalk.gray('\n Windows: double-click upgrade.ps1'));
1098
+ console.log(chalk.gray(' Linux/Mac: bash upgrade.sh'));
1099
+ process.exit(1);
1100
+ }
1101
+
1102
+ const mode = isDockerSetup ? 'docker' : 'native';
1103
+ const modeLabel = mode === 'docker' ? '🐳 Docker' : '⚡ Native / PM2';
1104
+ console.log(chalk.green('\n✅ Phat hien: ' + modeLabel));
1105
+ console.log(chalk.gray(' Project: ' + projectDir));
1106
+ console.log(chalk.cyan('\n📦 Cap nhat len ' + OPENCLAW_NPM_SPEC + '...\n'));
1107
+
1108
+ if (mode === 'docker') {
1109
+ // Patch Dockerfile: update pinned openclaw version + force CACHEBUST
1110
+ if (fs.existsSync(dockerfilePath)) {
1111
+ let fc = fs.readFileSync(dockerfilePath, 'utf8');
1112
+ const patched = fc
1113
+ .replace(/npm install -g openclaw@\S+/g, 'npm install -g ' + OPENCLAW_NPM_SPEC)
1114
+ .replace(/ARG CACHEBUST=\d+/g, 'ARG CACHEBUST=' + Date.now());
1115
+ if (patched !== fc) {
1116
+ fs.writeFileSync(dockerfilePath, patched);
1117
+ console.log(chalk.green(' ✅ Dockerfile updated → ' + OPENCLAW_NPM_SPEC));
1118
+ } else {
1119
+ const refreshed = fc.replace(/ARG CACHEBUST=\d+/g, 'ARG CACHEBUST=' + Date.now());
1120
+ if (refreshed !== fc) fs.writeFileSync(dockerfilePath, refreshed);
1121
+ console.log(chalk.gray(' ℹ️ Dockerfile da o ' + OPENCLAW_NPM_SPEC + ', refresh CACHEBUST'));
1122
+ }
1123
+ }
1124
+ console.log(chalk.cyan('\n🐳 Dang rebuild container...'));
1125
+ try {
1126
+ execSync('docker compose build --no-cache', { cwd: dockerDir, stdio: 'inherit', shell: true });
1127
+ execSync('docker compose up -d', { cwd: dockerDir, stdio: 'inherit', shell: true });
1128
+ console.log(chalk.green('\n✅ Upgrade hoan tat! Bot dang chay voi phien ban moi.'));
1129
+ } catch {
1130
+ console.log(chalk.red('\n❌ Loi Docker. Chay thu cong:'));
1131
+ console.log(chalk.white(' cd "' + dockerDir + '"'));
1132
+ console.log(chalk.white(' docker compose build --no-cache && docker compose up -d'));
1133
+ process.exit(1);
1134
+ }
1135
+ }
1136
+
1137
+ if (mode === 'native') {
1138
+ const osChoice = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'vps';
1139
+ const installed = installGlobalPackage(OPENCLAW_NPM_SPEC, { isVi: true, osChoice, displayName: 'openclaw' });
1140
+ if (installed) console.log(chalk.green(' ✅ openclaw → ' + OPENCLAW_NPM_SPEC));
1141
+ try {
1142
+ execSync('npm install -g 9router', { stdio: 'ignore', shell: true, env: process.env });
1143
+ console.log(chalk.green(' ✅ 9router → latest'));
1144
+ } catch { console.log(chalk.gray(' ℹ️ 9router update skipped')); }
1145
+ console.log(chalk.cyan('\n♻️ Restarting PM2...'));
1146
+ try {
1147
+ execSync('pm2 restart all', { stdio: 'inherit', shell: true });
1148
+ console.log(chalk.green('\n✅ Upgrade hoan tat! PM2 da duoc restart.'));
1149
+ } catch {
1150
+ console.log(chalk.yellow('\n⚠️ Khong the tu restart PM2. Chay thu cong: pm2 restart all'));
1151
+ }
1152
+ }
1153
+
1154
+ const gatewayUrls = getReachableDashboardHosts(18791);
1155
+ console.log(chalk.yellow('\n🧭 Dashboard: ' + (gatewayUrls[0] || 'http://localhost:18791')));
1156
+ if (gatewayUrls.length > 1) console.log(chalk.gray(' Hoac: ' + gatewayUrls.slice(1).join(' , ')));
1157
+ console.log(chalk.gray('\n 💡 Data cu (memory, sessions, 9Router OAuth) duoc giu nguyen.'));
1158
+ console.log(chalk.gray(' 💡 De thay doi config bot, mo lai index.html hoac chay npx create-openclaw-bot\n'));
1159
+ }
1160
+
1161
+ async function main() {
1162
+ // Upgrade subcommand: npx create-openclaw-bot@latest upgrade
1163
+ if (process.argv[2] === 'upgrade' || process.argv.includes('--upgrade')) {
1164
+ await runUpgrade();
1165
+ return;
1166
+ }
988
1167
 
989
- async function main() {
990
1168
  console.log(chalk.red('\n=================================='));
991
1169
  console.log(chalk.redBright(LOGO));
992
1170
  console.log(chalk.greenBright(' OpenClaw Auto Setup CLI '));
@@ -1238,10 +1416,10 @@ async function main() {
1238
1416
  }
1239
1417
 
1240
1418
  // 4. Skills
1241
- const selectedSkills = await checkbox({
1242
- message: isVi ? 'Bật tính năng bổ sung (Space để chọn):' : 'Enable extra skills (Space to select):',
1243
- choices: SKILLS
1244
- });
1419
+ const selectedSkills = await checkbox({
1420
+ message: isVi ? 'Bật tính năng bổ sung (Space để chọn):' : 'Enable extra skills (Space to select):',
1421
+ choices: getCliSkillChoices({ providerKey, isVi })
1422
+ });
1245
1423
 
1246
1424
  let tavilyKey = '';
1247
1425
  // (web-search removed — native search built-in)
@@ -1371,7 +1549,7 @@ async function main() {
1371
1549
  }
1372
1550
 
1373
1551
 
1374
- const patchScript = `const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://' + entry.address + ':18791');}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'custom',customBindHost:'0.0.0.0',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
1552
+ const patchScript = `const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://' + entry.address + ':18791');}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'loopback',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});delete c.gateway.customBindHost;fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
1375
1553
  const b64Patch = Buffer.from(patchScript).toString('base64');
1376
1554
 
1377
1555
  // Browser Playwright (both desktop & server modes need chromium)
@@ -1384,8 +1562,9 @@ async function main() {
1384
1562
  ].join('\n')
1385
1563
  : '';
1386
1564
  // socat only for Desktop mode (bridge to host Chrome)
1387
- const socatApt = hasBrowserDesktop ? ' socat' : '';
1388
- const socatBridge = hasBrowserDesktop ? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & ' : '';
1565
+ const socatApt = ' socat';
1566
+ const socatBridge = hasBrowserDesktop ? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & ' : '';
1567
+ const gatewayBridge = 'socat TCP-LISTEN:18791,fork,reuseaddr TCP:127.0.0.1:18791 & ';
1389
1568
 
1390
1569
  // Skills install at RUNTIME (not build-time — requires openclaw config + ClawHub auth)
1391
1570
  const skillSlugs = SKILLS
@@ -1409,7 +1588,7 @@ async function main() {
1409
1588
  dockerfileLines.push(
1410
1589
  '',
1411
1590
  `ARG CACHEBUST=${Date.now()}`,
1412
- `RUN npm install -g ${OPENCLAW_NPM_SPEC} grammy`,
1591
+ `RUN npm install -g ${OPENCLAW_NPM_SPEC} ${OPENCLAW_RUNTIME_PACKAGES}`,
1413
1592
  '',
1414
1593
  '# Fix chat.send dropping resolved agent timeout into reply pipeline.',
1415
1594
  '# Without this, Telegram/WebChat paths fall back to an internal 300s default even when',
@@ -1420,7 +1599,7 @@ async function main() {
1420
1599
  '',
1421
1600
  'EXPOSE 18791',
1422
1601
  '',
1423
- `CMD sh -c "node -e \\"eval(Buffer.from('${b64Patch}','base64').toString())\\" && ${skillInstallCmd}${relayInstallCmd}${socatBridge}(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) & openclaw gateway run"`
1602
+ `CMD sh -c "node -e \\"eval(Buffer.from('${b64Patch}','base64').toString())\\" && ${skillInstallCmd}${relayInstallCmd}${socatBridge}${gatewayBridge}(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) & openclaw gateway run"`
1424
1603
  );
1425
1604
  const dockerfile = dockerfileLines.join('\n');
1426
1605
 
@@ -1766,20 +1945,20 @@ ${hasBrowserDesktop ? ` extra_hosts:
1766
1945
 
1767
1946
  const sharedConfig = {
1768
1947
  meta: { lastTouchedVersion: '2026.3.24' },
1769
- agents: {
1770
- defaults: {
1948
+ agents: {
1949
+ defaults: {
1771
1950
  model: { primary: modelsPrimary, fallbacks: [] },
1772
1951
  compaction: { mode: 'safeguard' },
1773
1952
  timeoutSeconds: provider.isLocal ? 900 : 120,
1774
1953
  ...(provider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
1775
1954
  },
1776
- list: agentMetas.map((meta) => ({
1777
- id: meta.agentId,
1778
- name: meta.name,
1779
- workspace: `/root/.openclaw/${meta.workspaceDir}`,
1780
- agentDir: `/root/.openclaw/agents/${meta.agentId}/agent`,
1781
- model: { primary: modelsPrimary, fallbacks: [] },
1782
- })),
1955
+ list: agentMetas.map((meta) => ({
1956
+ id: meta.agentId,
1957
+ name: meta.name,
1958
+ workspace: `${deployMode === 'native' ? getNativeOpenClawRootDir(projectDir) : '/root/.openclaw'}/${meta.workspaceDir}`,
1959
+ agentDir: `${deployMode === 'native' ? getNativeOpenClawRootDir(projectDir) : '/root/.openclaw'}/agents/${meta.agentId}/agent`,
1960
+ model: { primary: modelsPrimary, fallbacks: [] },
1961
+ })),
1783
1962
  },
1784
1963
  ...(providerKey === '9router' ? {
1785
1964
  models: {
@@ -1836,19 +2015,21 @@ ${hasBrowserDesktop ? ` extra_hosts:
1836
2015
  gateway: {
1837
2016
  port: 18791,
1838
2017
  mode: 'local',
1839
- bind: 'custom',
1840
- customBindHost: '0.0.0.0',
1841
- controlUi: {
1842
- allowedOrigins: getGatewayAllowedOrigins(18791),
1843
- },
1844
- auth: { mode: 'token', token: 'cli-dummy-token-xyz123' },
1845
- },
1846
- };
1847
- sharedConfig.plugins = {
1848
- entries: {
1849
- [TELEGRAM_RELAY_PLUGIN_ID]: { enabled: true },
1850
- },
1851
- };
2018
+ bind: 'loopback',
2019
+ controlUi: {
2020
+ allowedOrigins: getGatewayAllowedOrigins(18791),
2021
+ },
2022
+ auth: { mode: 'token', token: 'cli-dummy-token-xyz123' },
2023
+ },
2024
+ };
2025
+ sharedConfig.plugins = {
2026
+ entries: {
2027
+ [TELEGRAM_RELAY_PLUGIN_RUNTIME_ID]: { enabled: true },
2028
+ },
2029
+ };
2030
+ if (!selectedSkills.includes('memory')) {
2031
+ sharedConfig.plugins.slots = { ...(sharedConfig.plugins.slots || {}), memory: 'none' };
2032
+ }
1852
2033
 
1853
2034
  if (hasBrowserDesktop) {
1854
2035
  sharedConfig.browser = {
@@ -1876,22 +2057,22 @@ ${hasBrowserDesktop ? ` extra_hosts:
1876
2057
  ` name: '${botName || 'openclaw-multibot'}',`,
1877
2058
  ` script: 'openclaw',`,
1878
2059
  ` args: 'gateway run',`,
1879
- ` cwd: '${projectDir.replace(/\\/g, '/')}',`,
1880
- ` interpreter: 'none',`,
1881
- ` autorestart: true,`,
1882
- ` watch: false,`,
1883
- ` env: { NODE_ENV: 'production' }`,
1884
- ' },',
1885
- ' {',
2060
+ ` cwd: '${projectDir.replace(/\\/g, '/')}',`,
2061
+ ` interpreter: 'none',`,
2062
+ ` autorestart: true,`,
2063
+ ` watch: false,`,
2064
+ ` env: { NODE_ENV: 'production', OPENCLAW_HOME: '${path.join(projectDir, '.openclaw').replace(/\\/g, '/')}', DATA_DIR: '${path.join(projectDir, '.9router').replace(/\\/g, '/')}' }`,
2065
+ ' },',
2066
+ ' {',
1886
2067
  ` name: '${botName || 'openclaw-multibot'}-auto-approve',`,
1887
2068
  ` script: 'sh',`,
1888
2069
  ` args: '-c "while true; do npx --yes openclaw devices approve --latest 2>/dev/null || true; sleep 5; done"',`,
1889
- ` cwd: '${projectDir.replace(/\\/g, '/')}',`,
1890
- ` interpreter: 'none',`,
1891
- ` autorestart: true,`,
1892
- ` watch: false,`,
1893
- ` env: { NODE_ENV: 'production' }`,
1894
- ' }',
2070
+ ` cwd: '${projectDir.replace(/\\/g, '/')}',`,
2071
+ ` interpreter: 'none',`,
2072
+ ` autorestart: true,`,
2073
+ ` watch: false,`,
2074
+ ` env: { NODE_ENV: 'production', OPENCLAW_HOME: '${path.join(projectDir, '.openclaw').replace(/\\/g, '/')}', DATA_DIR: '${path.join(projectDir, '.9router').replace(/\\/g, '/')}' }`,
2075
+ ' }',
1895
2076
  ].join('\n');
1896
2077
  const ecosystemContent = [
1897
2078
  '// PM2 ecosystem — run: pm2 start ecosystem.config.js',
@@ -1956,7 +2137,7 @@ ${hasBrowserDesktop ? ` extra_hosts:
1956
2137
  const relayTargetNames = otherAgents.length ? otherAgents.map((peer) => `\`${peer.name}\``).join(', ') : '`bot khac`';
1957
2138
  const relayTargetIds = otherAgents.length ? otherAgents.map((peer) => `\`${peer.agentId}\``).join(', ') : '`agent-khac`';
1958
2139
  const agentsMd = `# ${isVi ? 'Huong dan van hanh' : 'Operating Manual'}\n\n## ${isVi ? 'Vai tro' : 'Role'}\n${isVi ? `Ban la **${meta.name}**, chuyen ve ${meta.desc}.` : `You are **${meta.name}**, focused on ${meta.desc}.`}\n\n## ${isVi ? 'Khi nao nen tra loi' : 'When To Reply'}\n- ${isVi ? `Coi user dang goi ban neu tin nhan co mot trong cac alias: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')}.` : `Treat the message as addressed to you when it includes one of your aliases: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')}.`}\n- ${isVi ? 'Neu user tag username Telegram cua ban thi luon tra loi.' : 'Always reply when your Telegram username is tagged.'}\n- ${isVi ? 'Reaction xac nhan se duoc gateway tu dong tha bang `👍` ngay khi nhan tin; khong can tu tha bang tay neu da thay ack.' : 'The gateway will auto-ack with `👍` as soon as a message arrives; do not manually duplicate the reaction if the ack already appeared.'}\n- ${isVi ? `Neu user dang goi ro bot khac ${relayTargetNames} thi khong cuop loi.` : `If the message is clearly calling another bot such as ${relayTargetNames}, do not hijack it.`}\n\n## ${isVi ? 'Phoi hop' : 'Coordination'}\n- ${isVi ? 'Dung `TEAM.md` lam nguon su that cho vai tro cua ca doi.' : 'Use `TEAM.md` as the source of truth for team roles.'}\n- ${isVi ? `Neu user bao ban hoi, chuyen viec, xin y kien, hoac phoi hop voi ${otherAgents.length ? otherAgents.map((peer) => peer.name).join(', ') : 'bot khac'}, hay dung agent-to-agent noi bo ngay trong turn hien tai.` : `If the user asks you to consult, delegate to, or coordinate with ${otherAgents.length ? otherAgents.map((peer) => peer.name).join(', ') : 'another bot'}, use internal agent-to-agent messaging in the same turn.`}\n- ${isVi ? 'Neu ban la bot mo loi, chi gui 1 cau mo dau ngan roi handoff ngay. Khong tu noi thay bot dich tru khi handoff that bai ro rang.' : 'If you are the caller bot, send only one short opener then hand off immediately. Do not speak for the target bot unless the handoff clearly fails.'}\n- ${isVi ? `Khi handoff, phai goi dung agent id ky thuat ${relayTargetIds}, khong dung ten hien thi.` : `When handing off, use the exact technical agent id ${relayTargetIds}, not the display name.`}\n- ${isVi ? 'Neu ban la bot dich nhan handoff, hay tra loi cong khai ngay trong cung Telegram chat/thread bang chinh account cua minh. Uu tien tra loi co `[[reply_to_current]]`; neu can, dung Telegram send/sendMessage action thay vi chi output thuong.' : 'If you are the target bot receiving a handoff, publish the real answer immediately into the same Telegram chat/thread from your own account. Prefer replying with `[[reply_to_current]]`; if needed, use the Telegram send/sendMessage action instead of plain assistant output.'}\n- ${isVi ? 'Khong bao user phai tag lai bot kia neu ban co the hoi noi bo duoc.' : 'Do not ask the user to tag the other bot again if you can consult internally.'}\n`;
1959
- const toolsMd = `# ${isVi ? 'Huong dan dung tool' : 'Tool Usage Guide'}\n\n${skillListStr}\n\n- ${isVi ? 'Tom tat ket qua tool thay vi dump raw output.' : 'Summarize tool output instead of dumping raw output.'}\n- ${isVi ? `Workspace cua ban la \`/root/.openclaw/${meta.workspaceDir}/\`.` : `Your workspace is \`/root/.openclaw/${meta.workspaceDir}/\`.`}\n- ${isVi ? 'Telegram da bat `ackReaction`, `replyToMode:first`, `actions.sendMessage`, va `actions.reactions`.' : 'Telegram is configured with `ackReaction`, `replyToMode:first`, `actions.sendMessage`, and `actions.reactions`.'}\n- ${isVi ? 'Khi can relay public bang account cua minh sau internal handoff, uu tien dung chinh outbound Telegram action thay vi tra loi mo ho.' : 'When you need to publish a public relay from your own account after an internal handoff, prefer the Telegram outbound action over an ambiguous plain-text reply.'}\n`;
2140
+ const toolsMd = `# ${isVi ? 'Huong dan dung tool' : 'Tool Usage Guide'}\n\n${skillListStr}\n\n- ${isVi ? 'Tom tat ket qua tool thay vi dump raw output.' : 'Summarize tool output instead of dumping raw output.'}\n- ${isVi ? `Workspace cua ban la \`${getGeneratedWorkspaceRoot(deployMode, projectDir)}/${meta.workspaceDir}/\`.` : `Your workspace is \`${getGeneratedWorkspaceRoot(deployMode, projectDir)}/${meta.workspaceDir}/\`.`}\n- ${isVi ? 'Telegram da bat `ackReaction`, `replyToMode:first`, `actions.sendMessage`, va `actions.reactions`.' : 'Telegram is configured with `ackReaction`, `replyToMode:first`, `actions.sendMessage`, and `actions.reactions`.'}\n- ${isVi ? 'Khi can relay public bang account cua minh sau internal handoff, uu tien dung chinh outbound Telegram action thay vi tra loi mo ho.' : 'When you need to publish a public relay from your own account after an internal handoff, prefer the Telegram outbound action over an ambiguous plain-text reply.'}\n`;
1960
2141
  const relayMd = isVi
1961
2142
  ? `# Telegram Relay Playbook\n\n## Muc tieu\n- Cho phep bot mo loi goi bot dich noi bo, sau do bot dich tra loi cong khai bang chinh account cua minh.\n\n## Protocol\n1. Bot mo loi gui 1 cau ngan xac nhan se hoi bot dich.\n2. Bot mo loi handoff noi bo bang dung agent id trong \`TEAM.md\`.\n3. Bot dich tra loi cong khai trong cung chat/thread hien tai.\n4. Neu thay \`[[reply_to_current]]\` hoac Telegram send/sendMessage action kha dung, uu tien dung de bam dung message goc.\n5. Neu handoff that bai ro rang, chi bot mo loi moi duoc fallback tom tat.\n`
1962
2143
  : `# Telegram Relay Playbook\n\n## Goal\n- Let the caller bot consult the target bot internally, then have the target bot publish the real answer with its own Telegram account.\n\n## Protocol\n1. The caller bot sends one short acknowledgement.\n2. The caller bot hands off internally using the exact agent id from \`TEAM.md\`.\n3. The target bot publishes the real answer into the same chat/thread.\n4. If \`[[reply_to_current]]\` or Telegram send/sendMessage is available, prefer it so the answer attaches to the original user turn.\n5. Only the caller bot may summarize as fallback when the handoff clearly fails.\n`;
@@ -2080,11 +2261,14 @@ ${hasBrowserDesktop ? ` extra_hosts:
2080
2261
  channels: {},
2081
2262
  tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
2082
2263
  gateway: {
2083
- port: 18791 + (isMultiBot ? bIndex : 0), mode: 'local', bind: 'custom', customBindHost: '0.0.0.0',
2084
- controlUi: { allowedOrigins: getGatewayAllowedOrigins(18791 + (isMultiBot ? bIndex : 0)) },
2085
- auth: { mode: 'token', token: 'cli-dummy-token-xyz123' }
2086
- }
2087
- };
2264
+ port: 18791 + (isMultiBot ? bIndex : 0), mode: 'local', bind: 'loopback',
2265
+ controlUi: { allowedOrigins: getGatewayAllowedOrigins(18791 + (isMultiBot ? bIndex : 0)) },
2266
+ auth: { mode: 'token', token: 'cli-dummy-token-xyz123' }
2267
+ }
2268
+ };
2269
+ if (!selectedSkills.includes('memory')) {
2270
+ botConfig.plugins = { ...(botConfig.plugins || {}), slots: { ...((botConfig.plugins && botConfig.plugins.slots) || {}), memory: 'none' } };
2271
+ }
2088
2272
 
2089
2273
  if (hasBrowserDesktop) {
2090
2274
  botConfig.browser = {
@@ -2140,8 +2324,8 @@ ${hasBrowserDesktop ? ` extra_hosts:
2140
2324
  const skillListStr = selectedSkillNamesForMd.length > 0 ? selectedSkillNamesForMd.join('\n') : isVi ? '- _(Chưa có skill nào)_' : '- _(No skills installed)_';
2141
2325
 
2142
2326
  const toolsMd = isVi
2143
- ? `# Hướng dẫn sử dụng Tools\n\n## Danh sách skills đã cài\n${skillListStr}\n\n## Nguyên tắc chung\n- Ưu tiên dùng tool/skill phù hợp thay vì tự suy đoán\n- Nếu tool trả về lỗi → thử lại 1 lần, sau đó báo user\n- Không chạy tool liên tục mà không có mục đích rõ ràng\n- Luôn tóm tắt kết quả tool cho user thay vì dump raw output\n\n## Quy ước\n- Web Search: chỉ dùng khi cần thông tin realtime hoặc user yêu cầu\n- Browser: chỉ mở trang khi user yêu cầu cụ thể\n- Memory: tự ghi nhớ thông vị tự nhiên, không cần user nhắc\n\n## ⏰ Cron / Lên lịch nhắc nhở\n- OpenClaw CÓ hỗ trợ tool hệ thống để chạy Cron Job.\n- Khi user yêu cầu tạo nhắc nhở / lệnh tự động định kỳ, bạn hãy TỰ ĐỘNG dùng tool hệ thống để tạo. **Tuyệt đối không** bắt user dùng crontab hay Task Scheduler chạy tay trên host.\n- Ghi chú lỗi: Không điền "current" vào thư mục Session khi thao tác tool. Bỏ qua việc tra cứu file docs nội bộ ('cron-jobs.mdx') — hãy tin tưởng khả năng sử dụng tool của bạn.\n\n## 📁 File & Workspace\n- Bot có thể đọc/ghi file trong thư mục workspace: \`/root/.openclaw/workspace/\`\n- Dùng để lưu notes, scripts, cấu hình tạm\n\n## 🛠️ Tool Error Handling\n- Retry tối đa 2 lần nếu tool lỗi network\n- Nếu vẫn lỗi: báo user kèm mô tả lỗi cụ thể và gợi ý workaround\n`
2144
- : `# Tool Usage Guide\n\n## Installed Skills\n${skillListStr}\n\n## General Principles\n- Prefer using the right tool/skill over guessing\n- If a tool returns an error → retry once, then report to user\n- Don't run tools repeatedly without a clear purpose\n- Always summarize tool output for user instead of dumping raw data\n\n## Conventions\n- Web Search: only use when needing real-time info or user explicitly asks\n- Browser: only open pages when user specifically requests\n- Memory: proactively remember important info without user prompting\n\n## ⏰ Cron / Scheduled Tasks\n- OpenClaw natively supports system tools for Cron Jobs.\n- When the user asks to schedule tasks or reminders, use built-in tools automatically. Do NOT ask users to run manual crontab on the host.\n- Do NOT use "current" as a sessionKey for session tools.\n\n## 📁 File & Workspace\n- Bot can read/write files in workspace: \`/root/.openclaw/workspace/\`\n\n## 🛠️ Tool Error Handling\n- Retry up to 2 times on network errors\n- If still failing: report to user with specific error description and workaround\n`;
2327
+ ? `# Hướng dẫn sử dụng Tools\n\n## Danh sách skills đã cài\n${skillListStr}\n\n## Nguyên tắc chung\n- Ưu tiên dùng tool/skill phù hợp thay vì tự suy đoán\n- Nếu tool trả về lỗi → thử lại 1 lần, sau đó báo user\n- Không chạy tool liên tục mà không có mục đích rõ ràng\n- Luôn tóm tắt kết quả tool cho user thay vì dump raw output\n\n## Quy ước\n- Web Search: chỉ dùng khi cần thông tin realtime hoặc user yêu cầu\n- Browser: chỉ mở trang khi user yêu cầu cụ thể\n- Memory: tự ghi nhớ thông vị tự nhiên, không cần user nhắc\n\n## ⏰ Cron / Lên lịch nhắc nhở\n- OpenClaw CÓ hỗ trợ tool hệ thống để chạy Cron Job.\n- Khi user yêu cầu tạo nhắc nhở / lệnh tự động định kỳ, bạn hãy TỰ ĐỘNG dùng tool hệ thống để tạo. **Tuyệt đối không** bắt user dùng crontab hay Task Scheduler chạy tay trên host.\n- Ghi chú lỗi: Không điền "current" vào thư mục Session khi thao tác tool. Bỏ qua việc tra cứu file docs nội bộ ('cron-jobs.mdx') — hãy tin tưởng khả năng sử dụng tool của bạn.\n\n## 📁 File & Workspace\n- Bot có thể đọc/ghi file trong thư mục workspace: \`${getGeneratedWorkspaceRoot(deployMode, projectDir)}/workspace/\`\n- Dùng để lưu notes, scripts, cấu hình tạm\n\n## 🛠️ Tool Error Handling\n- Retry tối đa 2 lần nếu tool lỗi network\n- Nếu vẫn lỗi: báo user kèm mô tả lỗi cụ thể và gợi ý workaround\n`
2328
+ : `# Tool Usage Guide\n\n## Installed Skills\n${skillListStr}\n\n## General Principles\n- Prefer using the right tool/skill over guessing\n- If a tool returns an error → retry once, then report to user\n- Don't run tools repeatedly without a clear purpose\n- Always summarize tool output for user instead of dumping raw data\n\n## Conventions\n- Web Search: only use when needing real-time info or user explicitly asks\n- Browser: only open pages when user specifically requests\n- Memory: proactively remember important info without user prompting\n\n## ⏰ Cron / Scheduled Tasks\n- OpenClaw natively supports system tools for Cron Jobs.\n- When the user asks to schedule tasks or reminders, use built-in tools automatically. Do NOT ask users to run manual crontab on the host.\n- Do NOT use "current" as a sessionKey for session tools.\n\n## 📁 File & Workspace\n- Bot can read/write files in workspace: \`${getGeneratedWorkspaceRoot(deployMode, projectDir)}/workspace/\`\n\n## 🛠️ Tool Error Handling\n- Retry up to 2 times on network errors\n- If still failing: report to user with specific error description and workaround\n`;
2145
2329
 
2146
2330
  const memoryMd = `# ${isVi ? 'Bộ nhớ dài hạn' : 'Long-term Memory'}\n\n> File này lưu những điều quan trọng cần nhớ xuyên suốt các phiên hội thoại.\n\n## Ghi chú\n- _(Chưa có gì)_\n\n---`;
2147
2331
 
@@ -2210,7 +2394,7 @@ const { chromium } = require('playwright');
2210
2394
  })();
2211
2395
  `;
2212
2396
  await fs.writeFile(path.join(loopBotDir, '.openclaw', 'workspace', 'browser-tool.js'), browserToolJs);
2213
- const browserMd = `# Browser Automation (Desktop Mode)\n\nBot controls your actual Chrome on screen. Every action is visible!\n\n## Usage\n\`\`\`bash\nnode /root/.openclaw/workspace/browser-tool.js status\nnode /root/.openclaw/workspace/browser-tool.js open "https://google.com"\nnode /root/.openclaw/workspace/browser-tool.js get_text\nnode /root/.openclaw/workspace/browser-tool.js fill "input[name='q']" "search"\nnode /root/.openclaw/workspace/browser-tool.js press "Enter"\n\`\`\`\n\n## MANDATORY RULES\n- NEVER refuse to open the browser when user asks.\n- If ECONNREFUSED: tell user to run start-chrome-debug.bat first.\n`;
2397
+ const browserMd = `# Browser Automation (Desktop Mode)\n\nBot controls your actual Chrome on screen. Every action is visible!\n\n## Usage\n\`\`\`bash\nnode ${getGeneratedWorkspaceRoot(deployMode, projectDir)}/workspace/browser-tool.js status\nnode ${getGeneratedWorkspaceRoot(deployMode, projectDir)}/workspace/browser-tool.js open "https://google.com"\nnode ${getGeneratedWorkspaceRoot(deployMode, projectDir)}/workspace/browser-tool.js get_text\nnode ${getGeneratedWorkspaceRoot(deployMode, projectDir)}/workspace/browser-tool.js fill "input[name='q']" "search"\nnode ${getGeneratedWorkspaceRoot(deployMode, projectDir)}/workspace/browser-tool.js press "Enter"\n\`\`\`\n\n## MANDATORY RULES\n- NEVER refuse to open the browser when user asks.\n- If ECONNREFUSED: tell user to run start-chrome-debug.bat first.\n`;
2214
2398
  await fs.writeFile(path.join(loopBotDir, '.openclaw', 'workspace', 'BROWSER.md'), browserMd);
2215
2399
  } else if (hasBrowserServer) {
2216
2400
  const browserServerMd = `# Browser Automation (Headless Server Mode)\n\nBot uses a headless Chromium instance running inside the Docker container. No GUI needed!\n\n## Notes\n- Running on Ubuntu Server / VPS (no GUI required)\n- Uses Playwright + Headless Chromium installed inside Docker\n- For Cloudflare bypass, switch to Desktop mode (requires Windows/Mac with Chrome)\n`;
@@ -2394,24 +2578,7 @@ fi
2394
2578
  }
2395
2579
  }
2396
2580
 
2397
- // ── Auto-sync generated config to ~/.openclaw so `openclaw` picks it up ──
2398
-
2399
- const homedir = os.homedir();
2400
- const globalClawDir = path.join(homedir, '.openclaw');
2401
- const localClawDir = path.join(projectDir, '.openclaw');
2402
- try {
2403
- await fs.ensureDir(globalClawDir);
2404
- await fs.copy(localClawDir, globalClawDir, { overwrite: true });
2405
- console.log(chalk.green(`\n✅ ${isVi
2406
- ? `Config đã được sync vào ~/.openclaw/ — openclaw sẵn sàng!`
2407
- : `Config synced to ~/.openclaw/ — openclaw is ready!`}`));
2408
- } catch (syncErr) {
2409
- console.log(chalk.yellow(`\n⚠️ ${isVi
2410
- ? `Không thể tự sync config. Chạy thủ công:\n cp -rn ${localClawDir}/. ${globalClawDir}/`
2411
- : `Could not auto-sync config. Run manually:\n cp -rn ${localClawDir}/. ${globalClawDir}/`}`));
2412
- }
2413
-
2414
- if (isMultiBot && channelKey === 'telegram') {
2581
+ if (isMultiBot && channelKey === 'telegram') {
2415
2582
  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')}`));
2416
2583
  }
2417
2584
  } else {
@@ -2446,7 +2613,7 @@ fi
2446
2613
  native9RouterSyncScriptPath = await writeNative9RouterSyncScript(projectDir);
2447
2614
  }
2448
2615
 
2449
- await syncLocalConfigToHome(projectDir, isVi);
2616
+ await ensureProjectRuntimeDirs(projectDir, isVi);
2450
2617
 
2451
2618
  if (isMultiBot && channelKey === 'telegram') {
2452
2619
  installRelayPluginForProject(projectDir, isVi);
@@ -2464,11 +2631,12 @@ fi
2464
2631
  if (providerKey === '9router') {
2465
2632
  startNative9RouterPm2({ isVi, projectDir, appName: botName || 'openclaw-multibot', syncScriptPath: native9RouterSyncScriptPath });
2466
2633
  }
2467
- execSync('pm2 start ecosystem.config.js && pm2 save', {
2468
- cwd: projectDir,
2469
- stdio: 'inherit',
2470
- shell: true
2471
- });
2634
+ execSync('pm2 start ecosystem.config.js && pm2 save', {
2635
+ cwd: projectDir,
2636
+ stdio: 'inherit',
2637
+ shell: true,
2638
+ env: getProjectRuntimeEnv(projectDir)
2639
+ });
2472
2640
  console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoan tat! Multi-bot native dang chay qua PM2.' : 'Setup complete! Native multi-bot is running via PM2.'}`));
2473
2641
  console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${botName || 'openclaw-multibot'}` : ` View logs: pm2 logs ${botName || 'openclaw-multibot'}`));
2474
2642
  printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
@@ -2483,11 +2651,24 @@ fi
2483
2651
  if (channelKey === 'zalo-personal') {
2484
2652
  await runNativeZaloPersonalLoginFlow({ isVi, projectDir });
2485
2653
  }
2486
- execSync(`pm2 start "openclaw gateway run" --name "${appName}" --cwd "${projectDir.replace(/\\/g, '/')}" && pm2 save`, {
2487
- cwd: projectDir,
2488
- stdio: 'inherit',
2489
- shell: true
2490
- });
2654
+ execFileSync('pm2', [
2655
+ 'start',
2656
+ 'openclaw',
2657
+ '--name',
2658
+ appName,
2659
+ '--cwd',
2660
+ projectDir.replace(/\\/g, '/'),
2661
+ '--interpreter',
2662
+ 'none',
2663
+ '--',
2664
+ 'gateway',
2665
+ 'run'
2666
+ ], {
2667
+ cwd: projectDir,
2668
+ stdio: 'inherit',
2669
+ env: getProjectRuntimeEnv(projectDir)
2670
+ });
2671
+ runPm2Save({ projectDir, isVi });
2491
2672
  console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoan tat! Bot native dang chay qua PM2.' : 'Setup complete! Native bot is running via PM2.'}`));
2492
2673
  console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${appName}` : ` View logs: pm2 logs ${appName}`));
2493
2674
  printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
@@ -2499,16 +2680,17 @@ fi
2499
2680
  if (providerKey === '9router') {
2500
2681
  console.log(chalk.yellow(`\n${isVi ? 'Khoi dong 9Router native (background)...' : 'Starting native 9Router (background)...'}`));
2501
2682
  const native9RouterLaunch = resolveNative9RouterDesktopLaunch();
2502
- spawnBackgroundProcess(native9RouterLaunch.command, native9RouterLaunch.args, {
2503
- cwd: projectDir,
2504
- env: native9RouterLaunch.env
2505
- }).unref();
2683
+ spawnBackgroundProcess(native9RouterLaunch.command, native9RouterLaunch.args, {
2684
+ cwd: projectDir,
2685
+ env: getProjectRuntimeEnv(projectDir, native9RouterLaunch.env)
2686
+ }).unref();
2506
2687
  const routerHealth = await waitFor9RouterApiReady();
2507
- if (native9RouterSyncScriptPath) {
2508
- spawnBackgroundProcess(process.execPath, [native9RouterSyncScriptPath], {
2509
- cwd: projectDir
2510
- }).unref();
2511
- }
2688
+ if (native9RouterSyncScriptPath) {
2689
+ spawnBackgroundProcess(process.execPath, [native9RouterSyncScriptPath], {
2690
+ cwd: projectDir,
2691
+ env: getProjectRuntimeEnv(projectDir)
2692
+ }).unref();
2693
+ }
2512
2694
  console.log(chalk.gray(isVi
2513
2695
  ? ' 9Router dashboard: http://localhost:20128/dashboard'
2514
2696
  : ' 9Router dashboard: http://localhost:20128/dashboard'));
@@ -2523,11 +2705,12 @@ fi
2523
2705
  }
2524
2706
  console.log(chalk.yellow(`\n${isVi ? 'Khoi dong native bot (foreground)...' : 'Starting native bot (foreground)...'}`));
2525
2707
  const isZaloPersonal = channelKey === 'zalo-personal';
2526
- const child = spawn('openclaw', ['gateway', 'run'], {
2527
- cwd: projectDir,
2528
- stdio: isZaloPersonal ? ['inherit', 'pipe', 'pipe'] : 'inherit',
2529
- shell: process.platform === 'win32'
2530
- });
2708
+ const child = spawn('openclaw', ['gateway', 'run'], {
2709
+ cwd: projectDir,
2710
+ stdio: isZaloPersonal ? ['inherit', 'pipe', 'pipe'] : 'inherit',
2711
+ shell: process.platform === 'win32',
2712
+ env: getProjectRuntimeEnv(projectDir),
2713
+ });
2531
2714
  if (isZaloPersonal) {
2532
2715
  let approvedPairingCode = null;
2533
2716
  const onGatewayChunk = (chunk, target) => {