@moltbankhq/openclaw 0.1.10 → 0.1.11

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.
Files changed (2) hide show
  1. package/index.ts +312 -89
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { execSync, spawn } from 'child_process';
2
- import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
1
+ import { spawn, spawnSync } from 'child_process';
2
+ import { chmodSync, chownSync, existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, type Dirent } from 'fs';
3
3
  import { join, dirname } from 'path';
4
- import { homedir } from 'os';
4
+ import { homedir, userInfo } from 'os';
5
5
  const IS_WIN = process.platform === 'win32';
6
6
 
7
7
  type OpenclawConfig = Record<string, unknown>;
@@ -32,6 +32,21 @@ interface LoggerApi {
32
32
  logger: LoggerLike;
33
33
  }
34
34
 
35
+ type RunOptions = {
36
+ cwd?: string;
37
+ silent?: boolean;
38
+ env?: Record<string, string | undefined>;
39
+ timeoutMs?: number;
40
+ };
41
+
42
+ type RunResult = {
43
+ ok: boolean;
44
+ stdout: string;
45
+ stderr: string;
46
+ exitCode: number | null;
47
+ timedOut: boolean;
48
+ };
49
+
35
50
  export interface SetupCommandLoggerOptions {
36
51
  verbose?: boolean;
37
52
  statusCommand?: string;
@@ -350,31 +365,65 @@ function getExecErrorMessage(error: unknown): string {
350
365
  return String(error);
351
366
  }
352
367
 
353
- function run(
354
- cmd: string,
355
- opts: { cwd?: string; silent?: boolean; env?: Record<string, string | undefined>; timeoutMs?: number } = {}
356
- ) {
368
+ function runCommand(command: string, args: string[], opts: RunOptions = {}): RunResult {
357
369
  try {
358
370
  const mergedEnv: NodeJS.ProcessEnv = { ...process.env, ...(opts.env ?? {}) };
359
- const out = execSync(cmd, {
371
+ const result = spawnSync(command, args, {
360
372
  cwd: opts.cwd,
361
- stdio: opts.silent ? 'pipe' : 'inherit',
362
373
  env: mergedEnv,
374
+ encoding: 'utf8',
363
375
  timeout: opts.timeoutMs,
364
- shell: IS_WIN ? (process.env.ComSpec ?? 'cmd.exe') : '/bin/bash'
376
+ stdio: 'pipe',
377
+ windowsHide: true
365
378
  });
366
- return { ok: true, stdout: out?.toString().trim() ?? '' };
379
+
380
+ const stdout = (result.stdout ?? '').toString();
381
+ const stderr = (result.stderr ?? '').toString();
382
+
383
+ if (!opts.silent) {
384
+ if (stdout) process.stdout.write(stdout);
385
+ if (stderr) process.stderr.write(stderr);
386
+ }
387
+
388
+ const errorMessage = result.error ? getExecErrorMessage(result.error) : '';
389
+ const trimmedStdout = stdout.trim();
390
+ const trimmedStderr = stderr.trim();
391
+ const timedOut = Boolean(
392
+ result.error &&
393
+ ((isRecord(result.error) && asString((result.error as { code?: unknown }).code) === 'ETIMEDOUT') ||
394
+ errorMessage.toLowerCase().includes('timed out'))
395
+ );
396
+
397
+ if (result.status === 0) {
398
+ return {
399
+ ok: true,
400
+ stdout: trimmedStdout,
401
+ stderr: trimmedStderr,
402
+ exitCode: result.status,
403
+ timedOut: false
404
+ };
405
+ }
406
+
407
+ return {
408
+ ok: false,
409
+ stdout: trimmedStdout,
410
+ stderr: trimmedStderr || errorMessage || trimmedStdout,
411
+ exitCode: typeof result.status === 'number' ? result.status : null,
412
+ timedOut
413
+ };
367
414
  } catch (e: unknown) {
368
415
  return {
369
416
  ok: false,
370
417
  stdout: getExecOutputText(e, 'stdout'),
371
- stderr: getExecErrorMessage(e)
418
+ stderr: getExecErrorMessage(e),
419
+ exitCode: null,
420
+ timedOut: getExecErrorMessage(e).toLowerCase().includes('timed out')
372
421
  };
373
422
  }
374
423
  }
375
424
 
376
425
  function hasBin(bin: string) {
377
- return run(IS_WIN ? `where ${bin}` : `which ${bin}`, { silent: true }).ok;
426
+ return runCommand(IS_WIN ? 'where' : 'which', [bin], { silent: true }).ok;
378
427
  }
379
428
 
380
429
  function getJqInstallHint(): string {
@@ -396,7 +445,7 @@ function splitNonEmptyLines(text: string): string[] {
396
445
  }
397
446
 
398
447
  function getNpmGlobalBinDir(): string {
399
- const prefix = run('npm config get prefix', { silent: true });
448
+ const prefix = runCommand('npm', ['config', 'get', 'prefix'], { silent: true });
400
449
  if (!prefix.ok) return '';
401
450
 
402
451
  const value = prefix.stdout.trim();
@@ -412,7 +461,7 @@ function findFirstMatchingFile(rootDir: string, predicate: (name: string) => boo
412
461
  const current = queue.shift();
413
462
  if (!current) break;
414
463
 
415
- let entries: ReturnType<typeof readdirSync>;
464
+ let entries: Dirent[] = [];
416
465
  try {
417
466
  entries = readdirSync(current.dir, { withFileTypes: true });
418
467
  } catch {
@@ -433,13 +482,94 @@ function findFirstMatchingFile(rootDir: string, predicate: (name: string) => boo
433
482
  return '';
434
483
  }
435
484
 
485
+ function visitFiles(rootDir: string, visitor: (fullPath: string, entry: Dirent) => void, maxDepth = Number.POSITIVE_INFINITY): void {
486
+ if (!rootDir || !existsSync(rootDir)) return;
487
+
488
+ const queue: Array<{ dir: string; depth: number }> = [{ dir: rootDir, depth: 0 }];
489
+ while (queue.length) {
490
+ const current = queue.shift();
491
+ if (!current) break;
492
+
493
+ let entries: Dirent[] = [];
494
+ try {
495
+ entries = readdirSync(current.dir, { withFileTypes: true });
496
+ } catch {
497
+ continue;
498
+ }
499
+
500
+ for (const entry of entries) {
501
+ const fullPath = join(current.dir, entry.name);
502
+ visitor(fullPath, entry);
503
+ if (entry.isDirectory() && current.depth < maxDepth) {
504
+ queue.push({ dir: fullPath, depth: current.depth + 1 });
505
+ }
506
+ }
507
+ }
508
+ }
509
+
510
+ function normalizeFileLineEndings(filePath: string): void {
511
+ try {
512
+ const original = readFileSync(filePath, 'utf8');
513
+ const normalized = original.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
514
+ if (normalized !== original) {
515
+ writeFileSync(filePath, normalized, 'utf8');
516
+ }
517
+ } catch {
518
+ // ignore
519
+ }
520
+ }
521
+
522
+ function normalizeFilesMatching(rootDir: string, matcher: (fullPath: string, entry: Dirent) => boolean, maxDepth = Number.POSITIVE_INFINITY): void {
523
+ visitFiles(
524
+ rootDir,
525
+ (fullPath, entry) => {
526
+ if (entry.isFile() && matcher(fullPath, entry)) {
527
+ normalizeFileLineEndings(fullPath);
528
+ }
529
+ },
530
+ maxDepth
531
+ );
532
+ }
533
+
534
+ function applyRecursiveOwnership(rootDir: string, uid: number, gid: number): void {
535
+ try {
536
+ chownSync(rootDir, uid, gid);
537
+ } catch {
538
+ // ignore
539
+ }
540
+
541
+ visitFiles(rootDir, (fullPath) => {
542
+ try {
543
+ chownSync(fullPath, uid, gid);
544
+ } catch {
545
+ // ignore
546
+ }
547
+ });
548
+ }
549
+
550
+ function applyRecursiveMode(rootDir: string, mode: number): void {
551
+ try {
552
+ chmodSync(rootDir, mode);
553
+ } catch {
554
+ // ignore
555
+ }
556
+
557
+ visitFiles(rootDir, (fullPath) => {
558
+ try {
559
+ chmodSync(fullPath, mode);
560
+ } catch {
561
+ // ignore
562
+ }
563
+ });
564
+ }
565
+
436
566
  function findWindowsJqBinaryPath(): string {
437
567
  if (!IS_WIN) return '';
438
568
 
439
569
  const fromEnv = asString(process.env.JQ_PATH).trim();
440
570
  if (fromEnv && existsSync(fromEnv)) return fromEnv;
441
571
 
442
- const whereJq = run('where jq', { silent: true });
572
+ const whereJq = runCommand('where', ['jq'], { silent: true });
443
573
  if (whereJq.ok) {
444
574
  for (const line of splitNonEmptyLines(whereJq.stdout)) {
445
575
  if (existsSync(line)) {
@@ -482,7 +612,7 @@ function findWindowsJqBinaryPath(): string {
482
612
 
483
613
  const wingetPackagesRoot = localAppData ? join(localAppData, 'Microsoft', 'WinGet', 'Packages') : '';
484
614
  if (wingetPackagesRoot && existsSync(wingetPackagesRoot)) {
485
- let packageEntries: ReturnType<typeof readdirSync> = [];
615
+ let packageEntries: Dirent<string>[] = [];
486
616
  try {
487
617
  packageEntries = readdirSync(wingetPackagesRoot, { withFileTypes: true });
488
618
  } catch {
@@ -529,7 +659,7 @@ function ensureWindowsJqShim(api: LoggerApi): boolean {
529
659
 
530
660
  if (!hasBin('jq')) return false;
531
661
 
532
- const v = run('jq --version', { silent: true });
662
+ const v = runCommand('jq', ['--version'], { silent: true });
533
663
  api.logger.info(`[moltbank] ✓ jq available via npm global bin shim (${v.stdout || 'unknown version'})`);
534
664
  return true;
535
665
  }
@@ -665,12 +795,12 @@ function cleanupStaleMoltbankPluginLoadPaths(api: LoggerApi): boolean {
665
795
 
666
796
  function ensureMcporter(api: LoggerApi) {
667
797
  if (hasBin('mcporter')) {
668
- const v = run('mcporter --version', { silent: true });
798
+ const v = runCommand('mcporter', ['--version'], { silent: true });
669
799
  api.logger.info(`[moltbank] ✓ mcporter already installed (${v.stdout || 'unknown version'})`);
670
800
  return;
671
801
  }
672
802
  api.logger.info('[moltbank] installing mcporter globally...');
673
- const result = run('npm install -g mcporter');
803
+ const result = runCommand('npm', ['install', '-g', 'mcporter']);
674
804
  if (result.ok) {
675
805
  api.logger.info('[moltbank] ✓ mcporter installed');
676
806
  } else {
@@ -680,7 +810,7 @@ function ensureMcporter(api: LoggerApi) {
680
810
 
681
811
  function ensureJq(api: LoggerApi) {
682
812
  if (hasBin('jq')) {
683
- const v = run('jq --version', { silent: true });
813
+ const v = runCommand('jq', ['--version'], { silent: true });
684
814
  api.logger.info(`[moltbank] ✓ jq already installed (${v.stdout || 'unknown version'})`);
685
815
  return;
686
816
  }
@@ -698,29 +828,41 @@ function ensureJq(api: LoggerApi) {
698
828
 
699
829
  api.logger.info('[moltbank] jq not found — attempting Windows install...');
700
830
 
701
- const installers: Array<{ name: string; checkCmd: string; installCmd: string }> = [
831
+ const installers: Array<{
832
+ name: string;
833
+ checkCommand: string;
834
+ checkArgs: string[];
835
+ installCommand: string;
836
+ installArgs: string[];
837
+ }> = [
702
838
  {
703
839
  name: 'winget',
704
- checkCmd: 'where winget',
705
- installCmd: 'winget install -e --id jqlang.jq --accept-package-agreements --accept-source-agreements --silent'
840
+ checkCommand: 'where',
841
+ checkArgs: ['winget'],
842
+ installCommand: 'winget',
843
+ installArgs: ['install', '-e', '--id', 'jqlang.jq', '--accept-package-agreements', '--accept-source-agreements', '--silent']
706
844
  },
707
845
  {
708
846
  name: 'scoop',
709
- checkCmd: 'where scoop',
710
- installCmd: 'scoop install jq'
847
+ checkCommand: 'where',
848
+ checkArgs: ['scoop'],
849
+ installCommand: 'scoop',
850
+ installArgs: ['install', 'jq']
711
851
  },
712
852
  {
713
853
  name: 'choco',
714
- checkCmd: 'where choco',
715
- installCmd: 'choco install jq -y'
854
+ checkCommand: 'where',
855
+ checkArgs: ['choco'],
856
+ installCommand: 'choco',
857
+ installArgs: ['install', 'jq', '-y']
716
858
  }
717
859
  ];
718
860
 
719
861
  for (const installer of installers) {
720
- if (!run(installer.checkCmd, { silent: true }).ok) continue;
862
+ if (!runCommand(installer.checkCommand, installer.checkArgs, { silent: true }).ok) continue;
721
863
 
722
864
  api.logger.info(`[moltbank] trying jq install via ${installer.name}...`);
723
- const install = run(installer.installCmd, { timeoutMs: 180000 });
865
+ const install = runCommand(installer.installCommand, installer.installArgs, { timeoutMs: 180000 });
724
866
  const installOutput = `${install.stdout}\n${install.stderr}`;
725
867
  const installOutputLower = installOutput.toLowerCase();
726
868
  const alreadyInstalledOrNoUpdate =
@@ -741,7 +883,7 @@ function ensureJq(api: LoggerApi) {
741
883
  }
742
884
 
743
885
  if (hasBin('jq')) {
744
- const v = run('jq --version', { silent: true });
886
+ const v = runCommand('jq', ['--version'], { silent: true });
745
887
  api.logger.info(`[moltbank] ✓ jq installed via ${installer.name} (${v.stdout || 'unknown version'})`);
746
888
  return;
747
889
  }
@@ -821,13 +963,67 @@ function hasRequiredSkillFiles(skillDir: string): boolean {
821
963
  return SKILL_FILES.every((file) => existsSync(join(skillDir, file)));
822
964
  }
823
965
 
824
- function ensureSkillInstalled(
966
+ async function installSkillBundleFiles(appBaseUrl: string, skillDir: string): Promise<RunResult> {
967
+ const base = appBaseUrl.endsWith('/') ? appBaseUrl.slice(0, -1) : appBaseUrl;
968
+ const aliases: Record<string, string[]> = {
969
+ 'SKILL.md': ['SKILL.md', 'skill.md'],
970
+ 'skill.md': ['skill.md', 'SKILL.md']
971
+ };
972
+
973
+ try {
974
+ for (const file of SKILL_FILES) {
975
+ const outPath = join(skillDir, file);
976
+ mkdirSync(dirname(outPath), { recursive: true });
977
+
978
+ let body: string | null = null;
979
+ let lastUrl = '';
980
+ let lastStatus = '';
981
+ for (const candidate of aliases[file] ?? [file]) {
982
+ const url = `${base}/${candidate}`;
983
+ lastUrl = url;
984
+
985
+ const response = await fetch(url);
986
+ if (response.ok) {
987
+ body = await response.text();
988
+ break;
989
+ }
990
+
991
+ lastStatus = String(response.status);
992
+ }
993
+
994
+ if (body === null) {
995
+ return {
996
+ ok: false,
997
+ stdout: '',
998
+ stderr: `download failed ${lastUrl} ${lastStatus}`.trim(),
999
+ exitCode: 2,
1000
+ timedOut: false
1001
+ };
1002
+ }
1003
+
1004
+ writeFileSync(outPath, body, 'utf8');
1005
+ }
1006
+
1007
+ writeFileSync(join(skillDir, '.install_success'), 'ok\n', 'utf8');
1008
+ return { ok: true, stdout: '', stderr: '', exitCode: 0, timedOut: false };
1009
+ } catch (e) {
1010
+ return {
1011
+ ok: false,
1012
+ stdout: '',
1013
+ stderr: String(e),
1014
+ exitCode: null,
1015
+ timedOut: false
1016
+ };
1017
+ }
1018
+ }
1019
+
1020
+ async function ensureSkillInstalled(
825
1021
  skillDir: string,
826
1022
  appBaseUrl: string,
827
1023
  skillName: string,
828
1024
  api: LoggerApi,
829
1025
  mode: 'sandbox' | 'host' = 'sandbox'
830
- ) {
1026
+ ): Promise<boolean> {
831
1027
  const successFlag = join(skillDir, '.install_success');
832
1028
  if (existsSync(successFlag) && hasRequiredSkillFiles(skillDir)) {
833
1029
  ensureWrapperExecutable(skillDir, api);
@@ -847,11 +1043,7 @@ function ensureSkillInstalled(
847
1043
  api.logger.info(`[moltbank] installing skill '${skillName}' to ${skillDir} (mode: ${mode})`);
848
1044
  mkdirSync(skillDir, { recursive: true });
849
1045
 
850
- const filesJson = JSON.stringify(SKILL_FILES).replace(/"/g, '\\"');
851
- const installNode = run(
852
- `node --input-type=module -e "import fs from 'fs'; import path from 'path'; const baseRaw=process.argv[1]; const dir=process.argv[2]; const files=JSON.parse(process.argv[3]); const base=baseRaw.endsWith('/') ? baseRaw.slice(0,-1) : baseRaw; const aliases={ 'SKILL.md':['SKILL.md','skill.md'], 'skill.md':['skill.md','SKILL.md'] }; fs.mkdirSync(dir,{recursive:true}); for (const f of files){ const out=path.join(dir,f); fs.mkdirSync(path.dirname(out),{recursive:true}); let body=null; let lastUrl=''; let lastStatus=''; for (const candidate of (aliases[f] ?? [f])){ const u=base+'/'+candidate; lastUrl=u; const r=await fetch(u); if(r.ok){ body=await r.text(); break; } lastStatus=String(r.status); } if(body===null){ console.error('download failed',lastUrl,lastStatus); process.exit(2);} fs.writeFileSync(out, body,'utf8'); } fs.writeFileSync(path.join(dir,'.install_success'),'ok\\n','utf8');" "${appBaseUrl}" "${skillDir}" "${filesJson}"`,
853
- { cwd: dirname(skillDir), silent: true }
854
- );
1046
+ const installNode = await installSkillBundleFiles(appBaseUrl, skillDir);
855
1047
 
856
1048
  if (installNode.ok) {
857
1049
  ensureWrapperExecutable(skillDir, api);
@@ -906,34 +1098,42 @@ function ensureSkillPermissions(skillDir: string, api: LoggerApi) {
906
1098
  const referencesDir = join(skillDir, 'references');
907
1099
  const scriptsDir = join(skillDir, 'scripts');
908
1100
 
909
- const user = run('whoami', { silent: true }).stdout;
910
- if (user && existsSync(skillDir)) {
911
- run(`chown -R ${user} "${skillDir}"`, { silent: true });
912
- api.logger.info(`[moltbank] ownership corrected to ${user}`);
1101
+ let currentUserName = '';
1102
+ try {
1103
+ const currentUser = userInfo();
1104
+ currentUserName = currentUser.username;
1105
+ if (existsSync(skillDir)) {
1106
+ applyRecursiveOwnership(skillDir, currentUser.uid, currentUser.gid);
1107
+ api.logger.info(`[moltbank] ✓ ownership corrected to ${currentUserName}`);
1108
+ }
1109
+ } catch {
1110
+ // ignore ownership correction when uid/gid are unavailable
913
1111
  }
914
1112
 
915
1113
  if (existsSync(assetsDir)) {
916
- run(`chmod 777 "${assetsDir}"`, { silent: true });
1114
+ applyRecursiveMode(assetsDir, 0o777);
917
1115
  api.logger.info('[moltbank] ✓ assets/ permissions → 777');
918
1116
  }
919
1117
  if (existsSync(configFile)) {
920
- run(`chmod 666 "${configFile}"`, { silent: true });
1118
+ try {
1119
+ chmodSync(configFile, 0o666);
1120
+ } catch {
1121
+ // ignore
1122
+ }
921
1123
  api.logger.info('[moltbank] ✓ mcporter.json permissions → 666');
922
1124
  }
923
1125
  if (existsSync(scriptsDir)) {
924
- run(`chmod -R 755 "${scriptsDir}"`, { silent: true });
1126
+ applyRecursiveMode(scriptsDir, 0o755);
925
1127
  api.logger.info('[moltbank] ✓ scripts/ permissions → 755');
926
- run(`find "${scriptsDir}" -type f -exec sed -i 's/\r$//' {} +`, {
927
- silent: true
928
- });
1128
+ normalizeFilesMatching(scriptsDir, () => true);
929
1129
  api.logger.info('[moltbank] ✓ scripts/ line endings normalized (CRLF → LF)');
930
1130
  }
931
1131
 
932
1132
  if (existsSync(skillDir)) {
933
- run(`find "${skillDir}" -maxdepth 1 -type f -name "*.md" -exec sed -i 's/\r$//' {} +`, { silent: true });
1133
+ normalizeFilesMatching(skillDir, (fullPath, entry) => entry.isFile() && fullPath.endsWith('.md'), 0);
934
1134
  }
935
1135
  if (existsSync(referencesDir)) {
936
- run(`find "${referencesDir}" -maxdepth 1 -type f -name "*.md" -exec sed -i 's/\r$//' {} +`, { silent: true });
1136
+ normalizeFilesMatching(referencesDir, (fullPath, entry) => entry.isFile() && fullPath.endsWith('.md'), 0);
937
1137
  }
938
1138
  if (existsSync(skillDir) || existsSync(referencesDir)) {
939
1139
  api.logger.info('[moltbank] ✓ markdown line endings normalized (CRLF → LF)');
@@ -961,7 +1161,7 @@ function ensureNpmDeps(skillDir: string, api: LoggerApi, mode: 'sandbox' | 'host
961
1161
  }
962
1162
  api.logger.info('[moltbank] installing npm deps...');
963
1163
  if (mode === 'sandbox') {
964
- const result = run('npm install --ignore-scripts', { cwd: skillDir });
1164
+ const result = runCommand('npm', ['install', '--ignore-scripts'], { cwd: skillDir });
965
1165
  if (result.ok) {
966
1166
  api.logger.info('[moltbank] ✓ npm deps installed');
967
1167
  } else {
@@ -972,20 +1172,20 @@ function ensureNpmDeps(skillDir: string, api: LoggerApi, mode: 'sandbox' | 'host
972
1172
 
973
1173
  const hasLock = existsSync(join(skillDir, 'package-lock.json')) || existsSync(join(skillDir, 'npm-shrinkwrap.json'));
974
1174
  if (hasLock) {
975
- const ci = run('npm ci', { cwd: skillDir });
1175
+ const ci = runCommand('npm', ['ci'], { cwd: skillDir });
976
1176
  if (ci.ok) {
977
1177
  api.logger.info('[moltbank] ✓ npm deps installed with npm ci');
978
1178
  return;
979
1179
  }
980
1180
  api.logger.warn('[moltbank] npm ci failed; trying npm install: ' + ci.stderr);
981
1181
  }
982
- const install = run('npm install', { cwd: skillDir });
1182
+ const install = runCommand('npm', ['install'], { cwd: skillDir });
983
1183
  if (install.ok) {
984
1184
  api.logger.info('[moltbank] ✓ npm deps installed with npm install');
985
1185
  return;
986
1186
  }
987
1187
  api.logger.warn('[moltbank] npm install failed; trying safe fallback --ignore-scripts');
988
- const fallback = run('npm install --ignore-scripts', { cwd: skillDir });
1188
+ const fallback = runCommand('npm', ['install', '--ignore-scripts'], { cwd: skillDir });
989
1189
  if (fallback.ok) {
990
1190
  api.logger.warn('[moltbank] npm deps installed with fallback --ignore-scripts (some packages may require postinstall)');
991
1191
  } else {
@@ -994,7 +1194,7 @@ function ensureNpmDeps(skillDir: string, api: LoggerApi, mode: 'sandbox' | 'host
994
1194
  }
995
1195
 
996
1196
  function isMoltBankRegistered(): boolean {
997
- const result = run('mcporter config list', { silent: true });
1197
+ const result = runCommand('mcporter', ['config', 'list'], { silent: true });
998
1198
  return result.stdout.toLowerCase().includes('moltbank');
999
1199
  }
1000
1200
 
@@ -1266,7 +1466,7 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
1266
1466
  if (!deviceCode || !userCode) {
1267
1467
  api.logger.info('[moltbank] no valid credentials found — starting onboarding flow...');
1268
1468
 
1269
- const requestCode = run(`"${process.execPath}" "./scripts/request-oauth-device-code.mjs"`, {
1469
+ const requestCode = runCommand(process.execPath, ['./scripts/request-oauth-device-code.mjs'], {
1270
1470
  cwd: skillDir,
1271
1471
  silent: true,
1272
1472
  env: {
@@ -1341,8 +1541,9 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
1341
1541
  const pollIntervalSeconds = Number(process.env.MOLTBANK_OAUTH_POLL_INTERVAL_SECONDS ?? 5);
1342
1542
  const safePollIntervalSeconds = Number.isFinite(pollIntervalSeconds) && pollIntervalSeconds > 0 ? Math.floor(pollIntervalSeconds) : 5;
1343
1543
 
1344
- const poll = run(
1345
- `"${process.execPath}" "./scripts/poll-oauth-token.mjs" "${deviceCode}" ${safePollTimeoutSeconds} ${safePollIntervalSeconds} --save`,
1544
+ const poll = runCommand(
1545
+ process.execPath,
1546
+ ['./scripts/poll-oauth-token.mjs', deviceCode, String(safePollTimeoutSeconds), String(safePollIntervalSeconds), '--save'],
1346
1547
  {
1347
1548
  cwd: skillDir,
1348
1549
  silent: true,
@@ -1506,7 +1707,7 @@ export function injectSandboxEnv(skillDir: string, api: LoggerApi): boolean {
1506
1707
 
1507
1708
  if (!privateKey) {
1508
1709
  api.logger.info('[moltbank] x402_signer_private_key not found — generating EOA signer...');
1509
- const initResult = run(`"${process.execPath}" "./scripts/init-openclaw-signer.mjs"`, {
1710
+ const initResult = runCommand(process.execPath, ['./scripts/init-openclaw-signer.mjs'], {
1510
1711
  cwd: skillDir,
1511
1712
  silent: true,
1512
1713
  env: {
@@ -1629,39 +1830,52 @@ export function configureSandbox(api: LoggerApi): boolean {
1629
1830
  export function recreateSandboxAndRestart(api: LoggerApi) {
1630
1831
  api.logger.info('[moltbank] recreating sandbox containers...');
1631
1832
  api.logger.info('[moltbank] ⏳ waiting 8s before recreate (hot container protection)...');
1833
+
1834
+ const getGatewayPids = (): string[] => {
1835
+ const result = runCommand('pgrep', ['-f', 'openclaw-gateway'], { silent: true });
1836
+ if (!result.ok) return [];
1837
+ return splitNonEmptyLines(result.stdout).filter((pid) => /^\d+$/.test(pid));
1838
+ };
1839
+
1632
1840
  setTimeout(() => {
1633
1841
  api.logger.info('[moltbank] stopping gateway...');
1634
- run('openclaw gateway stop', { silent: true });
1635
-
1636
- const stillRunning = run("ps aux | grep openclaw-gateway | grep -v grep | awk '{print $2}'", { silent: true });
1637
- if (stillRunning.stdout.trim()) {
1638
- api.logger.info(`[moltbank] gateway still running (pids: ${stillRunning.stdout.trim()}) — sending SIGKILL...`);
1639
- run("kill -9 $(ps aux | grep openclaw-gateway | grep -v grep | awk '{print $2}') 2>/dev/null || true", { silent: true });
1842
+ runCommand('openclaw', ['gateway', 'stop'], { silent: true });
1843
+
1844
+ const stillRunning = getGatewayPids();
1845
+ if (stillRunning.length) {
1846
+ api.logger.info(`[moltbank] gateway still running (pids: ${stillRunning.join(' ')}) — sending SIGKILL...`);
1847
+ for (const pid of stillRunning) {
1848
+ try {
1849
+ process.kill(Number(pid), 'SIGKILL');
1850
+ } catch {
1851
+ // ignore
1852
+ }
1853
+ }
1640
1854
  api.logger.info('[moltbank] ✓ gateway process killed');
1641
1855
  } else {
1642
1856
  api.logger.info('[moltbank] ✓ gateway stopped cleanly');
1643
1857
  }
1644
1858
 
1645
- run('sleep 2', { silent: true });
1646
-
1647
- const recreate = run('openclaw sandbox recreate --all --force', {
1648
- silent: false
1649
- });
1650
- if (recreate.ok) {
1651
- api.logger.info('[moltbank] sandbox containers recreated — new container will be created on next agent message');
1652
- } else {
1653
- api.logger.warn('[moltbank] sandbox recreate failed');
1654
- api.logger.warn('[moltbank] run manually: openclaw sandbox recreate --all --force');
1655
- }
1859
+ setTimeout(() => {
1860
+ const recreate = runCommand('openclaw', ['sandbox', 'recreate', '--all', '--force'], {
1861
+ silent: false
1862
+ });
1863
+ if (recreate.ok) {
1864
+ api.logger.info('[moltbank] ✓ sandbox containers recreated — new container will be created on next agent message');
1865
+ } else {
1866
+ api.logger.warn('[moltbank] sandbox recreate failed');
1867
+ api.logger.warn('[moltbank] run manually: openclaw sandbox recreate --all --force');
1868
+ }
1656
1869
 
1657
- api.logger.info('[moltbank] restarting gateway...');
1658
- run('openclaw gateway', { silent: true });
1870
+ api.logger.info('[moltbank] restarting gateway...');
1871
+ runCommand('openclaw', ['gateway'], { silent: true });
1872
+ }, 2000);
1659
1873
  }, 8000);
1660
1874
  }
1661
1875
 
1662
1876
  function restartGatewayAfterHostSetup(api: LoggerApi): boolean {
1663
1877
  api.logger.info('[moltbank] restarting gateway to refresh host skill state...');
1664
- const restart = run('openclaw gateway restart', { silent: true, timeoutMs: 30000 });
1878
+ const restart = runCommand('openclaw', ['gateway', 'restart'], { silent: true, timeoutMs: 30000 });
1665
1879
  if (restart.ok) {
1666
1880
  api.logger.info('[moltbank] ✓ gateway restarted');
1667
1881
  return true;
@@ -1709,7 +1923,7 @@ export async function runSetup(
1709
1923
  ensureJq(api);
1710
1924
 
1711
1925
  api.logger.info('[moltbank] [sandbox 1/10] installing skill files...');
1712
- const skillInstalled = ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'sandbox');
1926
+ const skillInstalled = await ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'sandbox');
1713
1927
  if (!skillInstalled) {
1714
1928
  api.logger.warn('[moltbank] skill install failed — aborting sandbox setup');
1715
1929
  return;
@@ -1767,7 +1981,7 @@ export async function runSetup(
1767
1981
  ensureJq(api);
1768
1982
 
1769
1983
  api.logger.info('[moltbank] [host 2/8] installing skill files...');
1770
- const installed = ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'host');
1984
+ const installed = await ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'host');
1771
1985
  if (!installed) {
1772
1986
  api.logger.warn('[moltbank] host setup aborted: skill install failed. Verify install.sh/base URL and retry.');
1773
1987
  return;
@@ -1811,7 +2025,7 @@ export async function runSetup(
1811
2025
  if (IS_WIN) {
1812
2026
  const mcporterConfigPath = join(skillDir, 'assets', 'mcporter.json');
1813
2027
  const active = parseActiveTokenFromCredentials();
1814
- const mcpSmoke = run(`mcporter --config "${mcporterConfigPath}" list MoltBank`, {
2028
+ const mcpSmoke = runCommand('mcporter', ['--config', mcporterConfigPath, 'list', 'MoltBank'], {
1815
2029
  cwd: skillDir,
1816
2030
  silent: true,
1817
2031
  timeoutMs: smokeTimeoutMs,
@@ -1838,8 +2052,16 @@ export async function runSetup(
1838
2052
  );
1839
2053
 
1840
2054
  const ps1Path = join(skillDir, 'scripts', 'moltbank.ps1');
1841
- const fallback = run(
1842
- 'powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$ErrorActionPreference = \'Stop\'; if (-not (Test-Path -LiteralPath $env:MOLTBANK_WRAPPER_PATH)) { throw \'moltbank.ps1 not found\' }; if (-not (Get-Command bash -ErrorAction SilentlyContinue)) { throw \'bash is not installed or not on PATH\' }; if (-not (Get-Command mcporter -ErrorAction SilentlyContinue)) { throw \'mcporter is not installed or not on PATH\' }; Write-Output \'ok\'"',
2055
+ const fallback = runCommand(
2056
+ 'powershell',
2057
+ [
2058
+ '-NoProfile',
2059
+ '-NonInteractive',
2060
+ '-ExecutionPolicy',
2061
+ 'Bypass',
2062
+ '-Command',
2063
+ "$ErrorActionPreference = 'Stop'; if (-not (Test-Path -LiteralPath $env:MOLTBANK_WRAPPER_PATH)) { throw 'moltbank.ps1 not found' }; if (-not (Get-Command bash -ErrorAction SilentlyContinue)) { throw 'bash is not installed or not on PATH' }; if (-not (Get-Command mcporter -ErrorAction SilentlyContinue)) { throw 'mcporter is not installed or not on PATH' }; Write-Output 'ok'"
2064
+ ],
1843
2065
  {
1844
2066
  cwd: skillDir,
1845
2067
  silent: true,
@@ -1869,7 +2091,7 @@ export async function runSetup(
1869
2091
  }
1870
2092
  }
1871
2093
  } else {
1872
- const smoke = run(`"${skillDir}/scripts/moltbank.sh" list MoltBank`, {
2094
+ const smoke = runCommand(join(skillDir, 'scripts', 'moltbank.sh'), ['list', 'MoltBank'], {
1873
2095
  cwd: skillDir,
1874
2096
  silent: true,
1875
2097
  timeoutMs: smokeTimeoutMs
@@ -1925,10 +2147,11 @@ export async function runSetup(
1925
2147
  `[moltbank] setupCommand: ${finalCmd.includes('node_22.x') && !finalCmd.includes('/home/') ? '✓ ok (no host paths)' : '✗ may be corrupted'}`
1926
2148
  );
1927
2149
 
1928
- const containerCheck = run(
1929
- `docker inspect $(docker ps | grep openclaw-sbx | awk '{print $1}') --format '{{.HostConfig.NetworkMode}}' 2>/dev/null`,
1930
- { silent: true }
1931
- );
2150
+ const sandboxContainer = runCommand('docker', ['ps', '-q', '--filter', 'name=openclaw-sbx'], { silent: true });
2151
+ const sandboxContainerId = splitNonEmptyLines(sandboxContainer.stdout)[0] || '';
2152
+ const containerCheck = sandboxContainerId
2153
+ ? runCommand('docker', ['inspect', sandboxContainerId, '--format', '{{.HostConfig.NetworkMode}}'], { silent: true })
2154
+ : { ok: false, stdout: '', stderr: '', exitCode: 1, timedOut: false };
1932
2155
  if (!containerCheck.ok || !containerCheck.stdout) {
1933
2156
  api.logger.info(`[moltbank] container: not running yet — will be created on first agent message`);
1934
2157
  } else if (containerCheck.stdout.trim() === 'bridge') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moltbankhq/openclaw",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "MoltBank stablecoin treasury CLI and OpenClaw plugin",
5
5
  "main": "index.ts",
6
6
  "bin": {