@moltbankhq/openclaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +1468 -0
- package/openclaw.plugin.json +29 -0
- package/package.json +18 -0
package/index.ts
ADDED
|
@@ -0,0 +1,1468 @@
|
|
|
1
|
+
import { execSync, spawn } from 'child_process';
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
const IS_WIN = process.platform === 'win32';
|
|
6
|
+
|
|
7
|
+
type OpenclawConfig = Record<string, unknown>;
|
|
8
|
+
type ParsedJsonObject = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
interface MoltbankPluginConfig {
|
|
11
|
+
skillName?: string;
|
|
12
|
+
appBaseUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CredentialsOrganization {
|
|
16
|
+
name?: string;
|
|
17
|
+
access_token?: string;
|
|
18
|
+
x402_signer_private_key?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface CredentialsFile {
|
|
22
|
+
active_organization?: string;
|
|
23
|
+
organizations?: CredentialsOrganization[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface LoggerLike {
|
|
27
|
+
info(message: string): void;
|
|
28
|
+
warn(message: string): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface LoggerApi {
|
|
32
|
+
logger: LoggerLike;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ServiceDefinition {
|
|
36
|
+
id: string;
|
|
37
|
+
start: () => void | Promise<void>;
|
|
38
|
+
stop: () => void | Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface CliCommandLike {
|
|
42
|
+
command(name: string): CliCommandLike;
|
|
43
|
+
createCommand(name: string): CliCommandLike;
|
|
44
|
+
description(text: string): CliCommandLike;
|
|
45
|
+
addCommand(command: CliCommandLike): CliCommandLike;
|
|
46
|
+
action(handler: () => void | Promise<void>): CliCommandLike;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface PluginApiConfig {
|
|
50
|
+
plugins?: {
|
|
51
|
+
entries?: {
|
|
52
|
+
moltbank?: {
|
|
53
|
+
config?: MoltbankPluginConfig;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface PluginApi extends LoggerApi {
|
|
60
|
+
config?: PluginApiConfig;
|
|
61
|
+
registerService(service: ServiceDefinition): void;
|
|
62
|
+
registerCli(handler: (args: { program: CliCommandLike }) => void, options: { commands: string[] }): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type AuthWaitMode = 'blocking' | 'nonblocking';
|
|
66
|
+
const oauthPollers = new Map<string, ReturnType<typeof spawn>>();
|
|
67
|
+
const backgroundFinalizers = new Map<string, ReturnType<typeof spawn>>();
|
|
68
|
+
|
|
69
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
70
|
+
return typeof value === 'object' && value !== null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function asString(value: unknown, fallback = ''): string {
|
|
74
|
+
return typeof value === 'string' ? value : fallback;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function asStringRecord(value: unknown): Record<string, string> {
|
|
78
|
+
if (!isRecord(value)) return {};
|
|
79
|
+
|
|
80
|
+
const out: Record<string, string> = {};
|
|
81
|
+
for (const [key, v] of Object.entries(value)) {
|
|
82
|
+
if (typeof v === 'string') {
|
|
83
|
+
out[key] = v;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getSetupAuthWaitMode(defaultMode: AuthWaitMode): AuthWaitMode {
|
|
90
|
+
const raw = asString(process.env.MOLTBANK_SETUP_AUTH_WAIT_MODE).trim().toLowerCase();
|
|
91
|
+
if (raw === 'blocking' || raw === 'wait') return 'blocking';
|
|
92
|
+
if (raw === 'nonblocking' || raw === 'nowait') return 'nonblocking';
|
|
93
|
+
return defaultMode;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getExecErrorMessage(error: unknown): string {
|
|
97
|
+
if (isRecord(error) && 'stderr' in error) {
|
|
98
|
+
const stderr = (error as { stderr?: unknown }).stderr;
|
|
99
|
+
if (typeof stderr === 'string') {
|
|
100
|
+
return stderr.trim();
|
|
101
|
+
}
|
|
102
|
+
if (isRecord(stderr) && 'toString' in stderr && typeof (stderr as { toString?: unknown }).toString === 'function') {
|
|
103
|
+
return (stderr as { toString: () => string }).toString().trim();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return String(error);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function run(cmd: string, opts: { cwd?: string; silent?: boolean; env?: Record<string, string | undefined> } = {}) {
|
|
110
|
+
try {
|
|
111
|
+
const mergedEnv: NodeJS.ProcessEnv = { ...process.env, ...(opts.env ?? {}) };
|
|
112
|
+
const out = execSync(cmd, {
|
|
113
|
+
cwd: opts.cwd,
|
|
114
|
+
stdio: opts.silent ? 'pipe' : 'inherit',
|
|
115
|
+
env: mergedEnv,
|
|
116
|
+
shell: IS_WIN ? (process.env.ComSpec ?? 'cmd.exe') : '/bin/bash'
|
|
117
|
+
});
|
|
118
|
+
return { ok: true, stdout: out?.toString().trim() ?? '' };
|
|
119
|
+
} catch (e: unknown) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
stdout: '',
|
|
123
|
+
stderr: getExecErrorMessage(e)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hasBin(bin: string) {
|
|
129
|
+
return run(IS_WIN ? `where ${bin}` : `which ${bin}`, { silent: true }).ok;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getWorkspace(): string {
|
|
133
|
+
return process.env.OPENCLAW_WORKSPACE || join(homedir(), '.openclaw', 'workspace');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getSkillName(cfg: MoltbankPluginConfig): string {
|
|
137
|
+
return cfg?.skillName || process.env.MOLTBANK_SKILL_NAME || 'MoltBank';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getAppBaseUrl(cfg: MoltbankPluginConfig): string {
|
|
141
|
+
return (cfg?.appBaseUrl || process.env.APP_BASE_URL || 'https://app.moltbank.bot').trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isSandboxEnabled(): boolean {
|
|
145
|
+
try {
|
|
146
|
+
const configPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
147
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
148
|
+
const mode = config?.agents?.defaults?.sandbox?.mode?.toLowerCase();
|
|
149
|
+
return mode === 'all' || mode === 'non-main';
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getSkillDir(cfg: MoltbankPluginConfig): string {
|
|
156
|
+
const skillName = getSkillName(cfg);
|
|
157
|
+
return join(getWorkspace(), 'skills', skillName);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getCredentialsPath(): string {
|
|
161
|
+
return process.env.MOLTBANK_CREDENTIALS_PATH || join(homedir(), '.MoltBank', 'credentials.json');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readOpenclawConfig(): OpenclawConfig {
|
|
165
|
+
const configPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(readFileSync(configPath, 'utf8')) as unknown;
|
|
168
|
+
return isRecord(parsed) ? parsed : {};
|
|
169
|
+
} catch {
|
|
170
|
+
return {};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function writeOpenclawConfig(config: OpenclawConfig): void {
|
|
175
|
+
const configPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
176
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getNestedValue(obj: unknown, path: string[]): unknown {
|
|
180
|
+
let current: unknown = obj;
|
|
181
|
+
for (const key of path) {
|
|
182
|
+
if (!isRecord(current)) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
current = current[key];
|
|
186
|
+
}
|
|
187
|
+
return current;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function setNestedValue(obj: Record<string, unknown>, path: string[], value: unknown): void {
|
|
191
|
+
if (path.length === 0) return;
|
|
192
|
+
|
|
193
|
+
let current: Record<string, unknown> = obj;
|
|
194
|
+
for (const key of path.slice(0, -1)) {
|
|
195
|
+
const next = current[key];
|
|
196
|
+
if (!isRecord(next)) {
|
|
197
|
+
current[key] = {};
|
|
198
|
+
}
|
|
199
|
+
current = current[key] as Record<string, unknown>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
current[path[path.length - 1]] = value;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function cleanupStaleMoltbankPluginLoadPaths(api: LoggerApi): boolean {
|
|
206
|
+
try {
|
|
207
|
+
const config = readOpenclawConfig();
|
|
208
|
+
const loadPathsPath = ['plugins', 'load', 'paths'];
|
|
209
|
+
const current = getNestedValue(config, loadPathsPath);
|
|
210
|
+
if (!Array.isArray(current)) return false;
|
|
211
|
+
|
|
212
|
+
const removed: string[] = [];
|
|
213
|
+
const next: unknown[] = [];
|
|
214
|
+
|
|
215
|
+
for (const entry of current) {
|
|
216
|
+
if (typeof entry !== 'string') {
|
|
217
|
+
next.push(entry);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const normalized = entry.replace(/\\/g, '/').toLowerCase();
|
|
222
|
+
const looksLikeMoltbankPath = normalized.includes('moltbank');
|
|
223
|
+
if (looksLikeMoltbankPath && !existsSync(entry)) {
|
|
224
|
+
removed.push(entry);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
next.push(entry);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (removed.length === 0) {
|
|
232
|
+
api.logger.info('[moltbank] ✓ no stale MoltBank plugin load paths found');
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
setNestedValue(config, loadPathsPath, next);
|
|
237
|
+
writeOpenclawConfig(config);
|
|
238
|
+
api.logger.info(`[moltbank] ✓ removed stale MoltBank plugin load path(s): ${removed.join(', ')}`);
|
|
239
|
+
return true;
|
|
240
|
+
} catch (e) {
|
|
241
|
+
api.logger.warn('[moltbank] could not clean stale MoltBank plugin load paths: ' + String(e));
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─── mcporter ────────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
function ensureMcporter(api: LoggerApi) {
|
|
249
|
+
if (hasBin('mcporter')) {
|
|
250
|
+
const v = run('mcporter --version', { silent: true });
|
|
251
|
+
api.logger.info(`[moltbank] ✓ mcporter already installed (${v.stdout || 'unknown version'})`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
api.logger.info('[moltbank] installing mcporter globally...');
|
|
255
|
+
const result = run('npm install -g mcporter');
|
|
256
|
+
if (result.ok) {
|
|
257
|
+
api.logger.info('[moltbank] ✓ mcporter installed');
|
|
258
|
+
} else {
|
|
259
|
+
api.logger.warn('[moltbank] ✗ mcporter install failed: ' + result.stderr);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function ensureWrapperExecutable(skillDir: string, api: LoggerApi): void {
|
|
264
|
+
if (IS_WIN) return;
|
|
265
|
+
const wrapperPath = join(skillDir, 'scripts', 'moltbank.sh');
|
|
266
|
+
if (!existsSync(wrapperPath)) return;
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
chmodSync(wrapperPath, 0o755);
|
|
270
|
+
api.logger.info('[moltbank] ✓ wrapper script permissions ensured (scripts/moltbank.sh)');
|
|
271
|
+
} catch (e) {
|
|
272
|
+
api.logger.warn('[moltbank] could not ensure wrapper executable bit: ' + String(e));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── skill install ───────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
const SKILL_FILES = [
|
|
279
|
+
'skill.md',
|
|
280
|
+
'setup.md',
|
|
281
|
+
'onboarding.md',
|
|
282
|
+
'multi-org.md',
|
|
283
|
+
'tools-reference.md',
|
|
284
|
+
'x402-workflow.md',
|
|
285
|
+
'heartbeat.md',
|
|
286
|
+
'rules.md',
|
|
287
|
+
'skill.json',
|
|
288
|
+
'polymarket-workflow.md',
|
|
289
|
+
'polymarket-operation.md',
|
|
290
|
+
'polymarket-refill.md',
|
|
291
|
+
'openclaw-signer-eoa.md',
|
|
292
|
+
'openclaw-solana-signer.md',
|
|
293
|
+
'pumpfun-workflow.md',
|
|
294
|
+
'config/mcporter.json',
|
|
295
|
+
'scripts/openclaw-runtime-config.mjs',
|
|
296
|
+
'scripts/request-oauth-device-code.mjs',
|
|
297
|
+
'scripts/init-openclaw-signer.mjs',
|
|
298
|
+
'scripts/init-openclaw-solana-signer.mjs',
|
|
299
|
+
'scripts/bootstrap-openclaw-pumpfun-wallet.mjs',
|
|
300
|
+
'scripts/inspect-x402-requirements.mjs',
|
|
301
|
+
'scripts/inspect-solana-wallet.mjs',
|
|
302
|
+
'scripts/inspect-polygon-wallet.mjs',
|
|
303
|
+
'scripts/quote-solana-budget.mjs',
|
|
304
|
+
'scripts/polymarket-execute-lifi-tx.mjs',
|
|
305
|
+
'scripts/polymarket-signer-to-safe.mjs',
|
|
306
|
+
'scripts/poll-oauth-token.mjs',
|
|
307
|
+
'scripts/export-api-key.mjs',
|
|
308
|
+
'scripts/fetch-openrouter-intent.mjs',
|
|
309
|
+
'scripts/x402-pay-and-confirm.mjs',
|
|
310
|
+
'scripts/pumpportal-trade-local.mjs',
|
|
311
|
+
'scripts/moltbank.sh',
|
|
312
|
+
'scripts/moltbank.ps1',
|
|
313
|
+
'scripts/polymarket-service.mjs'
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
function ensureSkillInstalled(
|
|
317
|
+
skillDir: string,
|
|
318
|
+
appBaseUrl: string,
|
|
319
|
+
skillName: string,
|
|
320
|
+
api: LoggerApi,
|
|
321
|
+
mode: 'sandbox' | 'host' = 'sandbox'
|
|
322
|
+
) {
|
|
323
|
+
const successFlag = join(skillDir, '.install_success');
|
|
324
|
+
if (existsSync(successFlag)) {
|
|
325
|
+
ensureWrapperExecutable(skillDir, api);
|
|
326
|
+
api.logger.info('[moltbank] ✓ skill already installed at ' + skillDir);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
api.logger.info(`[moltbank] installing skill '${skillName}' to ${skillDir} (mode: ${mode})`);
|
|
331
|
+
mkdirSync(skillDir, { recursive: true });
|
|
332
|
+
|
|
333
|
+
const filesJson = JSON.stringify(SKILL_FILES).replace(/"/g, '\\"');
|
|
334
|
+
const installNode = run(
|
|
335
|
+
`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; fs.mkdirSync(dir,{recursive:true}); for (const f of files){ const u=base+'/'+f; const out=path.join(dir,f); fs.mkdirSync(path.dirname(out),{recursive:true}); const r=await fetch(u); if(!r.ok){ console.error('download failed',u,r.status); process.exit(2);} fs.writeFileSync(out, await r.text(),'utf8'); } fs.writeFileSync(path.join(dir,'.install_success'),'ok\\n','utf8');" "${appBaseUrl}" "${skillDir}" "${filesJson}"`,
|
|
336
|
+
{ cwd: dirname(skillDir), silent: true }
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
if (installNode.ok) {
|
|
340
|
+
ensureWrapperExecutable(skillDir, api);
|
|
341
|
+
api.logger.info('[moltbank] ✓ skill installed at ' + skillDir);
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
api.logger.warn('[moltbank] ✗ skill install failed: ' + installNode.stderr);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
// ─── SKILL.md uppercase + frontmatter ────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
function ensureSkillFilesUppercase(skillDir: string, api: LoggerApi) {
|
|
351
|
+
const lower = join(skillDir, 'skill.md');
|
|
352
|
+
const upper = join(skillDir, 'SKILL.md');
|
|
353
|
+
if (existsSync(upper)) {
|
|
354
|
+
api.logger.info('[moltbank] ✓ SKILL.md already exists');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (existsSync(lower)) {
|
|
358
|
+
renameSync(lower, upper);
|
|
359
|
+
api.logger.info('[moltbank] ✓ renamed skill.md → SKILL.md');
|
|
360
|
+
} else {
|
|
361
|
+
api.logger.warn('[moltbank] ✗ neither skill.md nor SKILL.md found');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function fixSkillFrontmatter(skillDir: string, skillName: string, api: LoggerApi) {
|
|
366
|
+
const skillFile = join(skillDir, 'SKILL.md');
|
|
367
|
+
if (!existsSync(skillFile)) {
|
|
368
|
+
api.logger.warn('[moltbank] ✗ SKILL.md not found — skipping frontmatter fix');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const content = readFileSync(skillFile, 'utf8').replace(/\r\n/g, '\n');
|
|
373
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
374
|
+
if (!frontmatterMatch) {
|
|
375
|
+
api.logger.warn('[moltbank] ✗ no frontmatter in SKILL.md');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const binsYaml = IS_WIN ? ' - mcporter' : ' - mcporter\n - jq';
|
|
380
|
+
const newFrontmatter = `---
|
|
381
|
+
name: ${skillName}
|
|
382
|
+
version: 1.5.3
|
|
383
|
+
description: MCP skill for MoltBank business banking workflows (treasury, approvals, allowances, x402, OpenRouter, Polymarket, and Pump.Fun).
|
|
384
|
+
homepage: \${APP_BASE_URL:-https://app.moltbank.bot}
|
|
385
|
+
metadata:
|
|
386
|
+
category: finance
|
|
387
|
+
api_base: \${APP_BASE_URL:-https://app.moltbank.bot}/api/mcp
|
|
388
|
+
install_script: \${APP_BASE_URL:-https://app.moltbank.bot}/install.sh
|
|
389
|
+
openclaw:
|
|
390
|
+
requires:
|
|
391
|
+
bins:
|
|
392
|
+
${binsYaml}
|
|
393
|
+
npm:
|
|
394
|
+
- '@x402/fetch@^2.3.0'
|
|
395
|
+
- '@x402/evm@^2.3.1'
|
|
396
|
+
- 'viem@^2.46.0'
|
|
397
|
+
- '@polymarket/clob-client'
|
|
398
|
+
- 'ethers@5'
|
|
399
|
+
- '@solana/web3.js@^1.98.4'
|
|
400
|
+
- 'bs58@^6.0.0'
|
|
401
|
+
primaryEnv: MOLTBANK
|
|
402
|
+
---`;
|
|
403
|
+
|
|
404
|
+
const body = content.slice(frontmatterMatch[0].length);
|
|
405
|
+
const fixed = newFrontmatter + body;
|
|
406
|
+
|
|
407
|
+
if (fixed !== content) {
|
|
408
|
+
writeFileSync(skillFile, fixed, 'utf8');
|
|
409
|
+
api.logger.info(`[moltbank] ✓ SKILL.md frontmatter fixed → name: ${skillName}`);
|
|
410
|
+
} else {
|
|
411
|
+
api.logger.info('[moltbank] ✓ SKILL.md frontmatter already correct');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function ensureSkillPermissions(skillDir: string, api: LoggerApi) {
|
|
416
|
+
if (IS_WIN) {
|
|
417
|
+
api.logger.info('[moltbank] ✓ skipping unix permissions (Windows)');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const configDir = join(skillDir, 'config');
|
|
422
|
+
const configFile = join(skillDir, 'config', 'mcporter.json');
|
|
423
|
+
const scriptsDir = join(skillDir, 'scripts');
|
|
424
|
+
|
|
425
|
+
const user = run('whoami', { silent: true }).stdout;
|
|
426
|
+
if (user && existsSync(skillDir)) {
|
|
427
|
+
run(`chown -R ${user} "${skillDir}"`, { silent: true });
|
|
428
|
+
api.logger.info(`[moltbank] ✓ ownership corrected to ${user}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (existsSync(configDir)) {
|
|
432
|
+
run(`chmod 777 "${configDir}"`, { silent: true });
|
|
433
|
+
api.logger.info('[moltbank] ✓ config/ permissions → 777');
|
|
434
|
+
}
|
|
435
|
+
if (existsSync(configFile)) {
|
|
436
|
+
run(`chmod 666 "${configFile}"`, { silent: true });
|
|
437
|
+
api.logger.info('[moltbank] ✓ mcporter.json permissions → 666');
|
|
438
|
+
}
|
|
439
|
+
if (existsSync(scriptsDir)) {
|
|
440
|
+
run(`chmod -R 755 "${scriptsDir}"`, { silent: true });
|
|
441
|
+
api.logger.info('[moltbank] ✓ scripts/ permissions → 755');
|
|
442
|
+
run(`find "${scriptsDir}" -type f | xargs sed -i 's/\r$//'`, {
|
|
443
|
+
silent: true
|
|
444
|
+
});
|
|
445
|
+
api.logger.info('[moltbank] ✓ scripts/ line endings normalized (CRLF → LF)');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (existsSync(skillDir)) {
|
|
449
|
+
run(`find "${skillDir}" -maxdepth 1 -name "*.md" | xargs sed -i 's/\r$//'`, { silent: true });
|
|
450
|
+
api.logger.info('[moltbank] ✓ .md files line endings normalized (CRLF → LF)');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ─── npm deps ─────────────────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
function ensureNpmDeps(skillDir: string, api: LoggerApi, mode: 'sandbox' | 'host' = 'sandbox') {
|
|
457
|
+
const pkgPath = join(skillDir, 'package.json');
|
|
458
|
+
if (!existsSync(pkgPath)) {
|
|
459
|
+
const fallbackPkg = {
|
|
460
|
+
name: 'moltbank-skill-runtime',
|
|
461
|
+
version: '1.0.0',
|
|
462
|
+
private: true,
|
|
463
|
+
type: 'module',
|
|
464
|
+
dependencies: {
|
|
465
|
+
'@x402/fetch': '^2.3.0',
|
|
466
|
+
'@x402/evm': '^2.3.1',
|
|
467
|
+
viem: '^2.46.0',
|
|
468
|
+
'@polymarket/clob-client': 'latest',
|
|
469
|
+
ethers: '^5.8.0',
|
|
470
|
+
'@solana/web3.js': '^1.98.4',
|
|
471
|
+
bs58: '^6.0.0'
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
writeFileSync(pkgPath, JSON.stringify(fallbackPkg, null, 2) + '\n', 'utf8');
|
|
475
|
+
api.logger.warn('[moltbank] package.json not found — created fallback package.json for skill runtime deps');
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
479
|
+
pkg.dependencies = pkg.dependencies ?? {};
|
|
480
|
+
const cur = String(pkg.dependencies['@polymarket/clob-client'] ?? '').trim();
|
|
481
|
+
if (!cur || cur === '^6.0.0') {
|
|
482
|
+
pkg.dependencies['@polymarket/clob-client'] = 'latest';
|
|
483
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
484
|
+
api.logger.info('[moltbank] normalized package.json: @polymarket/clob-client -> latest');
|
|
485
|
+
}
|
|
486
|
+
} catch (e) {
|
|
487
|
+
api.logger.warn('[moltbank] could not normalize package.json deps: ' + String(e));
|
|
488
|
+
}
|
|
489
|
+
api.logger.info('[moltbank] installing npm deps...');
|
|
490
|
+
if (mode === 'sandbox') {
|
|
491
|
+
const result = run('npm install --ignore-scripts', { cwd: skillDir });
|
|
492
|
+
if (result.ok) {
|
|
493
|
+
api.logger.info('[moltbank] ✓ npm deps installed');
|
|
494
|
+
} else {
|
|
495
|
+
api.logger.warn('[moltbank] ✗ npm install failed: ' + result.stderr);
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const hasLock = existsSync(join(skillDir, 'package-lock.json')) || existsSync(join(skillDir, 'npm-shrinkwrap.json'));
|
|
501
|
+
if (hasLock) {
|
|
502
|
+
const ci = run('npm ci', { cwd: skillDir });
|
|
503
|
+
if (ci.ok) {
|
|
504
|
+
api.logger.info('[moltbank] ✓ npm deps installed with npm ci');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
api.logger.warn('[moltbank] npm ci failed; trying npm install: ' + ci.stderr);
|
|
508
|
+
}
|
|
509
|
+
const install = run('npm install', { cwd: skillDir });
|
|
510
|
+
if (install.ok) {
|
|
511
|
+
api.logger.info('[moltbank] ✓ npm deps installed with npm install');
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
api.logger.warn('[moltbank] npm install failed; trying safe fallback --ignore-scripts');
|
|
515
|
+
const fallback = run('npm install --ignore-scripts', { cwd: skillDir });
|
|
516
|
+
if (fallback.ok) {
|
|
517
|
+
api.logger.warn('[moltbank] npm deps installed with fallback --ignore-scripts (some packages may require postinstall)');
|
|
518
|
+
} else {
|
|
519
|
+
api.logger.warn('[moltbank] ✗ npm dependency install failed: ' + fallback.stderr);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function isMoltBankRegistered(): boolean {
|
|
524
|
+
const result = run('mcporter config list', { silent: true });
|
|
525
|
+
return result.stdout.toLowerCase().includes('moltbank');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function parseActiveTokenFromCredentials(): {
|
|
529
|
+
ok: boolean;
|
|
530
|
+
activeOrg?: string;
|
|
531
|
+
token?: string;
|
|
532
|
+
privateKey?: string;
|
|
533
|
+
} {
|
|
534
|
+
const credsPath = getCredentialsPath();
|
|
535
|
+
if (!existsSync(credsPath)) return { ok: false };
|
|
536
|
+
try {
|
|
537
|
+
const creds = JSON.parse(readFileSync(credsPath, 'utf8')) as CredentialsFile;
|
|
538
|
+
const activeOrg = creds?.active_organization;
|
|
539
|
+
if (!activeOrg) return { ok: false };
|
|
540
|
+
const org = creds.organizations?.find((o: CredentialsOrganization) => o.name === activeOrg);
|
|
541
|
+
if (!org?.access_token) return { ok: false };
|
|
542
|
+
return {
|
|
543
|
+
ok: true,
|
|
544
|
+
activeOrg,
|
|
545
|
+
token: org.access_token,
|
|
546
|
+
privateKey: org.x402_signer_private_key ?? undefined
|
|
547
|
+
};
|
|
548
|
+
} catch {
|
|
549
|
+
return { ok: false };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function parseFirstJsonObject(output: string): ParsedJsonObject | null {
|
|
554
|
+
const trimmed = (output || '').trim();
|
|
555
|
+
if (!trimmed) return null;
|
|
556
|
+
try {
|
|
557
|
+
return JSON.parse(trimmed);
|
|
558
|
+
} catch {}
|
|
559
|
+
const start = trimmed.indexOf('{');
|
|
560
|
+
const end = trimmed.lastIndexOf('}');
|
|
561
|
+
if (start >= 0 && end > start) {
|
|
562
|
+
const maybe = trimmed.slice(start, end + 1);
|
|
563
|
+
try {
|
|
564
|
+
return JSON.parse(maybe);
|
|
565
|
+
} catch {}
|
|
566
|
+
}
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function readPendingOauthCode(skillDir: string): {
|
|
571
|
+
deviceCode: string;
|
|
572
|
+
userCode: string;
|
|
573
|
+
verificationUri: string;
|
|
574
|
+
expiresAt: number;
|
|
575
|
+
} | null {
|
|
576
|
+
const pendingPath = join(skillDir, '.oauth_device_code.json');
|
|
577
|
+
if (!existsSync(pendingPath)) return null;
|
|
578
|
+
try {
|
|
579
|
+
const pending = JSON.parse(readFileSync(pendingPath, 'utf8')) as Record<string, unknown>;
|
|
580
|
+
const deviceCode = asString(pending.device_code);
|
|
581
|
+
const userCode = asString(pending.user_code);
|
|
582
|
+
const verificationUri = asString(pending.verification_uri, '');
|
|
583
|
+
const expiresAtRaw = pending.expires_at;
|
|
584
|
+
const expiresAt = typeof expiresAtRaw === 'number' ? expiresAtRaw : 0;
|
|
585
|
+
if (!deviceCode || !userCode || expiresAt <= 0) return null;
|
|
586
|
+
return { deviceCode, userCode, verificationUri, expiresAt };
|
|
587
|
+
} catch {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function startBackgroundOauthPoll(
|
|
593
|
+
skillDir: string,
|
|
594
|
+
appBaseUrl: string,
|
|
595
|
+
credsPath: string,
|
|
596
|
+
deviceCode: string,
|
|
597
|
+
timeoutSeconds: number,
|
|
598
|
+
api: LoggerApi
|
|
599
|
+
): void {
|
|
600
|
+
const existing = oauthPollers.get(skillDir);
|
|
601
|
+
if (existing && existing.exitCode === null && !existing.killed) {
|
|
602
|
+
api.logger.info('[moltbank] background OAuth poll already running');
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (existing) oauthPollers.delete(skillDir);
|
|
606
|
+
|
|
607
|
+
const envTimeoutSeconds = Number(process.env.MOLTBANK_OAUTH_POLL_TIMEOUT_SECONDS ?? 180);
|
|
608
|
+
const fallbackTimeoutSeconds = Number.isFinite(envTimeoutSeconds) && envTimeoutSeconds > 0 ? Math.floor(envTimeoutSeconds) : 180;
|
|
609
|
+
const safePollTimeoutSeconds =
|
|
610
|
+
Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) : fallbackTimeoutSeconds;
|
|
611
|
+
const pollIntervalSeconds = Number(process.env.MOLTBANK_OAUTH_POLL_INTERVAL_SECONDS ?? 5);
|
|
612
|
+
const safePollIntervalSeconds = Number.isFinite(pollIntervalSeconds) && pollIntervalSeconds > 0 ? Math.floor(pollIntervalSeconds) : 5;
|
|
613
|
+
|
|
614
|
+
const child = spawn(
|
|
615
|
+
process.execPath,
|
|
616
|
+
['./scripts/poll-oauth-token.mjs', deviceCode, String(safePollTimeoutSeconds), String(safePollIntervalSeconds), '--save'],
|
|
617
|
+
{
|
|
618
|
+
cwd: skillDir,
|
|
619
|
+
env: {
|
|
620
|
+
...process.env,
|
|
621
|
+
APP_BASE_URL: appBaseUrl,
|
|
622
|
+
MOLTBANK_CREDENTIALS_PATH: credsPath
|
|
623
|
+
},
|
|
624
|
+
stdio: 'ignore', // Ignore logs so it doesn't hang the parent
|
|
625
|
+
detached: true // Detach from the parent process
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
child.unref(); // Tell Node not to wait for this process
|
|
630
|
+
|
|
631
|
+
oauthPollers.set(skillDir, child);
|
|
632
|
+
api.logger.info(`[moltbank] background OAuth poll started (pid: ${child.pid ?? 'unknown'})`);
|
|
633
|
+
|
|
634
|
+
child.on('error', (error) => {
|
|
635
|
+
oauthPollers.delete(skillDir);
|
|
636
|
+
api.logger.warn('[moltbank] background OAuth poll failed to start: ' + String(error));
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
child.on('close', (code) => {
|
|
640
|
+
oauthPollers.delete(skillDir);
|
|
641
|
+
const after = parseActiveTokenFromCredentials();
|
|
642
|
+
if (after.ok) {
|
|
643
|
+
const pendingPath = join(skillDir, '.oauth_device_code.json');
|
|
644
|
+
try {
|
|
645
|
+
if (existsSync(pendingPath)) unlinkSync(pendingPath);
|
|
646
|
+
} catch {
|
|
647
|
+
// ignore
|
|
648
|
+
}
|
|
649
|
+
api.logger.info(`[moltbank] ✓ background onboarding completed (active org: ${after.activeOrg})`);
|
|
650
|
+
try {
|
|
651
|
+
startBackgroundFinalizeAfterAuth(skillDir, appBaseUrl, api);
|
|
652
|
+
} catch (e) {
|
|
653
|
+
api.logger.warn('[moltbank] background finalize after auth failed: ' + String(e));
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
api.logger.warn(`[moltbank] background OAuth poll exited with code ${String(code ?? 'unknown')}`);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function stopBackgroundOauthPoll(skillDir: string): void {
|
|
662
|
+
const existing = oauthPollers.get(skillDir);
|
|
663
|
+
if (!existing) return;
|
|
664
|
+
if (existing.exitCode === null && !existing.killed) {
|
|
665
|
+
existing.kill();
|
|
666
|
+
}
|
|
667
|
+
oauthPollers.delete(skillDir);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function startBackgroundFinalizeAfterAuth(skillDir: string, appBaseUrl: string, api: LoggerApi): void {
|
|
671
|
+
const existing = backgroundFinalizers.get(skillDir);
|
|
672
|
+
if (existing && existing.exitCode === null && !existing.killed) {
|
|
673
|
+
api.logger.info('[moltbank] background finalize already running');
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (existing) backgroundFinalizers.delete(skillDir);
|
|
677
|
+
|
|
678
|
+
const child = spawn('openclaw', ['moltbank', 'setup-blocking'], {
|
|
679
|
+
cwd: skillDir,
|
|
680
|
+
env: {
|
|
681
|
+
...process.env,
|
|
682
|
+
APP_BASE_URL: appBaseUrl,
|
|
683
|
+
MOLTBANK_SETUP_AUTH_WAIT_MODE: 'blocking'
|
|
684
|
+
},
|
|
685
|
+
stdio: 'ignore', // Ignore logs so it doesn't hang the parent
|
|
686
|
+
detached: true // Detach from the parent process
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
child.unref(); // Tell Node not to wait for this process
|
|
690
|
+
|
|
691
|
+
backgroundFinalizers.set(skillDir, child);
|
|
692
|
+
api.logger.info(`[moltbank] background finalize subprocess started (pid: ${child.pid ?? 'unknown'})`);
|
|
693
|
+
|
|
694
|
+
child.on('error', (error) => {
|
|
695
|
+
backgroundFinalizers.delete(skillDir);
|
|
696
|
+
api.logger.warn('[moltbank] background finalize failed to start: ' + String(error));
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
child.on('close', (code) => {
|
|
700
|
+
backgroundFinalizers.delete(skillDir);
|
|
701
|
+
if (code === 0) {
|
|
702
|
+
api.logger.info('[moltbank] ✓ background finalize completed');
|
|
703
|
+
} else {
|
|
704
|
+
api.logger.warn(`[moltbank] background finalize exited with code ${String(code ?? 'unknown')}`);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function stopBackgroundFinalize(skillDir: string): void {
|
|
710
|
+
const existing = backgroundFinalizers.get(skillDir);
|
|
711
|
+
if (!existing) return;
|
|
712
|
+
if (existing.exitCode === null && !existing.killed) {
|
|
713
|
+
existing.kill();
|
|
714
|
+
}
|
|
715
|
+
backgroundFinalizers.delete(skillDir);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function ensureMoltbankAuth(skillDir: string, appBaseUrl: string, api: LoggerApi, options: { waitForApproval?: boolean } = {}): boolean {
|
|
719
|
+
const waitForApproval = options.waitForApproval ?? true;
|
|
720
|
+
if (waitForApproval) {
|
|
721
|
+
// Avoid racing sync polling with the nonblocking background poller.
|
|
722
|
+
stopBackgroundOauthPoll(skillDir);
|
|
723
|
+
stopBackgroundFinalize(skillDir);
|
|
724
|
+
}
|
|
725
|
+
const existing = parseActiveTokenFromCredentials();
|
|
726
|
+
if (existing.ok) {
|
|
727
|
+
stopBackgroundOauthPoll(skillDir);
|
|
728
|
+
api.logger.info(`[moltbank] ✓ credentials.json already available (active org: ${existing.activeOrg})`);
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const credsPath = getCredentialsPath();
|
|
733
|
+
const pendingPath = join(skillDir, '.oauth_device_code.json');
|
|
734
|
+
const now = Math.floor(Date.now() / 1000);
|
|
735
|
+
|
|
736
|
+
let deviceCode = '';
|
|
737
|
+
let userCode = '';
|
|
738
|
+
let verificationUri = `${appBaseUrl}/activate`;
|
|
739
|
+
let expiresAt = 0;
|
|
740
|
+
|
|
741
|
+
if (existsSync(pendingPath)) {
|
|
742
|
+
try {
|
|
743
|
+
const pending = JSON.parse(readFileSync(pendingPath, 'utf8')) as Record<string, unknown>;
|
|
744
|
+
const pendingDeviceCode = asString(pending.device_code);
|
|
745
|
+
const pendingUserCode = asString(pending.user_code);
|
|
746
|
+
const pendingVerificationUri = asString(pending.verification_uri, verificationUri);
|
|
747
|
+
const pendingExpiresAtRaw = pending.expires_at;
|
|
748
|
+
const pendingExpiresAt = typeof pendingExpiresAtRaw === 'number' ? pendingExpiresAtRaw : 0;
|
|
749
|
+
|
|
750
|
+
if (pendingDeviceCode && pendingUserCode && pendingExpiresAt > now + 5) {
|
|
751
|
+
deviceCode = pendingDeviceCode;
|
|
752
|
+
userCode = pendingUserCode;
|
|
753
|
+
verificationUri = pendingVerificationUri;
|
|
754
|
+
expiresAt = pendingExpiresAt;
|
|
755
|
+
api.logger.info(`[moltbank] reusing pending OAuth device code (expires in ~${Math.max(1, Math.ceil((expiresAt - now) / 60))} min)`);
|
|
756
|
+
} else {
|
|
757
|
+
unlinkSync(pendingPath);
|
|
758
|
+
}
|
|
759
|
+
} catch {
|
|
760
|
+
try {
|
|
761
|
+
unlinkSync(pendingPath);
|
|
762
|
+
} catch {
|
|
763
|
+
// ignore
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (!deviceCode || !userCode) {
|
|
769
|
+
api.logger.info('[moltbank] no valid credentials found — starting onboarding flow...');
|
|
770
|
+
|
|
771
|
+
const requestCode = run(`"${process.execPath}" "./scripts/request-oauth-device-code.mjs"`, {
|
|
772
|
+
cwd: skillDir,
|
|
773
|
+
silent: true,
|
|
774
|
+
env: {
|
|
775
|
+
APP_BASE_URL: appBaseUrl,
|
|
776
|
+
MOLTBANK_CREDENTIALS_PATH: credsPath
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
if (!requestCode.ok) {
|
|
781
|
+
api.logger.warn('[moltbank] ✗ could not request OAuth device code: ' + requestCode.stderr);
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const codeJson = parseFirstJsonObject(requestCode.stdout);
|
|
786
|
+
deviceCode = asString(codeJson?.device_code);
|
|
787
|
+
userCode = asString(codeJson?.user_code);
|
|
788
|
+
verificationUri = asString(codeJson?.verification_uri, `${appBaseUrl}/activate`);
|
|
789
|
+
|
|
790
|
+
const expiresInRaw = codeJson?.expires_in;
|
|
791
|
+
const expiresIn = typeof expiresInRaw === 'number' ? expiresInRaw : Number(expiresInRaw ?? 900);
|
|
792
|
+
const safeExpiresIn = Number.isFinite(expiresIn) && expiresIn > 0 ? Math.floor(expiresIn) : 900;
|
|
793
|
+
expiresAt = now + safeExpiresIn;
|
|
794
|
+
|
|
795
|
+
if (!deviceCode || !userCode) {
|
|
796
|
+
api.logger.warn('[moltbank] ✗ onboarding response missing device_code/user_code');
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
try {
|
|
801
|
+
writeFileSync(
|
|
802
|
+
pendingPath,
|
|
803
|
+
JSON.stringify(
|
|
804
|
+
{
|
|
805
|
+
device_code: deviceCode,
|
|
806
|
+
user_code: userCode,
|
|
807
|
+
verification_uri: verificationUri,
|
|
808
|
+
expires_at: expiresAt
|
|
809
|
+
},
|
|
810
|
+
null,
|
|
811
|
+
2
|
|
812
|
+
) + '\n',
|
|
813
|
+
'utf8'
|
|
814
|
+
);
|
|
815
|
+
} catch (e) {
|
|
816
|
+
api.logger.warn('[moltbank] could not persist pending OAuth device code: ' + String(e));
|
|
817
|
+
}
|
|
818
|
+
} else {
|
|
819
|
+
api.logger.info('[moltbank] no valid credentials found — pending onboarding code already issued');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
api.logger.info('[moltbank] ACTION REQUIRED: link this agent to your MoltBank account');
|
|
823
|
+
api.logger.info(`[moltbank] 1) Open: ${verificationUri}`);
|
|
824
|
+
api.logger.info(`[moltbank] 2) Enter code: ${userCode}`);
|
|
825
|
+
if (expiresAt > now) {
|
|
826
|
+
api.logger.info(`[moltbank] 3) Code expires in ~${Math.max(1, Math.ceil((expiresAt - now) / 60))} min`);
|
|
827
|
+
}
|
|
828
|
+
api.logger.info('[moltbank] 4) Optional: reply in chat "MoltBank done" for a live status check');
|
|
829
|
+
|
|
830
|
+
if (!waitForApproval) {
|
|
831
|
+
api.logger.info('[moltbank] nonblocking startup mode: skipping OAuth polling to keep gateway/channel startup responsive');
|
|
832
|
+
const backgroundTimeoutSeconds = Math.max(30, expiresAt - now - 5);
|
|
833
|
+
startBackgroundOauthPoll(skillDir, appBaseUrl, credsPath, deviceCode, backgroundTimeoutSeconds, api);
|
|
834
|
+
api.logger.info('[moltbank] once approved in browser, setup finalization will continue automatically');
|
|
835
|
+
api.logger.info('[moltbank] optional immediate check: `openclaw moltbank auth-status`');
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
api.logger.info('[moltbank] waiting for approval and polling token...');
|
|
840
|
+
|
|
841
|
+
const pollTimeoutSeconds = Number(process.env.MOLTBANK_OAUTH_POLL_TIMEOUT_SECONDS ?? 180);
|
|
842
|
+
const safePollTimeoutSeconds = Number.isFinite(pollTimeoutSeconds) && pollTimeoutSeconds > 0 ? Math.floor(pollTimeoutSeconds) : 180;
|
|
843
|
+
const pollIntervalSeconds = Number(process.env.MOLTBANK_OAUTH_POLL_INTERVAL_SECONDS ?? 5);
|
|
844
|
+
const safePollIntervalSeconds = Number.isFinite(pollIntervalSeconds) && pollIntervalSeconds > 0 ? Math.floor(pollIntervalSeconds) : 5;
|
|
845
|
+
|
|
846
|
+
const poll = run(
|
|
847
|
+
`"${process.execPath}" "./scripts/poll-oauth-token.mjs" "${deviceCode}" ${safePollTimeoutSeconds} ${safePollIntervalSeconds} --save`,
|
|
848
|
+
{
|
|
849
|
+
cwd: skillDir,
|
|
850
|
+
silent: true,
|
|
851
|
+
env: {
|
|
852
|
+
APP_BASE_URL: appBaseUrl,
|
|
853
|
+
MOLTBANK_CREDENTIALS_PATH: credsPath
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
);
|
|
857
|
+
if (!poll.ok) {
|
|
858
|
+
const pollJson = parseFirstJsonObject(`${poll.stdout}\n${poll.stderr}`);
|
|
859
|
+
let oauthError = '';
|
|
860
|
+
if (isRecord(pollJson) && isRecord(pollJson.payload)) {
|
|
861
|
+
oauthError = asString((pollJson.payload as Record<string, unknown>).error);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (oauthError === 'invalid_grant') {
|
|
865
|
+
api.logger.warn('[moltbank] ✗ onboarding code expired or already consumed (invalid_grant)');
|
|
866
|
+
try {
|
|
867
|
+
if (existsSync(pendingPath)) unlinkSync(pendingPath);
|
|
868
|
+
} catch {
|
|
869
|
+
// ignore
|
|
870
|
+
}
|
|
871
|
+
} else {
|
|
872
|
+
api.logger.warn('[moltbank] ✗ onboarding poll failed or timed out');
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (poll.stderr) {
|
|
876
|
+
api.logger.warn('[moltbank] poll detail: ' + poll.stderr);
|
|
877
|
+
}
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const after = parseActiveTokenFromCredentials();
|
|
882
|
+
if (!after.ok) {
|
|
883
|
+
api.logger.warn('[moltbank] ✗ onboarding finished but credentials are not usable');
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
if (existsSync(pendingPath)) unlinkSync(pendingPath);
|
|
889
|
+
} catch {
|
|
890
|
+
// ignore
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
api.logger.info(`[moltbank] ✓ onboarding completed (active org: ${after.activeOrg})`);
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function printAuthStatus(skillDir: string, appBaseUrl: string, api: LoggerApi): void {
|
|
898
|
+
const now = Math.floor(Date.now() / 1000);
|
|
899
|
+
const existing = parseActiveTokenFromCredentials();
|
|
900
|
+
const pending = readPendingOauthCode(skillDir);
|
|
901
|
+
const poller = oauthPollers.get(skillDir);
|
|
902
|
+
const finalizer = backgroundFinalizers.get(skillDir);
|
|
903
|
+
const pollerRunning = Boolean(poller && poller.exitCode === null && !poller.killed);
|
|
904
|
+
const finalizerRunning = Boolean(finalizer && finalizer.exitCode === null && !finalizer.killed);
|
|
905
|
+
|
|
906
|
+
api.logger.info('[moltbank] auth status:');
|
|
907
|
+
if (existing.ok) {
|
|
908
|
+
api.logger.info(`[moltbank] credentials: ready (active org: ${existing.activeOrg})`);
|
|
909
|
+
} else {
|
|
910
|
+
api.logger.info('[moltbank] credentials: missing');
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (pending) {
|
|
914
|
+
const mins = Math.ceil((pending.expiresAt - now) / 60);
|
|
915
|
+
const expiryLabel = mins > 0 ? `~${Math.max(1, mins)} min remaining` : 'expired';
|
|
916
|
+
api.logger.info(`[moltbank] pending code: ${pending.userCode} (${expiryLabel})`);
|
|
917
|
+
api.logger.info(`[moltbank] activation URL: ${pending.verificationUri || `${appBaseUrl}/activate`}`);
|
|
918
|
+
} else {
|
|
919
|
+
api.logger.info('[moltbank] pending code: none');
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
api.logger.info(`[moltbank] background poll: ${pollerRunning ? 'running' : 'idle'}`);
|
|
923
|
+
api.logger.info(`[moltbank] background finalize: ${finalizerRunning ? 'running' : 'idle'}`);
|
|
924
|
+
if (!existing.ok && !pending) {
|
|
925
|
+
api.logger.info('[moltbank] hint: run `openclaw moltbank setup` to start onboarding');
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function ensureMcporterConfig(skillDir: string, appBaseUrl: string, api: LoggerApi) {
|
|
930
|
+
const cfgPath = join(skillDir, 'config', 'mcporter.json');
|
|
931
|
+
mkdirSync(join(skillDir, 'config'), { recursive: true });
|
|
932
|
+
|
|
933
|
+
const cfg = {
|
|
934
|
+
mcpServers: {
|
|
935
|
+
MoltBank: {
|
|
936
|
+
description: 'MoltBank stablecoin banking MCP server powered by Fondu',
|
|
937
|
+
transport: 'sse',
|
|
938
|
+
url: `${appBaseUrl}/api/mcp`,
|
|
939
|
+
headers: {
|
|
940
|
+
'Content-Type': 'application/json',
|
|
941
|
+
Authorization: 'Bearer ${MOLTBANK}'
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
948
|
+
api.logger.info('[moltbank] ✓ mcporter.json written: ' + cfgPath);
|
|
949
|
+
|
|
950
|
+
if (!hasBin('mcporter')) {
|
|
951
|
+
api.logger.warn('[moltbank] ✗ mcporter not in PATH — cannot register');
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (isMoltBankRegistered()) {
|
|
956
|
+
api.logger.info('[moltbank] ✓ MoltBank already registered in mcporter');
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
api.logger.info('[moltbank] registering MoltBank in mcporter (scope: home)...');
|
|
961
|
+
|
|
962
|
+
const addCmd =
|
|
963
|
+
`mcporter config add MoltBank ` +
|
|
964
|
+
`--url "${appBaseUrl}/api/mcp" ` +
|
|
965
|
+
`--transport sse ` +
|
|
966
|
+
`--header "Authorization=Bearer \${MOLTBANK}" ` +
|
|
967
|
+
`--header "Content-Type=application/json" ` +
|
|
968
|
+
`--description "MoltBank stablecoin banking MCP server powered by Fondu" ` +
|
|
969
|
+
`--scope home`;
|
|
970
|
+
|
|
971
|
+
const result = run(addCmd, { silent: false });
|
|
972
|
+
|
|
973
|
+
if (result.ok) {
|
|
974
|
+
api.logger.info('[moltbank] ✓ MoltBank registered in mcporter');
|
|
975
|
+
const list = run('mcporter config list', { silent: true });
|
|
976
|
+
api.logger.info('[moltbank] mcporter config list: ' + list.stdout);
|
|
977
|
+
} else {
|
|
978
|
+
api.logger.warn('[moltbank] ✗ mcporter config add failed: ' + result.stderr);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// ─── sandbox env vars ────────────────────────────────────────────────────────
|
|
983
|
+
|
|
984
|
+
function injectSandboxEnv(skillDir: string, api: LoggerApi): boolean {
|
|
985
|
+
const credsPath = getCredentialsPath();
|
|
986
|
+
|
|
987
|
+
if (!existsSync(credsPath)) {
|
|
988
|
+
api.logger.info('[moltbank] no credentials.json found — skipping sandbox env injection');
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
let changed = false;
|
|
993
|
+
|
|
994
|
+
try {
|
|
995
|
+
const creds = JSON.parse(readFileSync(credsPath, 'utf8'));
|
|
996
|
+
const activeOrg = creds.active_organization;
|
|
997
|
+
|
|
998
|
+
if (!activeOrg) {
|
|
999
|
+
api.logger.warn('[moltbank] ✗ active_organization not set in credentials.json');
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const org = creds.organizations?.find((o: CredentialsOrganization) => o.name === activeOrg);
|
|
1004
|
+
if (!org?.access_token) {
|
|
1005
|
+
api.logger.warn(`[moltbank] ✗ no access_token for active org "${activeOrg}"`);
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const config = readOpenclawConfig();
|
|
1010
|
+
const envPath = ['agents', 'defaults', 'sandbox', 'docker', 'env'];
|
|
1011
|
+
setNestedValue(config, envPath, getNestedValue(config, envPath) ?? {});
|
|
1012
|
+
const envObj = getNestedValue(config, envPath) as Record<string, string>;
|
|
1013
|
+
|
|
1014
|
+
if (envObj.MOLTBANK !== org.access_token) {
|
|
1015
|
+
envObj.MOLTBANK = org.access_token;
|
|
1016
|
+
api.logger.info(`[moltbank] ✓ MOLTBANK injected (org: ${activeOrg})`);
|
|
1017
|
+
changed = true;
|
|
1018
|
+
} else {
|
|
1019
|
+
api.logger.info(`[moltbank] ✓ MOLTBANK already injected (org: ${activeOrg})`);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (envObj.ACTIVE_ORG_OVERRIDE !== activeOrg) {
|
|
1023
|
+
envObj.ACTIVE_ORG_OVERRIDE = activeOrg;
|
|
1024
|
+
api.logger.info(`[moltbank] ✓ ACTIVE_ORG_OVERRIDE injected ("${activeOrg}")`);
|
|
1025
|
+
changed = true;
|
|
1026
|
+
} else {
|
|
1027
|
+
api.logger.info(`[moltbank] ✓ ACTIVE_ORG_OVERRIDE already set ("${activeOrg}")`);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
let privateKey = org.x402_signer_private_key ?? '';
|
|
1031
|
+
|
|
1032
|
+
if (!privateKey) {
|
|
1033
|
+
api.logger.info('[moltbank] x402_signer_private_key not found — generating EOA signer...');
|
|
1034
|
+
const initResult = run(`"${process.execPath}" "./scripts/init-openclaw-signer.mjs"`, {
|
|
1035
|
+
cwd: skillDir,
|
|
1036
|
+
silent: true,
|
|
1037
|
+
env: {
|
|
1038
|
+
MOLTBANK_CREDENTIALS_PATH: credsPath
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
if (!initResult.ok) {
|
|
1042
|
+
api.logger.warn('[moltbank] ✗ could not generate EOA signer: ' + initResult.stderr);
|
|
1043
|
+
api.logger.warn('[moltbank] agent will generate signer on first x402/Polymarket use');
|
|
1044
|
+
} else {
|
|
1045
|
+
const freshCreds = JSON.parse(readFileSync(credsPath, 'utf8')) as CredentialsFile;
|
|
1046
|
+
const freshOrg = freshCreds.organizations?.find((o: CredentialsOrganization) => o.name === activeOrg);
|
|
1047
|
+
privateKey = freshOrg?.x402_signer_private_key ?? '';
|
|
1048
|
+
if (privateKey) {
|
|
1049
|
+
api.logger.info('[moltbank] ✓ EOA signer generated and saved');
|
|
1050
|
+
} else {
|
|
1051
|
+
api.logger.warn('[moltbank] ✗ init-openclaw-signer.mjs ran but key not found');
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (privateKey) {
|
|
1057
|
+
if (envObj.SIGNER !== privateKey) {
|
|
1058
|
+
envObj.SIGNER = privateKey;
|
|
1059
|
+
api.logger.info('[moltbank] ✓ SIGNER injected');
|
|
1060
|
+
changed = true;
|
|
1061
|
+
} else {
|
|
1062
|
+
api.logger.info('[moltbank] ✓ SIGNER already injected');
|
|
1063
|
+
}
|
|
1064
|
+
} else {
|
|
1065
|
+
api.logger.info('[moltbank] ℹ SIGNER not available — skipped (agent will handle on first use)');
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (changed) {
|
|
1069
|
+
writeOpenclawConfig(config);
|
|
1070
|
+
api.logger.info('[moltbank] ✓ openclaw.json updated with sandbox env vars');
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return changed;
|
|
1074
|
+
} catch (e) {
|
|
1075
|
+
api.logger.warn('[moltbank] ✗ error injecting sandbox env: ' + String(e));
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// ─── sandbox docker config ────────────────────────────────────────────────────
|
|
1081
|
+
|
|
1082
|
+
function configureSandbox(api: LoggerApi): boolean {
|
|
1083
|
+
const SETUP_CMD =
|
|
1084
|
+
'echo \'APT::Sandbox::User "root";\' > /etc/apt/apt.conf.d/99sandbox && ' +
|
|
1085
|
+
'apt-get update -qq && ' +
|
|
1086
|
+
"echo '[moltbank:sandbox] [1/7] installing base apt deps...' && " +
|
|
1087
|
+
'apt-get install -y curl wget jq ca-certificates gnupg && ' +
|
|
1088
|
+
"echo '[moltbank:sandbox] [2/7] configuring NodeSource (Node 22)...' && " +
|
|
1089
|
+
'mkdir -p /etc/apt/keyrings && ' +
|
|
1090
|
+
'curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | ' +
|
|
1091
|
+
'gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && ' +
|
|
1092
|
+
"echo 'deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main' > /etc/apt/sources.list.d/nodesource.list && " +
|
|
1093
|
+
"echo '[moltbank:sandbox] [3/7] apt update...' && " +
|
|
1094
|
+
'apt-get update -qq && ' +
|
|
1095
|
+
"echo '[moltbank:sandbox] [4/7] installing Node.js 22 + npm...' && " +
|
|
1096
|
+
'apt-get install -y nodejs npm && ' +
|
|
1097
|
+
"echo '[moltbank:sandbox] forcing Node 22 as default...' && " +
|
|
1098
|
+
'NODE22_BIN=$(which node) && ' +
|
|
1099
|
+
'update-alternatives --install /usr/local/bin/node node $NODE22_BIN 100 2>/dev/null || true && ' +
|
|
1100
|
+
'update-alternatives --set node $NODE22_BIN 2>/dev/null || true && ' +
|
|
1101
|
+
'node -v && npm -v && ' +
|
|
1102
|
+
"echo '[moltbank:sandbox] [5/7] installing npm global deps (mcporter + sdk libs)...' && " +
|
|
1103
|
+
'npm install -g mcporter @x402/fetch@2.3.0 @x402/evm@2.3.1 viem@2.46.0 @polymarket/clob-client ethers@5 @solana/web3.js@1.98.4 bs58@6.0.0 && ' +
|
|
1104
|
+
"echo '[moltbank:sandbox] [6/7] verifying mcporter binary...' && " +
|
|
1105
|
+
"NPM_GLOBAL=$(npm root -g 2>/dev/null | sed 's|/node_modules$|/bin|' || true) && " +
|
|
1106
|
+
'if [ -n "$NPM_GLOBAL" ] && [ -x "$NPM_GLOBAL/mcporter" ]; then ln -sf "$NPM_GLOBAL/mcporter" /usr/local/bin/mcporter || true; fi && ' +
|
|
1107
|
+
'command -v mcporter >/dev/null 2>&1 && ' +
|
|
1108
|
+
'mcporter --version && ' +
|
|
1109
|
+
"echo '[moltbank:sandbox] final versions:' && " +
|
|
1110
|
+
'node --version && ' +
|
|
1111
|
+
'mcporter --version && ' +
|
|
1112
|
+
"echo '[moltbank:sandbox] [7/7] sandbox setup finished successfully'";
|
|
1113
|
+
|
|
1114
|
+
try {
|
|
1115
|
+
let changed = false;
|
|
1116
|
+
const config = readOpenclawConfig();
|
|
1117
|
+
|
|
1118
|
+
const currentCmd = asString(getNestedValue(config, ['agents', 'defaults', 'sandbox', 'docker', 'setupCommand']));
|
|
1119
|
+
const hasMcporterSetup = currentCmd.includes('mcporter');
|
|
1120
|
+
const hasNode22Setup = currentCmd.includes('node_22.x');
|
|
1121
|
+
const hasNode22Fix = currentCmd.includes('update-alternatives');
|
|
1122
|
+
const isCorrupted = currentCmd.includes('/home/') || currentCmd.includes('Unknown command');
|
|
1123
|
+
|
|
1124
|
+
if (!hasMcporterSetup || !hasNode22Setup || !hasNode22Fix || isCorrupted) {
|
|
1125
|
+
setNestedValue(config, ['agents', 'defaults', 'sandbox', 'docker', 'setupCommand'], SETUP_CMD);
|
|
1126
|
+
api.logger.info('[moltbank] ✓ sandbox setupCommand written directly to JSON (no shell expansion)');
|
|
1127
|
+
changed = true;
|
|
1128
|
+
} else {
|
|
1129
|
+
api.logger.info('[moltbank] ✓ sandbox setupCommand already correct');
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const currentNetwork = getNestedValue(config, ['agents', 'defaults', 'sandbox', 'docker', 'network']);
|
|
1133
|
+
if (currentNetwork !== 'bridge') {
|
|
1134
|
+
setNestedValue(config, ['agents', 'defaults', 'sandbox', 'docker', 'network'], 'bridge');
|
|
1135
|
+
api.logger.info('[moltbank] ✓ sandbox network set to bridge');
|
|
1136
|
+
changed = true;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
setNestedValue(config, ['agents', 'defaults', 'sandbox', 'docker', 'readOnlyRoot'], false);
|
|
1140
|
+
setNestedValue(config, ['agents', 'defaults', 'sandbox', 'docker', 'user'], '0:0');
|
|
1141
|
+
setNestedValue(config, ['agents', 'defaults', 'sandbox', 'workspaceAccess'], 'rw');
|
|
1142
|
+
|
|
1143
|
+
writeOpenclawConfig(config);
|
|
1144
|
+
api.logger.info('[moltbank] ✓ sandbox docker configured (written directly to openclaw.json)');
|
|
1145
|
+
return changed;
|
|
1146
|
+
} catch (e) {
|
|
1147
|
+
api.logger.warn('[moltbank] ✗ sandbox configuration failed: ' + String(e));
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// ─── sandbox recreate + gateway restart ──────────────────────────────────────
|
|
1153
|
+
|
|
1154
|
+
function recreateSandboxAndRestart(api: LoggerApi) {
|
|
1155
|
+
api.logger.info('[moltbank] recreating sandbox containers...');
|
|
1156
|
+
api.logger.info('[moltbank] ⏳ waiting 8s before recreate (hot container protection)...');
|
|
1157
|
+
setTimeout(() => {
|
|
1158
|
+
api.logger.info('[moltbank] stopping gateway...');
|
|
1159
|
+
run('openclaw gateway stop', { silent: true });
|
|
1160
|
+
|
|
1161
|
+
const stillRunning = run("ps aux | grep openclaw-gateway | grep -v grep | awk '{print $2}'", { silent: true });
|
|
1162
|
+
if (stillRunning.stdout.trim()) {
|
|
1163
|
+
api.logger.info(`[moltbank] gateway still running (pids: ${stillRunning.stdout.trim()}) — sending SIGKILL...`);
|
|
1164
|
+
run("kill -9 $(ps aux | grep openclaw-gateway | grep -v grep | awk '{print $2}') 2>/dev/null || true", { silent: true });
|
|
1165
|
+
api.logger.info('[moltbank] ✓ gateway process killed');
|
|
1166
|
+
} else {
|
|
1167
|
+
api.logger.info('[moltbank] ✓ gateway stopped cleanly');
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
run('sleep 2', { silent: true });
|
|
1171
|
+
|
|
1172
|
+
const recreate = run('openclaw sandbox recreate --all --force', {
|
|
1173
|
+
silent: false
|
|
1174
|
+
});
|
|
1175
|
+
if (recreate.ok) {
|
|
1176
|
+
api.logger.info('[moltbank] ✓ sandbox containers recreated — new container will be created on next agent message');
|
|
1177
|
+
} else {
|
|
1178
|
+
api.logger.warn('[moltbank] ✗ sandbox recreate failed');
|
|
1179
|
+
api.logger.warn('[moltbank] run manually: openclaw sandbox recreate --all --force');
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
api.logger.info('[moltbank] restarting gateway...');
|
|
1183
|
+
run('openclaw gateway', { silent: true });
|
|
1184
|
+
}, 8000);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// ─── main setup ───────────────────────────────────────────────────────────────
|
|
1188
|
+
|
|
1189
|
+
async function runSetup(cfg: MoltbankPluginConfig, api: LoggerApi, options: { authWaitMode?: AuthWaitMode } = {}) {
|
|
1190
|
+
let hostReady = false;
|
|
1191
|
+
const appBaseUrl = getAppBaseUrl(cfg);
|
|
1192
|
+
const skillName = getSkillName(cfg);
|
|
1193
|
+
const sandbox = isSandboxEnabled();
|
|
1194
|
+
const skillDir = getSkillDir(cfg);
|
|
1195
|
+
const waitForAuth = (options.authWaitMode ?? 'blocking') === 'blocking';
|
|
1196
|
+
|
|
1197
|
+
api.logger.info(`[moltbank] ══════════════════════════════════════`);
|
|
1198
|
+
api.logger.info(`[moltbank] MoltBank plugin setup starting`);
|
|
1199
|
+
api.logger.info(`[moltbank] mode: ${sandbox ? 'sandbox (Docker)' : 'host (direct)'}`);
|
|
1200
|
+
api.logger.info(`[moltbank] skill dir: ${skillDir}`);
|
|
1201
|
+
api.logger.info(`[moltbank] base url: ${appBaseUrl}`);
|
|
1202
|
+
api.logger.info(`[moltbank] ══════════════════════════════════════`);
|
|
1203
|
+
api.logger.info('[moltbank] preflight: cleaning stale MoltBank plugin load paths...');
|
|
1204
|
+
cleanupStaleMoltbankPluginLoadPaths(api);
|
|
1205
|
+
|
|
1206
|
+
if (sandbox) {
|
|
1207
|
+
api.logger.info('[moltbank] configuring sandbox mode...');
|
|
1208
|
+
|
|
1209
|
+
api.logger.info('[moltbank] [sandbox 0/10] ensuring mcporter on host...');
|
|
1210
|
+
ensureMcporter(api);
|
|
1211
|
+
|
|
1212
|
+
api.logger.info('[moltbank] [sandbox 1/10] installing skill files...');
|
|
1213
|
+
const skillInstalled = ensureSkillInstalled(skillDir, appBaseUrl, skillName, api, 'sandbox');
|
|
1214
|
+
if (!skillInstalled) {
|
|
1215
|
+
api.logger.warn('[moltbank] skill install failed — aborting sandbox setup');
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
api.logger.info('[moltbank] [sandbox 2/10] ensuring SKILL.md naming...');
|
|
1220
|
+
ensureSkillFilesUppercase(skillDir, api);
|
|
1221
|
+
|
|
1222
|
+
api.logger.info('[moltbank] [sandbox 3/10] applying permissions (chown + chmod)...');
|
|
1223
|
+
ensureSkillPermissions(skillDir, api);
|
|
1224
|
+
|
|
1225
|
+
api.logger.info('[moltbank] [sandbox 4/10] ensuring sandbox authentication...');
|
|
1226
|
+
if (!ensureMoltbankAuth(skillDir, appBaseUrl, api, { waitForApproval: waitForAuth })) {
|
|
1227
|
+
if (!waitForAuth) {
|
|
1228
|
+
const pending = readPendingOauthCode(skillDir);
|
|
1229
|
+
if (pending) {
|
|
1230
|
+
api.logger.warn('[moltbank] sandbox auth pending — startup continues without blocking channel startup');
|
|
1231
|
+
api.logger.info('[moltbank] setup will finalize automatically after browser approval');
|
|
1232
|
+
api.logger.info('[moltbank] optional immediate check: `openclaw moltbank auth-status`');
|
|
1233
|
+
} else {
|
|
1234
|
+
api.logger.warn('[moltbank] sandbox auth setup failed before issuing a valid device code');
|
|
1235
|
+
api.logger.warn('[moltbank] review the previous error and rerun `openclaw moltbank setup`');
|
|
1236
|
+
}
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
api.logger.warn('[moltbank] sandbox auth not ready — complete onboarding and run setup again');
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
api.logger.info('[moltbank] [sandbox 5/10] installing skill npm dependencies...');
|
|
1244
|
+
ensureNpmDeps(skillDir, api, 'sandbox');
|
|
1245
|
+
|
|
1246
|
+
api.logger.info('[moltbank] [sandbox 6/10] normalizing SKILL.md frontmatter...');
|
|
1247
|
+
fixSkillFrontmatter(skillDir, skillName, api);
|
|
1248
|
+
|
|
1249
|
+
api.logger.info('[moltbank] [sandbox 7/10] writing/registering mcporter config...');
|
|
1250
|
+
ensureMcporterConfig(skillDir, appBaseUrl, api);
|
|
1251
|
+
|
|
1252
|
+
api.logger.info('[moltbank] [sandbox 8/10] configuring sandbox docker settings...');
|
|
1253
|
+
const sandboxChanged = configureSandbox(api);
|
|
1254
|
+
|
|
1255
|
+
api.logger.info('[moltbank] [sandbox 9/10] injecting sandbox env vars (MOLTBANK, ACTIVE_ORG_OVERRIDE, SIGNER)...');
|
|
1256
|
+
const envChanged = injectSandboxEnv(skillDir, api);
|
|
1257
|
+
|
|
1258
|
+
api.logger.info('[moltbank] [sandbox 10/10] apply sandbox changes (recreate + gateway stop)...');
|
|
1259
|
+
if (sandboxChanged || envChanged) {
|
|
1260
|
+
api.logger.info('[moltbank] config changed — recreating sandbox to apply new settings');
|
|
1261
|
+
recreateSandboxAndRestart(api);
|
|
1262
|
+
} else {
|
|
1263
|
+
api.logger.info('[moltbank] sandbox unchanged — no restart needed');
|
|
1264
|
+
}
|
|
1265
|
+
} else {
|
|
1266
|
+
api.logger.info('[moltbank] [host 1/8] ensuring mcporter on host...');
|
|
1267
|
+
ensureMcporter(api);
|
|
1268
|
+
|
|
1269
|
+
api.logger.info('[moltbank] [host 2/8] installing skill files...');
|
|
1270
|
+
const installed = ensureSkillInstalled(skillDir, appBaseUrl, skillName, api, 'host');
|
|
1271
|
+
if (!installed) {
|
|
1272
|
+
api.logger.warn('[moltbank] host setup aborted: skill install failed. Verify install.sh/base URL and retry.');
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
api.logger.info('[moltbank] [host 3/8] ensuring SKILL.md naming/frontmatter...');
|
|
1277
|
+
ensureSkillFilesUppercase(skillDir, api);
|
|
1278
|
+
fixSkillFrontmatter(skillDir, skillName, api);
|
|
1279
|
+
|
|
1280
|
+
api.logger.info('[moltbank] [host 4/8] applying permissions (chown + chmod)...');
|
|
1281
|
+
ensureSkillPermissions(skillDir, api);
|
|
1282
|
+
|
|
1283
|
+
api.logger.info('[moltbank] [host 5/8] ensuring account onboarding/authentication...');
|
|
1284
|
+
if (!ensureMoltbankAuth(skillDir, appBaseUrl, api, { waitForApproval: waitForAuth })) {
|
|
1285
|
+
if (!waitForAuth) {
|
|
1286
|
+
const pending = readPendingOauthCode(skillDir);
|
|
1287
|
+
if (pending) {
|
|
1288
|
+
api.logger.warn('[moltbank] host auth pending — startup continues without blocking channel startup');
|
|
1289
|
+
api.logger.info('[moltbank] setup will finalize automatically after browser approval');
|
|
1290
|
+
api.logger.info('[moltbank] optional immediate check: `openclaw moltbank auth-status`');
|
|
1291
|
+
} else {
|
|
1292
|
+
api.logger.warn('[moltbank] host auth setup failed before issuing a valid device code');
|
|
1293
|
+
api.logger.warn('[moltbank] review the previous error and rerun `openclaw moltbank setup`');
|
|
1294
|
+
}
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
api.logger.warn('[moltbank] host auth not ready — complete onboarding and run setup again');
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
api.logger.info('[moltbank] [host 6/8] installing skill npm dependencies...');
|
|
1302
|
+
ensureNpmDeps(skillDir, api, 'host');
|
|
1303
|
+
|
|
1304
|
+
api.logger.info('[moltbank] [host 7/8] writing/registering mcporter config...');
|
|
1305
|
+
ensureMcporterConfig(skillDir, appBaseUrl, api);
|
|
1306
|
+
|
|
1307
|
+
api.logger.info('[moltbank] [host 8/8] running wrapper smoke test...');
|
|
1308
|
+
// FIX: usar path absoluto para el .ps1 en Windows
|
|
1309
|
+
const ps1Path = join(skillDir, 'scripts', 'moltbank.ps1');
|
|
1310
|
+
const smokeCmd = IS_WIN
|
|
1311
|
+
? `powershell -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}" list MoltBank`
|
|
1312
|
+
: `"${skillDir}/scripts/moltbank.sh" list MoltBank`;
|
|
1313
|
+
const smoke = run(smokeCmd, { cwd: skillDir, silent: true });
|
|
1314
|
+
if (!smoke.ok) {
|
|
1315
|
+
api.logger.warn('[moltbank] host setup incomplete: smoke test failed (`moltbank list MoltBank` via platform script)');
|
|
1316
|
+
} else {
|
|
1317
|
+
api.logger.info('[moltbank] host smoke test passed (`moltbank list MoltBank`)');
|
|
1318
|
+
}
|
|
1319
|
+
hostReady = smoke.ok;
|
|
1320
|
+
|
|
1321
|
+
api.logger.info('[moltbank] host mode — setup completed with onboarding flow');
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
api.logger.info(`[moltbank] ══════════════════════════════════════`);
|
|
1325
|
+
api.logger.info(`[moltbank] ✓ setup complete`);
|
|
1326
|
+
if (sandbox) {
|
|
1327
|
+
api.logger.info('[moltbank] ⏳ sandbox will be recreated in ~8s — send a message to the agent after that');
|
|
1328
|
+
} else if (hostReady) {
|
|
1329
|
+
api.logger.info('[moltbank] ✓ host ready');
|
|
1330
|
+
}
|
|
1331
|
+
api.logger.info(`[moltbank] skill: ${skillDir}/SKILL.md`);
|
|
1332
|
+
api.logger.info(`[moltbank] mcporter: ${skillDir}/config/mcporter.json`);
|
|
1333
|
+
if (sandbox) {
|
|
1334
|
+
const finalConfig = readOpenclawConfig();
|
|
1335
|
+
const finalEnv = asStringRecord(getNestedValue(finalConfig, ['agents', 'defaults', 'sandbox', 'docker', 'env']));
|
|
1336
|
+
const finalNetwork = asString(getNestedValue(finalConfig, ['agents', 'defaults', 'sandbox', 'docker', 'network']));
|
|
1337
|
+
const finalCmd = asString(getNestedValue(finalConfig, ['agents', 'defaults', 'sandbox', 'docker', 'setupCommand']));
|
|
1338
|
+
|
|
1339
|
+
api.logger.info(`[moltbank] openclaw.json sandbox env:`);
|
|
1340
|
+
api.logger.info(`[moltbank] MOLTBANK: ${finalEnv.MOLTBANK ? '✓ set' : '✗ missing'}`);
|
|
1341
|
+
api.logger.info(
|
|
1342
|
+
`[moltbank] ACTIVE_ORG_OVERRIDE: ${finalEnv.ACTIVE_ORG_OVERRIDE ? `✓ "${finalEnv.ACTIVE_ORG_OVERRIDE}"` : '✗ missing'}`
|
|
1343
|
+
);
|
|
1344
|
+
api.logger.info(`[moltbank] SIGNER: ${finalEnv.SIGNER ? '✓ set' : '✗ missing (agent may generate on first use)'}`);
|
|
1345
|
+
api.logger.info(`[moltbank] sandbox docker:`);
|
|
1346
|
+
api.logger.info(
|
|
1347
|
+
`[moltbank] network: ${finalNetwork === 'bridge' ? '✓ bridge' : `✗ "${finalNetwork}" (expected bridge)`}`
|
|
1348
|
+
);
|
|
1349
|
+
api.logger.info(
|
|
1350
|
+
`[moltbank] setupCommand: ${finalCmd.includes('node_22.x') && !finalCmd.includes('/home/') ? '✓ ok (no host paths)' : '✗ may be corrupted'}`
|
|
1351
|
+
);
|
|
1352
|
+
|
|
1353
|
+
const containerCheck = run(
|
|
1354
|
+
`docker inspect $(docker ps | grep openclaw-sbx | awk '{print $1}') --format '{{.HostConfig.NetworkMode}}' 2>/dev/null`,
|
|
1355
|
+
{ silent: true }
|
|
1356
|
+
);
|
|
1357
|
+
if (!containerCheck.ok || !containerCheck.stdout) {
|
|
1358
|
+
api.logger.info(`[moltbank] container: not running yet — will be created on first agent message`);
|
|
1359
|
+
} else if (containerCheck.stdout.trim() === 'bridge') {
|
|
1360
|
+
api.logger.info(`[moltbank] container: ✓ bridge network — internet available`);
|
|
1361
|
+
} else {
|
|
1362
|
+
api.logger.warn(`[moltbank] container: ✗ network "${containerCheck.stdout.trim()}" — NO INTERNET`);
|
|
1363
|
+
api.logger.warn(`[moltbank] agent cannot reach MoltBank — stop gateway, reinstall plugin, run openclaw gateway`);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
api.logger.info(`[moltbank] ══════════════════════════════════════`);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// ─── plugin register ──────────────────────────────────────────────────────────
|
|
1370
|
+
|
|
1371
|
+
export default function register(api: PluginApi) {
|
|
1372
|
+
const cfg: MoltbankPluginConfig = api.config?.plugins?.entries?.moltbank?.config ?? {};
|
|
1373
|
+
|
|
1374
|
+
api.registerService({
|
|
1375
|
+
id: 'moltbank-setup',
|
|
1376
|
+
start: async () => {
|
|
1377
|
+
await runSetup(cfg, api, { authWaitMode: 'nonblocking' });
|
|
1378
|
+
},
|
|
1379
|
+
stop: async () => {
|
|
1380
|
+
stopBackgroundOauthPoll(getSkillDir(cfg));
|
|
1381
|
+
stopBackgroundFinalize(getSkillDir(cfg));
|
|
1382
|
+
api.logger.info('[moltbank] plugin stopped');
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
api.registerCli(
|
|
1387
|
+
({ program }: { program: CliCommandLike }) => {
|
|
1388
|
+
program
|
|
1389
|
+
.command('moltbank')
|
|
1390
|
+
.description('MoltBank plugin commands')
|
|
1391
|
+
.addCommand(
|
|
1392
|
+
program
|
|
1393
|
+
.createCommand('setup')
|
|
1394
|
+
.description('Re-run MoltBank setup (nonblocking auth by default)')
|
|
1395
|
+
.action(async () => {
|
|
1396
|
+
console.log('Running MoltBank setup...');
|
|
1397
|
+
const authWaitMode = getSetupAuthWaitMode('nonblocking');
|
|
1398
|
+
if (authWaitMode === 'nonblocking') {
|
|
1399
|
+
console.log('[moltbank] setup auth mode: nonblocking (default for channel reliability)');
|
|
1400
|
+
console.log('[moltbank] set MOLTBANK_SETUP_AUTH_WAIT_MODE=blocking to wait for OAuth approval');
|
|
1401
|
+
console.log('[moltbank] after browser approval, setup finalization continues automatically');
|
|
1402
|
+
console.log('[moltbank] optional immediate check: openclaw moltbank auth-status');
|
|
1403
|
+
} else {
|
|
1404
|
+
console.log('[moltbank] setup auth mode: blocking (waiting for OAuth approval)');
|
|
1405
|
+
}
|
|
1406
|
+
await runSetup(cfg, { logger: console }, { authWaitMode });
|
|
1407
|
+
})
|
|
1408
|
+
)
|
|
1409
|
+
.addCommand(
|
|
1410
|
+
program
|
|
1411
|
+
.createCommand('setup-blocking')
|
|
1412
|
+
.description('Re-run full MoltBank setup and wait for OAuth approval')
|
|
1413
|
+
.action(async () => {
|
|
1414
|
+
console.log('Running MoltBank setup (blocking auth mode)...');
|
|
1415
|
+
await runSetup(cfg, { logger: console }, { authWaitMode: 'blocking' });
|
|
1416
|
+
})
|
|
1417
|
+
)
|
|
1418
|
+
.addCommand(
|
|
1419
|
+
program
|
|
1420
|
+
.createCommand('sandbox-setup')
|
|
1421
|
+
.description('Reconfigure sandbox docker in openclaw.json')
|
|
1422
|
+
.action(() => {
|
|
1423
|
+
const changed = configureSandbox({ logger: console });
|
|
1424
|
+
if (changed) {
|
|
1425
|
+
recreateSandboxAndRestart({ logger: console });
|
|
1426
|
+
} else {
|
|
1427
|
+
console.log('[moltbank] No sandbox docker changes — not scheduling teardown');
|
|
1428
|
+
}
|
|
1429
|
+
})
|
|
1430
|
+
)
|
|
1431
|
+
.addCommand(
|
|
1432
|
+
program
|
|
1433
|
+
.createCommand('inject-key')
|
|
1434
|
+
.description('Re-inject sandbox env vars from credentials.json')
|
|
1435
|
+
.action(() => {
|
|
1436
|
+
const skillDir = getSkillDir(cfg);
|
|
1437
|
+
const changed = injectSandboxEnv(skillDir, { logger: console });
|
|
1438
|
+
if (changed) {
|
|
1439
|
+
recreateSandboxAndRestart({ logger: console });
|
|
1440
|
+
} else {
|
|
1441
|
+
console.log('[moltbank] No env changes — not scheduling teardown');
|
|
1442
|
+
}
|
|
1443
|
+
})
|
|
1444
|
+
)
|
|
1445
|
+
.addCommand(
|
|
1446
|
+
program
|
|
1447
|
+
.createCommand('auth-status')
|
|
1448
|
+
.description('Show current MoltBank auth state (credentials, pending code, background poll)')
|
|
1449
|
+
.action(() => {
|
|
1450
|
+
const appBaseUrl = getAppBaseUrl(cfg);
|
|
1451
|
+
const skillDir = getSkillDir(cfg);
|
|
1452
|
+
printAuthStatus(skillDir, appBaseUrl, { logger: console });
|
|
1453
|
+
})
|
|
1454
|
+
)
|
|
1455
|
+
.addCommand(
|
|
1456
|
+
program
|
|
1457
|
+
.createCommand('register')
|
|
1458
|
+
.description('Re-register mcporter server')
|
|
1459
|
+
.action(() => {
|
|
1460
|
+
const appBaseUrl = getAppBaseUrl(cfg);
|
|
1461
|
+
const skillDir = getSkillDir(cfg);
|
|
1462
|
+
ensureMcporterConfig(skillDir, appBaseUrl, { logger: console });
|
|
1463
|
+
})
|
|
1464
|
+
);
|
|
1465
|
+
},
|
|
1466
|
+
{ commands: ['moltbank'] }
|
|
1467
|
+
);
|
|
1468
|
+
}
|