create-openclaw-bot 5.1.13 → 5.1.15

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,10 +5,12 @@ 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';
10
- // Use plain npm package name — clawhub: protocol not supported in all OpenClaw versions
11
- const TELEGRAM_RELAY_PLUGIN_SPEC = TELEGRAM_RELAY_PLUGIN_ID;
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';
11
+ const OPENCLAW_NPM_SPEC = 'openclaw@2026.4.5';
12
+ // Use plain npm package name — clawhub: protocol not supported in all OpenClaw versions
13
+ const TELEGRAM_RELAY_PLUGIN_SPEC = TELEGRAM_RELAY_PLUGIN_PACKAGE;
12
14
 
13
15
  // Install command: only use clawhub: spec (published to ClawHub)
14
16
  function buildRelayPluginInstallCommand(prefix = 'openclaw') {
@@ -19,13 +21,17 @@ function buildRelayPluginInstallCommandWin(prefix = 'openclaw') {
19
21
  return `${prefix} plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC} || exit /b 0`;
20
22
  }
21
23
 
22
- function installRelayPluginForProject(projectDir, isVi) {
23
- try {
24
- execSync(`openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`, { cwd: projectDir, stdio: 'ignore' });
25
- return true;
26
- } catch {
27
- // silent fallback
28
- }
24
+ function installRelayPluginForProject(projectDir, isVi) {
25
+ try {
26
+ execSync(`openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`, {
27
+ cwd: projectDir,
28
+ stdio: 'ignore',
29
+ env: getProjectRuntimeEnv(projectDir),
30
+ });
31
+ return true;
32
+ } catch {
33
+ // silent fallback
34
+ }
29
35
  console.log(chalk.yellow(isVi
30
36
  ? `\n⚠️ Chua the tu dong cai plugin. Sau khi bot chay, chay thu cong:\n openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`
31
37
  : `\n⚠️ Could not auto-install plugin. After the bot starts, run manually:\n openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`));
@@ -113,19 +119,23 @@ function resolveWindowsCommand(command) {
113
119
  }
114
120
  }
115
121
 
116
- function spawnBackgroundProcess(command, args, options = {}) {
117
- const { cwd, env = {} } = options;
118
- const mergedEnv = { ...process.env, ...env };
119
-
120
- if (process.platform === 'win32') {
121
- const resolvedCommand = resolveWindowsCommand(command);
122
- const argList = args.map((arg) => quotePowerShellSingle(arg)).join(', ');
123
- const startProcessScript = [
124
- `$filePath = ${quotePowerShellSingle(resolvedCommand)}`,
125
- `$workingDir = ${quotePowerShellSingle(cwd || process.cwd())}`,
126
- `$argList = @(${argList})`,
127
- "Start-Process -WindowStyle Hidden -FilePath $filePath -WorkingDirectory $workingDir -ArgumentList $argList"
128
- ].join('; ');
122
+ function spawnBackgroundProcess(command, args, options = {}) {
123
+ const { cwd, env = {} } = options;
124
+ const mergedEnv = { ...process.env, ...env };
125
+
126
+ if (process.platform === 'win32') {
127
+ const resolvedCommand = resolveWindowsCommand(command);
128
+ const argList = args.map((arg) => quotePowerShellSingle(arg)).join(', ');
129
+ const envAssignments = Object.entries(env)
130
+ .map(([key, value]) => `$env:${key}=${quotePowerShellSingle(String(value))}`)
131
+ .join('; ');
132
+ const startProcessScript = [
133
+ `$filePath = ${quotePowerShellSingle(resolvedCommand)}`,
134
+ `$workingDir = ${quotePowerShellSingle(cwd || process.cwd())}`,
135
+ `$argList = @(${argList})`,
136
+ ...(envAssignments ? [envAssignments] : []),
137
+ "Start-Process -WindowStyle Hidden -FilePath $filePath -WorkingDirectory $workingDir -ArgumentList $argList"
138
+ ].join('; ');
129
139
 
130
140
  return spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', startProcessScript], {
131
141
  cwd,
@@ -145,26 +155,38 @@ function spawnBackgroundProcess(command, args, options = {}) {
145
155
  });
146
156
  }
147
157
 
148
- function resolveNative9RouterDesktopLaunch() {
149
- return {
150
- command: process.execPath,
151
- args: [path.join(getGlobalNpmRoot(), '9router', 'app', 'server.js')],
152
- env: {
153
- PORT: '20128',
154
- HOSTNAME: '0.0.0.0'
155
- }
156
- };
157
- }
158
-
159
- function getNative9RouterDataDir() {
160
- if (process.platform === 'win32') {
161
- return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), '9router');
162
- }
163
-
164
- return path.join(os.homedir(), '.9router');
165
- }
166
-
167
- function getGatewayAllowedOrigins(port) {
158
+ function resolveNative9RouterDesktopLaunch() {
159
+ const serverEntry = get9RouterServerEntryCandidates().find((candidate) => fs.existsSync(candidate))
160
+ || get9RouterServerEntryCandidates()[0];
161
+ return {
162
+ command: process.execPath,
163
+ args: [serverEntry],
164
+ env: {
165
+ PORT: '20128',
166
+ HOSTNAME: '0.0.0.0'
167
+ }
168
+ };
169
+ }
170
+
171
+ function getProjectOpenClawHome(projectDir) {
172
+ return path.join(projectDir, '.openclaw');
173
+ }
174
+
175
+ function getProject9RouterDataDir(projectDir) {
176
+ return path.join(projectDir, '.9router');
177
+ }
178
+
179
+ function getProjectRuntimeEnv(projectDir, extraEnv = {}) {
180
+ return {
181
+ ...process.env,
182
+ OPENCLAW_HOME: getProjectOpenClawHome(projectDir),
183
+ OPENCLAW_STATE_DIR: getProjectOpenClawHome(projectDir),
184
+ DATA_DIR: getProject9RouterDataDir(projectDir),
185
+ ...extraEnv,
186
+ };
187
+ }
188
+
189
+ function getGatewayAllowedOrigins(port) {
168
190
  const normalizedPort = Number(port) || 18791;
169
191
  const origins = new Set([
170
192
  `http://localhost:${normalizedPort}`,
@@ -345,11 +367,11 @@ function installLatestOpenClaw({ isVi, osChoice }) {
345
367
  return;
346
368
  }
347
369
 
348
- console.log(chalk.cyan(isVi
349
- ? '\n📦 Dang cai/cap nhat openclaw@latest...'
350
- : '\n📦 Installing/updating openclaw@latest...'));
351
-
352
- if (!installGlobalPackage('openclaw@latest', { isVi, osChoice, displayName: 'openclaw' })) {
370
+ console.log(chalk.cyan(isVi
371
+ ? `\n📦 Dang cai/cap nhat ${OPENCLAW_NPM_SPEC}...`
372
+ : `\n📦 Installing/updating ${OPENCLAW_NPM_SPEC}...`));
373
+
374
+ if (!installGlobalPackage(OPENCLAW_NPM_SPEC, { isVi, osChoice, displayName: 'openclaw' })) {
353
375
  process.exit(1);
354
376
  }
355
377
 
@@ -426,7 +448,7 @@ function resolveCommandOnPath(command) {
426
448
  }
427
449
  }
428
450
 
429
- function getGlobalNpmRoot() {
451
+ function getGlobalNpmRoot() {
430
452
  try {
431
453
  return execSync('npm root -g', {
432
454
  stdio: ['ignore', 'pipe', 'ignore'],
@@ -434,13 +456,57 @@ function getGlobalNpmRoot() {
434
456
  shell: true,
435
457
  env: process.env
436
458
  }).trim();
437
- } catch {
438
- if (process.platform === 'win32') {
439
- return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules');
440
- }
441
- return path.join(os.homedir(), '.local', 'lib', 'node_modules');
442
- }
443
- }
459
+ } catch {
460
+ if (process.platform === 'win32') {
461
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules');
462
+ }
463
+ return path.join(os.homedir(), '.local', 'share', 'npm', 'lib', 'node_modules');
464
+ }
465
+ }
466
+
467
+ function getNativeOpenClawRootDir(projectDir = '.') {
468
+ return path.join(projectDir, '.openclaw').replace(/\\/g, '/');
469
+ }
470
+
471
+ function getGeneratedWorkspaceRoot(deployMode, projectDir = '.') {
472
+ return deployMode === 'native' ? getNativeOpenClawRootDir(projectDir) : '/root/.openclaw';
473
+ }
474
+
475
+ function get9RouterServerEntryCandidates() {
476
+ const homeDir = os.homedir();
477
+ const npmRoots = [];
478
+
479
+ try {
480
+ const root = execSync('npm root -g', {
481
+ stdio: ['ignore', 'pipe', 'ignore'],
482
+ encoding: 'utf8',
483
+ shell: true,
484
+ env: process.env
485
+ }).trim();
486
+ if (root) npmRoots.push(root);
487
+ } catch {
488
+ // handled by fallback candidates below
489
+ }
490
+
491
+ const prefixes = [
492
+ process.env.npm_config_prefix,
493
+ process.env.NPM_CONFIG_PREFIX,
494
+ process.env.PREFIX,
495
+ process.env.NPM_PREFIX,
496
+ path.join(homeDir, '.local'),
497
+ path.join(homeDir, '.npm-global'),
498
+ path.join(homeDir, '.local', 'share', 'npm')
499
+ ].filter(Boolean);
500
+
501
+ for (const prefix of prefixes) {
502
+ npmRoots.push(path.join(prefix, 'lib', 'node_modules'));
503
+ }
504
+
505
+ npmRoots.push(path.join(homeDir, '.local', 'lib', 'node_modules'));
506
+ npmRoots.push(getGlobalNpmRoot());
507
+
508
+ return [...new Set(npmRoots.map((root) => path.join(root, '9router', 'app', 'server.js')))];
509
+ }
444
510
 
445
511
  function indentBlock(text, spaces) {
446
512
  const prefix = ' '.repeat(spaces);
@@ -459,26 +525,26 @@ function build9RouterComposeEntrypointScript(syncScriptBase64) {
459
525
  ].join('\n');
460
526
  }
461
527
 
462
- async function writeNative9RouterSyncScript(projectDir) {
463
- const syncScriptPath = path.join(projectDir, '.openclaw', '9router-smart-route-sync.js');
464
- await fs.ensureDir(path.dirname(syncScriptPath));
465
- await fs.writeFile(syncScriptPath, build9RouterSmartRouteSyncScript(path.join(getNative9RouterDataDir(), 'db.json')));
466
- return syncScriptPath;
467
- }
528
+ async function writeNative9RouterSyncScript(projectDir) {
529
+ const syncScriptPath = path.join(projectDir, '.openclaw', '9router-smart-route-sync.js');
530
+ await fs.ensureDir(path.dirname(syncScriptPath));
531
+ await fs.writeFile(syncScriptPath, build9RouterSmartRouteSyncScript(path.join(getProject9RouterDataDir(projectDir), 'db.json')));
532
+ return syncScriptPath;
533
+ }
468
534
 
469
535
  function extractFirstHttpUrl(text) {
470
536
  const match = String(text || '').match(/https?:\/\/[^\s"'`]+/);
471
537
  return match ? match[0] : null;
472
538
  }
473
539
 
474
- function getTokenizedDashboardUrl(projectDir) {
475
- try {
476
- const output = execSync('openclaw dashboard', {
477
- cwd: projectDir,
478
- env: process.env,
479
- encoding: 'utf8',
480
- shell: true,
481
- stdio: ['ignore', 'pipe', 'pipe'],
540
+ function getTokenizedDashboardUrl(projectDir) {
541
+ try {
542
+ const output = execSync('openclaw dashboard', {
543
+ cwd: projectDir,
544
+ env: getProjectRuntimeEnv(projectDir),
545
+ encoding: 'utf8',
546
+ shell: true,
547
+ stdio: ['ignore', 'pipe', 'pipe'],
482
548
  timeout: 15000
483
549
  });
484
550
  return extractFirstHttpUrl(output);
@@ -606,14 +672,14 @@ function extractZaloPairingCode(text) {
606
672
  return null;
607
673
  }
608
674
 
609
- function approveZaloPairingCode({ pairingCode, projectDir, isVi }) {
610
- try {
611
- execSync(`openclaw pairing approve zalouser ${pairingCode}`, {
612
- cwd: projectDir,
613
- stdio: 'inherit',
614
- shell: true,
615
- env: process.env
616
- });
675
+ function approveZaloPairingCode({ pairingCode, projectDir, isVi }) {
676
+ try {
677
+ execSync(`openclaw pairing approve zalouser ${pairingCode}`, {
678
+ cwd: projectDir,
679
+ stdio: 'inherit',
680
+ shell: true,
681
+ env: getProjectRuntimeEnv(projectDir)
682
+ });
617
683
  console.log(chalk.green(isVi
618
684
  ? `✅ Da tu dong approve pairing code Zalo: ${pairingCode}`
619
685
  : `✅ Automatically approved the Zalo pairing code: ${pairingCode}`));
@@ -644,11 +710,12 @@ async function runNativeZaloPersonalLoginFlow({ isVi, projectDir }) {
644
710
  // ignore stale project QR cleanup failures
645
711
  }
646
712
 
647
- const child = spawn('openclaw', ['channels', 'login', '--channel', 'zalouser', '--verbose'], {
648
- cwd: projectDir,
649
- stdio: ['inherit', 'pipe', 'pipe'],
650
- shell: process.platform === 'win32'
651
- });
713
+ const child = spawn('openclaw', ['channels', 'login', '--channel', 'zalouser', '--verbose'], {
714
+ cwd: projectDir,
715
+ stdio: ['inherit', 'pipe', 'pipe'],
716
+ shell: process.platform === 'win32',
717
+ env: getProjectRuntimeEnv(projectDir),
718
+ });
652
719
 
653
720
  let loginSucceeded = false;
654
721
  let approvedPairingCode = null;
@@ -711,14 +778,14 @@ async function runNativeZaloPersonalLoginFlow({ isVi, projectDir }) {
711
778
  }
712
779
  }
713
780
 
714
- function runPm2Save({ projectDir, isVi }) {
715
- try {
716
- execSync('pm2 save', {
717
- cwd: projectDir,
718
- stdio: 'inherit',
719
- shell: true,
720
- env: process.env
721
- });
781
+ function runPm2Save({ projectDir, isVi }) {
782
+ try {
783
+ execSync('pm2 save', {
784
+ cwd: projectDir,
785
+ stdio: 'inherit',
786
+ shell: true,
787
+ env: getProjectRuntimeEnv(projectDir)
788
+ });
722
789
  } catch {
723
790
  console.log(chalk.yellow(isVi
724
791
  ? '⚠️ PM2 save khong hoan tat. Bot van co the dang chay, nhung hay thu chay lai `pm2 save` sau.'
@@ -726,9 +793,9 @@ function runPm2Save({ projectDir, isVi }) {
726
793
  }
727
794
  }
728
795
 
729
- function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
730
- const routerAppName = `${appName}-9router`;
731
- const routerLaunch = resolveNative9RouterDesktopLaunch();
796
+ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
797
+ const routerAppName = `${appName}-9router`;
798
+ const routerLaunch = resolveNative9RouterDesktopLaunch();
732
799
  execFileSync('pm2', [
733
800
  'start',
734
801
  routerLaunch.command,
@@ -740,11 +807,11 @@ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
740
807
  'none',
741
808
  '--',
742
809
  ...routerLaunch.args
743
- ], {
744
- cwd: projectDir,
745
- stdio: 'inherit',
746
- env: { ...process.env, ...routerLaunch.env }
747
- });
810
+ ], {
811
+ cwd: projectDir,
812
+ stdio: 'inherit',
813
+ env: getProjectRuntimeEnv(projectDir, routerLaunch.env)
814
+ });
748
815
  if (syncScriptPath) {
749
816
  const syncAppName = `${appName}-9router-sync`;
750
817
  const normalizedSyncScriptPath = syncScriptPath.replace(/\\/g, '/');
@@ -758,19 +825,19 @@ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
758
825
  projectDir.replace(/\\/g, '/'),
759
826
  '--interpreter',
760
827
  process.execPath
761
- ], {
762
- cwd: projectDir,
763
- stdio: 'inherit',
764
- env: process.env
765
- });
828
+ ], {
829
+ cwd: projectDir,
830
+ stdio: 'inherit',
831
+ env: getProjectRuntimeEnv(projectDir)
832
+ });
766
833
  } catch {
767
834
  try {
768
- execSync(`nohup "${process.execPath}" "${normalizedSyncScriptPath}" >/tmp/${syncAppName}.log 2>&1 &`, {
769
- cwd: projectDir,
770
- stdio: 'ignore',
771
- shell: true,
772
- env: process.env
773
- });
835
+ execSync(`nohup "${process.execPath}" "${normalizedSyncScriptPath}" >/tmp/${syncAppName}.log 2>&1 &`, {
836
+ cwd: projectDir,
837
+ stdio: 'ignore',
838
+ shell: true,
839
+ env: getProjectRuntimeEnv(projectDir)
840
+ });
774
841
  console.log(chalk.yellow(isVi
775
842
  ? `⚠️ PM2 khong khoi dong duoc sync helper. Da fallback sang background node: /tmp/${syncAppName}.log`
776
843
  : `⚠️ PM2 could not start the sync helper. Fell back to a background node process: /tmp/${syncAppName}.log`));
@@ -786,24 +853,21 @@ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
786
853
  console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${routerAppName}` : ` View logs: pm2 logs ${routerAppName}`));
787
854
  }
788
855
 
789
- async function syncLocalConfigToHome(projectDir, isVi) {
790
- const homedir = os.homedir();
791
- const globalClawDir = path.join(homedir, '.openclaw');
792
- const localClawDir = path.join(projectDir, '.openclaw');
793
- try {
794
- await fs.ensureDir(globalClawDir);
795
- await fs.copy(localClawDir, globalClawDir, { overwrite: true });
796
- console.log(chalk.green(`\n✅ ${isVi
797
- ? 'Config đã được sync vào ~/.openclaw/ — openclaw sẵn sàng!'
798
- : 'Config synced to ~/.openclaw/ — openclaw is ready!'}`));
799
- return true;
800
- } catch {
801
- console.log(chalk.yellow(`\n⚠️ ${isVi
802
- ? `Không thể tự sync config. Chạy thủ công:\n cp -rn ${localClawDir}/. ${globalClawDir}/`
803
- : `Could not auto-sync config. Run manually:\n cp -rn ${localClawDir}/. ${globalClawDir}/`}`));
804
- return false;
805
- }
806
- }
856
+ async function ensureProjectRuntimeDirs(projectDir, isVi) {
857
+ try {
858
+ await fs.ensureDir(getProjectOpenClawHome(projectDir));
859
+ await fs.ensureDir(getProject9RouterDataDir(projectDir));
860
+ console.log(chalk.green(`\n✅ ${isVi
861
+ ? 'Runtime project đã sẵn sàng trong thư mục đã chọn.'
862
+ : 'The project runtime directories are ready inside the chosen folder.'}`));
863
+ return true;
864
+ } catch {
865
+ console.log(chalk.yellow(`\n⚠️ ${isVi
866
+ ? `Không thể tạo runtime folders trong project. Hãy tự kiểm tra: ${projectDir}`
867
+ : `Could not create the runtime folders inside the project. Check: ${projectDir}`}`));
868
+ return false;
869
+ }
870
+ }
807
871
 
808
872
  function buildTelegramPostInstallChecklist({ isVi, bots, groupId }) {
809
873
  const botList = bots.map((bot, idx) => `- **${bot?.name || `Bot ${idx + 1}`}** — token: ${String(bot?.token || '').slice(0, 10)}...`).join('\n');
@@ -1370,7 +1434,7 @@ async function main() {
1370
1434
  }
1371
1435
 
1372
1436
 
1373
- 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)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
1437
+ 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));}`;
1374
1438
  const b64Patch = Buffer.from(patchScript).toString('base64');
1375
1439
 
1376
1440
  // Browser Playwright (both desktop & server modes need chromium)
@@ -1405,10 +1469,10 @@ async function main() {
1405
1469
 
1406
1470
  ];
1407
1471
  if (browserDockerLines) dockerfileLines.push(browserDockerLines);
1408
- dockerfileLines.push(
1472
+ dockerfileLines.push(
1409
1473
  '',
1410
1474
  `ARG CACHEBUST=${Date.now()}`,
1411
- 'RUN npm install -g openclaw@latest',
1475
+ `RUN npm install -g ${OPENCLAW_NPM_SPEC} grammy`,
1412
1476
  '',
1413
1477
  '# Fix chat.send dropping resolved agent timeout into reply pipeline.',
1414
1478
  '# Without this, Telegram/WebChat paths fall back to an internal 300s default even when',
@@ -1420,10 +1484,12 @@ async function main() {
1420
1484
  'EXPOSE 18791',
1421
1485
  '',
1422
1486
  `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"`
1423
- );
1424
- const dockerfile = dockerfileLines.join('\n');
1425
-
1426
- await fs.writeFile(path.join(projectDir, 'docker', 'openclaw', 'Dockerfile'), dockerfile);
1487
+ );
1488
+ const dockerfile = dockerfileLines.join('\n');
1489
+
1490
+ const dockerDir = path.join(projectDir, 'docker', 'openclaw');
1491
+ await fs.ensureDir(dockerDir);
1492
+ await fs.writeFile(path.join(dockerDir, 'Dockerfile'), dockerfile);
1427
1493
 
1428
1494
  // agentId no longer tightly coupled here, handled inside bot processes
1429
1495
  const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-$/, '') || 'chat';
@@ -1658,7 +1724,8 @@ ${hasBrowserDesktop ? ` extra_hosts:
1658
1724
  - ../../.openclaw:/root/.openclaw`;
1659
1725
  }
1660
1726
 
1661
- await fs.writeFile(path.join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'), compose);
1727
+ await fs.ensureDir(dockerDir);
1728
+ await fs.writeFile(path.join(dockerDir, 'docker-compose.yml'), compose);
1662
1729
 
1663
1730
  let authProfilesJson = {};
1664
1731
  if (provider.isLocal) {
@@ -1762,20 +1829,20 @@ ${hasBrowserDesktop ? ` extra_hosts:
1762
1829
 
1763
1830
  const sharedConfig = {
1764
1831
  meta: { lastTouchedVersion: '2026.3.24' },
1765
- agents: {
1766
- defaults: {
1832
+ agents: {
1833
+ defaults: {
1767
1834
  model: { primary: modelsPrimary, fallbacks: [] },
1768
1835
  compaction: { mode: 'safeguard' },
1769
1836
  timeoutSeconds: provider.isLocal ? 900 : 120,
1770
1837
  ...(provider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
1771
1838
  },
1772
- list: agentMetas.map((meta) => ({
1773
- id: meta.agentId,
1774
- name: meta.name,
1775
- workspace: `/root/.openclaw/${meta.workspaceDir}`,
1776
- agentDir: `/root/.openclaw/agents/${meta.agentId}/agent`,
1777
- model: { primary: modelsPrimary, fallbacks: [] },
1778
- })),
1839
+ list: agentMetas.map((meta) => ({
1840
+ id: meta.agentId,
1841
+ name: meta.name,
1842
+ workspace: `${deployMode === 'native' ? getNativeOpenClawRootDir(projectDir) : '/root/.openclaw'}/${meta.workspaceDir}`,
1843
+ agentDir: `${deployMode === 'native' ? getNativeOpenClawRootDir(projectDir) : '/root/.openclaw'}/agents/${meta.agentId}/agent`,
1844
+ model: { primary: modelsPrimary, fallbacks: [] },
1845
+ })),
1779
1846
  },
1780
1847
  ...(providerKey === '9router' ? {
1781
1848
  models: {
@@ -1839,12 +1906,12 @@ ${hasBrowserDesktop ? ` extra_hosts:
1839
1906
  },
1840
1907
  auth: { mode: 'token', token: 'cli-dummy-token-xyz123' },
1841
1908
  },
1842
- };
1843
- sharedConfig.plugins = {
1844
- entries: {
1845
- [TELEGRAM_RELAY_PLUGIN_ID]: { enabled: true },
1846
- },
1847
- };
1909
+ };
1910
+ sharedConfig.plugins = {
1911
+ entries: {
1912
+ [TELEGRAM_RELAY_PLUGIN_RUNTIME_ID]: { enabled: true },
1913
+ },
1914
+ };
1848
1915
 
1849
1916
  if (hasBrowserDesktop) {
1850
1917
  sharedConfig.browser = {
@@ -1872,22 +1939,22 @@ ${hasBrowserDesktop ? ` extra_hosts:
1872
1939
  ` name: '${botName || 'openclaw-multibot'}',`,
1873
1940
  ` script: 'openclaw',`,
1874
1941
  ` args: 'gateway run',`,
1875
- ` cwd: '${projectDir.replace(/\\/g, '/')}',`,
1876
- ` interpreter: 'none',`,
1877
- ` autorestart: true,`,
1878
- ` watch: false,`,
1879
- ` env: { NODE_ENV: 'production' }`,
1880
- ' },',
1881
- ' {',
1942
+ ` cwd: '${projectDir.replace(/\\/g, '/')}',`,
1943
+ ` interpreter: 'none',`,
1944
+ ` autorestart: true,`,
1945
+ ` watch: false,`,
1946
+ ` env: { NODE_ENV: 'production', OPENCLAW_HOME: '${path.join(projectDir, '.openclaw').replace(/\\/g, '/')}', DATA_DIR: '${path.join(projectDir, '.9router').replace(/\\/g, '/')}' }`,
1947
+ ' },',
1948
+ ' {',
1882
1949
  ` name: '${botName || 'openclaw-multibot'}-auto-approve',`,
1883
1950
  ` script: 'sh',`,
1884
1951
  ` args: '-c "while true; do npx --yes openclaw devices approve --latest 2>/dev/null || true; sleep 5; done"',`,
1885
- ` cwd: '${projectDir.replace(/\\/g, '/')}',`,
1886
- ` interpreter: 'none',`,
1887
- ` autorestart: true,`,
1888
- ` watch: false,`,
1889
- ` env: { NODE_ENV: 'production' }`,
1890
- ' }',
1952
+ ` cwd: '${projectDir.replace(/\\/g, '/')}',`,
1953
+ ` interpreter: 'none',`,
1954
+ ` autorestart: true,`,
1955
+ ` watch: false,`,
1956
+ ` env: { NODE_ENV: 'production', OPENCLAW_HOME: '${path.join(projectDir, '.openclaw').replace(/\\/g, '/')}', DATA_DIR: '${path.join(projectDir, '.9router').replace(/\\/g, '/')}' }`,
1957
+ ' }',
1891
1958
  ].join('\n');
1892
1959
  const ecosystemContent = [
1893
1960
  '// PM2 ecosystem — run: pm2 start ecosystem.config.js',
@@ -1952,7 +2019,7 @@ ${hasBrowserDesktop ? ` extra_hosts:
1952
2019
  const relayTargetNames = otherAgents.length ? otherAgents.map((peer) => `\`${peer.name}\``).join(', ') : '`bot khac`';
1953
2020
  const relayTargetIds = otherAgents.length ? otherAgents.map((peer) => `\`${peer.agentId}\``).join(', ') : '`agent-khac`';
1954
2021
  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`;
1955
- 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`;
2022
+ 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`;
1956
2023
  const relayMd = isVi
1957
2024
  ? `# 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`
1958
2025
  : `# 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`;
@@ -2136,8 +2203,8 @@ ${hasBrowserDesktop ? ` extra_hosts:
2136
2203
  const skillListStr = selectedSkillNamesForMd.length > 0 ? selectedSkillNamesForMd.join('\n') : isVi ? '- _(Chưa có skill nào)_' : '- _(No skills installed)_';
2137
2204
 
2138
2205
  const toolsMd = isVi
2139
- ? `# 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`
2140
- : `# 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`;
2206
+ ? `# 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`
2207
+ : `# 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`;
2141
2208
 
2142
2209
  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---`;
2143
2210
 
@@ -2206,7 +2273,7 @@ const { chromium } = require('playwright');
2206
2273
  })();
2207
2274
  `;
2208
2275
  await fs.writeFile(path.join(loopBotDir, '.openclaw', 'workspace', 'browser-tool.js'), browserToolJs);
2209
- 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`;
2276
+ 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`;
2210
2277
  await fs.writeFile(path.join(loopBotDir, '.openclaw', 'workspace', 'BROWSER.md'), browserMd);
2211
2278
  } else if (hasBrowserServer) {
2212
2279
  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`;
@@ -2378,44 +2445,27 @@ fi
2378
2445
  const isOpenClawInstalled = () => { try { execSync('openclaw --version', { stdio: 'ignore' }); return true; } catch { return false; } };
2379
2446
  if (!isOpenClawInstalled()) {
2380
2447
  console.log(chalk.cyan(isVi
2381
- ? '\n📦 Đang cài openclaw binary (npm install -g openclaw)...'
2382
- : '\n📦 Installing openclaw binary (npm install -g openclaw)...'));
2383
- try {
2384
- execSync('npm install -g openclaw', { stdio: 'inherit' });
2385
- console.log(chalk.green(isVi ? '✅ openclaw đã cài xong!' : '✅ openclaw installed!'));
2386
- } catch {
2387
- console.log(chalk.yellow(isVi
2388
- ? '⚠️ Không tự cài được. Chạy thủ công: sudo npm install -g openclaw'
2389
- : '⚠️ Could not auto-install. Run manually: sudo npm install -g openclaw'));
2448
+ ? `\n📦 Đang cài openclaw binary (npm install -g ${OPENCLAW_NPM_SPEC})...`
2449
+ : `\n📦 Installing openclaw binary (npm install -g ${OPENCLAW_NPM_SPEC})...`));
2450
+ try {
2451
+ execSync(`npm install -g ${OPENCLAW_NPM_SPEC}`, { stdio: 'inherit' });
2452
+ console.log(chalk.green(isVi ? '✅ openclaw đã cài xong!' : '✅ openclaw installed!'));
2453
+ } catch {
2454
+ console.log(chalk.yellow(isVi
2455
+ ? `⚠️ Không tự cài được. Chạy thủ công: sudo npm install -g ${OPENCLAW_NPM_SPEC}`
2456
+ : `⚠️ Could not auto-install. Run manually: sudo npm install -g ${OPENCLAW_NPM_SPEC}`));
2390
2457
  }
2391
2458
  }
2392
2459
 
2393
- // ── Auto-sync generated config to ~/.openclaw so `openclaw` picks it up ──
2394
-
2395
- const homedir = os.homedir();
2396
- const globalClawDir = path.join(homedir, '.openclaw');
2397
- const localClawDir = path.join(projectDir, '.openclaw');
2398
- try {
2399
- await fs.ensureDir(globalClawDir);
2400
- await fs.copy(localClawDir, globalClawDir, { overwrite: true });
2401
- console.log(chalk.green(`\n✅ ${isVi
2402
- ? `Config đã được sync vào ~/.openclaw/ — openclaw sẵn sàng!`
2403
- : `Config synced to ~/.openclaw/ — openclaw is ready!`}`));
2404
- } catch (syncErr) {
2405
- console.log(chalk.yellow(`\n⚠️ ${isVi
2406
- ? `Không thể tự sync config. Chạy thủ công:\n cp -rn ${localClawDir}/. ${globalClawDir}/`
2407
- : `Could not auto-sync config. Run manually:\n cp -rn ${localClawDir}/. ${globalClawDir}/`}`));
2408
- }
2409
-
2410
- if (isMultiBot && channelKey === 'telegram') {
2460
+ if (isMultiBot && channelKey === 'telegram') {
2411
2461
  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')}`));
2412
2462
  }
2413
2463
  } else {
2414
2464
  if (!isOpenClawInstalled()) {
2415
2465
  console.log(chalk.cyan(isVi
2416
- ? '\n📦 Dang cai openclaw binary (npm install -g openclaw)...'
2417
- : '\n📦 Installing openclaw binary (npm install -g openclaw)...'));
2418
- if (!installGlobalPackage('openclaw@latest', { isVi, osChoice, displayName: 'openclaw' })) {
2466
+ ? `\n📦 Dang cai openclaw binary (npm install -g ${OPENCLAW_NPM_SPEC})...`
2467
+ : `\n📦 Installing openclaw binary (npm install -g ${OPENCLAW_NPM_SPEC})...`));
2468
+ if (!installGlobalPackage(OPENCLAW_NPM_SPEC, { isVi, osChoice, displayName: 'openclaw' })) {
2419
2469
  process.exit(1);
2420
2470
  }
2421
2471
  console.log(chalk.green(isVi ? '✅ openclaw da cai xong!' : '✅ openclaw installed!'));
@@ -2442,7 +2492,7 @@ fi
2442
2492
  native9RouterSyncScriptPath = await writeNative9RouterSyncScript(projectDir);
2443
2493
  }
2444
2494
 
2445
- await syncLocalConfigToHome(projectDir, isVi);
2495
+ await ensureProjectRuntimeDirs(projectDir, isVi);
2446
2496
 
2447
2497
  if (isMultiBot && channelKey === 'telegram') {
2448
2498
  installRelayPluginForProject(projectDir, isVi);
@@ -2460,11 +2510,12 @@ fi
2460
2510
  if (providerKey === '9router') {
2461
2511
  startNative9RouterPm2({ isVi, projectDir, appName: botName || 'openclaw-multibot', syncScriptPath: native9RouterSyncScriptPath });
2462
2512
  }
2463
- execSync('pm2 start ecosystem.config.js && pm2 save', {
2464
- cwd: projectDir,
2465
- stdio: 'inherit',
2466
- shell: true
2467
- });
2513
+ execSync('pm2 start ecosystem.config.js && pm2 save', {
2514
+ cwd: projectDir,
2515
+ stdio: 'inherit',
2516
+ shell: true,
2517
+ env: getProjectRuntimeEnv(projectDir)
2518
+ });
2468
2519
  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.'}`));
2469
2520
  console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${botName || 'openclaw-multibot'}` : ` View logs: pm2 logs ${botName || 'openclaw-multibot'}`));
2470
2521
  printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
@@ -2479,11 +2530,24 @@ fi
2479
2530
  if (channelKey === 'zalo-personal') {
2480
2531
  await runNativeZaloPersonalLoginFlow({ isVi, projectDir });
2481
2532
  }
2482
- execSync(`pm2 start "openclaw gateway run" --name "${appName}" --cwd "${projectDir.replace(/\\/g, '/')}" && pm2 save`, {
2483
- cwd: projectDir,
2484
- stdio: 'inherit',
2485
- shell: true
2486
- });
2533
+ execFileSync('pm2', [
2534
+ 'start',
2535
+ 'openclaw',
2536
+ '--name',
2537
+ appName,
2538
+ '--cwd',
2539
+ projectDir.replace(/\\/g, '/'),
2540
+ '--interpreter',
2541
+ 'none',
2542
+ '--',
2543
+ 'gateway',
2544
+ 'run'
2545
+ ], {
2546
+ cwd: projectDir,
2547
+ stdio: 'inherit',
2548
+ env: getProjectRuntimeEnv(projectDir)
2549
+ });
2550
+ runPm2Save({ projectDir, isVi });
2487
2551
  console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoan tat! Bot native dang chay qua PM2.' : 'Setup complete! Native bot is running via PM2.'}`));
2488
2552
  console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${appName}` : ` View logs: pm2 logs ${appName}`));
2489
2553
  printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
@@ -2495,16 +2559,17 @@ fi
2495
2559
  if (providerKey === '9router') {
2496
2560
  console.log(chalk.yellow(`\n${isVi ? 'Khoi dong 9Router native (background)...' : 'Starting native 9Router (background)...'}`));
2497
2561
  const native9RouterLaunch = resolveNative9RouterDesktopLaunch();
2498
- spawnBackgroundProcess(native9RouterLaunch.command, native9RouterLaunch.args, {
2499
- cwd: projectDir,
2500
- env: native9RouterLaunch.env
2501
- }).unref();
2562
+ spawnBackgroundProcess(native9RouterLaunch.command, native9RouterLaunch.args, {
2563
+ cwd: projectDir,
2564
+ env: getProjectRuntimeEnv(projectDir, native9RouterLaunch.env)
2565
+ }).unref();
2502
2566
  const routerHealth = await waitFor9RouterApiReady();
2503
- if (native9RouterSyncScriptPath) {
2504
- spawnBackgroundProcess(process.execPath, [native9RouterSyncScriptPath], {
2505
- cwd: projectDir
2506
- }).unref();
2507
- }
2567
+ if (native9RouterSyncScriptPath) {
2568
+ spawnBackgroundProcess(process.execPath, [native9RouterSyncScriptPath], {
2569
+ cwd: projectDir,
2570
+ env: getProjectRuntimeEnv(projectDir)
2571
+ }).unref();
2572
+ }
2508
2573
  console.log(chalk.gray(isVi
2509
2574
  ? ' 9Router dashboard: http://localhost:20128/dashboard'
2510
2575
  : ' 9Router dashboard: http://localhost:20128/dashboard'));
@@ -2519,11 +2584,12 @@ fi
2519
2584
  }
2520
2585
  console.log(chalk.yellow(`\n${isVi ? 'Khoi dong native bot (foreground)...' : 'Starting native bot (foreground)...'}`));
2521
2586
  const isZaloPersonal = channelKey === 'zalo-personal';
2522
- const child = spawn('openclaw', ['gateway', 'run'], {
2523
- cwd: projectDir,
2524
- stdio: isZaloPersonal ? ['inherit', 'pipe', 'pipe'] : 'inherit',
2525
- shell: process.platform === 'win32'
2526
- });
2587
+ const child = spawn('openclaw', ['gateway', 'run'], {
2588
+ cwd: projectDir,
2589
+ stdio: isZaloPersonal ? ['inherit', 'pipe', 'pipe'] : 'inherit',
2590
+ shell: process.platform === 'win32',
2591
+ env: getProjectRuntimeEnv(projectDir),
2592
+ });
2527
2593
  if (isZaloPersonal) {
2528
2594
  let approvedPairingCode = null;
2529
2595
  const onGatewayChunk = (chunk, target) => {