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