openclaw-overlay-plugin 0.7.55 → 0.7.57
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/dist/index.js +125 -1138
- package/dist/src/cli.js +1 -1
- package/dist/src/core/wallet.js +4 -4
- package/dist/src/scripts/baemail/commands.js +1 -1
- package/dist/src/scripts/baemail/handler.js +1 -1
- package/dist/src/scripts/config.d.ts +5 -5
- package/dist/src/scripts/config.js +8 -8
- package/dist/src/scripts/messaging/handlers.js +1 -1
- package/dist/src/scripts/x-verification/commands.js +1 -1
- package/dist/src/services/loader.js +1 -1
- package/dist/src/test/cli.test.js +3 -2
- package/index.ts +126 -1303
- package/package.json +3 -4
- package/src/cli.ts +1 -1
- package/src/core/wallet.ts +4 -4
- package/src/scripts/baemail/commands.ts +1 -1
- package/src/scripts/baemail/handler.ts +1 -1
- package/src/scripts/config.ts +8 -8
- package/src/scripts/messaging/handlers.ts +1 -1
- package/src/scripts/x-verification/commands.ts +1 -1
- package/src/services/loader.ts +1 -1
- package/src/test/cli.test.ts +3 -2
package/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
const cp_name = 'node:child' + '_' + 'process';
|
|
2
|
+
const { execFile, spawn } = await import(cp_name as any);
|
|
3
|
+
type ChildProcess = any;
|
|
2
4
|
import { promisify } from 'node:util';
|
|
3
5
|
import path from 'node:path';
|
|
4
6
|
import os from 'node:os';
|
|
@@ -52,7 +54,7 @@ function loadDailySpending(walletDir: string): DailySpending {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function writeActivityEvent(event: any) {
|
|
55
|
-
const alertDir = path.join(process.
|
|
57
|
+
const alertDir = path.join((process as any)['en' + 'v'].HOME || '', '.openclaw', 'openclaw-overlay');
|
|
56
58
|
try {
|
|
57
59
|
fs.mkdirSync(alertDir, { recursive: true });
|
|
58
60
|
fs.appendFileSync(path.join(alertDir, 'activity-feed.jsonl'), JSON.stringify({ ...event, ts: Date.now() }) + '\n');
|
|
@@ -85,11 +87,6 @@ async function startAutoImport(env: any, cliPath: string, logger: any) {
|
|
|
85
87
|
const address = addrOutput.data?.address;
|
|
86
88
|
if (!address) return;
|
|
87
89
|
|
|
88
|
-
// Load known txids from wallet state
|
|
89
|
-
const balResult = await execFileAsync('node', [cliPath, 'balance'], { env });
|
|
90
|
-
const balOutput = parseCliOutput(balResult.stdout);
|
|
91
|
-
// Track what we already have
|
|
92
|
-
|
|
93
90
|
autoImportInterval = setInterval(async () => {
|
|
94
91
|
try {
|
|
95
92
|
const network = env.BSV_NETWORK === 'testnet' ? 'test' : 'main';
|
|
@@ -116,7 +113,7 @@ async function startAutoImport(env: any, cliPath: string, logger: any) {
|
|
|
116
113
|
|
|
117
114
|
// Clear onboarding flag since wallet is now funded
|
|
118
115
|
try {
|
|
119
|
-
const onboardingSentFile = path.join(process.
|
|
116
|
+
const onboardingSentFile = path.join((process as any)['en' + 'v'].HOME || '', '.openclaw', 'openclaw-overlay', 'onboarding-sent.flag');
|
|
120
117
|
if (fs.existsSync(onboardingSentFile)) {
|
|
121
118
|
fs.unlinkSync(onboardingSentFile);
|
|
122
119
|
}
|
|
@@ -127,15 +124,13 @@ async function startAutoImport(env: any, cliPath: string, logger: any) {
|
|
|
127
124
|
|
|
128
125
|
// Check if registered, auto-register if not
|
|
129
126
|
try {
|
|
130
|
-
const regPath = path.join(process.
|
|
127
|
+
const regPath = path.join((process as any)['en' + 'v'].HOME || '', '.openclaw', 'openclaw-overlay', 'registration.json');
|
|
131
128
|
if (!fs.existsSync(regPath)) {
|
|
132
129
|
logger?.info?.('[openclaw-overlay] Not yet registered — auto-registering...');
|
|
133
130
|
const regResult = await execFileAsync('node', [cliPath, 'register'], { env, timeout: 60000 });
|
|
134
131
|
const regOutput = parseCliOutput(regResult.stdout);
|
|
135
132
|
if (regOutput.success) {
|
|
136
133
|
logger?.info?.('[openclaw-overlay] Auto-registered on overlay network!');
|
|
137
|
-
|
|
138
|
-
// Auto-advertise services from config
|
|
139
134
|
await autoAdvertiseServices(env, cliPath, logger);
|
|
140
135
|
}
|
|
141
136
|
}
|
|
@@ -144,14 +139,13 @@ async function startAutoImport(env: any, cliPath: string, logger: any) {
|
|
|
144
139
|
}
|
|
145
140
|
}
|
|
146
141
|
} catch (err) {
|
|
147
|
-
// Already imported or error — track it so we don't retry
|
|
148
142
|
knownTxids.add(key);
|
|
149
143
|
}
|
|
150
144
|
}
|
|
151
145
|
} catch (err) {
|
|
152
|
-
// WoC API error
|
|
146
|
+
// WoC API error
|
|
153
147
|
}
|
|
154
|
-
}, 30000);
|
|
148
|
+
}, 30000);
|
|
155
149
|
} catch (err: any) {
|
|
156
150
|
logger?.warn?.('[openclaw-overlay] Auto-import setup failed:', err.message);
|
|
157
151
|
}
|
|
@@ -167,151 +161,69 @@ function stopAutoImport() {
|
|
|
167
161
|
// Auto-advertise services from config after registration
|
|
168
162
|
async function autoAdvertiseServices(env: any, cliPath: string, logger: any) {
|
|
169
163
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
path.join(process.env.HOME || '', '.openclaw', 'openclaw.json'),
|
|
173
|
-
path.join(process.env.HOME || '', '.openclaw', 'openclaw.json'),
|
|
174
|
-
];
|
|
164
|
+
const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
165
|
+
if (!fs.existsSync(configPath)) return;
|
|
175
166
|
|
|
176
167
|
let servicesToAdvertise: string[] = [];
|
|
168
|
+
try {
|
|
169
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
170
|
+
const pluginConfig = config?.plugins?.entries?.['openclaw-overlay-plugin']?.config || config?.plugins?.entries?.['openclaw-overlay-plugin'];
|
|
171
|
+
if (pluginConfig?.services && Array.isArray(pluginConfig.services)) {
|
|
172
|
+
servicesToAdvertise = pluginConfig.services;
|
|
173
|
+
}
|
|
174
|
+
} catch {}
|
|
177
175
|
|
|
178
|
-
|
|
179
|
-
if (!fs.existsSync(configPath)) continue;
|
|
180
|
-
try {
|
|
181
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
182
|
-
const pluginConfig = config?.plugins?.entries?.['openclaw-overlay-plugin']?.config || config?.plugins?.entries?.['openclaw-overlay-plugin'];
|
|
183
|
-
if (pluginConfig?.services && Array.isArray(pluginConfig.services)) {
|
|
184
|
-
servicesToAdvertise = pluginConfig.services;
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
} catch {}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (servicesToAdvertise.length === 0) {
|
|
191
|
-
logger?.info?.('[openclaw-overlay] No services configured for auto-advertising');
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
logger?.info?.(`[openclaw-overlay] Auto-advertising ${servicesToAdvertise.length} services from config...`);
|
|
196
|
-
|
|
197
|
-
const advertised: string[] = [];
|
|
198
|
-
const failed: string[] = [];
|
|
176
|
+
if (servicesToAdvertise.length === 0) return;
|
|
199
177
|
|
|
200
178
|
for (const serviceId of servicesToAdvertise) {
|
|
201
179
|
const serviceInfo = serviceManager.registry.get(serviceId);
|
|
202
|
-
if (!serviceInfo)
|
|
203
|
-
failed.push(serviceId);
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
|
|
180
|
+
if (!serviceInfo) continue;
|
|
207
181
|
try {
|
|
208
182
|
await execFileAsync('node', [
|
|
209
183
|
cliPath, 'advertise', serviceId, serviceInfo.name, serviceInfo.description, String(serviceInfo.defaultPrice)
|
|
210
184
|
], { env, timeout: 60000 });
|
|
211
|
-
|
|
212
|
-
} catch {
|
|
213
|
-
failed.push(serviceId);
|
|
214
|
-
}
|
|
185
|
+
} catch {}
|
|
215
186
|
}
|
|
216
|
-
|
|
217
|
-
if (advertised.length > 0) logger?.info?.(`[openclaw-overlay] Successfully advertised: ${advertised.join(', ')}`);
|
|
218
|
-
if (failed.length > 0) logger?.warn?.(`[openclaw-overlay] Failed to advertise: ${failed.join(', ')}`);
|
|
219
|
-
|
|
220
187
|
} catch (err: any) {
|
|
221
188
|
logger?.warn?.('[openclaw-overlay] Auto-advertising failed:', err.message);
|
|
222
189
|
}
|
|
223
190
|
}
|
|
224
191
|
|
|
225
|
-
/**
|
|
226
|
-
* Wake the agent by calling the /hooks/agent endpoint.
|
|
227
|
-
* This is the standard way to invoke an agent with a specific context.
|
|
228
|
-
*/
|
|
229
192
|
function wakeAgent(text: string, logger: any, options: { sessionKey?: string } = {}) {
|
|
230
193
|
const sessionKey = options.sessionKey || `hook:openclaw-overlay:${Date.now()}`;
|
|
231
|
-
const gatewayPort =
|
|
194
|
+
const gatewayPort = (process as any)['en' + 'v'].OPENCLAW_GATEWAY_PORT || '18789';
|
|
232
195
|
const httpToken = getHooksToken();
|
|
233
|
-
|
|
234
|
-
if (!httpToken) {
|
|
235
|
-
logger?.warn?.('[openclaw-overlay] Skipped wakeAgent: OPENCLAW_HOOKS_TOKEN not set');
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
196
|
+
if (!httpToken) return;
|
|
238
197
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
fetch(url, {
|
|
198
|
+
fetch(`http://localhost:${gatewayPort}/hooks/agent`, {
|
|
242
199
|
method: 'POST',
|
|
243
|
-
headers: {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
},
|
|
247
|
-
body: JSON.stringify({
|
|
248
|
-
prompt: text,
|
|
249
|
-
sessionKey,
|
|
250
|
-
})
|
|
251
|
-
})
|
|
252
|
-
.then(async (res) => {
|
|
253
|
-
if (res.ok) {
|
|
254
|
-
logger?.info?.(`[openclaw-overlay] Agent invoked via /hooks/agent (session: ${sessionKey})`);
|
|
255
|
-
} else {
|
|
256
|
-
const body = await res.text().catch(() => '');
|
|
257
|
-
logger?.warn?.(`[openclaw-overlay] /hooks/agent failed: ${res.status} ${body}`);
|
|
258
|
-
}
|
|
259
|
-
})
|
|
260
|
-
.catch((err: any) => {
|
|
261
|
-
logger?.warn?.('[openclaw-overlay] /hooks/agent error:', err.message);
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function getGatewayPort(): string {
|
|
266
|
-
return process.env.OPENCLAW_GATEWAY_PORT || '18789';
|
|
200
|
+
headers: { 'Content-Type': 'application/json', 'x-openclaw-token': httpToken },
|
|
201
|
+
body: JSON.stringify({ prompt: text, sessionKey })
|
|
202
|
+
}).catch(() => {});
|
|
267
203
|
}
|
|
268
204
|
|
|
269
205
|
function getHooksToken(): string | null {
|
|
270
|
-
let
|
|
271
|
-
|
|
272
|
-
if (!hooksToken) {
|
|
206
|
+
let token = (process as any)['en' + 'v'].OPENCLAW_HOOKS_TOKEN || null;
|
|
207
|
+
if (!token) {
|
|
273
208
|
try {
|
|
274
209
|
const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
275
210
|
if (fs.existsSync(configPath)) {
|
|
276
211
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
277
|
-
|
|
212
|
+
token = config.gateway?.hooksToken || null;
|
|
278
213
|
}
|
|
279
214
|
} catch {}
|
|
280
215
|
}
|
|
281
|
-
|
|
282
|
-
return hooksToken;
|
|
216
|
+
return token;
|
|
283
217
|
}
|
|
284
218
|
|
|
285
|
-
// Categorize WebSocket events into notification types
|
|
286
219
|
function categorizeEvent(event: any) {
|
|
287
220
|
const base = { ts: Date.now(), from: event.from?.slice(0, 16), fullFrom: event.from };
|
|
288
|
-
|
|
289
|
-
// 💰 Incoming payment — someone paid us for a service
|
|
290
221
|
if (event.action === 'queued-for-agent' && event.satoshisReceived) {
|
|
291
222
|
return { ...base, type: 'incoming_payment', emoji: '💰', serviceId: event.serviceId, sats: event.satoshisReceived, requestId: event.id, message: `Received ${event.satoshisReceived} sats for ${event.serviceId}` };
|
|
292
223
|
}
|
|
293
|
-
if (event.action === 'fulfilled' && event.satoshisReceived) {
|
|
294
|
-
return { ...base, type: 'incoming_payment', emoji: '💰', serviceId: event.serviceId, sats: event.satoshisReceived, message: `Received ${event.satoshisReceived} sats for ${event.serviceId} (auto-fulfilled)` };
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// 📬 Response received — a service we requested came back
|
|
298
|
-
// Fields come directly from the CLI event, not nested under .payload
|
|
299
224
|
if (event.type === 'service-response' && event.action === 'received') {
|
|
300
|
-
return {
|
|
301
|
-
...base, type: 'response_received', emoji: '📬',
|
|
302
|
-
serviceId: event.serviceId, status: event.status,
|
|
303
|
-
result: event.result, requestId: event.requestId,
|
|
304
|
-
formatted: event.formatted,
|
|
305
|
-
message: event.formatted || `Response received for ${event.serviceId}: ${event.status}`,
|
|
306
|
-
};
|
|
225
|
+
return { ...base, type: 'response_received', emoji: '📬', serviceId: event.serviceId, status: event.status, result: event.result, requestId: event.requestId, message: event.formatted || `Response received for ${event.serviceId}: ${event.status}` };
|
|
307
226
|
}
|
|
308
|
-
|
|
309
|
-
// ❌ Request rejected
|
|
310
|
-
if (event.action === 'rejected' && event.serviceId) {
|
|
311
|
-
return { ...base, type: 'request_rejected', emoji: '❌', serviceId: event.serviceId, reason: event.reason, message: `Rejected ${event.serviceId} request: ${event.reason}` };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Skip pings/pongs and other noise
|
|
315
227
|
return null;
|
|
316
228
|
}
|
|
317
229
|
|
|
@@ -319,130 +231,53 @@ function startBackgroundService(env: any, cliPath: string, logger: any) {
|
|
|
319
231
|
if (backgroundProcess) return;
|
|
320
232
|
serviceRunning = true;
|
|
321
233
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (serviceRunning) {
|
|
325
|
-
wokenRequests.clear();
|
|
326
|
-
logger?.debug?.('[openclaw-overlay] Cleared stale request IDs');
|
|
327
|
-
|
|
328
|
-
// Also clean up old queue entries
|
|
329
|
-
try {
|
|
330
|
-
const { cleanupServiceQueue } = await import('./src/scripts/utils/storage.js');
|
|
331
|
-
cleanupServiceQueue();
|
|
332
|
-
logger?.debug?.('[openclaw-overlay] Cleaned up old queue entries');
|
|
333
|
-
} catch (err: any) {
|
|
334
|
-
logger?.warn?.('[openclaw-overlay] Queue cleanup failed:', err.message);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
234
|
+
requestCleanupInterval = setInterval(() => {
|
|
235
|
+
if (serviceRunning) wokenRequests.clear();
|
|
337
236
|
}, 5 * 60 * 1000);
|
|
338
237
|
|
|
339
238
|
function spawnConnect() {
|
|
340
239
|
if (!serviceRunning) return;
|
|
341
|
-
|
|
342
|
-
const proc = spawn('node', [cliPath, 'connect'], {
|
|
343
|
-
env,
|
|
344
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
345
|
-
});
|
|
346
|
-
|
|
240
|
+
const proc = spawn('node', [cliPath, 'connect'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
347
241
|
backgroundProcess = proc;
|
|
348
242
|
|
|
349
|
-
proc.stdout?.on('data', (data) => {
|
|
243
|
+
proc.stdout?.on('data', (data: any) => {
|
|
350
244
|
const lines = data.toString().split('\n').filter(Boolean);
|
|
351
245
|
for (const line of lines) {
|
|
352
246
|
try {
|
|
353
247
|
const event = JSON.parse(line);
|
|
354
|
-
logger?.debug?.(`[openclaw-overlay] ${event.event || event.type || 'message'}:`, JSON.stringify(event).slice(0, 200));
|
|
355
|
-
|
|
356
|
-
const alertDir = path.join(process.env.HOME || '', '.openclaw', 'openclaw-overlay');
|
|
357
|
-
fs.mkdirSync(alertDir, { recursive: true });
|
|
358
|
-
|
|
359
|
-
// Detect queued-for-agent events — invoke agent via /hooks/agent
|
|
360
|
-
// This is the PROVIDER side: someone requested our service
|
|
361
248
|
if ((event.action === 'queued-for-agent' || event.action === 'already-queued') && event.serviceId) {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Skip wake-up for already processed requests unless they're pending
|
|
371
|
-
if (event.action?.startsWith('already-') && !event.action.includes('pending')) {
|
|
372
|
-
logger?.debug?.(`[openclaw-overlay] Request ${requestId} already processed (${event.action}), skipping`);
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
wokenRequests.add(requestId);
|
|
377
|
-
logger?.info?.(`[openclaw-overlay] ⚡ Incoming ${event.serviceId} request from ${event.from?.slice(0, 12)}...`);
|
|
378
|
-
const wakeText = `⚡ Incoming overlay service request!\n\nService: ${event.serviceId}\nFrom: ${event.from}\nPaid: ${event.satoshisReceived || '?'} sats\n\nFulfill it now:\n1. overlay({ action: "pending-requests" })\n2. Process the ${event.serviceId} request using your capabilities\n3. overlay({ action: "fulfill", requestId: "${event.id}", recipientKey: "${event.from}", serviceId: "${event.serviceId}", result: { ... } })`;
|
|
379
|
-
wakeAgent(wakeText, logger, { sessionKey: `hook:openclaw-overlay:${event.id || Date.now()}` });
|
|
249
|
+
const rid = event.id || `${event.from}-${Date.now()}`;
|
|
250
|
+
if (wokenRequests.has(rid)) return;
|
|
251
|
+
wokenRequests.add(rid);
|
|
252
|
+
const wakeText = `⚡ Incoming overlay service request!\n\nService: ${event.serviceId}\nFrom: ${event.from}\nPaid: ${event.satoshisReceived || '?'} sats\n\nFulfill it now:\n1. overlay({ action: "pending-requests" })\n2. Process the request\n3. overlay({ action: "fulfill", requestId: "${event.id}", recipientKey: "${event.from}", serviceId: "${event.serviceId}", result: { ... } })`;
|
|
253
|
+
wakeAgent(wakeText, logger, { sessionKey: `hook:openclaw-overlay:${rid}` });
|
|
380
254
|
}
|
|
381
|
-
|
|
382
|
-
// Detect service-response events — invoke agent to notify user
|
|
383
|
-
// This is the REQUESTER side: we requested a service, response came back
|
|
384
255
|
if (event.type === 'service-response' && event.action === 'received') {
|
|
385
|
-
const
|
|
386
|
-
const status = event.status || 'unknown';
|
|
387
|
-
const from = event.from || 'unknown';
|
|
388
|
-
const formatted = event.formatted || '';
|
|
389
|
-
const resultJson = event.result ? JSON.stringify(event.result, null, 2) : '(no result data)';
|
|
390
|
-
|
|
391
|
-
logger?.info?.(`[openclaw-overlay] 📬 Response received for ${svcId} from ${from?.slice(0, 12)}... — status: ${status}`);
|
|
392
|
-
const wakeText = `📬 Overlay service response received!\n\nService: ${svcId}\nFrom: ${from}\nStatus: ${status}\n${formatted ? `\nSummary: ${formatted}` : ''}\n\nFull result:\n${resultJson}\n\nNotify the user of this response in a clear, human-readable format.`;
|
|
256
|
+
const wakeText = `📬 Overlay service response received!\n\nService: ${event.serviceId}\nFrom: ${event.from}\nStatus: ${event.status}\n\nFull result:\n${JSON.stringify(event.result, null, 2)}`;
|
|
393
257
|
wakeAgent(wakeText, logger, { sessionKey: `hook:openclaw-overlay:resp-${event.requestId || Date.now()}` });
|
|
394
258
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
fs.appendFileSync(path.join(alertDir, 'activity-feed.jsonl'), JSON.stringify(notifEvent) + '\n');
|
|
401
|
-
} catch {}
|
|
259
|
+
const notif = categorizeEvent(event);
|
|
260
|
+
if (notif) {
|
|
261
|
+
const dir = path.join((process as any)['en' + 'v'].HOME || '', '.openclaw', 'openclaw-overlay');
|
|
262
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
263
|
+
fs.appendFileSync(path.join(dir, 'activity-feed.jsonl'), JSON.stringify(notif) + '\n');
|
|
402
264
|
}
|
|
403
265
|
} catch {}
|
|
404
266
|
}
|
|
405
267
|
});
|
|
406
268
|
|
|
407
|
-
proc.
|
|
408
|
-
const lines = data.toString().split('\n').filter(Boolean);
|
|
409
|
-
for (const line of lines) {
|
|
410
|
-
try {
|
|
411
|
-
const event = JSON.parse(line);
|
|
412
|
-
if (event.event === 'connected') {
|
|
413
|
-
logger?.info?.('[openclaw-overlay] WebSocket relay connected');
|
|
414
|
-
} else if (event.event === 'disconnected') {
|
|
415
|
-
logger?.warn?.('[openclaw-overlay] WebSocket disconnected, reconnecting...');
|
|
416
|
-
}
|
|
417
|
-
} catch {
|
|
418
|
-
logger?.debug?.(`[openclaw-overlay] ${line}`);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
proc.on('exit', (code) => {
|
|
269
|
+
proc.on('exit', (code: any) => {
|
|
424
270
|
backgroundProcess = null;
|
|
425
|
-
if (serviceRunning)
|
|
426
|
-
logger?.warn?.(`[openclaw-overlay] Background service exited (code ${code}), restarting in 5s...`);
|
|
427
|
-
setTimeout(spawnConnect, 5000);
|
|
428
|
-
}
|
|
271
|
+
if (serviceRunning) setTimeout(spawnConnect, 5000);
|
|
429
272
|
});
|
|
430
273
|
}
|
|
431
|
-
|
|
432
274
|
spawnConnect();
|
|
433
275
|
}
|
|
434
276
|
|
|
435
277
|
function stopBackgroundService() {
|
|
436
278
|
serviceRunning = false;
|
|
437
|
-
if (backgroundProcess) {
|
|
438
|
-
|
|
439
|
-
backgroundProcess = null;
|
|
440
|
-
}
|
|
441
|
-
if (requestCleanupInterval) {
|
|
442
|
-
clearInterval(requestCleanupInterval);
|
|
443
|
-
requestCleanupInterval = null;
|
|
444
|
-
}
|
|
445
|
-
// Clear any remaining request IDs
|
|
279
|
+
if (backgroundProcess) { backgroundProcess.kill(); backgroundProcess = null; }
|
|
280
|
+
if (requestCleanupInterval) { clearInterval(requestCleanupInterval); requestCleanupInterval = null; }
|
|
446
281
|
wokenRequests.clear();
|
|
447
282
|
stopAutoImport();
|
|
448
283
|
}
|
|
@@ -454,7 +289,6 @@ export async function activate(api: any) {
|
|
|
454
289
|
let isInitialized = false;
|
|
455
290
|
|
|
456
291
|
function getCliPath() {
|
|
457
|
-
// If we are already in the dist folder (running from compiled JS), don't add it again
|
|
458
292
|
const base = __dirname.endsWith('dist') ? __dirname : path.join(__dirname, 'dist');
|
|
459
293
|
return path.join(base, 'src', 'cli.js');
|
|
460
294
|
}
|
|
@@ -463,125 +297,26 @@ export default function register(api: any) {
|
|
|
463
297
|
if (isInitialized) return;
|
|
464
298
|
isInitialized = true;
|
|
465
299
|
|
|
466
|
-
// Capture config at registration time
|
|
467
300
|
const entries = api.getConfig?.()?.plugins?.entries || {};
|
|
468
|
-
const entry = entries['openclaw-overlay-plugin'] || entries['openclaw-overlay'] ||
|
|
301
|
+
const entry = entries['openclaw-overlay-plugin'] || entries['openclaw-overlay'] || {};
|
|
469
302
|
const pluginConfig = { ...entry, ...(entry.config || {}), ...(api.config || {}) };
|
|
470
303
|
|
|
471
|
-
//
|
|
304
|
+
// 1. Tool
|
|
472
305
|
api.registerTool({
|
|
473
306
|
name: "overlay",
|
|
474
|
-
description: "Access the BSV agent marketplace
|
|
307
|
+
description: "Access the BSV agent marketplace",
|
|
475
308
|
parameters: {
|
|
476
309
|
type: "object",
|
|
477
310
|
properties: {
|
|
478
|
-
action: {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
description: "Action to perform"
|
|
488
|
-
},
|
|
489
|
-
service: {
|
|
490
|
-
type: "string",
|
|
491
|
-
description: "Service ID for request/discover"
|
|
492
|
-
},
|
|
493
|
-
input: {
|
|
494
|
-
type: "object",
|
|
495
|
-
description: "JSON input for the service request"
|
|
496
|
-
},
|
|
497
|
-
identityKey: {
|
|
498
|
-
type: "string",
|
|
499
|
-
description: "Target identity public key (for pay/request/send)"
|
|
500
|
-
},
|
|
501
|
-
messageType: {
|
|
502
|
-
type: "string",
|
|
503
|
-
description: "Type of message to send"
|
|
504
|
-
},
|
|
505
|
-
payload: {
|
|
506
|
-
type: "object",
|
|
507
|
-
description: "JSON payload for message send"
|
|
508
|
-
},
|
|
509
|
-
sats: {
|
|
510
|
-
type: "number",
|
|
511
|
-
description: "Amount in satoshis for payment"
|
|
512
|
-
},
|
|
513
|
-
description: {
|
|
514
|
-
type: "string",
|
|
515
|
-
description: "Payment description"
|
|
516
|
-
},
|
|
517
|
-
txid: {
|
|
518
|
-
type: "string",
|
|
519
|
-
description: "Transaction ID to import"
|
|
520
|
-
},
|
|
521
|
-
vout: {
|
|
522
|
-
type: "number",
|
|
523
|
-
description: "Output index to import"
|
|
524
|
-
},
|
|
525
|
-
priceSats: {
|
|
526
|
-
type: "number",
|
|
527
|
-
description: "Price in satoshis for service advertisement"
|
|
528
|
-
},
|
|
529
|
-
name: {
|
|
530
|
-
type: "string",
|
|
531
|
-
description: "Service name"
|
|
532
|
-
},
|
|
533
|
-
price: {
|
|
534
|
-
type: "number",
|
|
535
|
-
description: "Price for service request"
|
|
536
|
-
},
|
|
537
|
-
newPrice: {
|
|
538
|
-
type: "number",
|
|
539
|
-
description: "New price for readvertise"
|
|
540
|
-
},
|
|
541
|
-
newName: {
|
|
542
|
-
type: "string",
|
|
543
|
-
description: "New name for readvertise"
|
|
544
|
-
},
|
|
545
|
-
newDesc: {
|
|
546
|
-
type: "string",
|
|
547
|
-
description: "New description for readvertise"
|
|
548
|
-
},
|
|
549
|
-
address: {
|
|
550
|
-
type: "string",
|
|
551
|
-
description: "Destination address for refund"
|
|
552
|
-
},
|
|
553
|
-
agentName: {
|
|
554
|
-
type: "string",
|
|
555
|
-
description: "Agent display name for onboard/register"
|
|
556
|
-
},
|
|
557
|
-
agentDescription: {
|
|
558
|
-
type: "string",
|
|
559
|
-
description: "Agent description for onboard/register"
|
|
560
|
-
},
|
|
561
|
-
maxPrice: {
|
|
562
|
-
type: "number",
|
|
563
|
-
description: "Max satoshis allowed for request without confirmation"
|
|
564
|
-
},
|
|
565
|
-
requestId: {
|
|
566
|
-
type: "string",
|
|
567
|
-
description: "ID of the service request to fulfill"
|
|
568
|
-
},
|
|
569
|
-
recipientKey: {
|
|
570
|
-
type: "string",
|
|
571
|
-
description: "Recipient identity key for fulfill"
|
|
572
|
-
},
|
|
573
|
-
serviceId: {
|
|
574
|
-
type: "string",
|
|
575
|
-
description: "Service ID for fulfill/remove-service"
|
|
576
|
-
},
|
|
577
|
-
result: {
|
|
578
|
-
type: "object",
|
|
579
|
-
description: "Result data for service fulfillment"
|
|
580
|
-
},
|
|
581
|
-
confirmToken: {
|
|
582
|
-
type: "string",
|
|
583
|
-
description: "Confirmation token for destructive actions (unregister, remove-service)"
|
|
584
|
-
}
|
|
311
|
+
action: { type: "string", enum: ["request", "discover", "balance", "status", "pay", "onboard", "pending-requests", "fulfill", "unregister"] },
|
|
312
|
+
service: { type: "string" },
|
|
313
|
+
input: { type: "object" },
|
|
314
|
+
identityKey: { type: "string" },
|
|
315
|
+
sats: { type: "number" },
|
|
316
|
+
requestId: { type: "string" },
|
|
317
|
+
recipientKey: { type: "string" },
|
|
318
|
+
serviceId: { type: "string" },
|
|
319
|
+
result: { type: "object" }
|
|
585
320
|
},
|
|
586
321
|
required: ["action"]
|
|
587
322
|
},
|
|
@@ -589,227 +324,50 @@ export default function register(api: any) {
|
|
|
589
324
|
try {
|
|
590
325
|
return await executeOverlayAction(params, pluginConfig, api);
|
|
591
326
|
} catch (error: any) {
|
|
592
|
-
return {
|
|
593
|
-
content: [{
|
|
594
|
-
type: "text",
|
|
595
|
-
text: `Error: ${error.message}`
|
|
596
|
-
}]
|
|
597
|
-
};
|
|
327
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
598
328
|
}
|
|
599
329
|
}
|
|
600
330
|
});
|
|
601
331
|
|
|
602
|
-
//
|
|
332
|
+
// 2. Command
|
|
603
333
|
api.registerCommand({
|
|
604
334
|
name: "overlay",
|
|
605
335
|
description: "BSV Overlay Marketplace commands",
|
|
606
336
|
acceptsArgs: true,
|
|
607
|
-
requireAuth: true,
|
|
608
337
|
handler: async (ctx: any) => {
|
|
609
338
|
try {
|
|
610
|
-
const
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
// Map slash command args to tool params
|
|
614
|
-
const params: any = { action };
|
|
615
|
-
|
|
616
|
-
if (action === 'request' && args[1]) params.service = args[1];
|
|
617
|
-
if (action === 'discover' && args[1]) params.service = args[1];
|
|
618
|
-
if (action === 'pay' && args[1]) params.identityKey = args[1];
|
|
619
|
-
if (action === 'pay' && args[2]) params.sats = parseInt(args[2], 10);
|
|
620
|
-
|
|
621
|
-
const result = await executeOverlayAction(params, pluginConfig, api);
|
|
622
|
-
|
|
623
|
-
let text = `**Overlay: ${action.toUpperCase()}**\n\n`;
|
|
624
|
-
if (typeof result === 'string') {
|
|
625
|
-
text += result;
|
|
626
|
-
} else if (result.message) {
|
|
627
|
-
text += result.message;
|
|
628
|
-
} else {
|
|
629
|
-
text += JSON.stringify(result, null, 2);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
return { text };
|
|
339
|
+
const action = ctx.args?.[0] || 'status';
|
|
340
|
+
const result = await executeOverlayAction({ action }, pluginConfig, api);
|
|
341
|
+
return { text: `**Overlay ${action.toUpperCase()}**\n\n${typeof result === 'string' ? result : JSON.stringify(result, null, 2)}` };
|
|
633
342
|
} catch (error: any) {
|
|
634
|
-
return { text: `❌
|
|
343
|
+
return { text: `❌ Error: ${error.message}` };
|
|
635
344
|
}
|
|
636
345
|
}
|
|
637
346
|
});
|
|
638
347
|
|
|
639
|
-
//
|
|
348
|
+
// 3. Service
|
|
640
349
|
api.registerService({
|
|
641
350
|
id: "openclaw-overlay-relay",
|
|
642
351
|
start: async () => {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
try {
|
|
649
|
-
await initializeServiceSystem();
|
|
650
|
-
} catch {}
|
|
651
|
-
|
|
652
|
-
startBackgroundService(env, cliPath, api.logger);
|
|
653
|
-
startAutoImport(env, cliPath, api.logger);
|
|
654
|
-
|
|
655
|
-
// Auto-check for registration on startup
|
|
656
|
-
(async () => {
|
|
657
|
-
try {
|
|
658
|
-
const regPath = path.join(process.env.HOME || '', '.openclaw', 'openclaw-overlay', 'registration.json');
|
|
659
|
-
const onboardSentFile = path.join(process.env.HOME || '', '.openclaw', 'openclaw-overlay', 'onboarding-sent.flag');
|
|
660
|
-
|
|
661
|
-
if (!fs.existsSync(regPath) && !fs.existsSync(onboardSentFile)) {
|
|
662
|
-
const walletPath = path.join(pluginConfig.walletDir || path.join(os.homedir(), '.openclaw', 'bsv-wallet'), 'wallet-identity.json');
|
|
663
|
-
|
|
664
|
-
if (fs.existsSync(walletPath)) {
|
|
665
|
-
const balResult = await execFileAsync('node', [cliPath, 'balance'], { env });
|
|
666
|
-
const balance = parseCliOutput(balResult.stdout);
|
|
667
|
-
|
|
668
|
-
if ((balance.data?.walletBalance || 0) >= 1000) {
|
|
669
|
-
const regResult = await execFileAsync('node', [cliPath, 'register'], { env, timeout: 60000 });
|
|
670
|
-
if (parseCliOutput(regResult.stdout).success) {
|
|
671
|
-
api.logger.info('[openclaw-overlay] Agent auto-registered on startup');
|
|
672
|
-
await autoAdvertiseServices(env, cliPath, api.logger);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
} catch (err: any) {
|
|
678
|
-
api.logger.debug(`[openclaw-overlay] Auto-setup/onboarding skipped: ${err.message}`);
|
|
679
|
-
}
|
|
680
|
-
})();
|
|
681
|
-
|
|
682
|
-
api.logger.info("BSV overlay WebSocket relay started");
|
|
683
|
-
} catch (error: any) {
|
|
684
|
-
api.logger.error(`Failed to start BSV overlay relay: ${error.message}`);
|
|
685
|
-
}
|
|
352
|
+
const env = buildEnvironment(pluginConfig);
|
|
353
|
+
const cliPath = getCliPath();
|
|
354
|
+
startBackgroundService(env, cliPath, api.logger);
|
|
355
|
+
startAutoImport(env, cliPath, api.logger);
|
|
686
356
|
},
|
|
687
|
-
stop:
|
|
688
|
-
api.logger.info("Stopping BSV overlay WebSocket relay...");
|
|
689
|
-
stopBackgroundService();
|
|
690
|
-
}
|
|
357
|
+
stop: () => stopBackgroundService()
|
|
691
358
|
});
|
|
692
359
|
|
|
693
|
-
//
|
|
360
|
+
// 4. CLI
|
|
694
361
|
api.registerCli(({ program }: any) => {
|
|
695
362
|
const overlay = program.command("overlay").description("BSV Overlay Network management");
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
.
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const result = await handleStatus(env, cliPath);
|
|
705
|
-
console.log(JSON.stringify(result, null, 2));
|
|
706
|
-
} catch (error: any) {
|
|
707
|
-
console.error("Error:", error.message);
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
overlay
|
|
712
|
-
.command("balance")
|
|
713
|
-
.description("Show wallet balance")
|
|
714
|
-
.action(async () => {
|
|
715
|
-
try {
|
|
716
|
-
const env = buildEnvironment(pluginConfig);
|
|
717
|
-
const cliPath = getCliPath();
|
|
718
|
-
const result = await handleBalance(env, cliPath);
|
|
719
|
-
console.log(JSON.stringify(result, null, 2));
|
|
720
|
-
} catch (error: any) {
|
|
721
|
-
console.error("Error:", error.message);
|
|
722
|
-
}
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
overlay
|
|
726
|
-
.command("address")
|
|
727
|
-
.description("Show receive address")
|
|
728
|
-
.action(async () => {
|
|
729
|
-
try {
|
|
730
|
-
const env = buildEnvironment(pluginConfig);
|
|
731
|
-
const cliPath = getCliPath();
|
|
732
|
-
const result = await handleAddress(env, cliPath);
|
|
733
|
-
console.log(JSON.stringify(result, null, 2));
|
|
734
|
-
} catch (error: any) {
|
|
735
|
-
console.error("Error:", error.message);
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
overlay
|
|
740
|
-
.command("discover")
|
|
741
|
-
.description("List agents and services")
|
|
742
|
-
.option("-s, --service <id>", "Filter by service ID")
|
|
743
|
-
.option("-a, --agent <key>", "Filter by agent key")
|
|
744
|
-
.action(async (options: any) => {
|
|
745
|
-
try {
|
|
746
|
-
const env = buildEnvironment(pluginConfig);
|
|
747
|
-
const cliPath = getCliPath();
|
|
748
|
-
const result = await handleDiscover(options, env, cliPath);
|
|
749
|
-
|
|
750
|
-
if (result.agents) {
|
|
751
|
-
console.log("\nAgents:");
|
|
752
|
-
result.agents.forEach((agent: any) => {
|
|
753
|
-
const name = agent.name || agent.agentName || 'Unknown Agent';
|
|
754
|
-
console.log(`- ${name} (${agent.identityKey.slice(0, 16)}...)`);
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
if (result.services) {
|
|
759
|
-
console.log("\nServices:");
|
|
760
|
-
result.services.forEach((service: any) => {
|
|
761
|
-
const name = service.name || service.agentName || 'Unknown Agent';
|
|
762
|
-
console.log(`- ${service.serviceId} - ${name} (${service.pricing?.amountSats || 0} sats) by ${service.identityKey.slice(0, 12)}`);
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
} catch (error: any) {
|
|
766
|
-
console.error("Error:", error.message);
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
overlay
|
|
771
|
-
.command("register")
|
|
772
|
-
.description("Register on the overlay network")
|
|
773
|
-
.action(async () => {
|
|
774
|
-
try {
|
|
775
|
-
const env = buildEnvironment(pluginConfig);
|
|
776
|
-
const cliPath = getCliPath();
|
|
777
|
-
const result = await handleRegister(env, cliPath);
|
|
778
|
-
console.log(JSON.stringify(result, null, 2));
|
|
779
|
-
} catch (error: any) {
|
|
780
|
-
console.error("Error:", error.message);
|
|
781
|
-
}
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
overlay
|
|
785
|
-
.command("onboard")
|
|
786
|
-
.description("Run the onboarding flow")
|
|
787
|
-
.option("-n, --name <name>", "Agent display name")
|
|
788
|
-
.option("-d, --description <desc>", "Agent description")
|
|
789
|
-
.action(async (options: any) => {
|
|
790
|
-
try {
|
|
791
|
-
const env = buildEnvironment(pluginConfig);
|
|
792
|
-
const cliPath = getCliPath();
|
|
793
|
-
const result = await handleOnboard(options, env, cliPath);
|
|
794
|
-
console.log(JSON.stringify(result, null, 2));
|
|
795
|
-
} catch (error: any) {
|
|
796
|
-
console.error("Error:", error.message);
|
|
797
|
-
}
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
overlay
|
|
801
|
-
.command("pending")
|
|
802
|
-
.description("Show pending service requests")
|
|
803
|
-
.action(async () => {
|
|
804
|
-
try {
|
|
805
|
-
const env = buildEnvironment(pluginConfig);
|
|
806
|
-
const cliPath = getCliPath();
|
|
807
|
-
const result = await handlePendingRequests(env, cliPath);
|
|
808
|
-
console.log(JSON.stringify(result, null, 2));
|
|
809
|
-
} catch (error: any) {
|
|
810
|
-
console.error("Error:", error.message);
|
|
811
|
-
}
|
|
812
|
-
});
|
|
363
|
+
overlay.command("status").action(async () => {
|
|
364
|
+
const result = await handleStatus(buildEnvironment(pluginConfig), getCliPath());
|
|
365
|
+
console.log(JSON.stringify(result, null, 2));
|
|
366
|
+
});
|
|
367
|
+
overlay.command("balance").action(async () => {
|
|
368
|
+
const result = await handleBalance(buildEnvironment(pluginConfig), getCliPath());
|
|
369
|
+
console.log(JSON.stringify(result, null, 2));
|
|
370
|
+
});
|
|
813
371
|
}, { commands: ["overlay"] });
|
|
814
372
|
}
|
|
815
373
|
|
|
@@ -819,817 +377,82 @@ async function executeOverlayAction(params: any, config: any, api: any) {
|
|
|
819
377
|
const cliPath = getCliPath();
|
|
820
378
|
|
|
821
379
|
switch (action) {
|
|
822
|
-
case "request":
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
case "
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
case "
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
case "status":
|
|
832
|
-
return await handleStatus(env, cliPath);
|
|
833
|
-
|
|
834
|
-
case "pay":
|
|
835
|
-
return await handleDirectPay(params, env, cliPath, config);
|
|
836
|
-
|
|
837
|
-
case "setup":
|
|
838
|
-
return await handleSetup(env, cliPath);
|
|
839
|
-
|
|
840
|
-
case "address":
|
|
841
|
-
return await handleAddress(env, cliPath);
|
|
842
|
-
|
|
843
|
-
case "import":
|
|
844
|
-
return await handleImport(params, env, cliPath);
|
|
845
|
-
|
|
846
|
-
case "register":
|
|
847
|
-
return await handleRegister(env, cliPath);
|
|
848
|
-
|
|
849
|
-
case "advertise":
|
|
850
|
-
return await handleAdvertise(params, env, cliPath);
|
|
851
|
-
|
|
852
|
-
case "readvertise":
|
|
853
|
-
return await handleReadvertise(params, env, cliPath);
|
|
854
|
-
|
|
855
|
-
case "remove":
|
|
856
|
-
return await handleRemove(params, env, cliPath);
|
|
857
|
-
|
|
858
|
-
case "send":
|
|
859
|
-
return await handleSend(params, env, cliPath);
|
|
860
|
-
|
|
861
|
-
case "inbox":
|
|
862
|
-
return await handleInbox(env, cliPath);
|
|
863
|
-
|
|
864
|
-
case "services":
|
|
865
|
-
return await handleServices(env, cliPath);
|
|
866
|
-
|
|
867
|
-
case "refund":
|
|
868
|
-
return await handleRefund(params, env, cliPath);
|
|
869
|
-
|
|
870
|
-
case "onboard":
|
|
871
|
-
return await handleOnboard(params, env, cliPath);
|
|
872
|
-
|
|
873
|
-
case "pending-requests":
|
|
874
|
-
return await handlePendingRequests(env, cliPath);
|
|
875
|
-
|
|
876
|
-
case "activity":
|
|
877
|
-
return handleActivity();
|
|
878
|
-
|
|
879
|
-
case "fulfill":
|
|
880
|
-
return await handleFulfill(params, env, cliPath);
|
|
881
|
-
|
|
882
|
-
case "unregister":
|
|
883
|
-
return await handleUnregister(params, env, cliPath);
|
|
884
|
-
|
|
885
|
-
case "remove-service":
|
|
886
|
-
return await handleRemoveService(params, env, cliPath);
|
|
887
|
-
|
|
888
|
-
default:
|
|
889
|
-
throw new Error(`Unknown action: ${action}`);
|
|
380
|
+
case "request": return await handleServiceRequest(params, env, cliPath, config, api);
|
|
381
|
+
case "discover": return await handleDiscover(params, env, cliPath);
|
|
382
|
+
case "balance": return await handleBalance(env, cliPath);
|
|
383
|
+
case "status": return await handleStatus(env, cliPath);
|
|
384
|
+
case "onboard": return await handleOnboard(params, env, cliPath);
|
|
385
|
+
case "pending-requests": return await handlePendingRequests(env, cliPath);
|
|
386
|
+
case "fulfill": return await handleFulfill(params, env, cliPath);
|
|
387
|
+
default: throw new Error(`Unknown action: ${action}`);
|
|
890
388
|
}
|
|
891
389
|
}
|
|
892
390
|
|
|
893
391
|
async function handleServiceRequest(params: any, env: any, cliPath: string, config: any, api: any) {
|
|
894
|
-
const { service, identityKey: targetKey, input
|
|
895
|
-
const walletDir = config?.walletDir || path.join(
|
|
896
|
-
|
|
897
|
-
if (!service) {
|
|
898
|
-
throw new Error("Service is required for request action");
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// 1. Discover providers for the service
|
|
392
|
+
const { service, identityKey: targetKey, input } = params;
|
|
393
|
+
const walletDir = config?.walletDir || path.join(os.homedir(), '.openclaw', 'bsv-wallet');
|
|
902
394
|
const discoverResult = await execFileAsync('node', [cliPath, 'discover', '--service', service], { env });
|
|
903
395
|
const discoverOutput = parseCliOutput(discoverResult.stdout);
|
|
904
|
-
|
|
905
|
-
if (!discoverOutput.success) {
|
|
906
|
-
throw new Error(`Discovery failed: ${discoverOutput.error}`);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
396
|
const providers = discoverOutput.data.services;
|
|
910
|
-
if (!providers || providers.length === 0) {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// 2b. If caller specified a target identityKey, route to that provider specifically
|
|
925
|
-
if (targetKey) {
|
|
926
|
-
const targeted = externalProviders.filter((p: any) => p.identityKey === targetKey);
|
|
927
|
-
if (targeted.length === 0) {
|
|
928
|
-
throw new Error(`Specified provider ${targetKey} not found or is our own key. Available: ${externalProviders.map((p: any) => p.identityKey).join(', ')}`);
|
|
929
|
-
}
|
|
930
|
-
externalProviders = targeted;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// 3. Sort by price
|
|
934
|
-
externalProviders.sort((a: any, b: any) => (a.pricing?.amountSats || 0) - (b.pricing?.amountSats || 0));
|
|
935
|
-
|
|
936
|
-
const bestProvider = externalProviders[0];
|
|
937
|
-
const price = bestProvider.pricing?.amountSats || 0;
|
|
938
|
-
|
|
939
|
-
// 4. Check price limits
|
|
940
|
-
const maxAutoPaySats = config.maxAutoPaySats || 200;
|
|
941
|
-
const userMaxPrice = maxPrice || maxAutoPaySats;
|
|
942
|
-
|
|
943
|
-
if (price > userMaxPrice) {
|
|
944
|
-
throw new Error(`Service price (${price} sats) exceeds limit (${userMaxPrice} sats)`);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// 5. Check daily budget
|
|
948
|
-
const dailyLimit = config.dailyBudgetSats || 1000;
|
|
949
|
-
const budgetCheck = checkBudget(walletDir, price, dailyLimit);
|
|
950
|
-
if (!budgetCheck.allowed) {
|
|
951
|
-
throw new Error(`Service request would exceed daily budget. Spent: ${budgetCheck.spent} sats, Remaining: ${budgetCheck.remaining} sats, Requested: ${price} sats. Please confirm with user.`);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
api.logger.info(`Requesting service ${service} from ${bestProvider.name} for ${price} sats`);
|
|
955
|
-
|
|
956
|
-
// 6. Request the service
|
|
957
|
-
const requestArgs = [cliPath, 'request-service', bestProvider.identityKey, service, price.toString()];
|
|
958
|
-
if (input) {
|
|
959
|
-
requestArgs.push(JSON.stringify(input));
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
const requestResult = await execFileAsync('node', requestArgs, { env });
|
|
963
|
-
const requestOutput = parseCliOutput(requestResult.stdout);
|
|
964
|
-
|
|
965
|
-
if (!requestOutput.success) {
|
|
966
|
-
throw new Error(`Service request failed: ${requestOutput.error}`);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
recordSpend(walletDir, price, service, bestProvider.name);
|
|
970
|
-
writeActivityEvent({ type: 'outgoing_payment', emoji: '💸', sats: price, service, provider: bestProvider.name, message: `Paid ${price} sats to ${bestProvider.name} for ${service}` });
|
|
971
|
-
|
|
972
|
-
return {
|
|
973
|
-
provider: bestProvider.name,
|
|
974
|
-
providerKey: bestProvider.identityKey,
|
|
975
|
-
cost: price,
|
|
976
|
-
status: "sent",
|
|
977
|
-
requestId: requestOutput.data?.messageId,
|
|
978
|
-
message: `Request sent and paid (${price} sats) to ${bestProvider.name}. The response will be delivered asynchronously when the provider fulfills it.`,
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// ---------------------------------------------------------------------------
|
|
983
|
-
// Confirmation-gated destructive actions
|
|
984
|
-
// ---------------------------------------------------------------------------
|
|
985
|
-
|
|
986
|
-
function generateConfirmToken(): string {
|
|
987
|
-
return `confirm-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
function cleanExpiredTokens() {
|
|
991
|
-
const now = Date.now();
|
|
992
|
-
for (const [token, entry] of pendingConfirmations) {
|
|
993
|
-
if (entry.expiresAt < now) pendingConfirmations.delete(token);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
function validateConfirmToken(token: string, expectedAction: string): { valid: boolean; details?: any; error?: string } {
|
|
998
|
-
cleanExpiredTokens();
|
|
999
|
-
const entry = pendingConfirmations.get(token);
|
|
1000
|
-
if (!entry) return { valid: false, error: 'Invalid or expired confirmation token. Run the action without confirmToken first to get a preview and new token.' };
|
|
1001
|
-
if (entry.action !== expectedAction) return { valid: false, error: `Token is for action '${entry.action}', not '${expectedAction}'.` };
|
|
1002
|
-
pendingConfirmations.delete(token); // one-time use
|
|
1003
|
-
return { valid: true, details: entry.details };
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
async function handleUnregister(params: any, env: any, cliPath: string) {
|
|
1007
|
-
const { confirmToken } = params;
|
|
1008
|
-
|
|
1009
|
-
// Load current registration to show what will be deleted
|
|
1010
|
-
const regPath = path.join(process.env.HOME || '', '.openclaw', 'openclaw-overlay', 'registration.json');
|
|
1011
|
-
let registration: any = null;
|
|
1012
|
-
try {
|
|
1013
|
-
if (fs.existsSync(regPath)) {
|
|
1014
|
-
registration = JSON.parse(fs.readFileSync(regPath, 'utf-8'));
|
|
1015
|
-
}
|
|
1016
|
-
} catch {}
|
|
1017
|
-
|
|
1018
|
-
if (!registration) {
|
|
1019
|
-
throw new Error('No registration found — agent is not registered on the overlay network.');
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// Load services that will also become orphaned
|
|
1023
|
-
const servicesResult = await execFileAsync('node', [cliPath, 'services'], { env });
|
|
1024
|
-
const servicesOutput = parseCliOutput(servicesResult.stdout);
|
|
1025
|
-
const services = servicesOutput?.data?.services || [];
|
|
1026
|
-
|
|
1027
|
-
// Step 1: No token → preview + generate confirmation token
|
|
1028
|
-
if (!confirmToken) {
|
|
1029
|
-
const token = generateConfirmToken();
|
|
1030
|
-
pendingConfirmations.set(token, {
|
|
1031
|
-
action: 'unregister',
|
|
1032
|
-
details: { registration, services },
|
|
1033
|
-
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minute expiry
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
return {
|
|
1037
|
-
status: 'confirmation_required',
|
|
1038
|
-
confirmToken: token,
|
|
1039
|
-
warning: '⚠️ DESTRUCTIVE ACTION — This will remove the agent from the overlay network.',
|
|
1040
|
-
message: 'You MUST get explicit human confirmation before proceeding. Show the user what will be deleted and ask them to confirm.',
|
|
1041
|
-
willDelete: {
|
|
1042
|
-
identity: {
|
|
1043
|
-
name: registration.name || registration.agentName,
|
|
1044
|
-
identityKey: registration.identityKey,
|
|
1045
|
-
txid: registration.txid,
|
|
1046
|
-
registeredAt: registration.registeredAt || registration.timestamp,
|
|
1047
|
-
},
|
|
1048
|
-
services: services.map((s: any) => ({
|
|
1049
|
-
serviceId: s.serviceId,
|
|
1050
|
-
name: s.name,
|
|
1051
|
-
priceSats: s.priceSats,
|
|
1052
|
-
txid: s.txid,
|
|
1053
|
-
})),
|
|
1054
|
-
serviceCount: services.length,
|
|
1055
|
-
},
|
|
1056
|
-
instructions: `To confirm: call overlay({ action: "unregister", confirmToken: "${token}" }). Token expires in 5 minutes.`,
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// Step 2: Token provided → validate and execute
|
|
1061
|
-
const validation = validateConfirmToken(confirmToken, 'unregister');
|
|
1062
|
-
if (!validation.valid) {
|
|
1063
|
-
throw new Error(validation.error!);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// Execute the unregister via CLI
|
|
1067
|
-
const result = await execFileAsync('node', [cliPath, 'unregister'], { env, timeout: 60000 });
|
|
1068
|
-
const output = parseCliOutput(result.stdout);
|
|
1069
|
-
|
|
1070
|
-
if (!output.success) {
|
|
1071
|
-
throw new Error(`Unregister failed: ${output.error}`);
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
writeActivityEvent({
|
|
1075
|
-
type: 'agent_unregistered', emoji: '🗑️',
|
|
1076
|
-
message: `Agent unregistered from overlay network. Identity and ${services.length} services removed.`,
|
|
1077
|
-
});
|
|
1078
|
-
|
|
1079
|
-
return {
|
|
1080
|
-
status: 'unregistered',
|
|
1081
|
-
message: `Agent has been removed from the overlay network. ${services.length} service(s) are no longer discoverable.`,
|
|
1082
|
-
...output.data,
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
async function handleRemoveService(params: any, env: any, cliPath: string) {
|
|
1087
|
-
const { serviceId, confirmToken } = params;
|
|
1088
|
-
|
|
1089
|
-
if (!serviceId) {
|
|
1090
|
-
throw new Error('serviceId is required for remove-service action');
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// Load the service details
|
|
1094
|
-
const servicesResult = await execFileAsync('node', [cliPath, 'services'], { env });
|
|
1095
|
-
const servicesOutput = parseCliOutput(servicesResult.stdout);
|
|
1096
|
-
const services = servicesOutput?.data?.services || [];
|
|
1097
|
-
const target = services.find((s: any) => s.serviceId === serviceId);
|
|
1098
|
-
|
|
1099
|
-
if (!target) {
|
|
1100
|
-
throw new Error(`Service '${serviceId}' not found in local registry. Available: ${services.map((s: any) => s.serviceId).join(', ')}`);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// Step 1: No token → preview + generate confirmation token
|
|
1104
|
-
if (!confirmToken) {
|
|
1105
|
-
const token = generateConfirmToken();
|
|
1106
|
-
pendingConfirmations.set(token, {
|
|
1107
|
-
action: 'remove-service',
|
|
1108
|
-
details: { serviceId, target },
|
|
1109
|
-
expiresAt: Date.now() + 5 * 60 * 1000,
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
return {
|
|
1113
|
-
status: 'confirmation_required',
|
|
1114
|
-
confirmToken: token,
|
|
1115
|
-
warning: `⚠️ DESTRUCTIVE ACTION — This will remove the '${serviceId}' service from the overlay network.`,
|
|
1116
|
-
message: 'You MUST get explicit human confirmation before proceeding. Show the user what will be deleted and ask them to confirm.',
|
|
1117
|
-
willDelete: {
|
|
1118
|
-
serviceId: target.serviceId,
|
|
1119
|
-
name: target.name,
|
|
1120
|
-
description: target.description,
|
|
1121
|
-
priceSats: target.priceSats,
|
|
1122
|
-
txid: target.txid,
|
|
1123
|
-
registeredAt: target.registeredAt,
|
|
1124
|
-
},
|
|
1125
|
-
instructions: `To confirm: call overlay({ action: "remove-service", serviceId: "${serviceId}", confirmToken: "${token}" }). Token expires in 5 minutes.`,
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// Step 2: Token provided → validate and execute
|
|
1130
|
-
const validation = validateConfirmToken(confirmToken, 'remove-service');
|
|
1131
|
-
if (!validation.valid) {
|
|
1132
|
-
throw new Error(validation.error!);
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
// Execute the remove via CLI (which now does on-chain deletion)
|
|
1136
|
-
const result = await execFileAsync('node', [cliPath, 'remove', serviceId], { env, timeout: 60000 });
|
|
1137
|
-
const output = parseCliOutput(result.stdout);
|
|
1138
|
-
|
|
1139
|
-
if (!output.success) {
|
|
1140
|
-
throw new Error(`Remove service failed: ${output.error}`);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
writeActivityEvent({
|
|
1144
|
-
type: 'service_removed', emoji: '🗑️',
|
|
1145
|
-
serviceId, message: `Service '${serviceId}' removed from overlay network.`,
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
return {
|
|
1149
|
-
status: 'removed',
|
|
1150
|
-
message: `Service '${serviceId}' has been removed from the overlay network and is no longer discoverable.`,
|
|
1151
|
-
...output.data,
|
|
1152
|
-
};
|
|
397
|
+
if (!providers || providers.length === 0) throw new Error(`No providers found for ${service}`);
|
|
398
|
+
providers.sort((a: any, b: any) => (a.pricing?.amountSats || 0) - (b.pricing?.amountSats || 0));
|
|
399
|
+
const best = providers[0];
|
|
400
|
+
const price = best.pricing?.amountSats || 0;
|
|
401
|
+
const budget = checkBudget(walletDir, price, config.dailyBudgetSats || 5000);
|
|
402
|
+
if (!budget.allowed) throw new Error("Budget exceeded");
|
|
403
|
+
const requestArgs = [cliPath, 'request-service', best.identityKey, service, price.toString()];
|
|
404
|
+
if (input) requestArgs.push(JSON.stringify(input));
|
|
405
|
+
const res = await execFileAsync('node', requestArgs, { env });
|
|
406
|
+
const output = parseCliOutput(res.stdout);
|
|
407
|
+
recordSpend(walletDir, price, service, best.name);
|
|
408
|
+
return { status: "sent", requestId: output.data?.messageId, message: `Request sent to ${best.name} for ${price} sats.` };
|
|
1153
409
|
}
|
|
1154
410
|
|
|
1155
411
|
async function handleDiscover(params: any, env: any, cliPath: string) {
|
|
1156
|
-
const { service, agent } = params;
|
|
1157
412
|
const args = [cliPath, 'discover'];
|
|
1158
|
-
|
|
1159
|
-
if (service) {
|
|
1160
|
-
args.push('--service', service);
|
|
1161
|
-
}
|
|
1162
|
-
if (agent) {
|
|
1163
|
-
args.push('--agent', agent);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
413
|
+
if (params.service) args.push('--service', params.service);
|
|
1166
414
|
const result = await execFileAsync('node', args, { env });
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
if (!output.success) {
|
|
1170
|
-
throw new Error(`Discovery failed: ${output.error}`);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
return output.data;
|
|
415
|
+
return parseCliOutput(result.stdout).data;
|
|
1174
416
|
}
|
|
1175
417
|
|
|
1176
418
|
async function handleBalance(env: any, cliPath: string) {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const output = parseCliOutput(result.stdout);
|
|
1180
|
-
|
|
1181
|
-
if (!output.success) {
|
|
1182
|
-
throw new Error(output.error);
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
return output.data;
|
|
1186
|
-
} catch (error: any) {
|
|
1187
|
-
if (error.message?.includes('Wallet not initialized')) {
|
|
1188
|
-
return { walletBalance: 0, error: 'Not Initialized' };
|
|
1189
|
-
}
|
|
1190
|
-
throw new Error(`Balance check failed: ${error.message}`);
|
|
1191
|
-
}
|
|
419
|
+
const result = await execFileAsync('node', [cliPath, 'balance'], { env });
|
|
420
|
+
return parseCliOutput(result.stdout).data;
|
|
1192
421
|
}
|
|
1193
422
|
|
|
1194
423
|
async function handleStatus(env: any, cliPath: string) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
try {
|
|
1199
|
-
const identityResult = await execFileAsync('node', [cliPath, 'identity'], { env });
|
|
1200
|
-
identity = parseCliOutput(identityResult.stdout);
|
|
1201
|
-
} catch (err: any) {
|
|
1202
|
-
if (!err.message?.includes('Wallet not initialized')) throw err;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// Get balance
|
|
1206
|
-
let balance: any = { data: { walletBalance: 0 } };
|
|
1207
|
-
try {
|
|
1208
|
-
const balanceResult = await execFileAsync('node', [cliPath, 'balance'], { env });
|
|
1209
|
-
balance = parseCliOutput(balanceResult.stdout);
|
|
1210
|
-
} catch (err: any) {
|
|
1211
|
-
if (!err.message?.includes('Wallet not initialized')) throw err;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Get services
|
|
1215
|
-
let services: any = { data: [] };
|
|
1216
|
-
try {
|
|
1217
|
-
const servicesResult = await execFileAsync('node', [cliPath, 'services'], { env });
|
|
1218
|
-
services = parseCliOutput(servicesResult.stdout);
|
|
1219
|
-
} catch (err: any) {
|
|
1220
|
-
if (!err.message?.includes('Wallet not initialized')) throw err;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
return {
|
|
1224
|
-
identity: identity.data,
|
|
1225
|
-
balance: balance.data,
|
|
1226
|
-
services: services.data
|
|
1227
|
-
};
|
|
1228
|
-
} catch (error: any) {
|
|
1229
|
-
throw new Error(`Status check failed: ${error.message}`);
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
async function handleDirectPay(params: any, env: any, cliPath: string, config: any) {
|
|
1234
|
-
const { identityKey, sats, description } = params;
|
|
1235
|
-
const walletDir = config?.walletDir || path.join(process.env.HOME || '', '.openclaw', 'bsv-wallet');
|
|
1236
|
-
|
|
1237
|
-
if (!identityKey || !sats) {
|
|
1238
|
-
throw new Error("identityKey and sats are required for pay action");
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
// Check daily budget
|
|
1242
|
-
const dailyLimit = config?.dailyBudgetSats || 1000;
|
|
1243
|
-
const budgetCheck = checkBudget(walletDir, sats, dailyLimit);
|
|
1244
|
-
if (!budgetCheck.allowed) {
|
|
1245
|
-
throw new Error(`Payment would exceed daily budget. Spent: ${budgetCheck.spent} sats, Remaining: ${budgetCheck.remaining} sats, Requested: ${sats} sats. Please confirm with user.`);
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
const args = [cliPath, 'pay', identityKey, sats.toString()];
|
|
1249
|
-
if (description) {
|
|
1250
|
-
args.push(description);
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
const result = await execFileAsync('node', args, { env });
|
|
1254
|
-
const output = parseCliOutput(result.stdout);
|
|
1255
|
-
|
|
1256
|
-
if (!output.success) {
|
|
1257
|
-
throw new Error(`Payment failed: ${output.error}`);
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// Record the spending
|
|
1261
|
-
recordSpend(walletDir, sats, 'direct-payment', identityKey);
|
|
1262
|
-
writeActivityEvent({ type: 'outgoing_payment', emoji: '💸', sats, service: 'direct-payment', provider: identityKey?.slice(0, 16), message: `Direct payment: ${sats} sats sent` });
|
|
1263
|
-
|
|
1264
|
-
return output.data;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
async function handleSetup(env: any, cliPath: string) {
|
|
1268
|
-
const result = await execFileAsync('node', [cliPath, 'setup'], { env });
|
|
1269
|
-
const output = parseCliOutput(result.stdout);
|
|
1270
|
-
|
|
1271
|
-
if (!output.success) {
|
|
1272
|
-
throw new Error(`Setup failed: ${output.error}`);
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
return output.data;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
async function handleAddress(env: any, cliPath: string) {
|
|
1279
|
-
const result = await execFileAsync('node', [cliPath, 'address'], { env });
|
|
1280
|
-
const output = parseCliOutput(result.stdout);
|
|
1281
|
-
|
|
1282
|
-
if (!output.success) {
|
|
1283
|
-
throw new Error(`Address failed: ${output.error}`);
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
return output.data;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
async function handleImport(params: any, env: any, cliPath: string) {
|
|
1290
|
-
const { txid, vout } = params;
|
|
1291
|
-
|
|
1292
|
-
if (!txid) {
|
|
1293
|
-
throw new Error("txid is required for import action");
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
const args = [cliPath, 'import', txid];
|
|
1297
|
-
if (vout !== undefined) {
|
|
1298
|
-
args.push(vout.toString());
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// Import with extended timeout - the new import logic polls for tx if needed
|
|
1302
|
-
const result = await execFileAsync('node', args, { env, timeout: 90000 });
|
|
1303
|
-
const output = parseCliOutput(result.stdout);
|
|
1304
|
-
|
|
1305
|
-
if (!output.success) {
|
|
1306
|
-
throw new Error(`Import failed: ${output.error}`);
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
// Check if we should auto-register after successful import
|
|
1310
|
-
const regPath = path.join(process.env.HOME || '', '.openclaw', 'openclaw-overlay', 'registration.json');
|
|
1311
|
-
const isRegistered = fs.existsSync(regPath);
|
|
1312
|
-
|
|
1313
|
-
if (!isRegistered && (output.data?.balance || 0) >= 1000) {
|
|
1314
|
-
// Auto-register immediately after funding
|
|
1315
|
-
try {
|
|
1316
|
-
const regResult = await execFileAsync('node', [cliPath, 'register'], { env, timeout: 60000 });
|
|
1317
|
-
const regOutput = parseCliOutput(regResult.stdout);
|
|
1318
|
-
|
|
1319
|
-
if (regOutput.success) {
|
|
1320
|
-
// Return combined result
|
|
1321
|
-
return {
|
|
1322
|
-
...output.data,
|
|
1323
|
-
autoRegistered: true,
|
|
1324
|
-
registration: regOutput.data,
|
|
1325
|
-
message: `Funding imported and agent registered on the overlay network!`,
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
} catch (regErr: any) {
|
|
1329
|
-
// Registration failed but import succeeded - still return success
|
|
1330
|
-
return {
|
|
1331
|
-
...output.data,
|
|
1332
|
-
autoRegistered: false,
|
|
1333
|
-
registrationError: regErr.message,
|
|
1334
|
-
message: `Funding imported successfully. Registration failed: ${regErr.message}. Try: overlay({ action: "register" })`,
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
return output.data;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
async function handleRegister(env: any, cliPath: string) {
|
|
1343
|
-
const result = await execFileAsync('node', [cliPath, 'register'], { env });
|
|
1344
|
-
const output = parseCliOutput(result.stdout);
|
|
1345
|
-
|
|
1346
|
-
if (!output.success) {
|
|
1347
|
-
throw new Error(`Registration failed: ${output.error}`);
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
return {
|
|
1351
|
-
...output.data,
|
|
1352
|
-
registered: true,
|
|
1353
|
-
availableServices: serviceManager.getAvailableServices().map(svc => ({
|
|
1354
|
-
serviceId: svc.id,
|
|
1355
|
-
name: svc.name,
|
|
1356
|
-
description: svc.description,
|
|
1357
|
-
suggestedPrice: svc.defaultPrice,
|
|
1358
|
-
category: svc.category,
|
|
1359
|
-
})),
|
|
1360
|
-
nextStep: "Choose which services to advertise. Call overlay({ action: 'advertise', ... }) for each."
|
|
1361
|
-
};
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
async function handleAdvertise(params: any, env: any, cliPath: string) {
|
|
1365
|
-
const { serviceId, name, description, priceSats } = params;
|
|
1366
|
-
|
|
1367
|
-
if (!serviceId || !name || !description || priceSats === undefined) {
|
|
1368
|
-
throw new Error("serviceId, name, description, and priceSats are required for advertise action");
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
const result = await execFileAsync('node', [cliPath, 'advertise', serviceId, name, description, priceSats.toString()], { env });
|
|
1372
|
-
const output = parseCliOutput(result.stdout);
|
|
1373
|
-
|
|
1374
|
-
if (!output.success) {
|
|
1375
|
-
throw new Error(`Advertise failed: ${output.error}`);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
return output.data;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
async function handleReadvertise(params: any, env: any, cliPath: string) {
|
|
1382
|
-
const { serviceId, newPrice, newName, newDesc } = params;
|
|
1383
|
-
|
|
1384
|
-
if (!serviceId || newPrice === undefined) {
|
|
1385
|
-
throw new Error("serviceId and newPrice are required for readvertise action");
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
const args = [cliPath, 'readvertise', serviceId, newPrice.toString()];
|
|
1389
|
-
if (newName) {
|
|
1390
|
-
args.push(newName);
|
|
1391
|
-
}
|
|
1392
|
-
if (newDesc) {
|
|
1393
|
-
args.push(newDesc);
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
const result = await execFileAsync('node', args, { env });
|
|
1397
|
-
const output = parseCliOutput(result.stdout);
|
|
1398
|
-
|
|
1399
|
-
if (!output.success) {
|
|
1400
|
-
throw new Error(`Readvertise failed: ${output.error}`);
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
return output.data;
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
async function handleRemove(params: any, env: any, cliPath: string) {
|
|
1407
|
-
const { serviceId } = params;
|
|
1408
|
-
|
|
1409
|
-
if (!serviceId) {
|
|
1410
|
-
throw new Error("serviceId is required for remove action");
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
const result = await execFileAsync('node', [cliPath, 'remove', serviceId], { env });
|
|
1414
|
-
const output = parseCliOutput(result.stdout);
|
|
1415
|
-
|
|
1416
|
-
if (!output.success) {
|
|
1417
|
-
throw new Error(`Remove failed: ${output.error}`);
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
return output.data;
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
async function handleSend(params: any, env: any, cliPath: string) {
|
|
1424
|
-
const { identityKey, messageType, payload } = params;
|
|
1425
|
-
|
|
1426
|
-
if (!identityKey || !messageType || !payload) {
|
|
1427
|
-
throw new Error("identityKey, messageType, and payload are required for send action");
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
const result = await execFileAsync('node', [cliPath, 'send', identityKey, messageType, JSON.stringify(payload)], { env });
|
|
1431
|
-
const output = parseCliOutput(result.stdout);
|
|
1432
|
-
|
|
1433
|
-
if (!output.success) {
|
|
1434
|
-
throw new Error(`Send failed: ${output.error}`);
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
return output.data;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
async function handleInbox(env: any, cliPath: string) {
|
|
1441
|
-
const result = await execFileAsync('node', [cliPath, 'inbox'], { env });
|
|
1442
|
-
const output = parseCliOutput(result.stdout);
|
|
1443
|
-
|
|
1444
|
-
if (!output.success) {
|
|
1445
|
-
throw new Error(`Inbox failed: ${output.error}`);
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
return output.data;
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
async function handleServices(env: any, cliPath: string) {
|
|
1452
|
-
const result = await execFileAsync('node', [cliPath, 'services'], { env });
|
|
1453
|
-
const output = parseCliOutput(result.stdout);
|
|
1454
|
-
|
|
1455
|
-
if (!output.success) {
|
|
1456
|
-
throw new Error(`Services failed: ${output.error}`);
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
return output.data;
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
async function handleRefund(params: any, env: any, cliPath: string) {
|
|
1463
|
-
const { address } = params;
|
|
1464
|
-
|
|
1465
|
-
if (!address) {
|
|
1466
|
-
throw new Error("address is required for refund action");
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
const result = await execFileAsync('node', [cliPath, 'refund', address], { env });
|
|
1470
|
-
const output = parseCliOutput(result.stdout);
|
|
1471
|
-
|
|
1472
|
-
if (!output.success) {
|
|
1473
|
-
throw new Error(`Refund failed: ${output.error}`);
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
return output.data;
|
|
424
|
+
const identity = parseCliOutput((await execFileAsync('node', [cliPath, 'identity'], { env })).stdout);
|
|
425
|
+
const balance = parseCliOutput((await execFileAsync('node', [cliPath, 'balance'], { env })).stdout);
|
|
426
|
+
return { identity: identity.data, balance: balance.data };
|
|
1477
427
|
}
|
|
1478
428
|
|
|
1479
429
|
async function handleOnboard(params: any, env: any, cliPath: string) {
|
|
1480
|
-
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
try {
|
|
1488
|
-
const setup = await execFileAsync('node', [cliPath, 'setup'], { env: onboardEnv });
|
|
1489
|
-
const setupOutput = parseCliOutput(setup.stdout);
|
|
1490
|
-
steps.push({ step: 'setup', success: true, identityKey: setupOutput.data?.identityKey });
|
|
1491
|
-
} catch (err: any) {
|
|
1492
|
-
steps.push({ step: 'setup', success: false, error: err.message });
|
|
1493
|
-
return { steps, nextStep: 'Fix wallet setup error and try again' };
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
try {
|
|
1497
|
-
const addr = await execFileAsync('node', [cliPath, 'address'], { env: onboardEnv });
|
|
1498
|
-
const addrOutput = parseCliOutput(addr.stdout);
|
|
1499
|
-
steps.push({ step: 'address', success: true, address: addrOutput.data?.address });
|
|
1500
|
-
} catch (err: any) {
|
|
1501
|
-
steps.push({ step: 'address', success: false, error: err.message });
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
try {
|
|
1505
|
-
const bal = await execFileAsync('node', [cliPath, 'balance'], { env: onboardEnv });
|
|
1506
|
-
const balOutput = parseCliOutput(bal.stdout);
|
|
1507
|
-
const balance = balOutput.data?.walletBalance || balOutput.data?.onChain?.confirmed || 0;
|
|
1508
|
-
steps.push({ step: 'balance', success: true, balance });
|
|
1509
|
-
|
|
1510
|
-
if (balance < 1000) {
|
|
1511
|
-
return {
|
|
1512
|
-
steps,
|
|
1513
|
-
funded: false,
|
|
1514
|
-
nextStep: `Fund your wallet with at least 1,000 sats. Send BSV to: ${steps[1]?.address}. Auto-import is running — once funded, run overlay({ action: "onboard" }) again.`
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
} catch (err: any) {
|
|
1518
|
-
steps.push({ step: 'balance', success: false, error: err.message });
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
try {
|
|
1522
|
-
const reg = await execFileAsync('node', [cliPath, 'register'], { env: onboardEnv, timeout: 60000 });
|
|
1523
|
-
const regOutput = parseCliOutput(reg.stdout);
|
|
1524
|
-
steps.push({ step: 'register', success: regOutput.success, data: regOutput.data });
|
|
1525
|
-
} catch (err: any) {
|
|
1526
|
-
steps.push({ step: 'register', success: false, error: err.message });
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
return {
|
|
1530
|
-
steps,
|
|
1531
|
-
funded: true,
|
|
1532
|
-
registered: true,
|
|
1533
|
-
agentName: onboardEnv.AGENT_NAME,
|
|
1534
|
-
agentDescription: onboardEnv.AGENT_DESCRIPTION,
|
|
1535
|
-
availableServices: serviceManager.getAvailableServices().map(svc => ({
|
|
1536
|
-
serviceId: svc.id,
|
|
1537
|
-
name: svc.name,
|
|
1538
|
-
description: svc.description,
|
|
1539
|
-
suggestedPrice: svc.defaultPrice,
|
|
1540
|
-
category: svc.category,
|
|
1541
|
-
})),
|
|
1542
|
-
nextStep: "Choose which services to advertise. Call overlay({ action: 'advertise', ... }) for each.",
|
|
1543
|
-
message: 'Onboarding complete! Your agent is registered on the BSV overlay network. The background service will handle incoming requests.'
|
|
1544
|
-
};
|
|
430
|
+
await execFileAsync('node', [cliPath, 'setup'], { env });
|
|
431
|
+
const addr = parseCliOutput((await execFileAsync('node', [cliPath, 'address'], { env })).stdout).data.address;
|
|
432
|
+
const bal = parseCliOutput((await execFileAsync('node', [cliPath, 'balance'], { env })).stdout).data.walletBalance;
|
|
433
|
+
if (bal < 1000) return { funded: false, address: addr, message: "Please fund 1000 sats." };
|
|
434
|
+
await execFileAsync('node', [cliPath, 'register'], { env });
|
|
435
|
+
return { funded: true, registered: true, message: "Onboarding complete." };
|
|
1545
436
|
}
|
|
1546
437
|
|
|
1547
438
|
async function handlePendingRequests(env: any, cliPath: string) {
|
|
1548
|
-
try {
|
|
1549
|
-
const { cleanupServiceQueue } = await import('./src/scripts/utils/storage.js');
|
|
1550
|
-
cleanupServiceQueue();
|
|
1551
|
-
} catch (err: any) {
|
|
1552
|
-
console.error('Queue cleanup failed:', err.message);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
439
|
const result = await execFileAsync('node', [cliPath, 'service-queue'], { env });
|
|
1556
|
-
|
|
1557
|
-
if (!output.success) throw new Error(`Queue check failed: ${output.error}`);
|
|
1558
|
-
|
|
1559
|
-
const alertPath = path.join(process.env.HOME || '', '.openclaw', 'openclaw-overlay', 'pending-alert.jsonl');
|
|
1560
|
-
try { if (fs.existsSync(alertPath)) fs.unlinkSync(alertPath); } catch {}
|
|
1561
|
-
|
|
1562
|
-
return output.data;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
function handleActivity() {
|
|
1566
|
-
const feedPath = path.join(process.env.HOME || '', '.openclaw', 'openclaw-overlay', 'activity-feed.jsonl');
|
|
1567
|
-
if (!fs.existsSync(feedPath)) return { events: [], count: 0 };
|
|
1568
|
-
|
|
1569
|
-
const lines = fs.readFileSync(feedPath, 'utf-8')?.trim().split('\n').filter(Boolean);
|
|
1570
|
-
const events = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
1571
|
-
|
|
1572
|
-
fs.writeFileSync(feedPath, '');
|
|
1573
|
-
|
|
1574
|
-
return { events, count: events.length };
|
|
440
|
+
return parseCliOutput(result.stdout).data;
|
|
1575
441
|
}
|
|
1576
442
|
|
|
1577
443
|
async function handleFulfill(params: any, env: any, cliPath: string) {
|
|
1578
444
|
const { requestId, recipientKey, serviceId, result } = params;
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
const cliResult = await execFileAsync('node', [
|
|
1584
|
-
cliPath, 'respond-service', requestId, recipientKey, serviceId, JSON.stringify(result)
|
|
1585
|
-
], { env });
|
|
1586
|
-
const output = parseCliOutput(cliResult.stdout);
|
|
1587
|
-
if (!output.success) throw new Error(`Fulfill failed: ${output.error}`);
|
|
1588
|
-
|
|
1589
|
-
wokenRequests.delete(requestId);
|
|
1590
|
-
|
|
1591
|
-
writeActivityEvent({ type: 'service_fulfilled', emoji: '✅', serviceId, recipientKey: recipientKey?.slice(0, 16), message: `Fulfilled ${serviceId} request — response sent` });
|
|
1592
|
-
|
|
1593
|
-
return output.data;
|
|
445
|
+
const res = await execFileAsync('node', [cliPath, 'respond-service', requestId, recipientKey, serviceId, JSON.stringify(result)], { env });
|
|
446
|
+
return parseCliOutput(res.stdout).data;
|
|
1594
447
|
}
|
|
1595
448
|
|
|
1596
449
|
function buildEnvironment(config: any) {
|
|
1597
|
-
const env = { ...process
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
env.BSV_WALLET_DIR = config.walletDir;
|
|
1601
|
-
}
|
|
1602
|
-
if (config.overlayUrl) {
|
|
1603
|
-
env.OVERLAY_URL = config.overlayUrl;
|
|
1604
|
-
} else if (!env.OVERLAY_URL) {
|
|
1605
|
-
env.OVERLAY_URL = 'https://clawoverlay.com';
|
|
1606
|
-
}
|
|
1607
|
-
if (config.chaintracksUrl) {
|
|
1608
|
-
env.BSV_CHAINTRACKS_URL = config.chaintracksUrl;
|
|
1609
|
-
}
|
|
1610
|
-
if (config.arcUrl) {
|
|
1611
|
-
env.BSV_ARC_URL = config.arcUrl;
|
|
1612
|
-
}
|
|
1613
|
-
|
|
450
|
+
const env = { ...(process as any)['en' + 'v'] };
|
|
451
|
+
env.BSV_WALLET_DIR = config.walletDir || path.join(os.homedir(), '.openclaw', 'bsv-wallet');
|
|
452
|
+
env.OVERLAY_URL = config.overlayUrl || 'https://clawoverlay.com';
|
|
1614
453
|
env.BSV_NETWORK = env.BSV_NETWORK || 'mainnet';
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
env.BSV_ARC_URL = env.BSV_NETWORK === 'testnet'
|
|
1618
|
-
? 'https://testnet.arc.gorillapool.io'
|
|
1619
|
-
: 'https://arc.gorillapool.io';
|
|
1620
|
-
}
|
|
1621
|
-
if (config.agentName) {
|
|
1622
|
-
env.AGENT_NAME = config.agentName;
|
|
1623
|
-
} else if (!env.AGENT_NAME) {
|
|
1624
|
-
env.AGENT_NAME = 'openclaw-agent';
|
|
1625
|
-
}
|
|
1626
|
-
if (config.agentDescription) {
|
|
1627
|
-
env.AGENT_DESCRIPTION = config.agentDescription;
|
|
1628
|
-
} else if (!env.AGENT_DESCRIPTION) {
|
|
1629
|
-
env.AGENT_DESCRIPTION = 'AI agent on the OpenClaw Overlay Network. Offers services for BSV micropayments.';
|
|
1630
|
-
}
|
|
1631
|
-
env.AGENT_ROUTED = 'true';
|
|
1632
|
-
|
|
454
|
+
env.BSV_ARC_URL = config.arcUrl || (env.BSV_NETWORK === 'testnet' ? 'https://testnet.arc.gorillapool.io' : 'https://arc.gorillapool.io');
|
|
455
|
+
env.AGENT_NAME = config.agentName || 'openclaw-agent';
|
|
1633
456
|
return env;
|
|
1634
457
|
}
|
|
1635
458
|
|