@moltbankhq/openclaw 0.1.9 → 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/bin/moltbank.mjs +4 -3
- package/cli.ts +21 -3
- package/index.ts +753 -209
- package/package.json +1 -1
package/bin/moltbank.mjs
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { spawnSync } from 'child_process';
|
|
5
5
|
import { dirname, resolve } from 'path';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
7
|
|
|
8
8
|
const binDir = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const cliPath = resolve(binDir, '../cli.ts');
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
const tsxImportPath = require.resolve('tsx/esm');
|
|
12
|
+
const tsxImportUrl = pathToFileURL(tsxImportPath).href;
|
|
12
13
|
|
|
13
|
-
const result = spawnSync(process.execPath, ['--import',
|
|
14
|
+
const result = spawnSync(process.execPath, ['--import', tsxImportUrl, cliPath, ...process.argv.slice(2)], {
|
|
14
15
|
stdio: 'inherit',
|
|
15
16
|
env: process.env
|
|
16
17
|
});
|
|
@@ -20,4 +21,4 @@ if (result.error) {
|
|
|
20
21
|
process.exit(1);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
process.exit(result.status ?? 1);
|
|
24
|
+
process.exit(result.status ?? 1);
|
package/cli.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createSetupCommandLogger,
|
|
6
6
|
ensureMcporterConfig,
|
|
7
7
|
getAppBaseUrl,
|
|
8
|
+
getSetupGatewayRestartEnabled,
|
|
8
9
|
getSkillDir,
|
|
9
10
|
injectSandboxEnv,
|
|
10
11
|
printAuthStatus,
|
|
@@ -23,6 +24,7 @@ type ParsedArgs = {
|
|
|
23
24
|
help: boolean;
|
|
24
25
|
version: boolean;
|
|
25
26
|
blocking: boolean;
|
|
27
|
+
restartGateway: boolean | null;
|
|
26
28
|
verbose: boolean;
|
|
27
29
|
};
|
|
28
30
|
|
|
@@ -44,6 +46,8 @@ Options:
|
|
|
44
46
|
--app-base-url <url> Override MoltBank deployment URL
|
|
45
47
|
--skill-name <name> Override skill folder name
|
|
46
48
|
--blocking Compatibility flag; setup already waits for approval
|
|
49
|
+
--restart-gateway Force gateway restart after successful host setup
|
|
50
|
+
--no-restart-gateway Skip gateway restart after successful host setup
|
|
47
51
|
--verbose Show detailed setup logs
|
|
48
52
|
-h, --help Show help
|
|
49
53
|
-v, --version Show package version
|
|
@@ -67,6 +71,7 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
67
71
|
let help = false;
|
|
68
72
|
let version = false;
|
|
69
73
|
let blocking = false;
|
|
74
|
+
let restartGateway: boolean | null = null;
|
|
70
75
|
let verbose = false;
|
|
71
76
|
|
|
72
77
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -87,6 +92,16 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
87
92
|
continue;
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
if (arg === '--restart-gateway') {
|
|
96
|
+
restartGateway = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (arg === '--no-restart-gateway') {
|
|
101
|
+
restartGateway = false;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
90
105
|
if (arg === '--verbose') {
|
|
91
106
|
verbose = true;
|
|
92
107
|
continue;
|
|
@@ -135,6 +150,7 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
135
150
|
help,
|
|
136
151
|
version,
|
|
137
152
|
blocking,
|
|
153
|
+
restartGateway,
|
|
138
154
|
verbose
|
|
139
155
|
};
|
|
140
156
|
}
|
|
@@ -144,9 +160,10 @@ async function runCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
144
160
|
|
|
145
161
|
switch (parsed.command) {
|
|
146
162
|
case 'setup': {
|
|
163
|
+
const restartGatewayOnSuccess = parsed.restartGateway ?? getSetupGatewayRestartEnabled(process.platform === 'win32');
|
|
147
164
|
const logger = createSetupCommandLogger({ verbose: parsed.verbose, statusCommand: 'moltbank status' });
|
|
148
165
|
try {
|
|
149
|
-
await runSetup(cfg, logger, { authWaitMode: 'blocking' });
|
|
166
|
+
await runSetup(cfg, logger, { authWaitMode: 'blocking', restartGatewayOnSuccess });
|
|
150
167
|
} finally {
|
|
151
168
|
logger.finish();
|
|
152
169
|
}
|
|
@@ -154,9 +171,10 @@ async function runCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
case 'setup-blocking': {
|
|
174
|
+
const restartGatewayOnSuccess = parsed.restartGateway ?? getSetupGatewayRestartEnabled(process.platform === 'win32');
|
|
157
175
|
const logger = createSetupCommandLogger({ verbose: parsed.verbose, statusCommand: 'moltbank status' });
|
|
158
176
|
try {
|
|
159
|
-
await runSetup(cfg, logger, { authWaitMode: 'blocking' });
|
|
177
|
+
await runSetup(cfg, logger, { authWaitMode: 'blocking', restartGatewayOnSuccess });
|
|
160
178
|
} finally {
|
|
161
179
|
logger.finish();
|
|
162
180
|
}
|
|
@@ -223,4 +241,4 @@ main().catch((error: unknown) => {
|
|
|
223
241
|
const message = error instanceof Error ? error.message : String(error);
|
|
224
242
|
console.error(`[moltbank] ${message}`);
|
|
225
243
|
process.exit(1);
|
|
226
|
-
});
|
|
244
|
+
});
|
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { chmodSync, existsSync, mkdirSync, 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;
|
|
@@ -306,40 +321,355 @@ export function getSetupAuthWaitMode(defaultMode: AuthWaitMode): AuthWaitMode {
|
|
|
306
321
|
return defaultMode;
|
|
307
322
|
}
|
|
308
323
|
|
|
324
|
+
function parseBooleanEnv(raw: string): boolean | null {
|
|
325
|
+
const normalized = raw.trim().toLowerCase();
|
|
326
|
+
if (!normalized) return null;
|
|
327
|
+
|
|
328
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
|
329
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function getSetupGatewayRestartEnabled(defaultEnabled: boolean): boolean {
|
|
334
|
+
const parsed = parseBooleanEnv(asString(process.env.MOLTBANK_SETUP_RESTART_GATEWAY));
|
|
335
|
+
return parsed === null ? defaultEnabled : parsed;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function getExecOutputText(error: unknown, field: 'stderr' | 'stdout'): string {
|
|
339
|
+
if (!isRecord(error) || !(field in error)) return '';
|
|
340
|
+
|
|
341
|
+
const raw = (error as { stderr?: unknown; stdout?: unknown })[field];
|
|
342
|
+
if (typeof raw === 'string') return raw.trim();
|
|
343
|
+
if (Buffer.isBuffer(raw)) return raw.toString('utf8').trim();
|
|
344
|
+
|
|
345
|
+
if (isRecord(raw) && 'toString' in raw && typeof (raw as { toString?: unknown }).toString === 'function') {
|
|
346
|
+
const text = (raw as { toString: () => string }).toString().trim();
|
|
347
|
+
if (text && text !== '[object Object]') return text;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return '';
|
|
351
|
+
}
|
|
352
|
+
|
|
309
353
|
function getExecErrorMessage(error: unknown): string {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
354
|
+
const stderr = getExecOutputText(error, 'stderr');
|
|
355
|
+
if (stderr) return stderr;
|
|
356
|
+
|
|
357
|
+
const stdout = getExecOutputText(error, 'stdout');
|
|
358
|
+
if (stdout) return stdout;
|
|
359
|
+
|
|
360
|
+
if (isRecord(error) && 'message' in error) {
|
|
361
|
+
const message = asString((error as { message?: unknown }).message).trim();
|
|
362
|
+
if (message) return message;
|
|
318
363
|
}
|
|
364
|
+
|
|
319
365
|
return String(error);
|
|
320
366
|
}
|
|
321
367
|
|
|
322
|
-
function
|
|
368
|
+
function runCommand(command: string, args: string[], opts: RunOptions = {}): RunResult {
|
|
323
369
|
try {
|
|
324
370
|
const mergedEnv: NodeJS.ProcessEnv = { ...process.env, ...(opts.env ?? {}) };
|
|
325
|
-
const
|
|
371
|
+
const result = spawnSync(command, args, {
|
|
326
372
|
cwd: opts.cwd,
|
|
327
|
-
stdio: opts.silent ? 'pipe' : 'inherit',
|
|
328
373
|
env: mergedEnv,
|
|
329
|
-
|
|
374
|
+
encoding: 'utf8',
|
|
375
|
+
timeout: opts.timeoutMs,
|
|
376
|
+
stdio: 'pipe',
|
|
377
|
+
windowsHide: true
|
|
330
378
|
});
|
|
331
|
-
|
|
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
|
+
};
|
|
332
414
|
} catch (e: unknown) {
|
|
333
415
|
return {
|
|
334
416
|
ok: false,
|
|
335
|
-
stdout: '',
|
|
336
|
-
stderr: getExecErrorMessage(e)
|
|
417
|
+
stdout: getExecOutputText(e, 'stdout'),
|
|
418
|
+
stderr: getExecErrorMessage(e),
|
|
419
|
+
exitCode: null,
|
|
420
|
+
timedOut: getExecErrorMessage(e).toLowerCase().includes('timed out')
|
|
337
421
|
};
|
|
338
422
|
}
|
|
339
423
|
}
|
|
340
424
|
|
|
341
425
|
function hasBin(bin: string) {
|
|
342
|
-
return
|
|
426
|
+
return runCommand(IS_WIN ? 'where' : 'which', [bin], { silent: true }).ok;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function getJqInstallHint(): string {
|
|
430
|
+
if (hasBin('apt-get')) return '`sudo apt-get install -y jq`';
|
|
431
|
+
if (hasBin('dnf')) return '`sudo dnf install -y jq`';
|
|
432
|
+
if (hasBin('yum')) return '`sudo yum install -y jq`';
|
|
433
|
+
if (hasBin('pacman')) return '`sudo pacman -S --noconfirm jq`';
|
|
434
|
+
if (hasBin('apk')) return '`sudo apk add jq`';
|
|
435
|
+
if (hasBin('zypper')) return '`sudo zypper install -y jq`';
|
|
436
|
+
if (hasBin('brew')) return '`brew install jq`';
|
|
437
|
+
return 'using your distro package manager';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function splitNonEmptyLines(text: string): string[] {
|
|
441
|
+
return (text || '')
|
|
442
|
+
.split(/\r?\n/)
|
|
443
|
+
.map((line) => line.trim())
|
|
444
|
+
.filter(Boolean);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function getNpmGlobalBinDir(): string {
|
|
448
|
+
const prefix = runCommand('npm', ['config', 'get', 'prefix'], { silent: true });
|
|
449
|
+
if (!prefix.ok) return '';
|
|
450
|
+
|
|
451
|
+
const value = prefix.stdout.trim();
|
|
452
|
+
if (!value || value === 'undefined' || value === 'null') return '';
|
|
453
|
+
return IS_WIN ? value : join(value, 'bin');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function findFirstMatchingFile(rootDir: string, predicate: (name: string) => boolean, maxDepth = 3): string {
|
|
457
|
+
if (!rootDir || !existsSync(rootDir)) return '';
|
|
458
|
+
|
|
459
|
+
const queue: Array<{ dir: string; depth: number }> = [{ dir: rootDir, depth: 0 }];
|
|
460
|
+
while (queue.length) {
|
|
461
|
+
const current = queue.shift();
|
|
462
|
+
if (!current) break;
|
|
463
|
+
|
|
464
|
+
let entries: Dirent[] = [];
|
|
465
|
+
try {
|
|
466
|
+
entries = readdirSync(current.dir, { withFileTypes: true });
|
|
467
|
+
} catch {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (const entry of entries) {
|
|
472
|
+
const fullPath = join(current.dir, entry.name);
|
|
473
|
+
if (entry.isFile() && predicate(entry.name)) {
|
|
474
|
+
return fullPath;
|
|
475
|
+
}
|
|
476
|
+
if (entry.isDirectory() && current.depth < maxDepth) {
|
|
477
|
+
queue.push({ dir: fullPath, depth: current.depth + 1 });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return '';
|
|
483
|
+
}
|
|
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
|
+
|
|
566
|
+
function findWindowsJqBinaryPath(): string {
|
|
567
|
+
if (!IS_WIN) return '';
|
|
568
|
+
|
|
569
|
+
const fromEnv = asString(process.env.JQ_PATH).trim();
|
|
570
|
+
if (fromEnv && existsSync(fromEnv)) return fromEnv;
|
|
571
|
+
|
|
572
|
+
const whereJq = runCommand('where', ['jq'], { silent: true });
|
|
573
|
+
if (whereJq.ok) {
|
|
574
|
+
for (const line of splitNonEmptyLines(whereJq.stdout)) {
|
|
575
|
+
if (existsSync(line)) {
|
|
576
|
+
return line;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const localAppData = asString(process.env.LOCALAPPDATA).trim();
|
|
582
|
+
const appData = asString(process.env.APPDATA).trim();
|
|
583
|
+
const userProfile = asString(process.env.USERPROFILE).trim();
|
|
584
|
+
const programData = asString(process.env.ProgramData).trim();
|
|
585
|
+
const programFiles = asString(process.env.ProgramFiles).trim();
|
|
586
|
+
const chocolateyInstall = asString(process.env.ChocolateyInstall).trim();
|
|
587
|
+
|
|
588
|
+
const candidates = [
|
|
589
|
+
localAppData ? join(localAppData, 'Microsoft', 'WinGet', 'Links', 'jq.exe') : '',
|
|
590
|
+
localAppData ? join(localAppData, 'Microsoft', 'WindowsApps', 'jq.exe') : '',
|
|
591
|
+
localAppData ? join(localAppData, 'Programs', 'jq', 'jq.exe') : '',
|
|
592
|
+
localAppData ? join(localAppData, 'Programs', 'jqlang', 'jq.exe') : '',
|
|
593
|
+
appData ? join(appData, 'npm', 'jq.exe') : '',
|
|
594
|
+
appData ? join(appData, 'npm', 'jq.cmd') : '',
|
|
595
|
+
userProfile ? join(userProfile, 'scoop', 'shims', 'jq.exe') : '',
|
|
596
|
+
chocolateyInstall ? join(chocolateyInstall, 'bin', 'jq.exe') : '',
|
|
597
|
+
programData ? join(programData, 'chocolatey', 'bin', 'jq.exe') : '',
|
|
598
|
+
programFiles ? join(programFiles, 'jqlang', 'jq.exe') : '',
|
|
599
|
+
programFiles ? join(programFiles, 'jq', 'jq.exe') : ''
|
|
600
|
+
].filter(Boolean);
|
|
601
|
+
|
|
602
|
+
for (const candidate of candidates) {
|
|
603
|
+
if (existsSync(candidate)) {
|
|
604
|
+
return candidate;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const isJqExecutable = (name: string): boolean => {
|
|
609
|
+
const lower = name.toLowerCase();
|
|
610
|
+
return lower === 'jq.exe' || lower === 'jq-windows-amd64.exe' || (lower.startsWith('jq') && lower.endsWith('.exe'));
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const wingetPackagesRoot = localAppData ? join(localAppData, 'Microsoft', 'WinGet', 'Packages') : '';
|
|
614
|
+
if (wingetPackagesRoot && existsSync(wingetPackagesRoot)) {
|
|
615
|
+
let packageEntries: Dirent<string>[] = [];
|
|
616
|
+
try {
|
|
617
|
+
packageEntries = readdirSync(wingetPackagesRoot, { withFileTypes: true });
|
|
618
|
+
} catch {
|
|
619
|
+
packageEntries = [];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
for (const entry of packageEntries) {
|
|
623
|
+
if (!entry.isDirectory()) continue;
|
|
624
|
+
const lower = entry.name.toLowerCase();
|
|
625
|
+
if (!lower.includes('jqlang.jq') && !lower.startsWith('jq')) continue;
|
|
626
|
+
|
|
627
|
+
const found = findFirstMatchingFile(join(wingetPackagesRoot, entry.name), isJqExecutable, 3);
|
|
628
|
+
if (found) return found;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const fallback = findFirstMatchingFile(wingetPackagesRoot, isJqExecutable, 2);
|
|
632
|
+
if (fallback) return fallback;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return '';
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function ensureWindowsJqShim(api: LoggerApi): boolean {
|
|
639
|
+
if (!IS_WIN) return false;
|
|
640
|
+
|
|
641
|
+
const jqSourcePath = findWindowsJqBinaryPath();
|
|
642
|
+
if (!jqSourcePath) return false;
|
|
643
|
+
|
|
644
|
+
const npmBinDir = getNpmGlobalBinDir();
|
|
645
|
+
if (!npmBinDir) return false;
|
|
646
|
+
|
|
647
|
+
const shimPath = join(npmBinDir, 'jq.cmd');
|
|
648
|
+
try {
|
|
649
|
+
mkdirSync(npmBinDir, { recursive: true });
|
|
650
|
+
if (jqSourcePath.toLowerCase() !== shimPath.toLowerCase()) {
|
|
651
|
+
const escapedSource = jqSourcePath.replace(/"/g, '""');
|
|
652
|
+
const shimBody = `@echo off\r\n"${escapedSource}" %*\r\n`;
|
|
653
|
+
writeFileSync(shimPath, shimBody, 'utf8');
|
|
654
|
+
}
|
|
655
|
+
} catch (e) {
|
|
656
|
+
api.logger.warn('[moltbank] could not create jq shim in npm global bin: ' + String(e));
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (!hasBin('jq')) return false;
|
|
661
|
+
|
|
662
|
+
const v = runCommand('jq', ['--version'], { silent: true });
|
|
663
|
+
api.logger.info(`[moltbank] ✓ jq available via npm global bin shim (${v.stdout || 'unknown version'})`);
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function lastNonEmptyLine(text: string): string {
|
|
668
|
+
const lines = text
|
|
669
|
+
.split(/\r?\n/)
|
|
670
|
+
.map((line) => line.trim())
|
|
671
|
+
.filter(Boolean);
|
|
672
|
+
return lines.length ? lines[lines.length - 1] : '';
|
|
343
673
|
}
|
|
344
674
|
|
|
345
675
|
function getWorkspace(): string {
|
|
@@ -465,12 +795,12 @@ function cleanupStaleMoltbankPluginLoadPaths(api: LoggerApi): boolean {
|
|
|
465
795
|
|
|
466
796
|
function ensureMcporter(api: LoggerApi) {
|
|
467
797
|
if (hasBin('mcporter')) {
|
|
468
|
-
const v =
|
|
798
|
+
const v = runCommand('mcporter', ['--version'], { silent: true });
|
|
469
799
|
api.logger.info(`[moltbank] ✓ mcporter already installed (${v.stdout || 'unknown version'})`);
|
|
470
800
|
return;
|
|
471
801
|
}
|
|
472
802
|
api.logger.info('[moltbank] installing mcporter globally...');
|
|
473
|
-
const result =
|
|
803
|
+
const result = runCommand('npm', ['install', '-g', 'mcporter']);
|
|
474
804
|
if (result.ok) {
|
|
475
805
|
api.logger.info('[moltbank] ✓ mcporter installed');
|
|
476
806
|
} else {
|
|
@@ -478,6 +808,117 @@ function ensureMcporter(api: LoggerApi) {
|
|
|
478
808
|
}
|
|
479
809
|
}
|
|
480
810
|
|
|
811
|
+
function ensureJq(api: LoggerApi) {
|
|
812
|
+
if (hasBin('jq')) {
|
|
813
|
+
const v = runCommand('jq', ['--version'], { silent: true });
|
|
814
|
+
api.logger.info(`[moltbank] ✓ jq already installed (${v.stdout || 'unknown version'})`);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (!IS_WIN) {
|
|
819
|
+
api.logger.warn('[moltbank] ✗ jq not found in PATH (required by MoltBank wrapper on host mode)');
|
|
820
|
+
api.logger.warn(`[moltbank] install jq (${getJqInstallHint()}) and rerun setup`);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (ensureWindowsJqShim(api)) {
|
|
825
|
+
api.logger.info('[moltbank] ✓ jq found on disk and shimmed into current PATH scope');
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
api.logger.info('[moltbank] jq not found — attempting Windows install...');
|
|
830
|
+
|
|
831
|
+
const installers: Array<{
|
|
832
|
+
name: string;
|
|
833
|
+
checkCommand: string;
|
|
834
|
+
checkArgs: string[];
|
|
835
|
+
installCommand: string;
|
|
836
|
+
installArgs: string[];
|
|
837
|
+
}> = [
|
|
838
|
+
{
|
|
839
|
+
name: 'winget',
|
|
840
|
+
checkCommand: 'where',
|
|
841
|
+
checkArgs: ['winget'],
|
|
842
|
+
installCommand: 'winget',
|
|
843
|
+
installArgs: ['install', '-e', '--id', 'jqlang.jq', '--accept-package-agreements', '--accept-source-agreements', '--silent']
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
name: 'scoop',
|
|
847
|
+
checkCommand: 'where',
|
|
848
|
+
checkArgs: ['scoop'],
|
|
849
|
+
installCommand: 'scoop',
|
|
850
|
+
installArgs: ['install', 'jq']
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: 'choco',
|
|
854
|
+
checkCommand: 'where',
|
|
855
|
+
checkArgs: ['choco'],
|
|
856
|
+
installCommand: 'choco',
|
|
857
|
+
installArgs: ['install', 'jq', '-y']
|
|
858
|
+
}
|
|
859
|
+
];
|
|
860
|
+
|
|
861
|
+
for (const installer of installers) {
|
|
862
|
+
if (!runCommand(installer.checkCommand, installer.checkArgs, { silent: true }).ok) continue;
|
|
863
|
+
|
|
864
|
+
api.logger.info(`[moltbank] trying jq install via ${installer.name}...`);
|
|
865
|
+
const install = runCommand(installer.installCommand, installer.installArgs, { timeoutMs: 180000 });
|
|
866
|
+
const installOutput = `${install.stdout}\n${install.stderr}`;
|
|
867
|
+
const installOutputLower = installOutput.toLowerCase();
|
|
868
|
+
const alreadyInstalledOrNoUpdate =
|
|
869
|
+
installOutputLower.includes('already installed') ||
|
|
870
|
+
installOutputLower.includes('paquete existente ya instalado') ||
|
|
871
|
+
installOutputLower.includes('no update available') ||
|
|
872
|
+
installOutputLower.includes('no updates available') ||
|
|
873
|
+
installOutputLower.includes('no newer package versions are available') ||
|
|
874
|
+
installOutputLower.includes('no se ha encontrado ninguna actualización disponible') ||
|
|
875
|
+
installOutputLower.includes('no hay versiones más recientes');
|
|
876
|
+
|
|
877
|
+
if (!install.ok) {
|
|
878
|
+
if (!alreadyInstalledOrNoUpdate) {
|
|
879
|
+
api.logger.warn(`[moltbank] jq install via ${installer.name} failed: ${install.stderr}`);
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
api.logger.info(`[moltbank] jq already installed via ${installer.name}; validating availability...`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (hasBin('jq')) {
|
|
886
|
+
const v = runCommand('jq', ['--version'], { silent: true });
|
|
887
|
+
api.logger.info(`[moltbank] ✓ jq installed via ${installer.name} (${v.stdout || 'unknown version'})`);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (ensureWindowsJqShim(api)) {
|
|
892
|
+
api.logger.info(`[moltbank] ✓ jq installed via ${installer.name} and made available without shell restart`);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (alreadyInstalledOrNoUpdate) {
|
|
897
|
+
api.logger.warn(`[moltbank] jq appears installed via ${installer.name}, but is not visible in the current PATH`);
|
|
898
|
+
api.logger.warn('[moltbank] close and reopen PowerShell, then run: `where jq`');
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const installOutputLowerCheck = installOutputLower;
|
|
903
|
+
const shellRestartNeeded =
|
|
904
|
+
installOutputLowerCheck.includes('variable de entorno path modificada') ||
|
|
905
|
+
installOutputLowerCheck.includes('path modificada') ||
|
|
906
|
+
installOutputLowerCheck.includes('path modified') ||
|
|
907
|
+
installOutputLowerCheck.includes('reinicie el shell') ||
|
|
908
|
+
installOutputLowerCheck.includes('restart the shell') ||
|
|
909
|
+
installOutputLowerCheck.includes('restart your shell');
|
|
910
|
+
|
|
911
|
+
if (shellRestartNeeded) {
|
|
912
|
+
api.logger.warn(`[moltbank] jq was installed via ${installer.name}, but this terminal has a stale PATH`);
|
|
913
|
+
api.logger.warn('[moltbank] close and reopen PowerShell, then run: `jq --version`');
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
api.logger.warn('[moltbank] ✗ jq is still missing after auto-install attempts');
|
|
919
|
+
api.logger.warn('[moltbank] install manually on Windows: winget install jqlang.jq');
|
|
920
|
+
}
|
|
921
|
+
|
|
481
922
|
function ensureWrapperExecutable(skillDir: string, api: LoggerApi): void {
|
|
482
923
|
if (IS_WIN) return;
|
|
483
924
|
const wrapperPath = join(skillDir, 'scripts', 'moltbank.sh');
|
|
@@ -498,7 +939,6 @@ const SKILL_FILES = [
|
|
|
498
939
|
'SKILL.md',
|
|
499
940
|
'skill.json',
|
|
500
941
|
'install.sh',
|
|
501
|
-
'package.json',
|
|
502
942
|
'assets/mcporter.json',
|
|
503
943
|
'references/heartbeat.md',
|
|
504
944
|
'references/onboarding.md',
|
|
@@ -523,13 +963,67 @@ function hasRequiredSkillFiles(skillDir: string): boolean {
|
|
|
523
963
|
return SKILL_FILES.every((file) => existsSync(join(skillDir, file)));
|
|
524
964
|
}
|
|
525
965
|
|
|
526
|
-
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(
|
|
527
1021
|
skillDir: string,
|
|
528
1022
|
appBaseUrl: string,
|
|
529
1023
|
skillName: string,
|
|
530
1024
|
api: LoggerApi,
|
|
531
1025
|
mode: 'sandbox' | 'host' = 'sandbox'
|
|
532
|
-
) {
|
|
1026
|
+
): Promise<boolean> {
|
|
533
1027
|
const successFlag = join(skillDir, '.install_success');
|
|
534
1028
|
if (existsSync(successFlag) && hasRequiredSkillFiles(skillDir)) {
|
|
535
1029
|
ensureWrapperExecutable(skillDir, api);
|
|
@@ -549,11 +1043,7 @@ function ensureSkillInstalled(
|
|
|
549
1043
|
api.logger.info(`[moltbank] installing skill '${skillName}' to ${skillDir} (mode: ${mode})`);
|
|
550
1044
|
mkdirSync(skillDir, { recursive: true });
|
|
551
1045
|
|
|
552
|
-
const
|
|
553
|
-
const installNode = run(
|
|
554
|
-
`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}"`,
|
|
555
|
-
{ cwd: dirname(skillDir), silent: true }
|
|
556
|
-
);
|
|
1046
|
+
const installNode = await installSkillBundleFiles(appBaseUrl, skillDir);
|
|
557
1047
|
|
|
558
1048
|
if (installNode.ok) {
|
|
559
1049
|
ensureWrapperExecutable(skillDir, api);
|
|
@@ -608,34 +1098,42 @@ function ensureSkillPermissions(skillDir: string, api: LoggerApi) {
|
|
|
608
1098
|
const referencesDir = join(skillDir, 'references');
|
|
609
1099
|
const scriptsDir = join(skillDir, 'scripts');
|
|
610
1100
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
|
615
1111
|
}
|
|
616
1112
|
|
|
617
1113
|
if (existsSync(assetsDir)) {
|
|
618
|
-
|
|
1114
|
+
applyRecursiveMode(assetsDir, 0o777);
|
|
619
1115
|
api.logger.info('[moltbank] ✓ assets/ permissions → 777');
|
|
620
1116
|
}
|
|
621
1117
|
if (existsSync(configFile)) {
|
|
622
|
-
|
|
1118
|
+
try {
|
|
1119
|
+
chmodSync(configFile, 0o666);
|
|
1120
|
+
} catch {
|
|
1121
|
+
// ignore
|
|
1122
|
+
}
|
|
623
1123
|
api.logger.info('[moltbank] ✓ mcporter.json permissions → 666');
|
|
624
1124
|
}
|
|
625
1125
|
if (existsSync(scriptsDir)) {
|
|
626
|
-
|
|
1126
|
+
applyRecursiveMode(scriptsDir, 0o755);
|
|
627
1127
|
api.logger.info('[moltbank] ✓ scripts/ permissions → 755');
|
|
628
|
-
|
|
629
|
-
silent: true
|
|
630
|
-
});
|
|
1128
|
+
normalizeFilesMatching(scriptsDir, () => true);
|
|
631
1129
|
api.logger.info('[moltbank] ✓ scripts/ line endings normalized (CRLF → LF)');
|
|
632
1130
|
}
|
|
633
1131
|
|
|
634
1132
|
if (existsSync(skillDir)) {
|
|
635
|
-
|
|
1133
|
+
normalizeFilesMatching(skillDir, (fullPath, entry) => entry.isFile() && fullPath.endsWith('.md'), 0);
|
|
636
1134
|
}
|
|
637
1135
|
if (existsSync(referencesDir)) {
|
|
638
|
-
|
|
1136
|
+
normalizeFilesMatching(referencesDir, (fullPath, entry) => entry.isFile() && fullPath.endsWith('.md'), 0);
|
|
639
1137
|
}
|
|
640
1138
|
if (existsSync(skillDir) || existsSync(referencesDir)) {
|
|
641
1139
|
api.logger.info('[moltbank] ✓ markdown line endings normalized (CRLF → LF)');
|
|
@@ -663,7 +1161,7 @@ function ensureNpmDeps(skillDir: string, api: LoggerApi, mode: 'sandbox' | 'host
|
|
|
663
1161
|
}
|
|
664
1162
|
api.logger.info('[moltbank] installing npm deps...');
|
|
665
1163
|
if (mode === 'sandbox') {
|
|
666
|
-
const result =
|
|
1164
|
+
const result = runCommand('npm', ['install', '--ignore-scripts'], { cwd: skillDir });
|
|
667
1165
|
if (result.ok) {
|
|
668
1166
|
api.logger.info('[moltbank] ✓ npm deps installed');
|
|
669
1167
|
} else {
|
|
@@ -674,20 +1172,20 @@ function ensureNpmDeps(skillDir: string, api: LoggerApi, mode: 'sandbox' | 'host
|
|
|
674
1172
|
|
|
675
1173
|
const hasLock = existsSync(join(skillDir, 'package-lock.json')) || existsSync(join(skillDir, 'npm-shrinkwrap.json'));
|
|
676
1174
|
if (hasLock) {
|
|
677
|
-
const ci =
|
|
1175
|
+
const ci = runCommand('npm', ['ci'], { cwd: skillDir });
|
|
678
1176
|
if (ci.ok) {
|
|
679
1177
|
api.logger.info('[moltbank] ✓ npm deps installed with npm ci');
|
|
680
1178
|
return;
|
|
681
1179
|
}
|
|
682
1180
|
api.logger.warn('[moltbank] npm ci failed; trying npm install: ' + ci.stderr);
|
|
683
1181
|
}
|
|
684
|
-
const install =
|
|
1182
|
+
const install = runCommand('npm', ['install'], { cwd: skillDir });
|
|
685
1183
|
if (install.ok) {
|
|
686
1184
|
api.logger.info('[moltbank] ✓ npm deps installed with npm install');
|
|
687
1185
|
return;
|
|
688
1186
|
}
|
|
689
1187
|
api.logger.warn('[moltbank] npm install failed; trying safe fallback --ignore-scripts');
|
|
690
|
-
const fallback =
|
|
1188
|
+
const fallback = runCommand('npm', ['install', '--ignore-scripts'], { cwd: skillDir });
|
|
691
1189
|
if (fallback.ok) {
|
|
692
1190
|
api.logger.warn('[moltbank] npm deps installed with fallback --ignore-scripts (some packages may require postinstall)');
|
|
693
1191
|
} else {
|
|
@@ -696,10 +1194,35 @@ function ensureNpmDeps(skillDir: string, api: LoggerApi, mode: 'sandbox' | 'host
|
|
|
696
1194
|
}
|
|
697
1195
|
|
|
698
1196
|
function isMoltBankRegistered(): boolean {
|
|
699
|
-
const result =
|
|
1197
|
+
const result = runCommand('mcporter', ['config', 'list'], { silent: true });
|
|
700
1198
|
return result.stdout.toLowerCase().includes('moltbank');
|
|
701
1199
|
}
|
|
702
1200
|
|
|
1201
|
+
function removeHomeScopedMoltBankConfig(api: LoggerApi): boolean {
|
|
1202
|
+
const configPath = join(homedir(), '.mcporter', 'mcporter.json');
|
|
1203
|
+
if (!existsSync(configPath)) {
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
try {
|
|
1208
|
+
const parsed = JSON.parse(readFileSync(configPath, 'utf8')) as unknown;
|
|
1209
|
+
if (!isRecord(parsed)) return false;
|
|
1210
|
+
|
|
1211
|
+
const mcpServers = parsed.mcpServers;
|
|
1212
|
+
if (!isRecord(mcpServers) || !('MoltBank' in mcpServers)) {
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
delete mcpServers.MoltBank;
|
|
1217
|
+
writeFileSync(configPath, JSON.stringify(parsed, null, 2) + '\n', 'utf8');
|
|
1218
|
+
api.logger.info('[moltbank] ✓ removed stale home-scoped mcporter MoltBank config');
|
|
1219
|
+
return true;
|
|
1220
|
+
} catch (e) {
|
|
1221
|
+
api.logger.warn('[moltbank] could not clean home-scoped mcporter MoltBank config: ' + String(e));
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
703
1226
|
function parseActiveTokenFromCredentials(): {
|
|
704
1227
|
ok: boolean;
|
|
705
1228
|
activeOrg?: string;
|
|
@@ -764,93 +1287,6 @@ function readPendingOauthCode(skillDir: string): {
|
|
|
764
1287
|
}
|
|
765
1288
|
}
|
|
766
1289
|
|
|
767
|
-
function clearPendingOauthCode(skillDir: string): void {
|
|
768
|
-
const pendingPath = join(skillDir, '.oauth_device_code.json');
|
|
769
|
-
try {
|
|
770
|
-
if (existsSync(pendingPath)) unlinkSync(pendingPath);
|
|
771
|
-
} catch {
|
|
772
|
-
// ignore
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function runProcess(
|
|
777
|
-
command: string,
|
|
778
|
-
args: string[],
|
|
779
|
-
opts: { cwd?: string; silent?: boolean; env?: Record<string, string | undefined> } = {}
|
|
780
|
-
) {
|
|
781
|
-
try {
|
|
782
|
-
const mergedEnv: NodeJS.ProcessEnv = { ...process.env, ...(opts.env ?? {}) };
|
|
783
|
-
const result = spawnSync(command, args, {
|
|
784
|
-
cwd: opts.cwd,
|
|
785
|
-
env: mergedEnv,
|
|
786
|
-
encoding: 'utf8',
|
|
787
|
-
stdio: opts.silent ? 'pipe' : 'inherit'
|
|
788
|
-
});
|
|
789
|
-
return {
|
|
790
|
-
ok: result.status === 0,
|
|
791
|
-
stdout: (result.stdout ?? '').toString().trim(),
|
|
792
|
-
stderr: (result.stderr ?? '').toString().trim()
|
|
793
|
-
};
|
|
794
|
-
} catch (e: unknown) {
|
|
795
|
-
return {
|
|
796
|
-
ok: false,
|
|
797
|
-
stdout: '',
|
|
798
|
-
stderr: getExecErrorMessage(e)
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
function runMoltbankWrapper(
|
|
804
|
-
skillDir: string,
|
|
805
|
-
args: string[],
|
|
806
|
-
env: Record<string, string | undefined> = {}
|
|
807
|
-
) {
|
|
808
|
-
if (IS_WIN) {
|
|
809
|
-
const ps1Path = join(skillDir, 'scripts', 'moltbank.ps1');
|
|
810
|
-
return runProcess('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps1Path, ...args], {
|
|
811
|
-
cwd: skillDir,
|
|
812
|
-
silent: true,
|
|
813
|
-
env
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
return runProcess(join(skillDir, 'scripts', 'moltbank.sh'), args, {
|
|
818
|
-
cwd: skillDir,
|
|
819
|
-
silent: true,
|
|
820
|
-
env
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
function verifyMoltbankOperationalReadiness(skillDir: string, api: LoggerApi): boolean {
|
|
825
|
-
const existing = parseActiveTokenFromCredentials();
|
|
826
|
-
if (!existing.ok || !existing.activeOrg || !existing.token) {
|
|
827
|
-
api.logger.warn('[moltbank] ✗ setup incomplete: authenticated org could not be resolved for balance verification');
|
|
828
|
-
return false;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
const verification = runMoltbankWrapper(skillDir, [
|
|
832
|
-
'call',
|
|
833
|
-
'MoltBank.get_balance',
|
|
834
|
-
`organizationName=${existing.activeOrg}`,
|
|
835
|
-
'date=today'
|
|
836
|
-
], {
|
|
837
|
-
MOLTBANK: existing.token,
|
|
838
|
-
ACTIVE_ORG_OVERRIDE: existing.activeOrg
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
if (verification.ok) {
|
|
842
|
-
api.logger.info(`[moltbank] ✓ balance verification passed (org: ${existing.activeOrg})`);
|
|
843
|
-
return true;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
api.logger.warn('[moltbank] ✗ setup incomplete: auth is linked, but balance verification failed (`MoltBank.get_balance`)');
|
|
847
|
-
const detail = verification.stderr || verification.stdout;
|
|
848
|
-
if (detail) {
|
|
849
|
-
api.logger.warn('[moltbank] verification detail: ' + detail);
|
|
850
|
-
}
|
|
851
|
-
return false;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
1290
|
function startBackgroundOauthPoll(
|
|
855
1291
|
skillDir: string,
|
|
856
1292
|
appBaseUrl: string,
|
|
@@ -902,7 +1338,12 @@ function startBackgroundOauthPoll(
|
|
|
902
1338
|
oauthPollers.delete(skillDir);
|
|
903
1339
|
const after = parseActiveTokenFromCredentials();
|
|
904
1340
|
if (after.ok) {
|
|
905
|
-
|
|
1341
|
+
const pendingPath = join(skillDir, '.oauth_device_code.json');
|
|
1342
|
+
try {
|
|
1343
|
+
if (existsSync(pendingPath)) unlinkSync(pendingPath);
|
|
1344
|
+
} catch {
|
|
1345
|
+
// ignore
|
|
1346
|
+
}
|
|
906
1347
|
api.logger.info(`[moltbank] ✓ background onboarding completed (active org: ${after.activeOrg})`);
|
|
907
1348
|
try {
|
|
908
1349
|
startBackgroundFinalizeAfterAuth(skillDir, appBaseUrl, api);
|
|
@@ -982,8 +1423,6 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
|
|
|
982
1423
|
const existing = parseActiveTokenFromCredentials();
|
|
983
1424
|
if (existing.ok) {
|
|
984
1425
|
stopBackgroundOauthPoll(skillDir);
|
|
985
|
-
stopBackgroundFinalize(skillDir);
|
|
986
|
-
clearPendingOauthCode(skillDir);
|
|
987
1426
|
api.logger.info(`[moltbank] ✓ credentials.json already available (active org: ${existing.activeOrg})`);
|
|
988
1427
|
return true;
|
|
989
1428
|
}
|
|
@@ -1027,7 +1466,7 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
|
|
|
1027
1466
|
if (!deviceCode || !userCode) {
|
|
1028
1467
|
api.logger.info('[moltbank] no valid credentials found — starting onboarding flow...');
|
|
1029
1468
|
|
|
1030
|
-
const requestCode =
|
|
1469
|
+
const requestCode = runCommand(process.execPath, ['./scripts/request-oauth-device-code.mjs'], {
|
|
1031
1470
|
cwd: skillDir,
|
|
1032
1471
|
silent: true,
|
|
1033
1472
|
env: {
|
|
@@ -1102,8 +1541,9 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
|
|
|
1102
1541
|
const pollIntervalSeconds = Number(process.env.MOLTBANK_OAUTH_POLL_INTERVAL_SECONDS ?? 5);
|
|
1103
1542
|
const safePollIntervalSeconds = Number.isFinite(pollIntervalSeconds) && pollIntervalSeconds > 0 ? Math.floor(pollIntervalSeconds) : 5;
|
|
1104
1543
|
|
|
1105
|
-
const poll =
|
|
1106
|
-
|
|
1544
|
+
const poll = runCommand(
|
|
1545
|
+
process.execPath,
|
|
1546
|
+
['./scripts/poll-oauth-token.mjs', deviceCode, String(safePollTimeoutSeconds), String(safePollIntervalSeconds), '--save'],
|
|
1107
1547
|
{
|
|
1108
1548
|
cwd: skillDir,
|
|
1109
1549
|
silent: true,
|
|
@@ -1122,7 +1562,11 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
|
|
|
1122
1562
|
|
|
1123
1563
|
if (oauthError === 'invalid_grant') {
|
|
1124
1564
|
api.logger.warn('[moltbank] ✗ onboarding code expired or already consumed (invalid_grant)');
|
|
1125
|
-
|
|
1565
|
+
try {
|
|
1566
|
+
if (existsSync(pendingPath)) unlinkSync(pendingPath);
|
|
1567
|
+
} catch {
|
|
1568
|
+
// ignore
|
|
1569
|
+
}
|
|
1126
1570
|
} else {
|
|
1127
1571
|
api.logger.warn('[moltbank] ✗ onboarding poll failed or timed out');
|
|
1128
1572
|
}
|
|
@@ -1139,7 +1583,11 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
|
|
|
1139
1583
|
return false;
|
|
1140
1584
|
}
|
|
1141
1585
|
|
|
1142
|
-
|
|
1586
|
+
try {
|
|
1587
|
+
if (existsSync(pendingPath)) unlinkSync(pendingPath);
|
|
1588
|
+
} catch {
|
|
1589
|
+
// ignore
|
|
1590
|
+
}
|
|
1143
1591
|
|
|
1144
1592
|
api.logger.info(`[moltbank] ✓ onboarding completed (active org: ${after.activeOrg})`);
|
|
1145
1593
|
return true;
|
|
@@ -1148,10 +1596,7 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
|
|
|
1148
1596
|
export function printAuthStatus(skillDir: string, appBaseUrl: string, api: LoggerApi): void {
|
|
1149
1597
|
const now = Math.floor(Date.now() / 1000);
|
|
1150
1598
|
const existing = parseActiveTokenFromCredentials();
|
|
1151
|
-
|
|
1152
|
-
clearPendingOauthCode(skillDir);
|
|
1153
|
-
}
|
|
1154
|
-
const pending = existing.ok ? null : readPendingOauthCode(skillDir);
|
|
1599
|
+
const pending = readPendingOauthCode(skillDir);
|
|
1155
1600
|
const poller = oauthPollers.get(skillDir);
|
|
1156
1601
|
const finalizer = backgroundFinalizers.get(skillDir);
|
|
1157
1602
|
const pollerRunning = Boolean(poller && poller.exitCode === null && !poller.killed);
|
|
@@ -1202,35 +1647,12 @@ export function ensureMcporterConfig(skillDir: string, appBaseUrl: string, api:
|
|
|
1202
1647
|
api.logger.info('[moltbank] ✓ mcporter.json written: ' + cfgPath);
|
|
1203
1648
|
|
|
1204
1649
|
if (!hasBin('mcporter')) {
|
|
1205
|
-
api.logger.warn('[moltbank] ✗ mcporter not in PATH — cannot
|
|
1650
|
+
api.logger.warn('[moltbank] ✗ mcporter not in PATH — cannot validate local config usage');
|
|
1206
1651
|
return;
|
|
1207
1652
|
}
|
|
1208
1653
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
api.logger.info('[moltbank] registering MoltBank in mcporter (scope: home)...');
|
|
1215
|
-
|
|
1216
|
-
const addCmd =
|
|
1217
|
-
`mcporter config add MoltBank ` +
|
|
1218
|
-
`--url "${appBaseUrl}/api/mcp" ` +
|
|
1219
|
-
`--transport sse ` +
|
|
1220
|
-
`--header "Authorization=Bearer \${MOLTBANK}" ` +
|
|
1221
|
-
`--header "Content-Type=application/json" ` +
|
|
1222
|
-
`--description "MoltBank stablecoin treasury MCP server powered by Fondu" ` +
|
|
1223
|
-
`--scope home`;
|
|
1224
|
-
|
|
1225
|
-
const result = run(addCmd, { silent: false });
|
|
1226
|
-
|
|
1227
|
-
if (result.ok) {
|
|
1228
|
-
api.logger.info('[moltbank] ✓ MoltBank registered in mcporter');
|
|
1229
|
-
const list = run('mcporter config list', { silent: true });
|
|
1230
|
-
api.logger.info('[moltbank] mcporter config list: ' + list.stdout);
|
|
1231
|
-
} else {
|
|
1232
|
-
api.logger.warn('[moltbank] ✗ mcporter config add failed: ' + result.stderr);
|
|
1233
|
-
}
|
|
1654
|
+
removeHomeScopedMoltBankConfig(api);
|
|
1655
|
+
api.logger.info('[moltbank] ✓ MoltBank will use skill-local mcporter config only');
|
|
1234
1656
|
}
|
|
1235
1657
|
|
|
1236
1658
|
// ─── sandbox env vars ────────────────────────────────────────────────────────
|
|
@@ -1285,7 +1707,7 @@ export function injectSandboxEnv(skillDir: string, api: LoggerApi): boolean {
|
|
|
1285
1707
|
|
|
1286
1708
|
if (!privateKey) {
|
|
1287
1709
|
api.logger.info('[moltbank] x402_signer_private_key not found — generating EOA signer...');
|
|
1288
|
-
const initResult =
|
|
1710
|
+
const initResult = runCommand(process.execPath, ['./scripts/init-openclaw-signer.mjs'], {
|
|
1289
1711
|
cwd: skillDir,
|
|
1290
1712
|
silent: true,
|
|
1291
1713
|
env: {
|
|
@@ -1408,42 +1830,72 @@ export function configureSandbox(api: LoggerApi): boolean {
|
|
|
1408
1830
|
export function recreateSandboxAndRestart(api: LoggerApi) {
|
|
1409
1831
|
api.logger.info('[moltbank] recreating sandbox containers...');
|
|
1410
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
|
+
|
|
1411
1840
|
setTimeout(() => {
|
|
1412
1841
|
api.logger.info('[moltbank] stopping gateway...');
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
const stillRunning =
|
|
1416
|
-
if (stillRunning.
|
|
1417
|
-
api.logger.info(`[moltbank] gateway still running (pids: ${stillRunning.
|
|
1418
|
-
|
|
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
|
+
}
|
|
1419
1854
|
api.logger.info('[moltbank] ✓ gateway process killed');
|
|
1420
1855
|
} else {
|
|
1421
1856
|
api.logger.info('[moltbank] ✓ gateway stopped cleanly');
|
|
1422
1857
|
}
|
|
1423
1858
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
}
|
|
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
|
+
}
|
|
1435
1869
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1870
|
+
api.logger.info('[moltbank] restarting gateway...');
|
|
1871
|
+
runCommand('openclaw', ['gateway'], { silent: true });
|
|
1872
|
+
}, 2000);
|
|
1438
1873
|
}, 8000);
|
|
1439
1874
|
}
|
|
1440
1875
|
|
|
1876
|
+
function restartGatewayAfterHostSetup(api: LoggerApi): boolean {
|
|
1877
|
+
api.logger.info('[moltbank] restarting gateway to refresh host skill state...');
|
|
1878
|
+
const restart = runCommand('openclaw', ['gateway', 'restart'], { silent: true, timeoutMs: 30000 });
|
|
1879
|
+
if (restart.ok) {
|
|
1880
|
+
api.logger.info('[moltbank] ✓ gateway restarted');
|
|
1881
|
+
return true;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
const combined = `${restart.stderr}\n${restart.stdout}`.trim();
|
|
1885
|
+
const detail = lastNonEmptyLine(combined);
|
|
1886
|
+
if (detail) {
|
|
1887
|
+
api.logger.warn(`[moltbank] gateway restart detail: ${detail}`);
|
|
1888
|
+
}
|
|
1889
|
+
api.logger.warn('[moltbank] could not restart gateway automatically; run `openclaw gateway restart`');
|
|
1890
|
+
return false;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1441
1893
|
// ─── main setup ───────────────────────────────────────────────────────────────
|
|
1442
1894
|
|
|
1443
1895
|
export async function runSetup(
|
|
1444
1896
|
cfg: MoltbankPluginConfig,
|
|
1445
1897
|
api: LoggerApi,
|
|
1446
|
-
options: { authWaitMode?: AuthWaitMode } = {}
|
|
1898
|
+
options: { authWaitMode?: AuthWaitMode; restartGatewayOnSuccess?: boolean } = {}
|
|
1447
1899
|
) {
|
|
1448
1900
|
let hostReady = false;
|
|
1449
1901
|
const appBaseUrl = getAppBaseUrl(cfg);
|
|
@@ -1452,6 +1904,7 @@ export async function runSetup(
|
|
|
1452
1904
|
const sandbox = isSandboxEnabled();
|
|
1453
1905
|
const skillDir = getSkillDir(cfg);
|
|
1454
1906
|
const waitForAuth = (options.authWaitMode ?? 'blocking') === 'blocking';
|
|
1907
|
+
const restartGatewayOnSuccess = Boolean(options.restartGatewayOnSuccess);
|
|
1455
1908
|
|
|
1456
1909
|
api.logger.info(`[moltbank] ══════════════════════════════════════`);
|
|
1457
1910
|
api.logger.info(`[moltbank] MoltBank setup starting`);
|
|
@@ -1467,9 +1920,10 @@ export async function runSetup(
|
|
|
1467
1920
|
|
|
1468
1921
|
api.logger.info('[moltbank] [sandbox 0/10] ensuring mcporter on host...');
|
|
1469
1922
|
ensureMcporter(api);
|
|
1923
|
+
ensureJq(api);
|
|
1470
1924
|
|
|
1471
1925
|
api.logger.info('[moltbank] [sandbox 1/10] installing skill files...');
|
|
1472
|
-
const skillInstalled = ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'sandbox');
|
|
1926
|
+
const skillInstalled = await ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'sandbox');
|
|
1473
1927
|
if (!skillInstalled) {
|
|
1474
1928
|
api.logger.warn('[moltbank] skill install failed — aborting sandbox setup');
|
|
1475
1929
|
return;
|
|
@@ -1521,17 +1975,13 @@ export async function runSetup(
|
|
|
1521
1975
|
} else {
|
|
1522
1976
|
api.logger.info('[moltbank] sandbox unchanged — no restart needed');
|
|
1523
1977
|
}
|
|
1524
|
-
|
|
1525
|
-
api.logger.info('[moltbank] [sandbox 11/11] verifying authenticated balance read...');
|
|
1526
|
-
if (!verifyMoltbankOperationalReadiness(skillDir, api)) {
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
1978
|
} else {
|
|
1530
1979
|
api.logger.info('[moltbank] [host 1/8] ensuring mcporter on host...');
|
|
1531
1980
|
ensureMcporter(api);
|
|
1981
|
+
ensureJq(api);
|
|
1532
1982
|
|
|
1533
1983
|
api.logger.info('[moltbank] [host 2/8] installing skill files...');
|
|
1534
|
-
const installed = ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'host');
|
|
1984
|
+
const installed = await ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'host');
|
|
1535
1985
|
if (!installed) {
|
|
1536
1986
|
api.logger.warn('[moltbank] host setup aborted: skill install failed. Verify install.sh/base URL and retry.');
|
|
1537
1987
|
return;
|
|
@@ -1568,13 +2018,104 @@ export async function runSetup(
|
|
|
1568
2018
|
api.logger.info('[moltbank] [host 7/8] writing/registering mcporter config...');
|
|
1569
2019
|
ensureMcporterConfig(skillDir, appBaseUrl, api);
|
|
1570
2020
|
|
|
1571
|
-
api.logger.info('[moltbank] [host 8/8]
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
2021
|
+
api.logger.info('[moltbank] [host 8/8] running wrapper smoke test...');
|
|
2022
|
+
const smokeTimeoutMs = IS_WIN ? 15000 : 20000;
|
|
2023
|
+
let smokeOk = false;
|
|
2024
|
+
|
|
2025
|
+
if (IS_WIN) {
|
|
2026
|
+
const mcporterConfigPath = join(skillDir, 'assets', 'mcporter.json');
|
|
2027
|
+
const active = parseActiveTokenFromCredentials();
|
|
2028
|
+
const mcpSmoke = runCommand('mcporter', ['--config', mcporterConfigPath, 'list', 'MoltBank'], {
|
|
2029
|
+
cwd: skillDir,
|
|
2030
|
+
silent: true,
|
|
2031
|
+
timeoutMs: smokeTimeoutMs,
|
|
2032
|
+
env: {
|
|
2033
|
+
MOLTBANK: active.token ?? '__MOLTBANK_PLACEHOLDER__'
|
|
2034
|
+
}
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
if (mcpSmoke.ok) {
|
|
2038
|
+
api.logger.info('[moltbank] host smoke test passed (`mcporter list MoltBank`)');
|
|
2039
|
+
smokeOk = true;
|
|
2040
|
+
} else {
|
|
2041
|
+
const mcpCombined = `${mcpSmoke.stderr}\n${mcpSmoke.stdout}`.trim();
|
|
2042
|
+
const mcpCombinedLower = mcpCombined.toLowerCase();
|
|
2043
|
+
const mcpDetail = lastNonEmptyLine(mcpCombined);
|
|
2044
|
+
if (mcpDetail) {
|
|
2045
|
+
api.logger.warn(`[moltbank] smoke detail: ${mcpDetail}`);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
const timedOut = mcpSmoke.stderr.includes('ETIMEDOUT') || mcpCombinedLower.includes('timed out');
|
|
2049
|
+
if (timedOut) {
|
|
2050
|
+
api.logger.warn(
|
|
2051
|
+
`[moltbank] host MCP smoke timed out after ${Math.floor(smokeTimeoutMs / 1000)}s; running local fallback checks`
|
|
2052
|
+
);
|
|
2053
|
+
|
|
2054
|
+
const ps1Path = join(skillDir, 'scripts', 'moltbank.ps1');
|
|
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
|
+
],
|
|
2065
|
+
{
|
|
2066
|
+
cwd: skillDir,
|
|
2067
|
+
silent: true,
|
|
2068
|
+
timeoutMs: 10000,
|
|
2069
|
+
env: { MOLTBANK_WRAPPER_PATH: ps1Path }
|
|
2070
|
+
}
|
|
2071
|
+
);
|
|
2072
|
+
|
|
2073
|
+
if (fallback.ok) {
|
|
2074
|
+
api.logger.warn('[moltbank] host smoke fallback passed (wrapper prerequisites available), but MCP endpoint was not verified');
|
|
2075
|
+
smokeOk = true;
|
|
2076
|
+
} else {
|
|
2077
|
+
const fallbackCombined = `${fallback.stderr}\n${fallback.stdout}`.trim();
|
|
2078
|
+
const fallbackDetail = lastNonEmptyLine(fallbackCombined);
|
|
2079
|
+
if (fallbackDetail) {
|
|
2080
|
+
api.logger.warn(`[moltbank] fallback detail: ${fallbackDetail}`);
|
|
2081
|
+
}
|
|
2082
|
+
api.logger.warn('[moltbank] host setup incomplete: MCP smoke timed out and fallback checks failed');
|
|
2083
|
+
}
|
|
2084
|
+
} else if (
|
|
2085
|
+
mcpCombinedLower.includes('mcporter') &&
|
|
2086
|
+
(mcpCombinedLower.includes('not recognized') || mcpCombinedLower.includes('not found'))
|
|
2087
|
+
) {
|
|
2088
|
+
api.logger.warn('[moltbank] mcporter binary not found in PATH during smoke test');
|
|
2089
|
+
} else {
|
|
2090
|
+
api.logger.warn('[moltbank] host setup incomplete: smoke test failed (`mcporter list MoltBank`)');
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
} else {
|
|
2094
|
+
const smoke = runCommand(join(skillDir, 'scripts', 'moltbank.sh'), ['list', 'MoltBank'], {
|
|
2095
|
+
cwd: skillDir,
|
|
2096
|
+
silent: true,
|
|
2097
|
+
timeoutMs: smokeTimeoutMs
|
|
2098
|
+
});
|
|
2099
|
+
if (!smoke.ok) {
|
|
2100
|
+
const smokeCombined = `${smoke.stderr}\n${smoke.stdout}`.trim();
|
|
2101
|
+
const smokeDetail = lastNonEmptyLine(smokeCombined);
|
|
2102
|
+
if (smokeDetail) {
|
|
2103
|
+
api.logger.warn(`[moltbank] smoke detail: ${smokeDetail}`);
|
|
2104
|
+
}
|
|
2105
|
+
api.logger.warn('[moltbank] host setup incomplete: smoke test failed (`moltbank list MoltBank` via platform script)');
|
|
2106
|
+
} else {
|
|
2107
|
+
api.logger.info('[moltbank] host smoke test passed (`moltbank list MoltBank`)');
|
|
2108
|
+
smokeOk = true;
|
|
2109
|
+
}
|
|
1575
2110
|
}
|
|
1576
2111
|
|
|
2112
|
+
hostReady = smokeOk;
|
|
2113
|
+
|
|
1577
2114
|
api.logger.info('[moltbank] host mode — setup completed with onboarding flow');
|
|
2115
|
+
|
|
2116
|
+
if (hostReady && restartGatewayOnSuccess) {
|
|
2117
|
+
restartGatewayAfterHostSetup(api);
|
|
2118
|
+
}
|
|
1578
2119
|
}
|
|
1579
2120
|
|
|
1580
2121
|
api.logger.info(`[moltbank] ══════════════════════════════════════`);
|
|
@@ -1606,10 +2147,11 @@ export async function runSetup(
|
|
|
1606
2147
|
`[moltbank] setupCommand: ${finalCmd.includes('node_22.x') && !finalCmd.includes('/home/') ? '✓ ok (no host paths)' : '✗ may be corrupted'}`
|
|
1607
2148
|
);
|
|
1608
2149
|
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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 };
|
|
1613
2155
|
if (!containerCheck.ok || !containerCheck.stdout) {
|
|
1614
2156
|
api.logger.info(`[moltbank] container: not running yet — will be created on first agent message`);
|
|
1615
2157
|
} else if (containerCheck.stdout.trim() === 'bridge') {
|
|
@@ -1650,9 +2192,10 @@ export default function register(api: PluginApi) {
|
|
|
1650
2192
|
.description('Re-run MoltBank setup')
|
|
1651
2193
|
.action(async () => {
|
|
1652
2194
|
const verbose = process.argv.includes('--verbose');
|
|
2195
|
+
const restartGatewayOnSuccess = getSetupGatewayRestartEnabled(IS_WIN);
|
|
1653
2196
|
const logger = createSetupCommandLogger({ verbose, statusCommand: 'openclaw moltbank status' });
|
|
1654
2197
|
try {
|
|
1655
|
-
await runSetup(cfg, logger, { authWaitMode: 'blocking' });
|
|
2198
|
+
await runSetup(cfg, logger, { authWaitMode: 'blocking', restartGatewayOnSuccess });
|
|
1656
2199
|
} finally {
|
|
1657
2200
|
logger.finish();
|
|
1658
2201
|
}
|
|
@@ -1664,9 +2207,10 @@ export default function register(api: PluginApi) {
|
|
|
1664
2207
|
.description('Re-run full MoltBank setup and wait for OAuth approval')
|
|
1665
2208
|
.action(async () => {
|
|
1666
2209
|
const verbose = process.argv.includes('--verbose');
|
|
2210
|
+
const restartGatewayOnSuccess = getSetupGatewayRestartEnabled(IS_WIN);
|
|
1667
2211
|
const logger = createSetupCommandLogger({ verbose, statusCommand: 'openclaw moltbank status' });
|
|
1668
2212
|
try {
|
|
1669
|
-
await runSetup(cfg, logger, { authWaitMode: 'blocking' });
|
|
2213
|
+
await runSetup(cfg, logger, { authWaitMode: 'blocking', restartGatewayOnSuccess });
|
|
1670
2214
|
} finally {
|
|
1671
2215
|
logger.finish();
|
|
1672
2216
|
}
|