@moltbankhq/openclaw 0.1.8 → 0.1.10

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 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', tsxImportPath, cliPath, ...process.argv.slice(2)], {
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,5 +1,5 @@
1
- import { execSync, spawn, spawnSync } from 'child_process';
2
- import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
1
+ import { execSync, spawn } from 'child_process';
2
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
4
  import { homedir } from 'os';
5
5
  const IS_WIN = process.platform === 'win32';
@@ -306,33 +306,68 @@ export function getSetupAuthWaitMode(defaultMode: AuthWaitMode): AuthWaitMode {
306
306
  return defaultMode;
307
307
  }
308
308
 
309
+ function parseBooleanEnv(raw: string): boolean | null {
310
+ const normalized = raw.trim().toLowerCase();
311
+ if (!normalized) return null;
312
+
313
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
314
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
315
+ return null;
316
+ }
317
+
318
+ export function getSetupGatewayRestartEnabled(defaultEnabled: boolean): boolean {
319
+ const parsed = parseBooleanEnv(asString(process.env.MOLTBANK_SETUP_RESTART_GATEWAY));
320
+ return parsed === null ? defaultEnabled : parsed;
321
+ }
322
+
323
+ function getExecOutputText(error: unknown, field: 'stderr' | 'stdout'): string {
324
+ if (!isRecord(error) || !(field in error)) return '';
325
+
326
+ const raw = (error as { stderr?: unknown; stdout?: unknown })[field];
327
+ if (typeof raw === 'string') return raw.trim();
328
+ if (Buffer.isBuffer(raw)) return raw.toString('utf8').trim();
329
+
330
+ if (isRecord(raw) && 'toString' in raw && typeof (raw as { toString?: unknown }).toString === 'function') {
331
+ const text = (raw as { toString: () => string }).toString().trim();
332
+ if (text && text !== '[object Object]') return text;
333
+ }
334
+
335
+ return '';
336
+ }
337
+
309
338
  function getExecErrorMessage(error: unknown): string {
310
- if (isRecord(error) && 'stderr' in error) {
311
- const stderr = (error as { stderr?: unknown }).stderr;
312
- if (typeof stderr === 'string') {
313
- return stderr.trim();
314
- }
315
- if (isRecord(stderr) && 'toString' in stderr && typeof (stderr as { toString?: unknown }).toString === 'function') {
316
- return (stderr as { toString: () => string }).toString().trim();
317
- }
339
+ const stderr = getExecOutputText(error, 'stderr');
340
+ if (stderr) return stderr;
341
+
342
+ const stdout = getExecOutputText(error, 'stdout');
343
+ if (stdout) return stdout;
344
+
345
+ if (isRecord(error) && 'message' in error) {
346
+ const message = asString((error as { message?: unknown }).message).trim();
347
+ if (message) return message;
318
348
  }
349
+
319
350
  return String(error);
320
351
  }
321
352
 
322
- function run(cmd: string, opts: { cwd?: string; silent?: boolean; env?: Record<string, string | undefined> } = {}) {
353
+ function run(
354
+ cmd: string,
355
+ opts: { cwd?: string; silent?: boolean; env?: Record<string, string | undefined>; timeoutMs?: number } = {}
356
+ ) {
323
357
  try {
324
358
  const mergedEnv: NodeJS.ProcessEnv = { ...process.env, ...(opts.env ?? {}) };
325
359
  const out = execSync(cmd, {
326
360
  cwd: opts.cwd,
327
361
  stdio: opts.silent ? 'pipe' : 'inherit',
328
362
  env: mergedEnv,
363
+ timeout: opts.timeoutMs,
329
364
  shell: IS_WIN ? (process.env.ComSpec ?? 'cmd.exe') : '/bin/bash'
330
365
  });
331
366
  return { ok: true, stdout: out?.toString().trim() ?? '' };
332
367
  } catch (e: unknown) {
333
368
  return {
334
369
  ok: false,
335
- stdout: '',
370
+ stdout: getExecOutputText(e, 'stdout'),
336
371
  stderr: getExecErrorMessage(e)
337
372
  };
338
373
  }
@@ -342,6 +377,171 @@ function hasBin(bin: string) {
342
377
  return run(IS_WIN ? `where ${bin}` : `which ${bin}`, { silent: true }).ok;
343
378
  }
344
379
 
380
+ function getJqInstallHint(): string {
381
+ if (hasBin('apt-get')) return '`sudo apt-get install -y jq`';
382
+ if (hasBin('dnf')) return '`sudo dnf install -y jq`';
383
+ if (hasBin('yum')) return '`sudo yum install -y jq`';
384
+ if (hasBin('pacman')) return '`sudo pacman -S --noconfirm jq`';
385
+ if (hasBin('apk')) return '`sudo apk add jq`';
386
+ if (hasBin('zypper')) return '`sudo zypper install -y jq`';
387
+ if (hasBin('brew')) return '`brew install jq`';
388
+ return 'using your distro package manager';
389
+ }
390
+
391
+ function splitNonEmptyLines(text: string): string[] {
392
+ return (text || '')
393
+ .split(/\r?\n/)
394
+ .map((line) => line.trim())
395
+ .filter(Boolean);
396
+ }
397
+
398
+ function getNpmGlobalBinDir(): string {
399
+ const prefix = run('npm config get prefix', { silent: true });
400
+ if (!prefix.ok) return '';
401
+
402
+ const value = prefix.stdout.trim();
403
+ if (!value || value === 'undefined' || value === 'null') return '';
404
+ return IS_WIN ? value : join(value, 'bin');
405
+ }
406
+
407
+ function findFirstMatchingFile(rootDir: string, predicate: (name: string) => boolean, maxDepth = 3): string {
408
+ if (!rootDir || !existsSync(rootDir)) return '';
409
+
410
+ const queue: Array<{ dir: string; depth: number }> = [{ dir: rootDir, depth: 0 }];
411
+ while (queue.length) {
412
+ const current = queue.shift();
413
+ if (!current) break;
414
+
415
+ let entries: ReturnType<typeof readdirSync>;
416
+ try {
417
+ entries = readdirSync(current.dir, { withFileTypes: true });
418
+ } catch {
419
+ continue;
420
+ }
421
+
422
+ for (const entry of entries) {
423
+ const fullPath = join(current.dir, entry.name);
424
+ if (entry.isFile() && predicate(entry.name)) {
425
+ return fullPath;
426
+ }
427
+ if (entry.isDirectory() && current.depth < maxDepth) {
428
+ queue.push({ dir: fullPath, depth: current.depth + 1 });
429
+ }
430
+ }
431
+ }
432
+
433
+ return '';
434
+ }
435
+
436
+ function findWindowsJqBinaryPath(): string {
437
+ if (!IS_WIN) return '';
438
+
439
+ const fromEnv = asString(process.env.JQ_PATH).trim();
440
+ if (fromEnv && existsSync(fromEnv)) return fromEnv;
441
+
442
+ const whereJq = run('where jq', { silent: true });
443
+ if (whereJq.ok) {
444
+ for (const line of splitNonEmptyLines(whereJq.stdout)) {
445
+ if (existsSync(line)) {
446
+ return line;
447
+ }
448
+ }
449
+ }
450
+
451
+ const localAppData = asString(process.env.LOCALAPPDATA).trim();
452
+ const appData = asString(process.env.APPDATA).trim();
453
+ const userProfile = asString(process.env.USERPROFILE).trim();
454
+ const programData = asString(process.env.ProgramData).trim();
455
+ const programFiles = asString(process.env.ProgramFiles).trim();
456
+ const chocolateyInstall = asString(process.env.ChocolateyInstall).trim();
457
+
458
+ const candidates = [
459
+ localAppData ? join(localAppData, 'Microsoft', 'WinGet', 'Links', 'jq.exe') : '',
460
+ localAppData ? join(localAppData, 'Microsoft', 'WindowsApps', 'jq.exe') : '',
461
+ localAppData ? join(localAppData, 'Programs', 'jq', 'jq.exe') : '',
462
+ localAppData ? join(localAppData, 'Programs', 'jqlang', 'jq.exe') : '',
463
+ appData ? join(appData, 'npm', 'jq.exe') : '',
464
+ appData ? join(appData, 'npm', 'jq.cmd') : '',
465
+ userProfile ? join(userProfile, 'scoop', 'shims', 'jq.exe') : '',
466
+ chocolateyInstall ? join(chocolateyInstall, 'bin', 'jq.exe') : '',
467
+ programData ? join(programData, 'chocolatey', 'bin', 'jq.exe') : '',
468
+ programFiles ? join(programFiles, 'jqlang', 'jq.exe') : '',
469
+ programFiles ? join(programFiles, 'jq', 'jq.exe') : ''
470
+ ].filter(Boolean);
471
+
472
+ for (const candidate of candidates) {
473
+ if (existsSync(candidate)) {
474
+ return candidate;
475
+ }
476
+ }
477
+
478
+ const isJqExecutable = (name: string): boolean => {
479
+ const lower = name.toLowerCase();
480
+ return lower === 'jq.exe' || lower === 'jq-windows-amd64.exe' || (lower.startsWith('jq') && lower.endsWith('.exe'));
481
+ };
482
+
483
+ const wingetPackagesRoot = localAppData ? join(localAppData, 'Microsoft', 'WinGet', 'Packages') : '';
484
+ if (wingetPackagesRoot && existsSync(wingetPackagesRoot)) {
485
+ let packageEntries: ReturnType<typeof readdirSync> = [];
486
+ try {
487
+ packageEntries = readdirSync(wingetPackagesRoot, { withFileTypes: true });
488
+ } catch {
489
+ packageEntries = [];
490
+ }
491
+
492
+ for (const entry of packageEntries) {
493
+ if (!entry.isDirectory()) continue;
494
+ const lower = entry.name.toLowerCase();
495
+ if (!lower.includes('jqlang.jq') && !lower.startsWith('jq')) continue;
496
+
497
+ const found = findFirstMatchingFile(join(wingetPackagesRoot, entry.name), isJqExecutable, 3);
498
+ if (found) return found;
499
+ }
500
+
501
+ const fallback = findFirstMatchingFile(wingetPackagesRoot, isJqExecutable, 2);
502
+ if (fallback) return fallback;
503
+ }
504
+
505
+ return '';
506
+ }
507
+
508
+ function ensureWindowsJqShim(api: LoggerApi): boolean {
509
+ if (!IS_WIN) return false;
510
+
511
+ const jqSourcePath = findWindowsJqBinaryPath();
512
+ if (!jqSourcePath) return false;
513
+
514
+ const npmBinDir = getNpmGlobalBinDir();
515
+ if (!npmBinDir) return false;
516
+
517
+ const shimPath = join(npmBinDir, 'jq.cmd');
518
+ try {
519
+ mkdirSync(npmBinDir, { recursive: true });
520
+ if (jqSourcePath.toLowerCase() !== shimPath.toLowerCase()) {
521
+ const escapedSource = jqSourcePath.replace(/"/g, '""');
522
+ const shimBody = `@echo off\r\n"${escapedSource}" %*\r\n`;
523
+ writeFileSync(shimPath, shimBody, 'utf8');
524
+ }
525
+ } catch (e) {
526
+ api.logger.warn('[moltbank] could not create jq shim in npm global bin: ' + String(e));
527
+ return false;
528
+ }
529
+
530
+ if (!hasBin('jq')) return false;
531
+
532
+ const v = run('jq --version', { silent: true });
533
+ api.logger.info(`[moltbank] ✓ jq available via npm global bin shim (${v.stdout || 'unknown version'})`);
534
+ return true;
535
+ }
536
+
537
+ function lastNonEmptyLine(text: string): string {
538
+ const lines = text
539
+ .split(/\r?\n/)
540
+ .map((line) => line.trim())
541
+ .filter(Boolean);
542
+ return lines.length ? lines[lines.length - 1] : '';
543
+ }
544
+
345
545
  function getWorkspace(): string {
346
546
  return process.env.OPENCLAW_WORKSPACE || join(homedir(), '.openclaw', 'workspace');
347
547
  }
@@ -478,6 +678,105 @@ function ensureMcporter(api: LoggerApi) {
478
678
  }
479
679
  }
480
680
 
681
+ function ensureJq(api: LoggerApi) {
682
+ if (hasBin('jq')) {
683
+ const v = run('jq --version', { silent: true });
684
+ api.logger.info(`[moltbank] ✓ jq already installed (${v.stdout || 'unknown version'})`);
685
+ return;
686
+ }
687
+
688
+ if (!IS_WIN) {
689
+ api.logger.warn('[moltbank] ✗ jq not found in PATH (required by MoltBank wrapper on host mode)');
690
+ api.logger.warn(`[moltbank] install jq (${getJqInstallHint()}) and rerun setup`);
691
+ return;
692
+ }
693
+
694
+ if (ensureWindowsJqShim(api)) {
695
+ api.logger.info('[moltbank] ✓ jq found on disk and shimmed into current PATH scope');
696
+ return;
697
+ }
698
+
699
+ api.logger.info('[moltbank] jq not found — attempting Windows install...');
700
+
701
+ const installers: Array<{ name: string; checkCmd: string; installCmd: string }> = [
702
+ {
703
+ name: 'winget',
704
+ checkCmd: 'where winget',
705
+ installCmd: 'winget install -e --id jqlang.jq --accept-package-agreements --accept-source-agreements --silent'
706
+ },
707
+ {
708
+ name: 'scoop',
709
+ checkCmd: 'where scoop',
710
+ installCmd: 'scoop install jq'
711
+ },
712
+ {
713
+ name: 'choco',
714
+ checkCmd: 'where choco',
715
+ installCmd: 'choco install jq -y'
716
+ }
717
+ ];
718
+
719
+ for (const installer of installers) {
720
+ if (!run(installer.checkCmd, { silent: true }).ok) continue;
721
+
722
+ api.logger.info(`[moltbank] trying jq install via ${installer.name}...`);
723
+ const install = run(installer.installCmd, { timeoutMs: 180000 });
724
+ const installOutput = `${install.stdout}\n${install.stderr}`;
725
+ const installOutputLower = installOutput.toLowerCase();
726
+ const alreadyInstalledOrNoUpdate =
727
+ installOutputLower.includes('already installed') ||
728
+ installOutputLower.includes('paquete existente ya instalado') ||
729
+ installOutputLower.includes('no update available') ||
730
+ installOutputLower.includes('no updates available') ||
731
+ installOutputLower.includes('no newer package versions are available') ||
732
+ installOutputLower.includes('no se ha encontrado ninguna actualización disponible') ||
733
+ installOutputLower.includes('no hay versiones más recientes');
734
+
735
+ if (!install.ok) {
736
+ if (!alreadyInstalledOrNoUpdate) {
737
+ api.logger.warn(`[moltbank] jq install via ${installer.name} failed: ${install.stderr}`);
738
+ continue;
739
+ }
740
+ api.logger.info(`[moltbank] jq already installed via ${installer.name}; validating availability...`);
741
+ }
742
+
743
+ if (hasBin('jq')) {
744
+ const v = run('jq --version', { silent: true });
745
+ api.logger.info(`[moltbank] ✓ jq installed via ${installer.name} (${v.stdout || 'unknown version'})`);
746
+ return;
747
+ }
748
+
749
+ if (ensureWindowsJqShim(api)) {
750
+ api.logger.info(`[moltbank] ✓ jq installed via ${installer.name} and made available without shell restart`);
751
+ return;
752
+ }
753
+
754
+ if (alreadyInstalledOrNoUpdate) {
755
+ api.logger.warn(`[moltbank] jq appears installed via ${installer.name}, but is not visible in the current PATH`);
756
+ api.logger.warn('[moltbank] close and reopen PowerShell, then run: `where jq`');
757
+ return;
758
+ }
759
+
760
+ const installOutputLowerCheck = installOutputLower;
761
+ const shellRestartNeeded =
762
+ installOutputLowerCheck.includes('variable de entorno path modificada') ||
763
+ installOutputLowerCheck.includes('path modificada') ||
764
+ installOutputLowerCheck.includes('path modified') ||
765
+ installOutputLowerCheck.includes('reinicie el shell') ||
766
+ installOutputLowerCheck.includes('restart the shell') ||
767
+ installOutputLowerCheck.includes('restart your shell');
768
+
769
+ if (shellRestartNeeded) {
770
+ api.logger.warn(`[moltbank] jq was installed via ${installer.name}, but this terminal has a stale PATH`);
771
+ api.logger.warn('[moltbank] close and reopen PowerShell, then run: `jq --version`');
772
+ return;
773
+ }
774
+ }
775
+
776
+ api.logger.warn('[moltbank] ✗ jq is still missing after auto-install attempts');
777
+ api.logger.warn('[moltbank] install manually on Windows: winget install jqlang.jq');
778
+ }
779
+
481
780
  function ensureWrapperExecutable(skillDir: string, api: LoggerApi): void {
482
781
  if (IS_WIN) return;
483
782
  const wrapperPath = join(skillDir, 'scripts', 'moltbank.sh');
@@ -699,6 +998,31 @@ function isMoltBankRegistered(): boolean {
699
998
  return result.stdout.toLowerCase().includes('moltbank');
700
999
  }
701
1000
 
1001
+ function removeHomeScopedMoltBankConfig(api: LoggerApi): boolean {
1002
+ const configPath = join(homedir(), '.mcporter', 'mcporter.json');
1003
+ if (!existsSync(configPath)) {
1004
+ return false;
1005
+ }
1006
+
1007
+ try {
1008
+ const parsed = JSON.parse(readFileSync(configPath, 'utf8')) as unknown;
1009
+ if (!isRecord(parsed)) return false;
1010
+
1011
+ const mcpServers = parsed.mcpServers;
1012
+ if (!isRecord(mcpServers) || !('MoltBank' in mcpServers)) {
1013
+ return false;
1014
+ }
1015
+
1016
+ delete mcpServers.MoltBank;
1017
+ writeFileSync(configPath, JSON.stringify(parsed, null, 2) + '\n', 'utf8');
1018
+ api.logger.info('[moltbank] ✓ removed stale home-scoped mcporter MoltBank config');
1019
+ return true;
1020
+ } catch (e) {
1021
+ api.logger.warn('[moltbank] could not clean home-scoped mcporter MoltBank config: ' + String(e));
1022
+ return false;
1023
+ }
1024
+ }
1025
+
702
1026
  function parseActiveTokenFromCredentials(): {
703
1027
  ok: boolean;
704
1028
  activeOrg?: string;
@@ -763,84 +1087,6 @@ function readPendingOauthCode(skillDir: string): {
763
1087
  }
764
1088
  }
765
1089
 
766
- function clearPendingOauthCode(skillDir: string): void {
767
- const pendingPath = join(skillDir, '.oauth_device_code.json');
768
- try {
769
- if (existsSync(pendingPath)) unlinkSync(pendingPath);
770
- } catch {
771
- // ignore
772
- }
773
- }
774
-
775
- function runProcess(
776
- command: string,
777
- args: string[],
778
- opts: { cwd?: string; silent?: boolean; env?: Record<string, string | undefined> } = {}
779
- ) {
780
- try {
781
- const mergedEnv: NodeJS.ProcessEnv = { ...process.env, ...(opts.env ?? {}) };
782
- const result = spawnSync(command, args, {
783
- cwd: opts.cwd,
784
- env: mergedEnv,
785
- encoding: 'utf8',
786
- stdio: opts.silent ? 'pipe' : 'inherit'
787
- });
788
- return {
789
- ok: result.status === 0,
790
- stdout: (result.stdout ?? '').toString().trim(),
791
- stderr: (result.stderr ?? '').toString().trim()
792
- };
793
- } catch (e: unknown) {
794
- return {
795
- ok: false,
796
- stdout: '',
797
- stderr: getExecErrorMessage(e)
798
- };
799
- }
800
- }
801
-
802
- function runMoltbankWrapper(skillDir: string, args: string[]) {
803
- if (IS_WIN) {
804
- const ps1Path = join(skillDir, 'scripts', 'moltbank.ps1');
805
- return runProcess('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps1Path, ...args], {
806
- cwd: skillDir,
807
- silent: true
808
- });
809
- }
810
-
811
- return runProcess(join(skillDir, 'scripts', 'moltbank.sh'), args, {
812
- cwd: skillDir,
813
- silent: true
814
- });
815
- }
816
-
817
- function verifyMoltbankOperationalReadiness(skillDir: string, api: LoggerApi): boolean {
818
- const existing = parseActiveTokenFromCredentials();
819
- if (!existing.ok || !existing.activeOrg) {
820
- api.logger.warn('[moltbank] ✗ setup incomplete: authenticated org could not be resolved for balance verification');
821
- return false;
822
- }
823
-
824
- const verification = runMoltbankWrapper(skillDir, [
825
- 'call',
826
- 'MoltBank.get_balance',
827
- `organizationName=${existing.activeOrg}`,
828
- 'date=today'
829
- ]);
830
-
831
- if (verification.ok) {
832
- api.logger.info(`[moltbank] ✓ balance verification passed (org: ${existing.activeOrg})`);
833
- return true;
834
- }
835
-
836
- api.logger.warn('[moltbank] ✗ setup incomplete: auth is linked, but balance verification failed (`MoltBank.get_balance`)');
837
- const detail = verification.stderr || verification.stdout;
838
- if (detail) {
839
- api.logger.warn('[moltbank] verification detail: ' + detail);
840
- }
841
- return false;
842
- }
843
-
844
1090
  function startBackgroundOauthPoll(
845
1091
  skillDir: string,
846
1092
  appBaseUrl: string,
@@ -892,7 +1138,12 @@ function startBackgroundOauthPoll(
892
1138
  oauthPollers.delete(skillDir);
893
1139
  const after = parseActiveTokenFromCredentials();
894
1140
  if (after.ok) {
895
- clearPendingOauthCode(skillDir);
1141
+ const pendingPath = join(skillDir, '.oauth_device_code.json');
1142
+ try {
1143
+ if (existsSync(pendingPath)) unlinkSync(pendingPath);
1144
+ } catch {
1145
+ // ignore
1146
+ }
896
1147
  api.logger.info(`[moltbank] ✓ background onboarding completed (active org: ${after.activeOrg})`);
897
1148
  try {
898
1149
  startBackgroundFinalizeAfterAuth(skillDir, appBaseUrl, api);
@@ -972,8 +1223,6 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
972
1223
  const existing = parseActiveTokenFromCredentials();
973
1224
  if (existing.ok) {
974
1225
  stopBackgroundOauthPoll(skillDir);
975
- stopBackgroundFinalize(skillDir);
976
- clearPendingOauthCode(skillDir);
977
1226
  api.logger.info(`[moltbank] ✓ credentials.json already available (active org: ${existing.activeOrg})`);
978
1227
  return true;
979
1228
  }
@@ -1112,7 +1361,11 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
1112
1361
 
1113
1362
  if (oauthError === 'invalid_grant') {
1114
1363
  api.logger.warn('[moltbank] ✗ onboarding code expired or already consumed (invalid_grant)');
1115
- clearPendingOauthCode(skillDir);
1364
+ try {
1365
+ if (existsSync(pendingPath)) unlinkSync(pendingPath);
1366
+ } catch {
1367
+ // ignore
1368
+ }
1116
1369
  } else {
1117
1370
  api.logger.warn('[moltbank] ✗ onboarding poll failed or timed out');
1118
1371
  }
@@ -1129,7 +1382,11 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
1129
1382
  return false;
1130
1383
  }
1131
1384
 
1132
- clearPendingOauthCode(skillDir);
1385
+ try {
1386
+ if (existsSync(pendingPath)) unlinkSync(pendingPath);
1387
+ } catch {
1388
+ // ignore
1389
+ }
1133
1390
 
1134
1391
  api.logger.info(`[moltbank] ✓ onboarding completed (active org: ${after.activeOrg})`);
1135
1392
  return true;
@@ -1138,10 +1395,7 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
1138
1395
  export function printAuthStatus(skillDir: string, appBaseUrl: string, api: LoggerApi): void {
1139
1396
  const now = Math.floor(Date.now() / 1000);
1140
1397
  const existing = parseActiveTokenFromCredentials();
1141
- if (existing.ok) {
1142
- clearPendingOauthCode(skillDir);
1143
- }
1144
- const pending = existing.ok ? null : readPendingOauthCode(skillDir);
1398
+ const pending = readPendingOauthCode(skillDir);
1145
1399
  const poller = oauthPollers.get(skillDir);
1146
1400
  const finalizer = backgroundFinalizers.get(skillDir);
1147
1401
  const pollerRunning = Boolean(poller && poller.exitCode === null && !poller.killed);
@@ -1192,35 +1446,12 @@ export function ensureMcporterConfig(skillDir: string, appBaseUrl: string, api:
1192
1446
  api.logger.info('[moltbank] ✓ mcporter.json written: ' + cfgPath);
1193
1447
 
1194
1448
  if (!hasBin('mcporter')) {
1195
- api.logger.warn('[moltbank] ✗ mcporter not in PATH — cannot register');
1449
+ api.logger.warn('[moltbank] ✗ mcporter not in PATH — cannot validate local config usage');
1196
1450
  return;
1197
1451
  }
1198
1452
 
1199
- if (isMoltBankRegistered()) {
1200
- api.logger.info('[moltbank] ✓ MoltBank already registered in mcporter');
1201
- return;
1202
- }
1203
-
1204
- api.logger.info('[moltbank] registering MoltBank in mcporter (scope: home)...');
1205
-
1206
- const addCmd =
1207
- `mcporter config add MoltBank ` +
1208
- `--url "${appBaseUrl}/api/mcp" ` +
1209
- `--transport sse ` +
1210
- `--header "Authorization=Bearer \${MOLTBANK}" ` +
1211
- `--header "Content-Type=application/json" ` +
1212
- `--description "MoltBank stablecoin treasury MCP server powered by Fondu" ` +
1213
- `--scope home`;
1214
-
1215
- const result = run(addCmd, { silent: false });
1216
-
1217
- if (result.ok) {
1218
- api.logger.info('[moltbank] ✓ MoltBank registered in mcporter');
1219
- const list = run('mcporter config list', { silent: true });
1220
- api.logger.info('[moltbank] mcporter config list: ' + list.stdout);
1221
- } else {
1222
- api.logger.warn('[moltbank] ✗ mcporter config add failed: ' + result.stderr);
1223
- }
1453
+ removeHomeScopedMoltBankConfig(api);
1454
+ api.logger.info('[moltbank] ✓ MoltBank will use skill-local mcporter config only');
1224
1455
  }
1225
1456
 
1226
1457
  // ─── sandbox env vars ────────────────────────────────────────────────────────
@@ -1428,12 +1659,29 @@ export function recreateSandboxAndRestart(api: LoggerApi) {
1428
1659
  }, 8000);
1429
1660
  }
1430
1661
 
1662
+ function restartGatewayAfterHostSetup(api: LoggerApi): boolean {
1663
+ api.logger.info('[moltbank] restarting gateway to refresh host skill state...');
1664
+ const restart = run('openclaw gateway restart', { silent: true, timeoutMs: 30000 });
1665
+ if (restart.ok) {
1666
+ api.logger.info('[moltbank] ✓ gateway restarted');
1667
+ return true;
1668
+ }
1669
+
1670
+ const combined = `${restart.stderr}\n${restart.stdout}`.trim();
1671
+ const detail = lastNonEmptyLine(combined);
1672
+ if (detail) {
1673
+ api.logger.warn(`[moltbank] gateway restart detail: ${detail}`);
1674
+ }
1675
+ api.logger.warn('[moltbank] could not restart gateway automatically; run `openclaw gateway restart`');
1676
+ return false;
1677
+ }
1678
+
1431
1679
  // ─── main setup ───────────────────────────────────────────────────────────────
1432
1680
 
1433
1681
  export async function runSetup(
1434
1682
  cfg: MoltbankPluginConfig,
1435
1683
  api: LoggerApi,
1436
- options: { authWaitMode?: AuthWaitMode } = {}
1684
+ options: { authWaitMode?: AuthWaitMode; restartGatewayOnSuccess?: boolean } = {}
1437
1685
  ) {
1438
1686
  let hostReady = false;
1439
1687
  const appBaseUrl = getAppBaseUrl(cfg);
@@ -1442,6 +1690,7 @@ export async function runSetup(
1442
1690
  const sandbox = isSandboxEnabled();
1443
1691
  const skillDir = getSkillDir(cfg);
1444
1692
  const waitForAuth = (options.authWaitMode ?? 'blocking') === 'blocking';
1693
+ const restartGatewayOnSuccess = Boolean(options.restartGatewayOnSuccess);
1445
1694
 
1446
1695
  api.logger.info(`[moltbank] ══════════════════════════════════════`);
1447
1696
  api.logger.info(`[moltbank] MoltBank setup starting`);
@@ -1457,6 +1706,7 @@ export async function runSetup(
1457
1706
 
1458
1707
  api.logger.info('[moltbank] [sandbox 0/10] ensuring mcporter on host...');
1459
1708
  ensureMcporter(api);
1709
+ ensureJq(api);
1460
1710
 
1461
1711
  api.logger.info('[moltbank] [sandbox 1/10] installing skill files...');
1462
1712
  const skillInstalled = ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'sandbox');
@@ -1511,14 +1761,10 @@ export async function runSetup(
1511
1761
  } else {
1512
1762
  api.logger.info('[moltbank] sandbox unchanged — no restart needed');
1513
1763
  }
1514
-
1515
- api.logger.info('[moltbank] [sandbox 11/11] verifying authenticated balance read...');
1516
- if (!verifyMoltbankOperationalReadiness(skillDir, api)) {
1517
- return;
1518
- }
1519
1764
  } else {
1520
1765
  api.logger.info('[moltbank] [host 1/8] ensuring mcporter on host...');
1521
1766
  ensureMcporter(api);
1767
+ ensureJq(api);
1522
1768
 
1523
1769
  api.logger.info('[moltbank] [host 2/8] installing skill files...');
1524
1770
  const installed = ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'host');
@@ -1558,13 +1804,96 @@ export async function runSetup(
1558
1804
  api.logger.info('[moltbank] [host 7/8] writing/registering mcporter config...');
1559
1805
  ensureMcporterConfig(skillDir, appBaseUrl, api);
1560
1806
 
1561
- api.logger.info('[moltbank] [host 8/8] verifying authenticated balance read...');
1562
- hostReady = verifyMoltbankOperationalReadiness(skillDir, api);
1563
- if (!hostReady) {
1564
- return;
1807
+ api.logger.info('[moltbank] [host 8/8] running wrapper smoke test...');
1808
+ const smokeTimeoutMs = IS_WIN ? 15000 : 20000;
1809
+ let smokeOk = false;
1810
+
1811
+ if (IS_WIN) {
1812
+ const mcporterConfigPath = join(skillDir, 'assets', 'mcporter.json');
1813
+ const active = parseActiveTokenFromCredentials();
1814
+ const mcpSmoke = run(`mcporter --config "${mcporterConfigPath}" list MoltBank`, {
1815
+ cwd: skillDir,
1816
+ silent: true,
1817
+ timeoutMs: smokeTimeoutMs,
1818
+ env: {
1819
+ MOLTBANK: active.token ?? '__MOLTBANK_PLACEHOLDER__'
1820
+ }
1821
+ });
1822
+
1823
+ if (mcpSmoke.ok) {
1824
+ api.logger.info('[moltbank] host smoke test passed (`mcporter list MoltBank`)');
1825
+ smokeOk = true;
1826
+ } else {
1827
+ const mcpCombined = `${mcpSmoke.stderr}\n${mcpSmoke.stdout}`.trim();
1828
+ const mcpCombinedLower = mcpCombined.toLowerCase();
1829
+ const mcpDetail = lastNonEmptyLine(mcpCombined);
1830
+ if (mcpDetail) {
1831
+ api.logger.warn(`[moltbank] smoke detail: ${mcpDetail}`);
1832
+ }
1833
+
1834
+ const timedOut = mcpSmoke.stderr.includes('ETIMEDOUT') || mcpCombinedLower.includes('timed out');
1835
+ if (timedOut) {
1836
+ api.logger.warn(
1837
+ `[moltbank] host MCP smoke timed out after ${Math.floor(smokeTimeoutMs / 1000)}s; running local fallback checks`
1838
+ );
1839
+
1840
+ 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\'"',
1843
+ {
1844
+ cwd: skillDir,
1845
+ silent: true,
1846
+ timeoutMs: 10000,
1847
+ env: { MOLTBANK_WRAPPER_PATH: ps1Path }
1848
+ }
1849
+ );
1850
+
1851
+ if (fallback.ok) {
1852
+ api.logger.warn('[moltbank] host smoke fallback passed (wrapper prerequisites available), but MCP endpoint was not verified');
1853
+ smokeOk = true;
1854
+ } else {
1855
+ const fallbackCombined = `${fallback.stderr}\n${fallback.stdout}`.trim();
1856
+ const fallbackDetail = lastNonEmptyLine(fallbackCombined);
1857
+ if (fallbackDetail) {
1858
+ api.logger.warn(`[moltbank] fallback detail: ${fallbackDetail}`);
1859
+ }
1860
+ api.logger.warn('[moltbank] host setup incomplete: MCP smoke timed out and fallback checks failed');
1861
+ }
1862
+ } else if (
1863
+ mcpCombinedLower.includes('mcporter') &&
1864
+ (mcpCombinedLower.includes('not recognized') || mcpCombinedLower.includes('not found'))
1865
+ ) {
1866
+ api.logger.warn('[moltbank] mcporter binary not found in PATH during smoke test');
1867
+ } else {
1868
+ api.logger.warn('[moltbank] host setup incomplete: smoke test failed (`mcporter list MoltBank`)');
1869
+ }
1870
+ }
1871
+ } else {
1872
+ const smoke = run(`"${skillDir}/scripts/moltbank.sh" list MoltBank`, {
1873
+ cwd: skillDir,
1874
+ silent: true,
1875
+ timeoutMs: smokeTimeoutMs
1876
+ });
1877
+ if (!smoke.ok) {
1878
+ const smokeCombined = `${smoke.stderr}\n${smoke.stdout}`.trim();
1879
+ const smokeDetail = lastNonEmptyLine(smokeCombined);
1880
+ if (smokeDetail) {
1881
+ api.logger.warn(`[moltbank] smoke detail: ${smokeDetail}`);
1882
+ }
1883
+ api.logger.warn('[moltbank] host setup incomplete: smoke test failed (`moltbank list MoltBank` via platform script)');
1884
+ } else {
1885
+ api.logger.info('[moltbank] host smoke test passed (`moltbank list MoltBank`)');
1886
+ smokeOk = true;
1887
+ }
1565
1888
  }
1566
1889
 
1890
+ hostReady = smokeOk;
1891
+
1567
1892
  api.logger.info('[moltbank] host mode — setup completed with onboarding flow');
1893
+
1894
+ if (hostReady && restartGatewayOnSuccess) {
1895
+ restartGatewayAfterHostSetup(api);
1896
+ }
1568
1897
  }
1569
1898
 
1570
1899
  api.logger.info(`[moltbank] ══════════════════════════════════════`);
@@ -1640,9 +1969,10 @@ export default function register(api: PluginApi) {
1640
1969
  .description('Re-run MoltBank setup')
1641
1970
  .action(async () => {
1642
1971
  const verbose = process.argv.includes('--verbose');
1972
+ const restartGatewayOnSuccess = getSetupGatewayRestartEnabled(IS_WIN);
1643
1973
  const logger = createSetupCommandLogger({ verbose, statusCommand: 'openclaw moltbank status' });
1644
1974
  try {
1645
- await runSetup(cfg, logger, { authWaitMode: 'blocking' });
1975
+ await runSetup(cfg, logger, { authWaitMode: 'blocking', restartGatewayOnSuccess });
1646
1976
  } finally {
1647
1977
  logger.finish();
1648
1978
  }
@@ -1654,9 +1984,10 @@ export default function register(api: PluginApi) {
1654
1984
  .description('Re-run full MoltBank setup and wait for OAuth approval')
1655
1985
  .action(async () => {
1656
1986
  const verbose = process.argv.includes('--verbose');
1987
+ const restartGatewayOnSuccess = getSetupGatewayRestartEnabled(IS_WIN);
1657
1988
  const logger = createSetupCommandLogger({ verbose, statusCommand: 'openclaw moltbank status' });
1658
1989
  try {
1659
- await runSetup(cfg, logger, { authWaitMode: 'blocking' });
1990
+ await runSetup(cfg, logger, { authWaitMode: 'blocking', restartGatewayOnSuccess });
1660
1991
  } finally {
1661
1992
  logger.finish();
1662
1993
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moltbankhq/openclaw",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "MoltBank stablecoin treasury CLI and OpenClaw plugin",
5
5
  "main": "index.ts",
6
6
  "bin": {