openclaw-overlay-plugin 0.7.55 → 0.7.56

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 CHANGED
@@ -85,11 +85,6 @@ async function startAutoImport(env: any, cliPath: string, logger: any) {
85
85
  const address = addrOutput.data?.address;
86
86
  if (!address) return;
87
87
 
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
88
  autoImportInterval = setInterval(async () => {
94
89
  try {
95
90
  const network = env.BSV_NETWORK === 'testnet' ? 'test' : 'main';
@@ -134,8 +129,6 @@ async function startAutoImport(env: any, cliPath: string, logger: any) {
134
129
  const regOutput = parseCliOutput(regResult.stdout);
135
130
  if (regOutput.success) {
136
131
  logger?.info?.('[openclaw-overlay] Auto-registered on overlay network!');
137
-
138
- // Auto-advertise services from config
139
132
  await autoAdvertiseServices(env, cliPath, logger);
140
133
  }
141
134
  }
@@ -144,14 +137,13 @@ async function startAutoImport(env: any, cliPath: string, logger: any) {
144
137
  }
145
138
  }
146
139
  } catch (err) {
147
- // Already imported or error — track it so we don't retry
148
140
  knownTxids.add(key);
149
141
  }
150
142
  }
151
143
  } catch (err) {
152
- // WoC API error — just skip this cycle
144
+ // WoC API error
153
145
  }
154
- }, 30000); // Check every 30 seconds for faster onboarding
146
+ }, 30000);
155
147
  } catch (err: any) {
156
148
  logger?.warn?.('[openclaw-overlay] Auto-import setup failed:', err.message);
157
149
  }
