openclaw-overlay-plugin 0.7.30 → 0.7.32

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