@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.
- package/index.ts +312 -89
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
|
|
376
|
+
stdio: 'pipe',
|
|
377
|
+
windowsHide: true
|
|
365
378
|
});
|
|
366
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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<{
|
|
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
|
-
|
|
705
|
-
|
|
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
|
-
|
|
710
|
-
|
|
847
|
+
checkCommand: 'where',
|
|
848
|
+
checkArgs: ['scoop'],
|
|
849
|
+
installCommand: 'scoop',
|
|
850
|
+
installArgs: ['install', 'jq']
|
|
711
851
|
},
|
|
712
852
|
{
|
|
713
853
|
name: 'choco',
|
|
714
|
-
|
|
715
|
-
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
|
|
1114
|
+
applyRecursiveMode(assetsDir, 0o777);
|
|
917
1115
|
api.logger.info('[moltbank] ✓ assets/ permissions → 777');
|
|
918
1116
|
}
|
|
919
1117
|
if (existsSync(configFile)) {
|
|
920
|
-
|
|
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
|
-
|
|
1126
|
+
applyRecursiveMode(scriptsDir, 0o755);
|
|
925
1127
|
api.logger.info('[moltbank] ✓ scripts/ permissions → 755');
|
|
926
|
-
|
|
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
|
-
|
|
1133
|
+
normalizeFilesMatching(skillDir, (fullPath, entry) => entry.isFile() && fullPath.endsWith('.md'), 0);
|
|
934
1134
|
}
|
|
935
1135
|
if (existsSync(referencesDir)) {
|
|
936
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1345
|
-
|
|
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 =
|
|
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
|
-
|
|
1635
|
-
|
|
1636
|
-
const stillRunning =
|
|
1637
|
-
if (stillRunning.
|
|
1638
|
-
api.logger.info(`[moltbank] gateway still running (pids: ${stillRunning.
|
|
1639
|
-
|
|
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
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
-
|
|
1658
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1842
|
-
'powershell
|
|
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 =
|
|
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
|
|
1929
|
-
|
|
1930
|
-
|
|
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') {
|