@@ -167,151 +159,69 @@ function stopAutoImport() {
167
159
  // Auto-advertise services from config after registration
168
160
  async function autoAdvertiseServices(env: any, cliPath: string, logger: any) {
169
161
  try {
170
- // Read config to get services list
171
- const configPaths = [
172
- path.join(process.env.HOME || '', '.openclaw', 'openclaw.json'),
173
- path.join(process.env.HOME || '', '.openclaw', 'openclaw.json'),
174
- ];
162
+ const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
163
+ if (!fs.existsSync(configPath)) return;
175
164
 
176
165
  let servicesToAdvertise: string[] = [];
166
+ try {
167
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
168
+ const pluginConfig = config?.plugins?.entries?.['openclaw-overlay-plugin']?.config || config?.plugins?.entries?.['openclaw-overlay-plugin'];
169
+ if (pluginConfig?.services && Array.isArray(pluginConfig.services)) {
170
+ servicesToAdvertise = pluginConfig.services;
171
+ }
172
+ } catch {}
177
173
 
178
- for (const configPath of configPaths) {
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[] = [];
174
+ if (servicesToAdvertise.length === 0) return;
199
175
 
200
176
  for (const serviceId of servicesToAdvertise) {
201
177
  const serviceInfo = serviceManager.registry.get(serviceId);
202
- if (!serviceInfo) {
203
- failed.push(serviceId);
204
- continue;
205
- }
206
-
178
+ if (!serviceInfo) continue;
207
179
  try {
208
180
  await execFileAsync('node', [
209
181
  cliPath, 'advertise', serviceId, serviceInfo.name, serviceInfo.description, String(serviceInfo.defaultPrice)
210
182
  ], { env, timeout: 60000 });
211
- advertised.push(serviceId);
212
- } catch {
213
- failed.push(serviceId);
214
- }
183
+ } catch {}
215
184
  }
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
185
  } catch (err: any) {
221
186
  logger?.warn?.('[openclaw-overlay] Auto-advertising failed:', err.message);
222
187
  }
223
188
  }
224
189
 
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
190
  function wakeAgent(text: string, logger: any, options: { sessionKey?: string } = {}) {
230
191
  const sessionKey = options.sessionKey || `hook:openclaw-overlay:${Date.now()}`;
231
- const gatewayPort = getGatewayPort();
192
+ const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT || '18789';
232
193
  const httpToken = getHooksToken();
233
-
234
- if (!httpToken) {
235
- logger?.warn?.('[openclaw-overlay] Skipped wakeAgent: OPENCLAW_HOOKS_TOKEN not set');
236
- return;
237
- }
194
+ if (!httpToken) return;
238
195
 
239
- const url = `http://localhost:${gatewayPort}/hooks/agent`;
240
-
241
- fetch(url, {
196
+ fetch(`http://localhost:${gatewayPort}/hooks/agent`, {
242
197
  method: 'POST',
243
- headers: {
244
- 'Content-Type': 'application/json',
245
- 'x-openclaw-token': httpToken,
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';
198
+ headers: { 'Content-Type': 'application/json', 'x-openclaw-token': httpToken },
199
+ body: JSON.stringify({ prompt: text, sessionKey })
200
+ }).catch(() => {});
267
201
  }
268
202
 
269
203
  function getHooksToken(): string | null {
270
- let hooksToken: string | null = process.env.OPENCLAW_HOOKS_TOKEN || null;
271
-
272
- if (!hooksToken) {
204
+ let token = process.env.OPENCLAW_HOOKS_TOKEN || null;
205
+ if (!token) {
273
206
  try {
274
207
  const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
275
208
  if (fs.existsSync(configPath)) {
276
209
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
277
- hooksToken = config.gateway?.hooksToken || null;
210
+ token = config.gateway?.hooksToken || null;
278
211
  }
279
212
  } catch {}
280
213
  }
281
-
282
- return hooksToken;
214
+ return token;
283
215
  }
284
216
 
285
- // Categorize WebSocket events into notification types
286
217
  function categorizeEvent(event: any) {
287
218
  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
219
  if (event.action === 'queued-for-agent' && event.satoshisReceived) {
291
220
  return { ...base, type: 'incoming_payment', emoji: '💰', serviceId: event.serviceId, sats: event.satoshisReceived, requestId: event.id, message: `Received ${event.satoshisReceived} sats for ${event.serviceId}` };
292
221
  }
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
222
  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
- };
223
+ 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
224
  }
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
225
  return null;
316
226
  }
317
227
 
@@ -319,31 +229,13 @@ function startBackgroundService(env: any, cliPath: string, logger: any) {
319
229
  if (backgroundProcess) return;
320
230
  serviceRunning = true;
321
231
 
322
- // Clean up old request IDs every 5 minutes to prevent memory bloat
323
- requestCleanupInterval = setInterval(async () => {
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
- }
232
+ requestCleanupInterval = setInterval(() => {
233
+ if (serviceRunning) wokenRequests.clear();
337
234
  }, 5 * 60 * 1000);
338
235
 
339
236
  function spawnConnect() {
340
237
  if (!serviceRunning) return;
341
-
342
- const proc = spawn('node', [cliPath, 'connect'], {
343
- env,
344
- stdio: ['ignore', 'pipe', 'pipe']
345
- });
346
-
238
+ const proc = spawn('node', [cliPath, 'connect'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
347
239
  backgroundProcess = proc;
348
240
 
349
241
  proc.stdout?.on('data', (data) => {
@@ -351,98 +243,39 @@ function startBackgroundService(env: any, cliPath: string, logger: any) {
351
243
  for (const line of lines) {
352
244
  try {
353
245
  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
246
  if ((event.action === 'queued-for-agent' || event.action === 'already-queued') && event.serviceId) {
362
- const requestId = event.id || `${event.from}-${Date.now()}`;
363
-
364
- // Check if already woken to prevent duplicate processing
365
- if (wokenRequests.has(requestId)) {
366
- logger?.debug?.(`[openclaw-overlay] Request ${requestId} already woken, skipping duplicate`);
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()}` });
247
+ const rid = event.id || `${event.from}-${Date.now()}`;
248
+ if (wokenRequests.has(rid)) return;
249
+ wokenRequests.add(rid);
250
+ 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: { ... } })`;
251
+ wakeAgent(wakeText, logger, { sessionKey: `hook:openclaw-overlay:${rid}` });
380
252
  }
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
253
  if (event.type === 'service-response' && event.action === 'received') {
385
- const svcId = event.serviceId || 'unknown';
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.`;
254
+ 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
255
  wakeAgent(wakeText, logger, { sessionKey: `hook:openclaw-overlay:resp-${event.requestId || Date.now()}` });
394
256
  }
395
-
396
- // Write payment/activity notifications for ALL significant events
397
- const notifEvent = categorizeEvent(event);
398
- if (notifEvent) {
399
- try {
400
- fs.appendFileSync(path.join(alertDir, 'activity-feed.jsonl'), JSON.stringify(notifEvent) + '\n');
401
- } catch {}
257
+ const notif = categorizeEvent(event);
258
+ if (notif) {
259
+ const dir = path.join(process.env.HOME || '', '.openclaw', 'openclaw-overlay');
260
+ fs.mkdirSync(dir, { recursive: true });
261
+ fs.appendFileSync(path.join(dir, 'activity-feed.jsonl'), JSON.stringify(notif) + '\n');
402
262
  }
403
263
  } catch {}
404
264
  }
405
265
  });
406
266
 
407
- proc.stderr?.on('data', (data) => {
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
267
  proc.on('exit', (code) => {
424
268
  backgroundProcess = null;
425
- if (serviceRunning) {
426
- logger?.warn?.(`[openclaw-overlay] Background service exited (code ${code}), restarting in 5s...`);
427
- setTimeout(spawnConnect, 5000);
428
- }
269
+ if (serviceRunning) setTimeout(spawnConnect, 5000);
429
270
  });
430
271
  }
431
-
432
272
  spawnConnect();
433
273
  }
434
274
 
435
275
  function stopBackgroundService() {
436
276
  serviceRunning = false;
437
- if (backgroundProcess) {
438
- backgroundProcess.kill('SIGTERM');
439
- backgroundProcess = null;
440
- }
441
- if (requestCleanupInterval) {
442
- clearInterval(requestCleanupInterval);
443
- requestCleanupInterval = null;
444
- }
445
- // Clear any remaining request IDs
277
+ if (backgroundProcess) { backgroundProcess.kill(); backgroundProcess = null; }
278
+ if (requestCleanupInterval) { clearInterval(requestCleanupInterval); requestCleanupInterval = null; }
446
279
  wokenRequests.clear();
447
280
  stopAutoImport();
448
281
  }
@@ -454,7 +287,6 @@ export async function activate(api: any) {
454
287
  let isInitialized = false;
455
288
 
456
289
  function getCliPath() {
457
- // If we are already in the dist folder (running from compiled JS), don't add it again
458
290
  const base = __dirname.endsWith('dist') ? __dirname : path.join(__dirname, 'dist');
459
291
  return path.join(base, 'src', 'cli.js');
460
292
  }
@@ -463,125 +295,26 @@ export default function register(api: any) {
463
295
  if (isInitialized) return;
464
296
  isInitialized = true;
465
297
 
466
- // Capture config at registration time
467
298
  const entries = api.getConfig?.()?.plugins?.entries || {};
468
- const entry = entries['openclaw-overlay-plugin'] || entries['openclaw-overlay'] || entries['bsv-overlay-skill'] || {};
299
+ const entry = entries['openclaw-overlay-plugin'] || entries['openclaw-overlay'] || {};
469
300
  const pluginConfig = { ...entry, ...(entry.config || {}), ...(api.config || {}) };
470
301
 
471
- // Register the overlay agent tool
302
+ // 1. Tool
472
303
  api.registerTool({
473
304
  name: "overlay",
474
- description: "Access the BSV agent marketplace - discover agents and exchange BSV micropayments for services",
305
+ description: "Access the BSV agent marketplace",
475
306
  parameters: {
476
307
  type: "object",
477
308
  properties: {
478
- action: {
479
- type: "string",
480
- enum: [
481
- "request", "discover", "balance", "status", "pay",
482
- "setup", "address", "import", "register", "advertise",
483
- "readvertise", "remove", "send", "inbox", "services", "refund",
484
- "onboard", "pending-requests", "fulfill",
485
- "unregister", "remove-service"
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
- }
309
+ action: { type: "string", enum: ["request", "discover", "balance", "status", "pay", "onboard", "pending-requests", "fulfill", "unregister"] },
310
+ service: { type: "string" },
311
+ input: { type: "object" },
312
+ identityKey: { type: "string" },
313
+ sats: { type: "number" },
314
+ requestId: { type: "string" },
315
+ recipientKey: { type: "string" },
316
+ serviceId: { type: "string" },
317
+ result: { type: "object" }
585
318
  },
586
319
  required: ["action"]
587
320
  },
@@ -589,227 +322,50 @@ export default function register(api: any) {
589
322
  try {
590
323
  return await executeOverlayAction(params, pluginConfig, api);
591
324
  } catch (error: any) {
592
- return {
593
- content: [{
594
- type: "text",
595
- text: `Error: ${error.message}`
596
- }]
597
- };
325
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
598
326
  }
599
327
  }
600
328
  });
601
329
 
602
- // Register the /overlay slash command for direct chat interaction (Autoreply)
330
+ // 2. Command
603
331
  api.registerCommand({
604
332
  name: "overlay",
605
333
  description: "BSV Overlay Marketplace commands",
606
334
  acceptsArgs: true,
607
- requireAuth: true,
608
335
  handler: async (ctx: any) => {
609
336
  try {
610
- const args = ctx.args || [];
611
- const action = args[0] || 'status';
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 };
337
+ const action = ctx.args?.[0] || 'status';
338
+ const result = await executeOverlayAction({ action }, pluginConfig, api);
339
+ return { text: `**Overlay ${action.toUpperCase()}**\n\n${typeof result === 'string' ? result : JSON.stringify(result, null, 2)}` };
633
340
  } catch (error: any) {
634
- return { text: `❌ Overlay Error: ${error.message}` };
341
+ return { text: `❌ Error: ${error.message}` };
635
342
  }
636
343
  }
637
344
  });
638
345
 
639
- // Register background relay service
346
+ // 3. Service
640
347
  api.registerService({
641
348
  id: "openclaw-overlay-relay",
642
349
  start: async () => {
643
- try {
644
- api.logger.info("Starting BSV overlay WebSocket relay...");
645
- const env = buildEnvironment(pluginConfig);
646
- const cliPath = getCliPath();
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
- }
350
+ const env = buildEnvironment(pluginConfig);
351
+ const cliPath = getCliPath();
352
+ startBackgroundService(env, cliPath, api.logger);
353
+ startAutoImport(env, cliPath, api.logger);
686
354
  },
687
- stop: async () => {
688
- api.logger.info("Stopping BSV overlay WebSocket relay...");
689
- stopBackgroundService();
690
- }
355
+ stop: () => stopBackgroundService()
691
356
  });
692
357
 
693
- // Register CLI extensions
358
+ // 4. CLI
694
359
  api.registerCli(({ program }: any) => {
695
360
  const overlay = program.command("overlay").description("BSV Overlay Network management");
696
-
697
- overlay
698
- .command("status")
699
- .description("Show agent identity and balance")
700
- .action(async () => {
701
- try {
702
- const env = buildEnvironment(pluginConfig);
703
- const cliPath = getCliPath();
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
- });
361
+ overlay.command("status").action(async () => {
362
+ const result = await handleStatus(buildEnvironment(pluginConfig), getCliPath());
363
+ console.log(JSON.stringify(result, null, 2));
364
+ });
365
+ overlay.command("balance").action(async () => {
366
+ const result = await handleBalance(buildEnvironment(pluginConfig), getCliPath());
367
+ console.log(JSON.stringify(result, null, 2));
368
+ });
813
369
  }, { commands: ["overlay"] });
814
370
  }
815
371
 
@@ -819,817 +375,82 @@ async function executeOverlayAction(params: any, config: any, api: any) {
819
375
  const cliPath = getCliPath();
820
376
 
821
377
  switch (action) {
822
- case "request":
823
- return await handleServiceRequest(params, env, cliPath, config, api);
824
-
825
- case "discover":
826
- return await handleDiscover(params, env, cliPath);
827
-
828
- case "balance":
829
- return await handleBalance(env, cliPath);
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}`);
378
+ case "request": return await handleServiceRequest(params, env, cliPath, config, api);
379
+ case "discover": return await handleDiscover(params, env, cliPath);
380
+ case "balance": return await handleBalance(env, cliPath);
381
+ case "status": return await handleStatus(env, cliPath);
382
+ case "onboard": return await handleOnboard(params, env, cliPath);
383
+ case "pending-requests": return await handlePendingRequests(env, cliPath);
384
+ case "fulfill": return await handleFulfill(params, env, cliPath);
385
+ default: throw new Error(`Unknown action: ${action}`);
890
386
  }
891
387
  }
892
388
 
893
389
  async function handleServiceRequest(params: any, env: any, cliPath: string, config: any, api: any) {
894
- const { service, identityKey: targetKey, input, maxPrice } = params;
895
- const walletDir = config?.walletDir || path.join(process.env.HOME || '', '.openclaw', 'bsv-wallet');
896
-
897
- if (!service) {
898
- throw new Error("Service is required for request action");
899
- }
900
-
901
- // 1. Discover providers for the service
390
+ const { service, identityKey: targetKey, input } = params;
391
+ const walletDir = config?.walletDir || path.join(os.homedir(), '.openclaw', 'bsv-wallet');
902
392
  const discoverResult = await execFileAsync('node', [cliPath, 'discover', '--service', service], { env });
903
393
  const discoverOutput = parseCliOutput(discoverResult.stdout);
904
-
905
- if (!discoverOutput.success) {
906
- throw new Error(`Discovery failed: ${discoverOutput.error}`);
907
- }
908
-
909
394
  const providers = discoverOutput.data.services;
910
- if (!providers || providers.length === 0) {
911
- throw new Error(`No providers found for service: ${service}`);
912
- }
913
-
914
- // 2. Filter out our own identity key
915
- const identityResult = await execFileAsync('node', [cliPath, 'identity'], { env });
916
- const identityOutput = parseCliOutput(identityResult.stdout);
917
- const ourKey = identityOutput.data?.identityKey;
918
-
919
- let externalProviders = providers.filter((p: any) => p.identityKey !== ourKey);
920
- if (externalProviders.length === 0) {
921
- throw new Error("No external providers available (only found our own services)");
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
- };
395
+ if (!providers || providers.length === 0) throw new Error(`No providers found for ${service}`);
396
+ providers.sort((a: any, b: any) => (a.pricing?.amountSats || 0) - (b.pricing?.amountSats || 0));
397
+ const best = providers[0];
398
+ const price = best.pricing?.amountSats || 0;
399
+ const budget = checkBudget(walletDir, price, config.dailyBudgetSats || 5000);
400
+ if (!budget.allowed) throw new Error("Budget exceeded");
401
+ const requestArgs = [cliPath, 'request-service', best.identityKey, service, price.toString()];
402
+ if (input) requestArgs.push(JSON.stringify(input));
403
+ const res = await execFileAsync('node', requestArgs, { env });
404
+ const output = parseCliOutput(res.stdout);
405
+ recordSpend(walletDir, price, service, best.name);
406
+ return { status: "sent", requestId: output.data?.messageId, message: `Request sent to ${best.name} for ${price} sats.` };
1153
407
  }
1154
408
 
1155
409
  async function handleDiscover(params: any, env: any, cliPath: string) {
1156
- const { service, agent } = params;
1157
410
  const args = [cliPath, 'discover'];
1158
-
1159
- if (service) {
1160
- args.push('--service', service);
1161
- }
1162
- if (agent) {
1163
- args.push('--agent', agent);
1164
- }
1165
-
411
+ if (params.service) args.push('--service', params.service);
1166
412
  const result = await execFileAsync('node', args, { env });
1167
- const output = parseCliOutput(result.stdout);
1168
-
1169
- if (!output.success) {
1170
- throw new Error(`Discovery failed: ${output.error}`);
1171
- }
1172
-
1173
- return output.data;
413
+ return parseCliOutput(result.stdout).data;
1174
414
  }
1175
415
 
1176
416
  async function handleBalance(env: any, cliPath: string) {
1177
- try {
1178
- const result = await execFileAsync('node', [cliPath, 'balance'], { env });
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
- }
417
+ const result = await execFileAsync('node', [cliPath, 'balance'], { env });
418
+ return parseCliOutput(result.stdout).data;
1192
419
  }
1193
420
 
1194
421
  async function handleStatus(env: any, cliPath: string) {
1195
- try {
1196
- // Get identity
1197
- let identity: any = { data: { identityKey: 'Not Initialized' } };
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;
422
+ const identity = parseCliOutput((await execFileAsync('node', [cliPath, 'identity'], { env })).stdout);
423
+ const balance = parseCliOutput((await execFileAsync('node', [cliPath, 'balance'], { env })).stdout);
424
+ return { identity: identity.data, balance: balance.data };
1477
425
  }
1478
426
 
1479
427
  async function handleOnboard(params: any, env: any, cliPath: string) {
1480
- const { agentName, agentDescription } = params;
1481
- const steps = [];
1482
-
1483
- const onboardEnv = { ...env };
1484
- if (agentName) onboardEnv.AGENT_NAME = agentName;
1485
- if (agentDescription) onboardEnv.AGENT_DESCRIPTION = agentDescription;
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
- };
428
+ await execFileAsync('node', [cliPath, 'setup'], { env });
429
+ const addr = parseCliOutput((await execFileAsync('node', [cliPath, 'address'], { env })).stdout).data.address;
430
+ const bal = parseCliOutput((await execFileAsync('node', [cliPath, 'balance'], { env })).stdout).data.walletBalance;
431
+ if (bal < 1000) return { funded: false, address: addr, message: "Please fund 1000 sats." };
432
+ await execFileAsync('node', [cliPath, 'register'], { env });
433
+ return { funded: true, registered: true, message: "Onboarding complete." };
1545
434
  }
1546
435
 
1547
436
  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
437
  const result = await execFileAsync('node', [cliPath, 'service-queue'], { env });
1556
- const output = parseCliOutput(result.stdout);
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 };
438
+ return parseCliOutput(result.stdout).data;
1575
439
  }
1576
440
 
1577
441
  async function handleFulfill(params: any, env: any, cliPath: string) {
1578
442
  const { requestId, recipientKey, serviceId, result } = params;
1579
- if (!requestId || !recipientKey || !serviceId || !result) {
1580
- throw new Error("requestId, recipientKey, serviceId, and result are required");
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;
443
+ const res = await execFileAsync('node', [cliPath, 'respond-service', requestId, recipientKey, serviceId, JSON.stringify(result)], { env });
444
+ return parseCliOutput(res.stdout).data;
1594
445
  }
1595
446
 
1596
447
  function buildEnvironment(config: any) {
1597
448
  const env = { ...process.env };
1598
-
1599
- if (config.walletDir) {
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
-
449
+ env.BSV_WALLET_DIR = config.walletDir || path.join(os.homedir(), '.openclaw', 'bsv-wallet');
450
+ env.OVERLAY_URL = config.overlayUrl || 'https://clawoverlay.com';
1614
451
  env.BSV_NETWORK = env.BSV_NETWORK || 'mainnet';
1615
-
1616
- if (!env.BSV_ARC_URL) {
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
-
452
+ env.BSV_ARC_URL = config.arcUrl || (env.BSV_NETWORK === 'testnet' ? 'https://testnet.arc.gorillapool.io' : 'https://arc.gorillapool.io');
453
+ env.AGENT_NAME = config.agentName || 'openclaw-agent';
1633
454
  return env;
1634
455
  }
1635
456