@moltbankhq/openclaw 0.1.9 → 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');
@@ -498,7 +797,6 @@ const SKILL_FILES = [
498
797
  'SKILL.md',
499
798
  'skill.json',
500
799
  'install.sh',
501
- 'package.json',
502
800
  'assets/mcporter.json',
503
801
  'references/heartbeat.md',
504
802
  'references/onboarding.md',
@@ -700,6 +998,31 @@ function isMoltBankRegistered(): boolean {
700
998
  return result.stdout.toLowerCase().includes('moltbank');
701
999
  }
702
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
+
703
1026
  function parseActiveTokenFromCredentials(): {
704
1027
  ok: boolean;
705
1028
  activeOrg?: string;
@@ -764,93 +1087,6 @@ function readPendingOauthCode(skillDir: string): {
764
1087
  }
765
1088
  }
766
1089
 
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
1090
  function startBackgroundOauthPoll(
855
1091
  skillDir: string,
856
1092
  appBaseUrl: string,
@@ -902,7 +1138,12 @@ function startBackgroundOauthPoll(
902
1138
  oauthPollers.delete(skillDir);
903
1139
  const after = parseActiveTokenFromCredentials();
904
1140
  if (after.ok) {
905
- clearPendingOauthCode(skillDir);
1141
+ const pendingPath = join(skillDir, '.oauth_device_code.json');
1142
+ try {
1143
+ if (existsSync(pendingPath)) unlinkSync(pendingPath);
1144
+ } catch {
1145
+ // ignore
1146
+ }
906
1147
  api.logger.info(`[moltbank] ✓ background onboarding completed (active org: ${after.activeOrg})`);
907
1148
  try {
908
1149
  startBackgroundFinalizeAfterAuth(skillDir, appBaseUrl, api);
@@ -982,8 +1223,6 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
982
1223
  const existing = parseActiveTokenFromCredentials();
983
1224
  if (existing.ok) {
984
1225
  stopBackgroundOauthPoll(skillDir);
985
- stopBackgroundFinalize(skillDir);
986
- clearPendingOauthCode(skillDir);
987
1226
  api.logger.info(`[moltbank] ✓ credentials.json already available (active org: ${existing.activeOrg})`);
988
1227
  return true;
989
1228
  }
@@ -1122,7 +1361,11 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
1122
1361
 
1123
1362
  if (oauthError === 'invalid_grant') {
1124
1363
  api.logger.warn('[moltbank] ✗ onboarding code expired or already consumed (invalid_grant)');
1125
- clearPendingOauthCode(skillDir);
1364
+ try {
1365
+ if (existsSync(pendingPath)) unlinkSync(pendingPath);
1366
+ } catch {
1367
+ // ignore
1368
+ }
1126
1369
  } else {
1127
1370
  api.logger.warn('[moltbank] ✗ onboarding poll failed or timed out');
1128
1371
  }
@@ -1139,7 +1382,11 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
1139
1382
  return false;
1140
1383
  }
1141
1384
 
1142
- clearPendingOauthCode(skillDir);
1385
+ try {
1386
+ if (existsSync(pendingPath)) unlinkSync(pendingPath);
1387
+ } catch {
1388
+ // ignore
1389
+ }
1143
1390
 
1144
1391
  api.logger.info(`[moltbank] ✓ onboarding completed (active org: ${after.activeOrg})`);
1145
1392
  return true;
@@ -1148,10 +1395,7 @@ function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi
1148
1395
  export function printAuthStatus(skillDir: string, appBaseUrl: string, api: LoggerApi): void {
1149
1396
  const now = Math.floor(Date.now() / 1000);
1150
1397
  const existing = parseActiveTokenFromCredentials();
1151
- if (existing.ok) {
1152
- clearPendingOauthCode(skillDir);
1153
- }
1154
- const pending = existing.ok ? null : readPendingOauthCode(skillDir);
1398
+ const pending = readPendingOauthCode(skillDir);
1155
1399
  const poller = oauthPollers.get(skillDir);
1156
1400
  const finalizer = backgroundFinalizers.get(skillDir);
1157
1401
  const pollerRunning = Boolean(poller && poller.exitCode === null && !poller.killed);
@@ -1202,35 +1446,12 @@ export function ensureMcporterConfig(skillDir: string, appBaseUrl: string, api:
1202
1446
  api.logger.info('[moltbank] ✓ mcporter.json written: ' + cfgPath);
1203
1447
 
1204
1448
  if (!hasBin('mcporter')) {
1205
- api.logger.warn('[moltbank] ✗ mcporter not in PATH — cannot register');
1449
+ api.logger.warn('[moltbank] ✗ mcporter not in PATH — cannot validate local config usage');
1206
1450
  return;
1207
1451
  }
1208
1452
 
1209
- if (isMoltBankRegistered()) {
1210
- api.logger.info('[moltbank] ✓ MoltBank already registered in mcporter');
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
- }
1453
+ removeHomeScopedMoltBankConfig(api);
1454
+ api.logger.info('[moltbank] ✓ MoltBank will use skill-local mcporter config only');
1234
1455
  }
1235
1456
 
1236
1457
  // ─── sandbox env vars ────────────────────────────────────────────────────────
@@ -1438,12 +1659,29 @@ export function recreateSandboxAndRestart(api: LoggerApi) {
1438
1659
  }, 8000);
1439
1660
  }
1440
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
+
1441
1679
  // ─── main setup ───────────────────────────────────────────────────────────────
1442
1680
 
1443
1681
  export async function runSetup(
1444
1682
  cfg: MoltbankPluginConfig,
1445
1683
  api: LoggerApi,
1446
- options: { authWaitMode?: AuthWaitMode } = {}
1684
+ options: { authWaitMode?: AuthWaitMode; restartGatewayOnSuccess?: boolean } = {}
1447
1685
  ) {
1448
1686
  let hostReady = false;
1449
1687
  const appBaseUrl = getAppBaseUrl(cfg);
@@ -1452,6 +1690,7 @@ export async function runSetup(
1452
1690
  const sandbox = isSandboxEnabled();
1453
1691
  const skillDir = getSkillDir(cfg);
1454
1692
  const waitForAuth = (options.authWaitMode ?? 'blocking') === 'blocking';
1693
+ const restartGatewayOnSuccess = Boolean(options.restartGatewayOnSuccess);
1455
1694
 
1456
1695
  api.logger.info(`[moltbank] ══════════════════════════════════════`);
1457
1696
  api.logger.info(`[moltbank] MoltBank setup starting`);
@@ -1467,6 +1706,7 @@ export async function runSetup(
1467
1706
 
1468
1707
  api.logger.info('[moltbank] [sandbox 0/10] ensuring mcporter on host...');
1469
1708
  ensureMcporter(api);
1709
+ ensureJq(api);
1470
1710
 
1471
1711
  api.logger.info('[moltbank] [sandbox 1/10] installing skill files...');
1472
1712
  const skillInstalled = ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'sandbox');
@@ -1521,14 +1761,10 @@ export async function runSetup(
1521
1761
  } else {
1522
1762
  api.logger.info('[moltbank] sandbox unchanged — no restart needed');
1523
1763
  }
1524
-
1525
- api.logger.info('[moltbank] [sandbox 11/11] verifying authenticated balance read...');
1526
- if (!verifyMoltbankOperationalReadiness(skillDir, api)) {
1527
- return;
1528
- }
1529
1764
  } else {
1530
1765
  api.logger.info('[moltbank] [host 1/8] ensuring mcporter on host...');
1531
1766
  ensureMcporter(api);
1767
+ ensureJq(api);
1532
1768
 
1533
1769
  api.logger.info('[moltbank] [host 2/8] installing skill files...');
1534
1770
  const installed = ensureSkillInstalled(skillDir, skillBundleBaseUrl, skillName, api, 'host');
@@ -1568,13 +1804,96 @@ export async function runSetup(
1568
1804
  api.logger.info('[moltbank] [host 7/8] writing/registering mcporter config...');
1569
1805
  ensureMcporterConfig(skillDir, appBaseUrl, api);
1570
1806
 
1571
- api.logger.info('[moltbank] [host 8/8] verifying authenticated balance read...');
1572
- hostReady = verifyMoltbankOperationalReadiness(skillDir, api);
1573
- if (!hostReady) {
1574
- 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
+ }
1575
1888
  }
1576
1889
 
1890
+ hostReady = smokeOk;
1891
+
1577
1892
  api.logger.info('[moltbank] host mode — setup completed with onboarding flow');
1893
+
1894
+ if (hostReady && restartGatewayOnSuccess) {
1895
+ restartGatewayAfterHostSetup(api);
1896
+ }
1578
1897
  }
1579
1898
 
1580
1899
  api.logger.info(`[moltbank] ══════════════════════════════════════`);
@@ -1650,9 +1969,10 @@ export default function register(api: PluginApi) {
1650
1969
  .description('Re-run MoltBank setup')
1651
1970
  .action(async () => {
1652
1971
  const verbose = process.argv.includes('--verbose');
1972
+ const restartGatewayOnSuccess = getSetupGatewayRestartEnabled(IS_WIN);
1653
1973
  const logger = createSetupCommandLogger({ verbose, statusCommand: 'openclaw moltbank status' });
1654
1974
  try {
1655
- await runSetup(cfg, logger, { authWaitMode: 'blocking' });
1975
+ await runSetup(cfg, logger, { authWaitMode: 'blocking', restartGatewayOnSuccess });
1656
1976
  } finally {
1657
1977
  logger.finish();
1658
1978
  }
@@ -1664,9 +1984,10 @@ export default function register(api: PluginApi) {
1664
1984
  .description('Re-run full MoltBank setup and wait for OAuth approval')
1665
1985
  .action(async () => {
1666
1986
  const verbose = process.argv.includes('--verbose');
1987
+ const restartGatewayOnSuccess = getSetupGatewayRestartEnabled(IS_WIN);
1667
1988
  const logger = createSetupCommandLogger({ verbose, statusCommand: 'openclaw moltbank status' });
1668
1989
  try {
1669
- await runSetup(cfg, logger, { authWaitMode: 'blocking' });
1990
+ await runSetup(cfg, logger, { authWaitMode: 'blocking', restartGatewayOnSuccess });
1670
1991
  } finally {
1671
1992
  logger.finish();
1672
1993
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moltbankhq/openclaw",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "MoltBank stablecoin treasury CLI and OpenClaw plugin",
5
5
  "main": "index.ts",
6
6
  "bin": {