@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 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,7 +1,7 @@
1
- import { execSync, spawn, spawnSync } from 'child_process';
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
- 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
- }
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 run(cmd: string, opts: { cwd?: string; silent?: boolean; env?: Record<string, string | undefined> } = {}) {
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 out = execSync(cmd, {
371
+ const result = spawnSync(command, args, {
326
372
  cwd: opts.cwd,
327
- stdio: opts.silent ? 'pipe' : 'inherit',
328
373
  env: mergedEnv,
329
- shell: IS_WIN ? (process.env.ComSpec ?? 'cmd.exe') : '/bin/bash'
374
+ encoding: 'utf8',
375
+ timeout: opts.timeoutMs,
376
+ stdio: 'pipe',
377
+ windowsHide: true
330
378
  });
331
- return { ok: true, stdout: out?.toString().trim() ?? '' };
379
+
380
+ const stdout = (result.stdout ?? '').toString();
381
+ const stderr = (result.stderr ?? '').toString();
382
+
383
+ if (!opts.silent) {
384
+ if (stdout) process.stdout.write(stdout);
385
+ if (stderr) process.stderr.write(stderr);
386
+ }
387
+
388
+ const errorMessage = result.error ? getExecErrorMessage(result.error) : '';
389
+ const trimmedStdout = stdout.trim();
390
+ const trimmedStderr = stderr.trim();
391
+ const timedOut = Boolean(
392
+ result.error &&
393
+ ((isRecord(result.error) && asString((result.error as { code?: unknown }).code) === 'ETIMEDOUT') ||
394
+ errorMessage.toLowerCase().includes('timed out'))
395
+ );
396
+
397
+ if (result.status === 0) {
398
+ return {
399
+ ok: true,
400
+ stdout: trimmedStdout,
401
+ stderr: trimmedStderr,
402
+ exitCode: result.status,
403
+ timedOut: false
404
+ };
405
+ }
406
+
407
+ return {
408
+ ok: false,
409
+ stdout: trimmedStdout,
410
+ stderr: trimmedStderr || errorMessage || trimmedStdout,
411
+ exitCode: typeof result.status === 'number' ? result.status : null,
412
+ timedOut
413
+ };
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 run(IS_WIN ? `where ${bin}` : `which ${bin}`, { silent: true }).ok;
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 = run('mcporter --version', { silent: true });
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 = run('npm install -g mcporter');
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 ensureSkillInstalled(
966
+ async function installSkillBundleFiles(appBaseUrl: string, skillDir: string): Promise<RunResult> {
967
+ const base = appBaseUrl.endsWith('/') ? appBaseUrl.slice(0, -1) : appBaseUrl;
968
+ const aliases: Record<string, string[]> = {
969
+ 'SKILL.md': ['SKILL.md', 'skill.md'],
970
+ 'skill.md': ['skill.md', 'SKILL.md']
971
+ };
972
+
973
+ try {
974
+ for (const file of SKILL_FILES) {
975
+ const outPath = join(skillDir, file);
976
+ mkdirSync(dirname(outPath), { recursive: true });
977
+
978
+ let body: string | null = null;
979
+ let lastUrl = '';
980
+ let lastStatus = '';
981
+ for (const candidate of aliases[file] ?? [file]) {
982
+ const url = `${base}/${candidate}`;
983
+ lastUrl = url;
984
+
985
+ const response = await fetch(url);
986
+ if (response.ok) {
987
+ body = await response.text();
988
+ break;
989
+ }
990
+
991
+ lastStatus = String(response.status);
992
+ }
993
+
994
+ if (body === null) {
995
+ return {
996
+ ok: false,
997
+ stdout: '',
998
+ stderr: `download failed ${lastUrl} ${lastStatus}`.trim(),
999
+ exitCode: 2,
1000
+ timedOut: false
1001
+ };
1002
+ }
1003
+
1004
+ writeFileSync(outPath, body, 'utf8');
1005
+ }
1006
+
1007
+ writeFileSync(join(skillDir, '.install_success'), 'ok\n', 'utf8');
1008
+ return { ok: true, stdout: '', stderr: '', exitCode: 0, timedOut: false };
1009
+ } catch (e) {
1010
+ return {
1011
+ ok: false,
1012
+ stdout: '',
1013
+ stderr: String(e),
1014
+ exitCode: null,
1015
+ timedOut: false
1016
+ };
1017
+ }
1018
+ }
1019
+
1020
+ async function ensureSkillInstalled(
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 filesJson = JSON.stringify(SKILL_FILES).replace(/"/g, '\\"');
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
- const user = run('whoami', { silent: true }).stdout;
612
- if (user && existsSync(skillDir)) {
613
- run(`chown -R ${user} "${skillDir}"`, { silent: true });
614
- api.logger.info(`[moltbank] ownership corrected to ${user}`);
1101
+ let currentUserName = '';
1102
+ try {
1103
+ const currentUser = userInfo();
1104
+ currentUserName = currentUser.username;
1105
+ if (existsSync(skillDir)) {
1106
+ applyRecursiveOwnership(skillDir, currentUser.uid, currentUser.gid);
1107
+ api.logger.info(`[moltbank] ✓ ownership corrected to ${currentUserName}`);
1108
+ }
1109
+ } catch {
1110
+ // ignore ownership correction when uid/gid are unavailable
615
1111
  }
616
1112
 
617
1113
  if (existsSync(assetsDir)) {
618
- run(`chmod 777 "${assetsDir}"`, { silent: true });
1114
+ applyRecursiveMode(assetsDir, 0o777);
619
1115
  api.logger.info('[moltbank] ✓ assets/ permissions → 777');
620
1116
  }
621
1117
  if (existsSync(configFile)) {
622
- run(`chmod 666 "${configFile}"`, { silent: true });
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
- run(`chmod -R 755 "${scriptsDir}"`, { silent: true });
1126
+ applyRecursiveMode(scriptsDir, 0o755);
627
1127
  api.logger.info('[moltbank] ✓ scripts/ permissions → 755');
628
- run(`find "${scriptsDir}" -type f -exec sed -i 's/\r$//' {} +`, {
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
- run(`find "${skillDir}" -maxdepth 1 -type f -name "*.md" -exec sed -i 's/\r$//' {} +`, { silent: true });
1133
+ normalizeFilesMatching(skillDir, (fullPath, entry) => entry.isFile() && fullPath.endsWith('.md'), 0);
636
1134
  }
637
1135
  if (existsSync(referencesDir)) {
638
- run(`find "${referencesDir}" -maxdepth 1 -type f -name "*.md" -exec sed -i 's/\r$//' {} +`, { silent: true });
1136
+ normalizeFilesMatching(referencesDir, (fullPath, entry) => entry.isFile() && fullPath.endsWith('.md'), 0);
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 = run('npm install --ignore-scripts', { cwd: skillDir });
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 = run('npm ci', { cwd: skillDir });
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 = run('npm install', { cwd: skillDir });
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 = run('npm install --ignore-scripts', { cwd: skillDir });
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 = run('mcporter config list', { silent: true });
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
- clearPendingOauthCode(skillDir);
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 = run(`"${process.execPath}" "./scripts/request-oauth-device-code.mjs"`, {
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 = run(
1106
- `"${process.execPath}" "./scripts/poll-oauth-token.mjs" "${deviceCode}" ${safePollTimeoutSeconds} ${safePollIntervalSeconds} --save`,
1544
+ const poll = runCommand(
1545
+ process.execPath,
1546
+ ['./scripts/poll-oauth-token.mjs', deviceCode, String(safePollTimeoutSeconds), String(safePollIntervalSeconds), '--save'],
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
- clearPendingOauthCode(skillDir);
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
- clearPendingOauthCode(skillDir);
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
- if (existing.ok) {
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 register');
1650
+ api.logger.warn('[moltbank] ✗ mcporter not in PATH — cannot validate local config usage');
1206
1651
  return;
1207
1652
  }
1208
1653
 
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
- }
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 = run(`"${process.execPath}" "./scripts/init-openclaw-signer.mjs"`, {
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
- run('openclaw gateway stop', { silent: true });
1414
-
1415
- const stillRunning = run("ps aux | grep openclaw-gateway | grep -v grep | awk '{print $2}'", { silent: true });
1416
- if (stillRunning.stdout.trim()) {
1417
- api.logger.info(`[moltbank] gateway still running (pids: ${stillRunning.stdout.trim()}) — sending SIGKILL...`);
1418
- run("kill -9 $(ps aux | grep openclaw-gateway | grep -v grep | awk '{print $2}') 2>/dev/null || true", { silent: true });
1842
+ runCommand('openclaw', ['gateway', 'stop'], { silent: true });
1843
+
1844
+ const stillRunning = getGatewayPids();
1845
+ if (stillRunning.length) {
1846
+ api.logger.info(`[moltbank] gateway still running (pids: ${stillRunning.join(' ')}) — sending SIGKILL...`);
1847
+ for (const pid of stillRunning) {
1848
+ try {
1849
+ process.kill(Number(pid), 'SIGKILL');
1850
+ } catch {
1851
+ // ignore
1852
+ }
1853
+ }
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
- run('sleep 2', { silent: true });
1425
-
1426
- const recreate = run('openclaw sandbox recreate --all --force', {
1427
- silent: false
1428
- });
1429
- if (recreate.ok) {
1430
- api.logger.info('[moltbank] sandbox containers recreated — new container will be created on next agent message');
1431
- } else {
1432
- api.logger.warn('[moltbank] sandbox recreate failed');
1433
- api.logger.warn('[moltbank] run manually: openclaw sandbox recreate --all --force');
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
- api.logger.info('[moltbank] restarting gateway...');
1437
- run('openclaw gateway', { silent: true });
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] verifying authenticated balance read...');
1572
- hostReady = verifyMoltbankOperationalReadiness(skillDir, api);
1573
- if (!hostReady) {
1574
- return;
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 containerCheck = run(
1610
- `docker inspect $(docker ps | grep openclaw-sbx | awk '{print $1}') --format '{{.HostConfig.NetworkMode}}' 2>/dev/null`,
1611
- { silent: true }
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moltbankhq/openclaw",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "MoltBank stablecoin treasury CLI and OpenClaw plugin",
5
5
  "main": "index.ts",
6
6
  "bin": {