openclaw-overlay-plugin 0.7.22

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 (221) hide show
  1. package/README.md +406 -0
  2. package/SKILL.md +78 -0
  3. package/clawdbot.plugin.json +106 -0
  4. package/dist/cli-main.d.ts +7 -0
  5. package/dist/cli-main.js +192 -0
  6. package/dist/cli.d.ts +8 -0
  7. package/dist/cli.js +14 -0
  8. package/dist/core/config.d.ts +11 -0
  9. package/dist/core/config.js +13 -0
  10. package/dist/core/index.d.ts +25 -0
  11. package/dist/core/index.js +26 -0
  12. package/dist/core/payment.d.ts +16 -0
  13. package/dist/core/payment.js +94 -0
  14. package/dist/core/types.d.ts +94 -0
  15. package/dist/core/types.js +4 -0
  16. package/dist/core/verify.d.ts +28 -0
  17. package/dist/core/verify.js +104 -0
  18. package/dist/core/wallet.d.ts +99 -0
  19. package/dist/core/wallet.js +219 -0
  20. package/dist/scripts/baemail/commands.d.ts +64 -0
  21. package/dist/scripts/baemail/commands.js +258 -0
  22. package/dist/scripts/baemail/handler.d.ts +36 -0
  23. package/dist/scripts/baemail/handler.js +284 -0
  24. package/dist/scripts/baemail/index.d.ts +5 -0
  25. package/dist/scripts/baemail/index.js +5 -0
  26. package/dist/scripts/config.d.ts +48 -0
  27. package/dist/scripts/config.js +68 -0
  28. package/dist/scripts/index.d.ts +7 -0
  29. package/dist/scripts/index.js +7 -0
  30. package/dist/scripts/messaging/connect.d.ts +8 -0
  31. package/dist/scripts/messaging/connect.js +114 -0
  32. package/dist/scripts/messaging/handlers.d.ts +21 -0
  33. package/dist/scripts/messaging/handlers.js +334 -0
  34. package/dist/scripts/messaging/inbox.d.ts +11 -0
  35. package/dist/scripts/messaging/inbox.js +51 -0
  36. package/dist/scripts/messaging/index.d.ts +8 -0
  37. package/dist/scripts/messaging/index.js +8 -0
  38. package/dist/scripts/messaging/poll.d.ts +7 -0
  39. package/dist/scripts/messaging/poll.js +52 -0
  40. package/dist/scripts/messaging/send.d.ts +7 -0
  41. package/dist/scripts/messaging/send.js +43 -0
  42. package/dist/scripts/output.d.ts +12 -0
  43. package/dist/scripts/output.js +19 -0
  44. package/dist/scripts/overlay/discover.d.ts +7 -0
  45. package/dist/scripts/overlay/discover.js +72 -0
  46. package/dist/scripts/overlay/index.d.ts +7 -0
  47. package/dist/scripts/overlay/index.js +7 -0
  48. package/dist/scripts/overlay/registration.d.ts +19 -0
  49. package/dist/scripts/overlay/registration.js +176 -0
  50. package/dist/scripts/overlay/services.d.ts +29 -0
  51. package/dist/scripts/overlay/services.js +167 -0
  52. package/dist/scripts/overlay/transaction.d.ts +42 -0
  53. package/dist/scripts/overlay/transaction.js +103 -0
  54. package/dist/scripts/payment/build.d.ts +24 -0
  55. package/dist/scripts/payment/build.js +54 -0
  56. package/dist/scripts/payment/commands.d.ts +15 -0
  57. package/dist/scripts/payment/commands.js +73 -0
  58. package/dist/scripts/payment/index.d.ts +6 -0
  59. package/dist/scripts/payment/index.js +6 -0
  60. package/dist/scripts/payment/types.d.ts +56 -0
  61. package/dist/scripts/payment/types.js +4 -0
  62. package/dist/scripts/services/index.d.ts +6 -0
  63. package/dist/scripts/services/index.js +6 -0
  64. package/dist/scripts/services/queue.d.ts +11 -0
  65. package/dist/scripts/services/queue.js +28 -0
  66. package/dist/scripts/services/request.d.ts +7 -0
  67. package/dist/scripts/services/request.js +82 -0
  68. package/dist/scripts/services/respond.d.ts +11 -0
  69. package/dist/scripts/services/respond.js +132 -0
  70. package/dist/scripts/types.d.ts +107 -0
  71. package/dist/scripts/types.js +4 -0
  72. package/dist/scripts/utils/index.d.ts +6 -0
  73. package/dist/scripts/utils/index.js +6 -0
  74. package/dist/scripts/utils/merkle.d.ts +12 -0
  75. package/dist/scripts/utils/merkle.js +47 -0
  76. package/dist/scripts/utils/storage.d.ts +66 -0
  77. package/dist/scripts/utils/storage.js +211 -0
  78. package/dist/scripts/utils/woc.d.ts +26 -0
  79. package/dist/scripts/utils/woc.js +91 -0
  80. package/dist/scripts/wallet/balance.d.ts +22 -0
  81. package/dist/scripts/wallet/balance.js +240 -0
  82. package/dist/scripts/wallet/identity.d.ts +70 -0
  83. package/dist/scripts/wallet/identity.js +151 -0
  84. package/dist/scripts/wallet/index.d.ts +6 -0
  85. package/dist/scripts/wallet/index.js +6 -0
  86. package/dist/scripts/wallet/setup.d.ts +15 -0
  87. package/dist/scripts/wallet/setup.js +105 -0
  88. package/dist/scripts/x-verification/commands.d.ts +27 -0
  89. package/dist/scripts/x-verification/commands.js +222 -0
  90. package/dist/scripts/x-verification/index.d.ts +4 -0
  91. package/dist/scripts/x-verification/index.js +4 -0
  92. package/dist/services/built-in/api-proxy/index.d.ts +6 -0
  93. package/dist/services/built-in/api-proxy/index.js +23 -0
  94. package/dist/services/built-in/code-develop/index.d.ts +6 -0
  95. package/dist/services/built-in/code-develop/index.js +23 -0
  96. package/dist/services/built-in/code-review/index.d.ts +10 -0
  97. package/dist/services/built-in/code-review/index.js +51 -0
  98. package/dist/services/built-in/image-analysis/index.d.ts +6 -0
  99. package/dist/services/built-in/image-analysis/index.js +33 -0
  100. package/dist/services/built-in/memory-store/index.d.ts +6 -0
  101. package/dist/services/built-in/memory-store/index.js +22 -0
  102. package/dist/services/built-in/roulette/index.d.ts +6 -0
  103. package/dist/services/built-in/roulette/index.js +27 -0
  104. package/dist/services/built-in/summarize/index.d.ts +6 -0
  105. package/dist/services/built-in/summarize/index.js +21 -0
  106. package/dist/services/built-in/tell-joke/handler.d.ts +7 -0
  107. package/dist/services/built-in/tell-joke/handler.js +122 -0
  108. package/dist/services/built-in/tell-joke/index.d.ts +9 -0
  109. package/dist/services/built-in/tell-joke/index.js +31 -0
  110. package/dist/services/built-in/translate/index.d.ts +6 -0
  111. package/dist/services/built-in/translate/index.js +21 -0
  112. package/dist/services/built-in/web-research/index.d.ts +9 -0
  113. package/dist/services/built-in/web-research/index.js +51 -0
  114. package/dist/services/index.d.ts +13 -0
  115. package/dist/services/index.js +14 -0
  116. package/dist/services/loader.d.ts +77 -0
  117. package/dist/services/loader.js +292 -0
  118. package/dist/services/manager.d.ts +86 -0
  119. package/dist/services/manager.js +255 -0
  120. package/dist/services/registry.d.ts +98 -0
  121. package/dist/services/registry.js +204 -0
  122. package/dist/services/types.d.ts +230 -0
  123. package/dist/services/types.js +30 -0
  124. package/dist/test/cli.test.d.ts +7 -0
  125. package/dist/test/cli.test.js +329 -0
  126. package/dist/test/comprehensive-overlay.test.d.ts +13 -0
  127. package/dist/test/comprehensive-overlay.test.js +593 -0
  128. package/dist/test/key-derivation.test.d.ts +12 -0
  129. package/dist/test/key-derivation.test.js +86 -0
  130. package/dist/test/overlay-submit.test.d.ts +10 -0
  131. package/dist/test/overlay-submit.test.js +460 -0
  132. package/dist/test/request-response-flow.test.d.ts +5 -0
  133. package/dist/test/request-response-flow.test.js +209 -0
  134. package/dist/test/service-system.test.d.ts +5 -0
  135. package/dist/test/service-system.test.js +190 -0
  136. package/dist/test/utils/server-logic.d.ts +98 -0
  137. package/dist/test/utils/server-logic.js +286 -0
  138. package/dist/test/wallet.test.d.ts +7 -0
  139. package/dist/test/wallet.test.js +146 -0
  140. package/index.ts +1965 -0
  141. package/openclaw.plugin.json +106 -0
  142. package/package.json +73 -0
  143. package/src/cli-main.ts +230 -0
  144. package/src/cli.ts +16 -0
  145. package/src/core/README.md +246 -0
  146. package/src/core/config.ts +21 -0
  147. package/src/core/index.ts +42 -0
  148. package/src/core/payment.ts +111 -0
  149. package/src/core/types.ts +102 -0
  150. package/src/core/verify.ts +119 -0
  151. package/src/core/wallet.ts +282 -0
  152. package/src/scripts/baemail/commands.ts +326 -0
  153. package/src/scripts/baemail/handler.ts +338 -0
  154. package/src/scripts/baemail/index.ts +6 -0
  155. package/src/scripts/config.ts +81 -0
  156. package/src/scripts/index.ts +8 -0
  157. package/src/scripts/messaging/connect.ts +121 -0
  158. package/src/scripts/messaging/handlers.ts +394 -0
  159. package/src/scripts/messaging/inbox.ts +64 -0
  160. package/src/scripts/messaging/index.ts +9 -0
  161. package/src/scripts/messaging/poll.ts +59 -0
  162. package/src/scripts/messaging/send.ts +54 -0
  163. package/src/scripts/output.ts +21 -0
  164. package/src/scripts/overlay/discover.ts +81 -0
  165. package/src/scripts/overlay/index.ts +8 -0
  166. package/src/scripts/overlay/registration.ts +199 -0
  167. package/src/scripts/overlay/services.ts +199 -0
  168. package/src/scripts/overlay/transaction.ts +124 -0
  169. package/src/scripts/payment/build.ts +65 -0
  170. package/src/scripts/payment/commands.ts +92 -0
  171. package/src/scripts/payment/index.ts +7 -0
  172. package/src/scripts/payment/types.ts +62 -0
  173. package/src/scripts/services/index.ts +7 -0
  174. package/src/scripts/services/queue.ts +35 -0
  175. package/src/scripts/services/request.ts +98 -0
  176. package/src/scripts/services/respond.ts +149 -0
  177. package/src/scripts/types.ts +121 -0
  178. package/src/scripts/utils/index.ts +7 -0
  179. package/src/scripts/utils/merkle.ts +57 -0
  180. package/src/scripts/utils/storage.ts +231 -0
  181. package/src/scripts/utils/woc.ts +106 -0
  182. package/src/scripts/wallet/balance.ts +277 -0
  183. package/src/scripts/wallet/identity.ts +203 -0
  184. package/src/scripts/wallet/index.ts +7 -0
  185. package/src/scripts/wallet/setup.ts +121 -0
  186. package/src/scripts/x-verification/commands.ts +256 -0
  187. package/src/scripts/x-verification/index.ts +5 -0
  188. package/src/services/built-in/api-proxy/index.ts +26 -0
  189. package/src/services/built-in/api-proxy/prompt.md +26 -0
  190. package/src/services/built-in/code-develop/index.ts +26 -0
  191. package/src/services/built-in/code-develop/prompt.md +35 -0
  192. package/src/services/built-in/code-review/index.ts +54 -0
  193. package/src/services/built-in/code-review/prompt.md +105 -0
  194. package/src/services/built-in/image-analysis/index.ts +36 -0
  195. package/src/services/built-in/image-analysis/prompt.md +42 -0
  196. package/src/services/built-in/memory-store/index.ts +25 -0
  197. package/src/services/built-in/memory-store/prompt.md +45 -0
  198. package/src/services/built-in/roulette/index.ts +30 -0
  199. package/src/services/built-in/roulette/prompt.md +35 -0
  200. package/src/services/built-in/summarize/index.ts +24 -0
  201. package/src/services/built-in/summarize/prompt.md +27 -0
  202. package/src/services/built-in/tell-joke/handler.ts +134 -0
  203. package/src/services/built-in/tell-joke/index.ts +34 -0
  204. package/src/services/built-in/tell-joke/prompt.md +59 -0
  205. package/src/services/built-in/translate/index.ts +24 -0
  206. package/src/services/built-in/translate/prompt.md +23 -0
  207. package/src/services/built-in/web-research/index.ts +54 -0
  208. package/src/services/built-in/web-research/prompt.md +110 -0
  209. package/src/services/index.ts +16 -0
  210. package/src/services/loader.ts +344 -0
  211. package/src/services/manager.ts +304 -0
  212. package/src/services/registry.ts +246 -0
  213. package/src/services/types.ts +259 -0
  214. package/src/test/cli.test.ts +352 -0
  215. package/src/test/comprehensive-overlay.test.ts +729 -0
  216. package/src/test/key-derivation.test.ts +102 -0
  217. package/src/test/overlay-submit.test.ts +570 -0
  218. package/src/test/request-response-flow.test.ts +252 -0
  219. package/src/test/service-system.test.ts +241 -0
  220. package/src/test/utils/server-logic.ts +368 -0
  221. package/src/test/wallet.test.ts +166 -0
package/index.ts ADDED
@@ -0,0 +1,1965 @@
1
+ import { execFile, spawn, ChildProcess } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import fs from 'fs';
6
+ import { initializeServiceSystem, serviceManager } from './src/services/index.js';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ // Track background process for proper lifecycle management
13
+ let backgroundProcess: ChildProcess | null = null;
14
+ let serviceRunning = false;
15
+
16
+ // Confirmation tokens for destructive actions — maps token → { action, details, expiresAt }
17
+ const pendingConfirmations: Map<string, { action: string; details: any; expiresAt: number }> = new Map();
18
+
19
+ // Auto-import tracking
20
+ let autoImportInterval: any = null;
21
+ let knownTxids: Set<string> = new Set();
22
+
23
+ // Track woken service requests to prevent duplicate processing
24
+ let wokenRequests: Set<string> = new Set();
25
+ let requestCleanupInterval: any = null;
26
+
27
+ // Budget tracking
28
+ const BUDGET_FILE = 'daily-spending.json';
29
+
30
+
31
+ interface DailySpending {
32
+ date: string; // YYYY-MM-DD
33
+ totalSats: number;
34
+ transactions: Array<{ ts: number; sats: number; service: string; provider: string }>;
35
+ }
36
+
37
+ function getBudgetPath(walletDir: string): string {
38
+ return path.join(walletDir, BUDGET_FILE);
39
+ }
40
+
41
+ function loadDailySpending(walletDir: string): DailySpending {
42
+ const today = new Date().toISOString().slice(0, 10);
43
+ const budgetPath = getBudgetPath(walletDir);
44
+ try {
45
+ const data = JSON.parse(fs.readFileSync(budgetPath, 'utf-8'));
46
+ if (data.date === today) return data;
47
+ } catch {
48
+ // Ignore parse errors - return fresh daily spending for corrupted/missing file
49
+ }
50
+ return { date: today, totalSats: 0, transactions: [] };
51
+ }
52
+
53
+ function writeActivityEvent(event) {
54
+ const alertDir = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay');
55
+ try {
56
+ fs.mkdirSync(alertDir, { recursive: true });
57
+ fs.appendFileSync(path.join(alertDir, 'activity-feed.jsonl'), JSON.stringify({ ...event, ts: Date.now() }) + '\n');
58
+ } catch {}
59
+ }
60
+
61
+ function recordSpend(walletDir: string, sats: number, service: string, provider: string) {
62
+ const spending = loadDailySpending(walletDir);
63
+ spending.totalSats += sats;
64
+ spending.transactions.push({ ts: Date.now(), sats, service, provider });
65
+ fs.writeFileSync(getBudgetPath(walletDir), JSON.stringify(spending, null, 2));
66
+ }
67
+
68
+ function checkBudget(walletDir: string, requestedSats: number, dailyLimit: number): { allowed: boolean; remaining: number; spent: number } {
69
+ const spending = loadDailySpending(walletDir);
70
+ const remaining = dailyLimit - spending.totalSats;
71
+ return {
72
+ allowed: remaining >= requestedSats,
73
+ remaining,
74
+ spent: spending.totalSats
75
+ };
76
+ }
77
+
78
+ async function startAutoImport(env, cliPath, logger) {
79
+ // Get our address
80
+ try {
81
+ const addrResult = await execFileAsync('node', [cliPath, 'address'], { env });
82
+ const addrOutput = parseCliOutput(addrResult.stdout);
83
+ if (!addrOutput.success) return;
84
+ const address = addrOutput.data?.address;
85
+ if (!address) return;
86
+
87
+ // Load known txids from wallet state
88
+ const balResult = await execFileAsync('node', [cliPath, 'balance'], { env });
89
+ const balOutput = parseCliOutput(balResult.stdout);
90
+ // Track what we already have
91
+
92
+ autoImportInterval = setInterval(async () => {
93
+ try {
94
+ const network = env.BSV_NETWORK === 'testnet' ? 'test' : 'main';
95
+ const controller = new AbortController();
96
+ const timeout = setTimeout(() => controller.abort(), 15000);
97
+ const resp = await fetch(`https://api.whatsonchain.com/v1/bsv/${network}/address/${address}/unspent`, { signal: controller.signal });
98
+ clearTimeout(timeout);
99
+ if (!resp.ok) return;
100
+ const utxos = await resp.json();
101
+
102
+ for (const utxo of utxos) {
103
+ const key = `${utxo.tx_hash}:${utxo.tx_pos}`;
104
+ if (knownTxids.has(key)) continue;
105
+ if (utxo.value < 200) continue; // skip dust
106
+
107
+ logger?.info?.(`[bsv-overlay] Auto-importing UTXO: ${utxo.tx_hash}:${utxo.tx_pos} (${utxo.value} sats)`);
108
+ try {
109
+ const importResult = await execFileAsync('node', [cliPath, 'import', utxo.tx_hash, String(utxo.tx_pos)], { env });
110
+ const importOutput = parseCliOutput(importResult.stdout);
111
+ if (importOutput.success) {
112
+ knownTxids.add(key);
113
+ logger?.info?.(`[bsv-overlay] Auto-imported ${utxo.value} sats from ${utxo.tx_hash}`);
114
+
115
+ // Clear onboarding flag since wallet is now funded
116
+ try {
117
+ const onboardingSentFile = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay', 'onboarding-sent.flag');
118
+ if (fs.existsSync(onboardingSentFile)) {
119
+ fs.unlinkSync(onboardingSentFile);
120
+ }
121
+ } catch {}
122
+
123
+ // Notify agent of successful import
124
+ 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' });
125
+
126
+ // Check if registered, auto-register if not
127
+ try {
128
+ const regPath = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay', 'registration.json');
129
+ if (!fs.existsSync(regPath)) {
130
+ logger?.info?.('[bsv-overlay] Not yet registered — auto-registering...');
131
+ const regResult = await execFileAsync('node', [cliPath, 'register'], { env, timeout: 60000 });
132
+ const regOutput = parseCliOutput(regResult.stdout);
133
+ if (regOutput.success) {
134
+ logger?.info?.('[bsv-overlay] Auto-registered on overlay network!');
135
+
136
+ // Auto-advertise services from config
137
+ await autoAdvertiseServices(env, cliPath, logger);
138
+ }
139
+ }
140
+ } catch (err) {
141
+ logger?.warn?.('[bsv-overlay] Auto-registration failed:', err.message);
142
+ }
143
+ }
144
+ } catch (err) {
145
+ // Already imported or error — track it so we don't retry
146
+ knownTxids.add(key);
147
+ }
148
+ }
149
+ } catch (err) {
150
+ // WoC API error — just skip this cycle
151
+ }
152
+ }, 30000); // Check every 30 seconds for faster onboarding
153
+ } catch (err) {
154
+ logger?.warn?.('[bsv-overlay] Auto-import setup failed:', err.message);
155
+ }
156
+ }
157
+
158
+ function stopAutoImport() {
159
+ if (autoImportInterval) {
160
+ clearInterval(autoImportInterval);
161
+ autoImportInterval = null;
162
+ }
163
+ }
164
+
165
+ // Auto-advertise services from config after registration
166
+ async function autoAdvertiseServices(env, cliPath, logger) {
167
+ try {
168
+ // Read config to get services list
169
+ const configPaths = [
170
+ path.join(process.env.HOME || '', '.openclaw', 'openclaw.json'),
171
+ path.join(process.env.HOME || '', '.clawdbot', 'clawdbot.json'),
172
+ ];
173
+
174
+ let servicesToAdvertise: string[] = [];
175
+
176
+ for (const configPath of configPaths) {
177
+ if (!fs.existsSync(configPath)) continue;
178
+ try {
179
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
180
+ const pluginConfig = config?.plugins?.entries?.['bsv-overlay']?.config;
181
+ if (pluginConfig?.services && Array.isArray(pluginConfig.services)) {
182
+ servicesToAdvertise = pluginConfig.services;
183
+ break;
184
+ }
185
+ } catch {}
186
+ }
187
+
188
+ if (servicesToAdvertise.length === 0) {
189
+ logger?.info?.('[bsv-overlay] No services configured for auto-advertising');
190
+ return;
191
+ }
192
+
193
+ logger?.info?.(`[bsv-overlay] Auto-advertising ${servicesToAdvertise.length} services from config...`);
194
+
195
+ const advertised: string[] = [];
196
+ const failed: string[] = [];
197
+
198
+ for (const serviceId of servicesToAdvertise) {
199
+ const serviceInfo = serviceManager.registry.get(serviceId);
200
+ if (!serviceInfo) {
201
+ logger?.warn?.(`[bsv-overlay] Unknown service ID: ${serviceId}. Skipping.`);
202
+ failed.push(serviceId);
203
+ continue;
204
+ }
205
+
206
+ try {
207
+ const result = await execFileAsync('node', [
208
+ cliPath, 'advertise',
209
+ serviceId,
210
+ serviceInfo.name,
211
+ serviceInfo.defaultPrice.toString(),
212
+ serviceInfo.description
213
+ ], { env, timeout: 60000 });
214
+
215
+ const output = parseCliOutput(result.stdout);
216
+ if (output.success) {
217
+ advertised.push(serviceId);
218
+ logger?.info?.(`[bsv-overlay] Advertised service: ${serviceInfo.name} (${serviceId}) for ${serviceInfo.defaultPrice} sats`);
219
+ } else {
220
+ failed.push(serviceId);
221
+ logger?.warn?.(`[bsv-overlay] Failed to advertise ${serviceId}: ${output.error}`);
222
+ }
223
+ } catch (err: any) {
224
+ failed.push(serviceId);
225
+ logger?.warn?.(`[bsv-overlay] Error advertising ${serviceId}: ${err.message}`);
226
+ }
227
+ }
228
+
229
+ // Wake agent with results
230
+ if (advertised.length > 0) {
231
+ const serviceList = advertised.map(id => {
232
+ const info = serviceManager.registry.get(id);
233
+ return `• ${info?.name || id} (${info?.defaultPrice || '?'} sats)`;
234
+ }).join('\n');
235
+
236
+ wakeAgent(
237
+ `🎉 **Services Auto-Advertised!**\n\nThe following services are now live on the overlay network:\n\n${serviceList}\n\n${failed.length > 0 ? `⚠️ Failed to advertise: ${failed.join(', ')}` : ''}`,
238
+ logger,
239
+ { sessionKey: 'hook:bsv-overlay:services' }
240
+ );
241
+ }
242
+ } catch (err: any) {
243
+ logger?.warn?.(`[bsv-overlay] Auto-advertise failed: ${err.message}`);
244
+ }
245
+ }
246
+
247
+ // Auto-enable hooks in Clawdbot config if not already configured.
248
+ // Returns true if config was modified (gateway restart needed to activate).
249
+ function autoEnableHooks(api: any): boolean {
250
+ try {
251
+ const configPaths = [
252
+ path.join(process.env.HOME || '', '.clawdbot', 'clawdbot.json'),
253
+ path.join(process.env.HOME || '', '.openclaw', 'openclaw.json'),
254
+ ];
255
+
256
+ for (const configPath of configPaths) {
257
+ if (!fs.existsSync(configPath)) continue;
258
+
259
+ const raw = fs.readFileSync(configPath, 'utf-8');
260
+ const config = JSON.parse(raw);
261
+
262
+ // Check if hooks are already enabled with a token
263
+ if (config?.hooks?.enabled && config?.hooks?.token) {
264
+ api?.log?.debug?.('[bsv-overlay] Hooks already configured.');
265
+ return false;
266
+ }
267
+
268
+ // Generate a random token
269
+ const tokenBytes = new Uint8Array(24);
270
+ for (let i = 0; i < 24; i++) tokenBytes[i] = Math.floor(Math.random() * 256);
271
+ const token = Array.from(tokenBytes).map(b => b.toString(16).padStart(2, '0')).join('');
272
+
273
+ // Merge hooks config — preserve existing hooks.internal etc.
274
+ config.hooks = {
275
+ ...config.hooks,
276
+ enabled: true,
277
+ token: config.hooks?.token || token,
278
+ };
279
+
280
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
281
+ api?.log?.info?.(`[bsv-overlay] Auto-enabled hooks in config (${configPath}). Gateway restart needed to activate.`);
282
+ return true;
283
+ }
284
+ } catch (err: any) {
285
+ api?.log?.warn?.(`[bsv-overlay] Failed to auto-enable hooks: ${err.message}`);
286
+ }
287
+ return false;
288
+ }
289
+
290
+ // Discover the gateway HTTP port from environment
291
+ function getGatewayPort(): string {
292
+ return process.env.CLAWDBOT_GATEWAY_PORT || process.env.OPENCLAW_GATEWAY_PORT || '18789';
293
+ }
294
+
295
+ // Read tokens from env vars or config files.
296
+ // Returns { hooksToken, gatewayToken } — hooksToken is preferred for HTTP wake.
297
+ function getTokens(): { hooksToken: string | null; gatewayToken: string | null } {
298
+ let hooksToken: string | null = process.env.CLAWDBOT_HOOKS_TOKEN || process.env.OPENCLAW_HOOKS_TOKEN || null;
299
+ let gatewayToken: string | null = process.env.CLAWDBOT_GATEWAY_TOKEN || process.env.OPENCLAW_GATEWAY_TOKEN || null;
300
+
301
+ try {
302
+ const configPaths = [
303
+ path.join(process.env.HOME || '', '.openclaw', 'openclaw.json'),
304
+ path.join(process.env.HOME || '', '.clawdbot', 'clawdbot.json'),
305
+ ];
306
+ for (const p of configPaths) {
307
+ if (!fs.existsSync(p)) continue;
308
+ const config = JSON.parse(fs.readFileSync(p, 'utf-8'));
309
+ if (!hooksToken) hooksToken = config?.hooks?.token || null;
310
+ if (!gatewayToken) gatewayToken = config?.gateway?.auth?.token || null;
311
+ if (hooksToken && gatewayToken) break;
312
+ }
313
+ } catch {}
314
+ return { hooksToken, gatewayToken };
315
+ }
316
+
317
+ // Wake the agent via /hooks/agent — runs an isolated agent turn with the
318
+ // message as the actual prompt, so the agent sees the request and can act.
319
+ // NOTE: /hooks/wake only triggers a heartbeat (reads HEARTBEAT.md) which
320
+ // won't surface the overlay request to the agent. /hooks/agent is required.
321
+ function wakeAgent(text: string, logger?: any, opts?: { sessionKey?: string }) {
322
+ const { hooksToken, gatewayToken } = getTokens();
323
+ const port = getGatewayPort();
324
+ const httpToken = hooksToken || gatewayToken;
325
+
326
+ if (!httpToken) {
327
+ logger?.warn?.('[bsv-overlay] No gateway/hooks token available — cannot invoke agent');
328
+ return;
329
+ }
330
+
331
+ const url = `http://127.0.0.1:${port}/hooks/agent`;
332
+ const sessionKey = opts?.sessionKey || `hook:bsv-overlay:${Date.now()}`;
333
+
334
+ fetch(url, {
335
+ method: 'POST',
336
+ headers: {
337
+ 'Content-Type': 'application/json',
338
+ 'Authorization': `Bearer ${httpToken}`,
339
+ 'x-clawdbot-token': httpToken,
340
+ },
341
+ body: JSON.stringify({
342
+ message: text,
343
+ name: 'BSV Overlay',
344
+ sessionKey,
345
+ wakeMode: 'now',
346
+ deliver: true,
347
+ channel: 'last',
348
+ }),
349
+ })
350
+ .then(async (res) => {
351
+ if (res.ok) {
352
+ logger?.info?.(`[bsv-overlay] Agent invoked via /hooks/agent (session: ${sessionKey})`);
353
+ } else {
354
+ const body = await res.text().catch(() => '');
355
+ logger?.warn?.(`[bsv-overlay] /hooks/agent failed: ${res.status} ${body}`);
356
+ }
357
+ })
358
+ .catch((err) => {
359
+ logger?.warn?.('[bsv-overlay] /hooks/agent error:', err.message);
360
+ });
361
+ }
362
+
363
+ // NOTE: WebSocket wake fallback removed — it used cron.wake which triggers
364
+ // a heartbeat (same problem as /hooks/wake). /hooks/agent is the correct
365
+ // approach for invoking the agent with a specific prompt.
366
+
367
+ // Categorize WebSocket events into notification types
368
+ function categorizeEvent(event) {
369
+ const base = { ts: Date.now(), from: event.from?.slice(0, 16), fullFrom: event.from };
370
+
371
+ // 💰 Incoming payment — someone paid us for a service
372
+ if (event.action === 'queued-for-agent' && event.satoshisReceived) {
373
+ return { ...base, type: 'incoming_payment', emoji: '💰', serviceId: event.serviceId, sats: event.satoshisReceived, requestId: event.id, message: `Received ${event.satoshisReceived} sats for ${event.serviceId}` };
374
+ }
375
+ if (event.action === 'fulfilled' && event.satoshisReceived) {
376
+ return { ...base, type: 'incoming_payment', emoji: '💰', serviceId: event.serviceId, sats: event.satoshisReceived, message: `Received ${event.satoshisReceived} sats for ${event.serviceId} (auto-fulfilled)` };
377
+ }
378
+
379
+ // 📬 Response received — a service we requested came back
380
+ // Fields come directly from the CLI event, not nested under .payload
381
+ if (event.type === 'service-response' && event.action === 'received') {
382
+ return {
383
+ ...base, type: 'response_received', emoji: '📬',
384
+ serviceId: event.serviceId, status: event.status,
385
+ result: event.result, requestId: event.requestId,
386
+ formatted: event.formatted,
387
+ message: event.formatted || `Response received for ${event.serviceId}: ${event.status}`,
388
+ };
389
+ }
390
+
391
+ // ❌ Request rejected
392
+ if (event.action === 'rejected' && event.serviceId) {
393
+ return { ...base, type: 'request_rejected', emoji: '❌', serviceId: event.serviceId, reason: event.reason, message: `Rejected ${event.serviceId} request: ${event.reason}` };
394
+ }
395
+
396
+ // Skip pings/pongs and other noise
397
+ return null;
398
+ }
399
+
400
+ function startBackgroundService(env, cliPath, logger) {
401
+ if (backgroundProcess) return;
402
+ serviceRunning = true;
403
+
404
+ // Clean up old request IDs every 5 minutes to prevent memory bloat
405
+ requestCleanupInterval = setInterval(async () => {
406
+ if (serviceRunning) {
407
+ wokenRequests.clear();
408
+ logger?.debug?.('[bsv-overlay] Cleared stale request IDs');
409
+
410
+ // Also clean up old queue entries
411
+ try {
412
+ const { cleanupServiceQueue } = await import('./src/scripts/utils/storage.js');
413
+ cleanupServiceQueue();
414
+ logger?.debug?.('[bsv-overlay] Cleaned up old queue entries');
415
+ } catch (err) {
416
+ logger?.warn?.('[bsv-overlay] Queue cleanup failed:', err.message);
417
+ }
418
+ }
419
+ }, 5 * 60 * 1000);
420
+
421
+ function spawnConnect() {
422
+ if (!serviceRunning) return;
423
+
424
+ const proc = spawn('node', [cliPath, 'connect'], {
425
+ env,
426
+ stdio: ['ignore', 'pipe', 'pipe']
427
+ });
428
+
429
+ backgroundProcess = proc;
430
+
431
+ proc.stdout?.on('data', (data) => {
432
+ const lines = data.toString().split('\n').filter(Boolean);
433
+ for (const line of lines) {
434
+ try {
435
+ const event = JSON.parse(line);
436
+ logger?.debug?.(`[bsv-overlay] ${event.event || event.type || 'message'}:`, JSON.stringify(event).slice(0, 200));
437
+
438
+ const alertDir = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay');
439
+ fs.mkdirSync(alertDir, { recursive: true });
440
+
441
+ // Detect queued-for-agent events — invoke agent via /hooks/agent
442
+ // This is the PROVIDER side: someone requested our service
443
+ if ((event.action === 'queued-for-agent' || event.action === 'already-queued') && event.serviceId) {
444
+ const requestId = event.id || `${event.from}-${Date.now()}`;
445
+
446
+ // Check if already woken to prevent duplicate processing
447
+ if (wokenRequests.has(requestId)) {
448
+ logger?.debug?.(`[bsv-overlay] Request ${requestId} already woken, skipping duplicate`);
449
+ return;
450
+ }
451
+
452
+ // Skip wake-up for already processed requests unless they're pending
453
+ if (event.action?.startsWith('already-') && !event.action.includes('pending')) {
454
+ logger?.debug?.(`[bsv-overlay] Request ${requestId} already processed (${event.action}), skipping`);
455
+ return;
456
+ }
457
+
458
+ wokenRequests.add(requestId);
459
+ logger?.info?.(`[bsv-overlay] ⚡ Incoming ${event.serviceId} request from ${event.from?.slice(0, 12)}...`);
460
+ 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: { ... } })`;
461
+ wakeAgent(wakeText, logger, { sessionKey: `hook:bsv-overlay:${event.id || Date.now()}` });
462
+ }
463
+
464
+ // Detect service-response events — invoke agent to notify user
465
+ // This is the REQUESTER side: we requested a service, response came back
466
+ if (event.type === 'service-response' && event.action === 'received') {
467
+ const svcId = event.serviceId || 'unknown';
468
+ const status = event.status || 'unknown';
469
+ const from = event.from || 'unknown';
470
+ const formatted = event.formatted || '';
471
+ const resultJson = event.result ? JSON.stringify(event.result, null, 2) : '(no result data)';
472
+
473
+ logger?.info?.(`[bsv-overlay] 📬 Response received for ${svcId} from ${from?.slice(0, 12)}... — status: ${status}`);
474
+ 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.`;
475
+ wakeAgent(wakeText, logger, { sessionKey: `hook:bsv-overlay:resp-${event.requestId || Date.now()}` });
476
+ }
477
+
478
+ // Write payment/activity notifications for ALL significant events
479
+ const notifEvent = categorizeEvent(event);
480
+ if (notifEvent) {
481
+ try {
482
+ fs.appendFileSync(path.join(alertDir, 'activity-feed.jsonl'), JSON.stringify(notifEvent) + '\n');
483
+ } catch {}
484
+ }
485
+ } catch {}
486
+ }
487
+ });
488
+
489
+ proc.stderr?.on('data', (data) => {
490
+ const lines = data.toString().split('\n').filter(Boolean);
491
+ for (const line of lines) {
492
+ try {
493
+ const event = JSON.parse(line);
494
+ if (event.event === 'connected') {
495
+ logger?.info?.('[bsv-overlay] WebSocket relay connected');
496
+ } else if (event.event === 'disconnected') {
497
+ logger?.warn?.('[bsv-overlay] WebSocket disconnected, reconnecting...');
498
+ }
499
+ } catch {
500
+ logger?.debug?.(`[bsv-overlay] ${line}`);
501
+ }
502
+ }
503
+ });
504
+
505
+ proc.on('exit', (code) => {
506
+ backgroundProcess = null;
507
+ if (serviceRunning) {
508
+ logger?.warn?.(`[bsv-overlay] Background service exited (code ${code}), restarting in 5s...`);
509
+ setTimeout(spawnConnect, 5000);
510
+ }
511
+ });
512
+ }
513
+
514
+ spawnConnect();
515
+ }
516
+
517
+ function stopBackgroundService() {
518
+ serviceRunning = false;
519
+ if (backgroundProcess) {
520
+ backgroundProcess.kill('SIGTERM');
521
+ backgroundProcess = null;
522
+ }
523
+ if (requestCleanupInterval) {
524
+ clearInterval(requestCleanupInterval);
525
+ requestCleanupInterval = null;
526
+ }
527
+ // Clear any remaining request IDs
528
+ wokenRequests.clear();
529
+ stopAutoImport();
530
+ }
531
+
532
+ export default function register(api) {
533
+ // Capture config at registration time (api.getConfig may not be available later)
534
+ const pluginConfig = api.getConfig?.()?.plugins?.entries?.['bsv-overlay']?.config || api.config || {};
535
+
536
+ // Register the overlay agent tool
537
+ api.registerTool({
538
+ name: "overlay",
539
+ description: "Access the BSV agent marketplace - discover agents and exchange BSV micropayments for services",
540
+ parameters: {
541
+ type: "object",
542
+ properties: {
543
+ action: {
544
+ type: "string",
545
+ enum: [
546
+ "request", "discover", "balance", "status", "pay",
547
+ "setup", "address", "import", "register", "advertise",
548
+ "readvertise", "remove", "send", "inbox", "services", "refund",
549
+ "onboard", "pending-requests", "fulfill",
550
+ "unregister", "remove-service"
551
+ ],
552
+ description: "Action to perform"
553
+ },
554
+ service: {
555
+ type: "string",
556
+ description: "Service ID for request/discover"
557
+ },
558
+ input: {
559
+ type: "object",
560
+ description: "Service-specific input data"
561
+ },
562
+ maxPrice: {
563
+ type: "number",
564
+ description: "Max sats willing to pay"
565
+ },
566
+ identityKey: {
567
+ type: "string",
568
+ description: "Target agent key for direct pay/send"
569
+ },
570
+ sats: {
571
+ type: "number",
572
+ description: "Amount for direct pay"
573
+ },
574
+ description: {
575
+ type: "string"
576
+ },
577
+ agent: {
578
+ type: "string",
579
+ description: "Agent name filter for discover"
580
+ },
581
+ // Import parameters
582
+ txid: {
583
+ type: "string",
584
+ description: "Transaction ID for import"
585
+ },
586
+ vout: {
587
+ type: "number",
588
+ description: "Output index for import (optional)"
589
+ },
590
+ // Service management parameters
591
+ serviceId: {
592
+ type: "string",
593
+ description: "Service ID for advertise/readvertise/remove"
594
+ },
595
+ name: {
596
+ type: "string",
597
+ description: "Service name for advertise/readvertise"
598
+ },
599
+ priceSats: {
600
+ type: "number",
601
+ description: "Price in satoshis for advertise"
602
+ },
603
+ newPrice: {
604
+ type: "number",
605
+ description: "New price for readvertise"
606
+ },
607
+ newName: {
608
+ type: "string",
609
+ description: "New name for readvertise (optional)"
610
+ },
611
+ newDesc: {
612
+ type: "string",
613
+ description: "New description for readvertise (optional)"
614
+ },
615
+ // Messaging parameters
616
+ messageType: {
617
+ type: "string",
618
+ description: "Message type for send"
619
+ },
620
+ payload: {
621
+ type: "object",
622
+ description: "Message payload for send"
623
+ },
624
+ // Refund parameters
625
+ address: {
626
+ type: "string",
627
+ description: "Destination address for refund"
628
+ },
629
+ // Confirmation token for destructive actions (unregister, remove-service)
630
+ confirmToken: {
631
+ type: "string",
632
+ description: "Confirmation token from a previous preview call — required to execute destructive actions"
633
+ },
634
+ // Fulfill parameters
635
+ requestId: {
636
+ type: "string",
637
+ description: "Request ID for fulfill"
638
+ },
639
+ recipientKey: {
640
+ type: "string",
641
+ description: "Recipient identity key for fulfill"
642
+ },
643
+ result: {
644
+ type: "object",
645
+ description: "Service result for fulfill"
646
+ },
647
+ // Onboard parameters
648
+ agentName: {
649
+ type: "string",
650
+ description: "Agent display name for onboard/register"
651
+ },
652
+ agentDescription: {
653
+ type: "string",
654
+ description: "Agent description for onboard/register"
655
+ }
656
+ },
657
+ required: ["action"]
658
+ },
659
+ async execute(id, params) {
660
+ const config = pluginConfig;
661
+
662
+ try {
663
+ const result = await executeOverlayAction(params, config, api);
664
+ return {
665
+ content: [{
666
+ type: "text",
667
+ text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
668
+ }]
669
+ };
670
+ } catch (error) {
671
+ return {
672
+ content: [{
673
+ type: "text",
674
+ text: `Error: ${error.message}`
675
+ }]
676
+ };
677
+ }
678
+ }
679
+ });
680
+
681
+ // Register background service for WebSocket relay
682
+ api.registerService({
683
+ id: "bsv-overlay-relay",
684
+ start: async () => {
685
+ api.logger.info("Starting BSV overlay WebSocket relay...");
686
+ try {
687
+ const config = pluginConfig;
688
+ const env = buildEnvironment(config);
689
+ const cliPath = path.join(__dirname, 'dist', 'cli.js');
690
+
691
+ // Use the improved background service
692
+ startBackgroundService(env, cliPath, api.logger);
693
+
694
+ // Start auto-import
695
+ startAutoImport(env, cliPath, api.logger);
696
+
697
+ api.logger.info("BSV overlay WebSocket relay started");
698
+ } catch (error) {
699
+ api.logger.error(`Failed to start BSV overlay relay: ${error.message}`);
700
+ }
701
+ },
702
+ stop: async () => {
703
+ api.logger.info("Stopping BSV overlay WebSocket relay...");
704
+ stopBackgroundService();
705
+ api.logger.info("BSV overlay WebSocket relay stopped");
706
+ }
707
+ });
708
+
709
+ // Register /overlay auto-reply command for instant status
710
+ api.registerCommand?.({
711
+ name: 'overlay',
712
+ description: 'Check BSV Overlay Network status instantly',
713
+ handler: async (ctx) => {
714
+ try {
715
+ const config = pluginConfig;
716
+ const env = buildEnvironment(config);
717
+ const cliPath = path.join(__dirname, 'dist', 'cli.js');
718
+
719
+ // Check registration status
720
+ const regPath = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay', 'registration.json');
721
+ const isRegistered = fs.existsSync(regPath);
722
+
723
+ // Get balance
724
+ let balance = 0;
725
+ let address = '';
726
+ try {
727
+ const balResult = await execFileAsync('node', [cliPath, 'balance'], { env, timeout: 15000 });
728
+ const balOutput = parseCliOutput(balResult.stdout);
729
+ balance = balOutput?.data?.walletBalance || 0;
730
+ } catch {}
731
+
732
+ try {
733
+ const addrResult = await execFileAsync('node', [cliPath, 'address'], { env, timeout: 15000 });
734
+ const addrOutput = parseCliOutput(addrResult.stdout);
735
+ address = addrOutput?.data?.address || '';
736
+ } catch {}
737
+
738
+ // Get services count
739
+ let servicesCount = 0;
740
+ try {
741
+ const svcResult = await execFileAsync('node', [cliPath, 'services'], { env, timeout: 15000 });
742
+ const svcOutput = parseCliOutput(svcResult.stdout);
743
+ servicesCount = svcOutput?.data?.count || 0;
744
+ } catch {}
745
+
746
+ // Build status message
747
+ let text = '**BSV Overlay Status**\n\n';
748
+
749
+ if (isRegistered) {
750
+ const reg = JSON.parse(fs.readFileSync(regPath, 'utf-8'));
751
+ text += `✅ **Registered** as ${reg.agentName || 'Agent'}\n`;
752
+ text += `💰 **Balance:** ${balance.toLocaleString()} sats\n`;
753
+ text += `📋 **Services:** ${servicesCount} advertised\n`;
754
+ text += `🌐 **Network:** ${config?.overlayUrl || 'https://clawoverlay.com'}`;
755
+ } else if (balance >= 1000) {
756
+ text += `💰 **Funded** (${balance.toLocaleString()} sats)\n`;
757
+ text += `⏳ Registering on next cycle...\n`;
758
+ text += `\nRun \`overlay({ action: "register" })\` to register now.`;
759
+ } else {
760
+ text += `❌ **Not Registered**\n\n`;
761
+ text += `📬 Fund this address to join:\n\`${address}\`\n\n`;
762
+ text += `💰 Need: 1,000+ sats (~$0.05)`;
763
+ }
764
+
765
+ return { text };
766
+ } catch (err: any) {
767
+ return { text: `❌ Error checking status: ${err.message}` };
768
+ }
769
+ }
770
+ });
771
+
772
+ // Register CLI commands
773
+ api.registerCli(({ program }) => {
774
+ const overlay = program.command("overlay").description("BSV Overlay Network commands");
775
+
776
+ overlay.command("status")
777
+ .description("Show identity, balance, registration, and services")
778
+ .action(async () => {
779
+ try {
780
+ const config = pluginConfig;
781
+ const result = await handleStatus(buildEnvironment(config), path.join(__dirname, 'dist', 'cli.js'));
782
+ console.log("BSV Overlay Status:");
783
+ console.log("Identity:", result.identity);
784
+ console.log("Balance:", result.balance);
785
+ console.log("Services:", result.services);
786
+ } catch (error) {
787
+ console.error("Error:", error.message);
788
+ }
789
+ });
790
+
791
+ overlay.command("balance")
792
+ .description("Show wallet balance")
793
+ .action(async () => {
794
+ try {
795
+ const config = pluginConfig;
796
+ const result = await handleBalance(buildEnvironment(config), path.join(__dirname, 'dist', 'cli.js'));
797
+ console.log("Balance:", result);
798
+ } catch (error) {
799
+ console.error("Error:", error.message);
800
+ }
801
+ });
802
+
803
+ overlay.command("address")
804
+ .description("Show receive address")
805
+ .action(async () => {
806
+ try {
807
+ const config = pluginConfig;
808
+ const result = await handleAddress(buildEnvironment(config), path.join(__dirname, 'dist', 'cli.js'));
809
+ console.log("Address:", result);
810
+ } catch (error) {
811
+ console.error("Error:", error.message);
812
+ }
813
+ });
814
+
815
+ overlay.command("discover")
816
+ .description("List agents and services on the network")
817
+ .option("--service <type>", "Filter by service type")
818
+ .option("--agent <name>", "Filter by agent name")
819
+ .action(async (options) => {
820
+ try {
821
+ const config = pluginConfig;
822
+ const result = await handleDiscover(options, buildEnvironment(config), path.join(__dirname, 'dist', 'cli.js'));
823
+ console.log("Discovery results:");
824
+ console.log(`Overlay URL: ${result.overlayUrl}`);
825
+ console.log(`Agents: ${result.agentCount}, Services: ${result.serviceCount}`);
826
+ if (result.agents?.length > 0) {
827
+ console.log("\nAgents:");
828
+ result.agents.forEach(agent => {
829
+ console.log(` ${agent.agentName} (${agent.identityKey})`);
830
+ });
831
+ }
832
+ if (result.services?.length > 0) {
833
+ console.log("\nServices:");
834
+ result.services.forEach(service => {
835
+ console.log(` ${service.serviceId} - ${service.name} (${service.pricing?.amountSats || 0} sats) by ${service.agentName}`);
836
+ });
837
+ }
838
+ } catch (error) {
839
+ console.error("Error:", error.message);
840
+ }
841
+ });
842
+
843
+ overlay.command("services")
844
+ .description("List our advertised services")
845
+ .action(async () => {
846
+ try {
847
+ const config = pluginConfig;
848
+ const result = await handleServices(buildEnvironment(config), path.join(__dirname, 'dist', 'cli.js'));
849
+ console.log("Our services:", result);
850
+ } catch (error) {
851
+ console.error("Error:", error.message);
852
+ }
853
+ });
854
+
855
+ overlay.command("setup")
856
+ .description("Run initial wallet setup")
857
+ .action(async () => {
858
+ try {
859
+ const config = pluginConfig;
860
+ const env = buildEnvironment(config);
861
+ const cliPath = path.join(__dirname, 'dist', 'cli.js');
862
+
863
+ const result = await execFileAsync('node', [cliPath, 'setup'], { env });
864
+ const output = parseCliOutput(result.stdout);
865
+ console.log("Setup result:", output);
866
+ } catch (error) {
867
+ console.error("Error:", error.message);
868
+ }
869
+ });
870
+
871
+ overlay.command("register")
872
+ .description("Register with the overlay network")
873
+ .action(async () => {
874
+ try {
875
+ const config = pluginConfig;
876
+ const env = buildEnvironment(config);
877
+ const cliPath = path.join(__dirname, 'dist', 'cli.js');
878
+
879
+ const result = await execFileAsync('node', [cliPath, 'register'], { env });
880
+ const output = parseCliOutput(result.stdout);
881
+ console.log("Registration result:", output);
882
+ } catch (error) {
883
+ console.error("Error:", error.message);
884
+ }
885
+ });
886
+
887
+ overlay.command("wizard")
888
+ .description("Interactive setup wizard for BSV Overlay Network")
889
+ .action(async () => {
890
+ const readline = await import('readline');
891
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
892
+
893
+ const prompt = (question: string): Promise<string> =>
894
+ new Promise(resolve => rl.question(question, resolve));
895
+
896
+ console.log('\n🔌 BSV Overlay Network — Setup Wizard\n');
897
+ console.log('This wizard will help you configure and join the overlay network.\n');
898
+
899
+ try {
900
+ const config = pluginConfig;
901
+ const env = buildEnvironment(config);
902
+ const cliPath = path.join(__dirname, 'dist', 'cli.js');
903
+
904
+ // Step 1: Agent Identity
905
+ console.log('─'.repeat(50));
906
+ console.log('Step 1: Agent Identity\n');
907
+ console.log('Your agent identity is how other agents will see you on the network.\n');
908
+
909
+ const currentName = config?.agentName || env.AGENT_NAME || 'clawdbot-agent';
910
+ const agentName = await prompt(`Agent name [${currentName}]: `) || currentName;
911
+
912
+ const currentDesc = config?.agentDescription || env.AGENT_DESCRIPTION || 'AI agent on the OpenClaw Overlay Network.';
913
+ console.log('\nDescribe what your agent does (1-2 sentences):');
914
+ const agentDescription = await prompt(`Description [${currentDesc}]: `) || currentDesc;
915
+
916
+ // Step 2: Service Selection
917
+ console.log('\n' + '─'.repeat(50));
918
+ console.log('Step 2: Services to Offer\n');
919
+ console.log('Available services:');
920
+ const availableServices = serviceManager.getAvailableServices();
921
+ availableServices.forEach((svc, i) => {
922
+ console.log(` ${i + 1}. ${svc.name} (${svc.defaultPrice} sats) - ${svc.id}`);
923
+ });
924
+ console.log('\nEnter service numbers separated by commas (e.g., 1,2,5)');
925
+ console.log('Or press Enter to skip service selection.\n');
926
+ const serviceInput = await prompt('Services to advertise: ');
927
+
928
+ const selectedServices: string[] = [];
929
+ if (serviceInput.trim()) {
930
+ const nums = serviceInput.split(',').map(s => parseInt(s.trim()) - 1);
931
+ for (const n of nums) {
932
+ if (n >= 0 && n < availableServices.length) {
933
+ selectedServices.push(availableServices[n].id);
934
+ }
935
+ }
936
+ }
937
+
938
+ // Step 3: Budget Configuration
939
+ console.log('\n' + '─'.repeat(50));
940
+ console.log('Step 3: Budget Limits\n');
941
+ const maxPay = await prompt(`Max auto-pay per request [${config?.maxAutoPaySats || 200}]: `) || String(config?.maxAutoPaySats || 200);
942
+ const dailyBudget = await prompt(`Daily spending limit [${config?.dailyBudgetSats || 5000}]: `) || String(config?.dailyBudgetSats || 5000);
943
+
944
+ // Generate config
945
+ console.log('\n' + '─'.repeat(50));
946
+ console.log('Configuration\n');
947
+ const newConfig = {
948
+ agentName,
949
+ agentDescription,
950
+ ...(selectedServices.length > 0 && { services: selectedServices }),
951
+ maxAutoPaySats: parseInt(maxPay),
952
+ dailyBudgetSats: parseInt(dailyBudget)
953
+ };
954
+ console.log('Add this to your config under plugins.entries.bsv-overlay.config:\n');
955
+ console.log(JSON.stringify(newConfig, null, 2));
956
+
957
+ // Step 4: Show funding address
958
+ console.log('\n' + '─'.repeat(50));
959
+ console.log('Step 4: Funding\n');
960
+
961
+ // Ensure wallet exists
962
+ try {
963
+ await execFileAsync('node', [cliPath, 'setup'], { env });
964
+ } catch {}
965
+
966
+ const addrResult = await execFileAsync('node', [cliPath, 'address'], { env });
967
+ const addrOutput = parseCliOutput(addrResult.stdout);
968
+ const address = addrOutput?.data?.address;
969
+
970
+ const balResult = await execFileAsync('node', [cliPath, 'balance'], { env });
971
+ const balOutput = parseCliOutput(balResult.stdout);
972
+ const balance = balOutput?.data?.walletBalance || 0;
973
+
974
+ if (balance >= 1000) {
975
+ console.log(`✅ Wallet already funded: ${balance.toLocaleString()} sats`);
976
+ } else {
977
+ console.log('Send BSV to this address to fund your agent:\n');
978
+ console.log(` 📬 ${address}`);
979
+ console.log(` 💰 Minimum: 1,000 sats (~$0.05)\n`);
980
+ }
981
+
982
+ // Step 5: Registration
983
+ console.log('─'.repeat(50));
984
+ console.log('Step 5: Registration\n');
985
+
986
+ if (balance >= 1000) {
987
+ const doRegister = await prompt('Register now? [Y/n]: ');
988
+ if (doRegister.toLowerCase() !== 'n') {
989
+ console.log('\nRegistering...');
990
+ const regResult = await execFileAsync('node', [cliPath, 'register'], {
991
+ env: { ...env, AGENT_NAME: agentName, AGENT_DESCRIPTION: agentDescription },
992
+ timeout: 60000
993
+ });
994
+ const regOutput = parseCliOutput(regResult.stdout);
995
+ if (regOutput.success) {
996
+ console.log('✅ Registered on the overlay network!');
997
+
998
+ // Auto-advertise selected services
999
+ if (selectedServices.length > 0) {
1000
+ console.log(`\nAdvertising ${selectedServices.length} services...`);
1001
+ for (const serviceId of selectedServices) {
1002
+ const svc = serviceManager.registry.get(serviceId);
1003
+ if (svc) {
1004
+ try {
1005
+ await execFileAsync('node', [
1006
+ cliPath, 'advertise', serviceId, svc.name, svc.defaultPrice.toString(), svc.description
1007
+ ], { env, timeout: 60000 });
1008
+ console.log(` ✅ ${svc.name} (${svc.defaultPrice} sats)`);
1009
+ } catch (err: any) {
1010
+ console.log(` ❌ ${svc.name}: ${err.message}`);
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+ } else {
1016
+ console.log(`❌ Registration failed: ${regOutput.error}`);
1017
+ }
1018
+ }
1019
+ } else {
1020
+ console.log('Fund your wallet, then run: openclaw overlay register');
1021
+ }
1022
+
1023
+ console.log('\n' + '─'.repeat(50));
1024
+ console.log('Setup complete! 🎉\n');
1025
+
1026
+ } catch (error: any) {
1027
+ console.error('\nError:', error.message);
1028
+ } finally {
1029
+ rl.close();
1030
+ }
1031
+ });
1032
+ }, { commands: ["overlay"] });
1033
+
1034
+ // ---------------------------------------------------------------------------
1035
+ // Auto-setup + onboarding (best-effort, non-fatal, fire-and-forget)
1036
+ // ---------------------------------------------------------------------------
1037
+ (async () => {
1038
+ try {
1039
+ const config = pluginConfig;
1040
+ const walletDir = config?.walletDir || path.join(process.env.HOME || '', '.clawdbot', 'bsv-wallet');
1041
+ const identityFile = path.join(walletDir, 'wallet-identity.json');
1042
+ const env = buildEnvironment(config || {});
1043
+ const cliPath = path.join(__dirname, 'dist', 'cli.js');
1044
+
1045
+ // Step 0: Auto-enable hooks if not configured
1046
+ // The plugin needs hooks.enabled + hooks.token for async wake-ups via /hooks/agent
1047
+ const hooksAutoConfigured = autoEnableHooks(api);
1048
+
1049
+ // Step 1: Create wallet if missing
1050
+ let walletJustCreated = false;
1051
+ if (!fs.existsSync(identityFile)) {
1052
+ api.log?.info?.('[bsv-overlay] No wallet found — running auto-setup...');
1053
+ await execFileAsync('node', [cliPath, 'setup'], { env });
1054
+ api.log?.info?.('[bsv-overlay] Wallet initialized.');
1055
+ walletJustCreated = true;
1056
+ }
1057
+
1058
+ // Step 2: Get wallet address for onboarding message
1059
+ let walletAddress = '';
1060
+ try {
1061
+ const addrResult = await execFileAsync('node', [cliPath, 'address'], { env });
1062
+ const addrOutput = parseCliOutput(addrResult.stdout);
1063
+ walletAddress = addrOutput?.data?.address || '';
1064
+ } catch {}
1065
+
1066
+ // Step 3: Check registration and balance state
1067
+ const regPath = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay', 'registration.json');
1068
+ const isRegistered = fs.existsSync(regPath);
1069
+ let balance = 0;
1070
+ try {
1071
+ const balResult = await execFileAsync('node', [cliPath, 'balance'], { env });
1072
+ const balOutput = parseCliOutput(balResult.stdout);
1073
+ balance = balOutput?.data?.walletBalance || 0;
1074
+ } catch {}
1075
+
1076
+ // Step 4: If funded and not registered → auto-register
1077
+ if (!isRegistered && balance >= 1000) {
1078
+ // Clear onboarding flag since wallet is now funded
1079
+ try {
1080
+ const onboardingSentFile = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay', 'onboarding-sent.flag');
1081
+ if (fs.existsSync(onboardingSentFile)) {
1082
+ fs.unlinkSync(onboardingSentFile);
1083
+ }
1084
+ } catch {}
1085
+
1086
+ api.log?.info?.('[bsv-overlay] Wallet funded but not registered — auto-registering...');
1087
+ const regResult = await execFileAsync('node', [cliPath, 'register'], { env, timeout: 60000 });
1088
+ const regOutput = parseCliOutput(regResult.stdout);
1089
+ if (regOutput.success) {
1090
+ api.log?.info?.('[bsv-overlay] Auto-registered on overlay network!');
1091
+
1092
+ // Auto-advertise services from config
1093
+ await autoAdvertiseServices(env, cliPath, api.log);
1094
+
1095
+ const wakeText = `🎉 **BSV Overlay: Registered on the network!**\n\nYour agent is now live on the OpenClaw Overlay Network.\n\nCurrent name: "${env.AGENT_NAME}"\n\nUse /overlay for instant status or ask me about your services.`;
1096
+ wakeAgent(wakeText, api.log);
1097
+ return; // Registered — done with onboarding
1098
+ }
1099
+ }
1100
+
1101
+ // Step 5: If already registered, nothing to onboard
1102
+ if (isRegistered) return;
1103
+
1104
+ // Step 6: Not registered + not funded → send onboarding message (only once per wallet)
1105
+ // This runs on first startup after plugin install (wallet just created or exists but empty)
1106
+ const onboardingSentFile = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay', 'onboarding-sent.flag');
1107
+
1108
+ // Check if we already sent onboarding message for this wallet
1109
+ let alreadySent = false;
1110
+ try {
1111
+ if (fs.existsSync(onboardingSentFile)) {
1112
+ const flagData = fs.readFileSync(onboardingSentFile, 'utf-8');
1113
+ if (flagData.trim() === walletAddress) {
1114
+ alreadySent = true;
1115
+ }
1116
+ }
1117
+ } catch {
1118
+ // Ignore errors reading flag file
1119
+ }
1120
+
1121
+ if (alreadySent) {
1122
+ api.log?.debug?.('[bsv-overlay] Onboarding message already sent for this wallet address');
1123
+ return;
1124
+ }
1125
+
1126
+ const needsRestart = hooksAutoConfigured;
1127
+ let onboardingMsg = `The BSV Overlay Network plugin is installed. Ask the user:\n`;
1128
+ onboardingMsg += `1. What name they want for their agent on the network\n`;
1129
+ onboardingMsg += `2. A short description of what their agent does\n\n`;
1130
+ onboardingMsg += `Wallet address: ${walletAddress}\n`;
1131
+ onboardingMsg += `Minimum funding: 1,000 sats (~$0.05). Auto-import runs every 30 seconds.\n`;
1132
+
1133
+ if (needsRestart) {
1134
+ onboardingMsg += `\nNote: Hooks were auto-configured. A gateway restart is needed to activate async wake-ups.`;
1135
+ }
1136
+
1137
+ onboardingMsg += `\n\nOnce the user provides name and description, and the wallet is funded, run:\n`;
1138
+ onboardingMsg += `overlay({ action: "onboard", agentName: "<name>", agentDescription: "<description>" })`;
1139
+
1140
+ wakeAgent(onboardingMsg, api.log, { sessionKey: 'hook:bsv-overlay:onboarding' });
1141
+
1142
+ // Mark onboarding as sent for this wallet address
1143
+ try {
1144
+ fs.mkdirSync(path.dirname(onboardingSentFile), { recursive: true });
1145
+ fs.writeFileSync(onboardingSentFile, walletAddress);
1146
+ } catch (err: any) {
1147
+ api.log?.warn?.(`[bsv-overlay] Failed to save onboarding flag: ${err.message}`);
1148
+ }
1149
+
1150
+ } catch (err: any) {
1151
+ api.log?.debug?.('[bsv-overlay] Auto-setup/onboarding skipped:', err.message);
1152
+ }
1153
+ })();
1154
+ }
1155
+
1156
+ async function executeOverlayAction(params, config, api) {
1157
+ const { action } = params;
1158
+ const env = buildEnvironment(config);
1159
+ const cliPath = path.join(__dirname, 'dist', 'cli.js');
1160
+
1161
+ switch (action) {
1162
+ case "request":
1163
+ return await handleServiceRequest(params, env, cliPath, config, api);
1164
+
1165
+ case "discover":
1166
+ return await handleDiscover(params, env, cliPath);
1167
+
1168
+ case "balance":
1169
+ return await handleBalance(env, cliPath);
1170
+
1171
+ case "status":
1172
+ return await handleStatus(env, cliPath);
1173
+
1174
+ case "pay":
1175
+ return await handleDirectPay(params, env, cliPath, config);
1176
+
1177
+ case "setup":
1178
+ return await handleSetup(env, cliPath);
1179
+
1180
+ case "address":
1181
+ return await handleAddress(env, cliPath);
1182
+
1183
+ case "import":
1184
+ return await handleImport(params, env, cliPath);
1185
+
1186
+ case "register":
1187
+ return await handleRegister(env, cliPath);
1188
+
1189
+ case "advertise":
1190
+ return await handleAdvertise(params, env, cliPath);
1191
+
1192
+ case "readvertise":
1193
+ return await handleReadvertise(params, env, cliPath);
1194
+
1195
+ case "remove":
1196
+ return await handleRemove(params, env, cliPath);
1197
+
1198
+ case "send":
1199
+ return await handleSend(params, env, cliPath);
1200
+
1201
+ case "inbox":
1202
+ return await handleInbox(env, cliPath);
1203
+
1204
+ case "services":
1205
+ return await handleServices(env, cliPath);
1206
+
1207
+ case "refund":
1208
+ return await handleRefund(params, env, cliPath);
1209
+
1210
+ case "onboard":
1211
+ return await handleOnboard(params, env, cliPath);
1212
+
1213
+ case "pending-requests":
1214
+ return await handlePendingRequests(env, cliPath);
1215
+
1216
+ case "activity":
1217
+ return handleActivity();
1218
+
1219
+ case "fulfill":
1220
+ return await handleFulfill(params, env, cliPath);
1221
+
1222
+ case "unregister":
1223
+ return await handleUnregister(params, env, cliPath);
1224
+
1225
+ case "remove-service":
1226
+ return await handleRemoveService(params, env, cliPath);
1227
+
1228
+ default:
1229
+ throw new Error(`Unknown action: ${action}`);
1230
+ }
1231
+ }
1232
+
1233
+ async function handleServiceRequest(params, env, cliPath, config, api) {
1234
+ const { service, identityKey: targetKey, input, maxPrice } = params;
1235
+ const walletDir = config?.walletDir || path.join(process.env.HOME || '', '.clawdbot', 'bsv-wallet');
1236
+
1237
+ if (!service) {
1238
+ throw new Error("Service is required for request action");
1239
+ }
1240
+
1241
+ // 1. Discover providers for the service
1242
+ const discoverResult = await execFileAsync('node', [cliPath, 'discover', '--service', service], { env });
1243
+ const discoverOutput = parseCliOutput(discoverResult.stdout);
1244
+
1245
+ if (!discoverOutput.success) {
1246
+ throw new Error(`Discovery failed: ${discoverOutput.error}`);
1247
+ }
1248
+
1249
+ // FIX: Use discoverOutput.data.services instead of treating data as flat array
1250
+ const providers = discoverOutput.data.services;
1251
+ if (!providers || providers.length === 0) {
1252
+ throw new Error(`No providers found for service: ${service}`);
1253
+ }
1254
+
1255
+ // 2. Filter out our own identity key
1256
+ const identityResult = await execFileAsync('node', [cliPath, 'identity'], { env });
1257
+ const identityOutput = parseCliOutput(identityResult.stdout);
1258
+ const ourKey = identityOutput.data?.identityKey;
1259
+
1260
+ let externalProviders = providers.filter(p => p.identityKey !== ourKey);
1261
+ if (externalProviders.length === 0) {
1262
+ throw new Error("No external providers available (only found our own services)");
1263
+ }
1264
+
1265
+ // 2b. If caller specified a target identityKey, route to that provider specifically
1266
+ if (targetKey) {
1267
+ const targeted = externalProviders.filter(p => p.identityKey === targetKey);
1268
+ if (targeted.length === 0) {
1269
+ throw new Error(`Specified provider ${targetKey} not found or is our own key. Available: ${externalProviders.map(p => p.identityKey).join(', ')}`);
1270
+ }
1271
+ externalProviders = targeted;
1272
+ }
1273
+
1274
+ // 3. Sort by price - FIX: Use pricing.amountSats instead of pricingSats
1275
+ externalProviders.sort((a, b) => (a.pricing?.amountSats || 0) - (b.pricing?.amountSats || 0));
1276
+
1277
+ const bestProvider = externalProviders[0];
1278
+ const price = bestProvider.pricing?.amountSats || 0;
1279
+
1280
+ // 4. Check price limits
1281
+ const maxAutoPaySats = config.maxAutoPaySats || 200;
1282
+ const userMaxPrice = maxPrice || maxAutoPaySats;
1283
+
1284
+ if (price > userMaxPrice) {
1285
+ throw new Error(`Service price (${price} sats) exceeds limit (${userMaxPrice} sats)`);
1286
+ }
1287
+
1288
+ // 5. Check daily budget
1289
+ const dailyLimit = config.dailyBudgetSats || 1000;
1290
+ const budgetCheck = checkBudget(walletDir, price, dailyLimit);
1291
+ if (!budgetCheck.allowed) {
1292
+ throw new Error(`Service request would exceed daily budget. Spent: ${budgetCheck.spent} sats, Remaining: ${budgetCheck.remaining} sats, Requested: ${price} sats. Please confirm with user.`);
1293
+ }
1294
+
1295
+ api.logger.info(`Requesting service ${service} from ${bestProvider.name} for ${price} sats`);
1296
+
1297
+ // 6. Request the service
1298
+ const requestArgs = [cliPath, 'request-service', bestProvider.identityKey, service, price.toString()];
1299
+ if (input) {
1300
+ requestArgs.push(JSON.stringify(input));
1301
+ }
1302
+
1303
+ const requestResult = await execFileAsync('node', requestArgs, { env });
1304
+ const requestOutput = parseCliOutput(requestResult.stdout);
1305
+
1306
+ if (!requestOutput.success) {
1307
+ throw new Error(`Service request failed: ${requestOutput.error}`);
1308
+ }
1309
+
1310
+ // 7. Return immediately — no polling.
1311
+ // The WebSocket background service handles incoming responses
1312
+ // asynchronously and wakes the agent via /hooks/agent when a
1313
+ // response arrives. This avoids blocking for up to 120s.
1314
+ recordSpend(walletDir, price, service, bestProvider.name);
1315
+ writeActivityEvent({ type: 'outgoing_payment', emoji: '💸', sats: price, service, provider: bestProvider.name, message: `Paid ${price} sats to ${bestProvider.name} for ${service}` });
1316
+
1317
+ return {
1318
+ provider: bestProvider.name,
1319
+ providerKey: bestProvider.identityKey,
1320
+ cost: price,
1321
+ status: "sent",
1322
+ requestId: requestOutput.data?.messageId,
1323
+ message: `Request sent and paid (${price} sats) to ${bestProvider.name}. The response will be delivered asynchronously when the provider fulfills it.`,
1324
+ };
1325
+ }
1326
+
1327
+ // ---------------------------------------------------------------------------
1328
+ // Confirmation-gated destructive actions
1329
+ // ---------------------------------------------------------------------------
1330
+
1331
+ function generateConfirmToken(): string {
1332
+ return `confirm-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
1333
+ }
1334
+
1335
+ function cleanExpiredTokens() {
1336
+ const now = Date.now();
1337
+ for (const [token, entry] of pendingConfirmations) {
1338
+ if (entry.expiresAt < now) pendingConfirmations.delete(token);
1339
+ }
1340
+ }
1341
+
1342
+ function validateConfirmToken(token: string, expectedAction: string): { valid: boolean; details?: any; error?: string } {
1343
+ cleanExpiredTokens();
1344
+ const entry = pendingConfirmations.get(token);
1345
+ if (!entry) return { valid: false, error: 'Invalid or expired confirmation token. Run the action without confirmToken first to get a preview and new token.' };
1346
+ if (entry.action !== expectedAction) return { valid: false, error: `Token is for action '${entry.action}', not '${expectedAction}'.` };
1347
+ pendingConfirmations.delete(token); // one-time use
1348
+ return { valid: true, details: entry.details };
1349
+ }
1350
+
1351
+ async function handleUnregister(params, env, cliPath) {
1352
+ const { confirmToken } = params;
1353
+
1354
+ // Load current registration to show what will be deleted
1355
+ const regPath = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay', 'registration.json');
1356
+ let registration: any = null;
1357
+ try {
1358
+ if (fs.existsSync(regPath)) {
1359
+ registration = JSON.parse(fs.readFileSync(regPath, 'utf-8'));
1360
+ }
1361
+ } catch {}
1362
+
1363
+ if (!registration) {
1364
+ throw new Error('No registration found — agent is not registered on the overlay network.');
1365
+ }
1366
+
1367
+ // Load services that will also become orphaned
1368
+ const servicesResult = await execFileAsync('node', [cliPath, 'services'], { env });
1369
+ const servicesOutput = parseCliOutput(servicesResult.stdout);
1370
+ const services = servicesOutput?.data?.services || [];
1371
+
1372
+ // Step 1: No token → preview + generate confirmation token
1373
+ if (!confirmToken) {
1374
+ const token = generateConfirmToken();
1375
+ pendingConfirmations.set(token, {
1376
+ action: 'unregister',
1377
+ details: { registration, services },
1378
+ expiresAt: Date.now() + 5 * 60 * 1000, // 5 minute expiry
1379
+ });
1380
+
1381
+ return {
1382
+ status: 'confirmation_required',
1383
+ confirmToken: token,
1384
+ warning: '⚠️ DESTRUCTIVE ACTION — This will remove the agent from the overlay network.',
1385
+ message: 'You MUST get explicit human confirmation before proceeding. Show the user what will be deleted and ask them to confirm.',
1386
+ willDelete: {
1387
+ identity: {
1388
+ name: registration.name || registration.agentName,
1389
+ identityKey: registration.identityKey,
1390
+ txid: registration.txid,
1391
+ registeredAt: registration.registeredAt || registration.timestamp,
1392
+ },
1393
+ services: services.map((s: any) => ({
1394
+ serviceId: s.serviceId,
1395
+ name: s.name,
1396
+ priceSats: s.priceSats,
1397
+ txid: s.txid,
1398
+ })),
1399
+ serviceCount: services.length,
1400
+ },
1401
+ instructions: `To confirm: call overlay({ action: "unregister", confirmToken: "${token}" }). Token expires in 5 minutes.`,
1402
+ };
1403
+ }
1404
+
1405
+ // Step 2: Token provided → validate and execute
1406
+ const validation = validateConfirmToken(confirmToken, 'unregister');
1407
+ if (!validation.valid) {
1408
+ throw new Error(validation.error!);
1409
+ }
1410
+
1411
+ // Execute the unregister via CLI
1412
+ const result = await execFileAsync('node', [cliPath, 'unregister'], { env, timeout: 60000 });
1413
+ const output = parseCliOutput(result.stdout);
1414
+
1415
+ if (!output.success) {
1416
+ throw new Error(`Unregister failed: ${output.error}`);
1417
+ }
1418
+
1419
+ writeActivityEvent({
1420
+ type: 'agent_unregistered', emoji: '🗑️',
1421
+ message: `Agent unregistered from overlay network. Identity and ${services.length} services removed.`,
1422
+ });
1423
+
1424
+ return {
1425
+ status: 'unregistered',
1426
+ message: `Agent has been removed from the overlay network. ${services.length} service(s) are no longer discoverable.`,
1427
+ ...output.data,
1428
+ };
1429
+ }
1430
+
1431
+ async function handleRemoveService(params, env, cliPath) {
1432
+ const { serviceId, confirmToken } = params;
1433
+
1434
+ if (!serviceId) {
1435
+ throw new Error('serviceId is required for remove-service action');
1436
+ }
1437
+
1438
+ // Load the service details
1439
+ const servicesResult = await execFileAsync('node', [cliPath, 'services'], { env });
1440
+ const servicesOutput = parseCliOutput(servicesResult.stdout);
1441
+ const services = servicesOutput?.data?.services || [];
1442
+ const target = services.find((s: any) => s.serviceId === serviceId);
1443
+
1444
+ if (!target) {
1445
+ throw new Error(`Service '${serviceId}' not found in local registry. Available: ${services.map((s: any) => s.serviceId).join(', ')}`);
1446
+ }
1447
+
1448
+ // Step 1: No token → preview + generate confirmation token
1449
+ if (!confirmToken) {
1450
+ const token = generateConfirmToken();
1451
+ pendingConfirmations.set(token, {
1452
+ action: 'remove-service',
1453
+ details: { serviceId, target },
1454
+ expiresAt: Date.now() + 5 * 60 * 1000,
1455
+ });
1456
+
1457
+ return {
1458
+ status: 'confirmation_required',
1459
+ confirmToken: token,
1460
+ warning: `⚠️ DESTRUCTIVE ACTION — This will remove the '${serviceId}' service from the overlay network.`,
1461
+ message: 'You MUST get explicit human confirmation before proceeding. Show the user what will be deleted and ask them to confirm.',
1462
+ willDelete: {
1463
+ serviceId: target.serviceId,
1464
+ name: target.name,
1465
+ description: target.description,
1466
+ priceSats: target.priceSats,
1467
+ txid: target.txid,
1468
+ registeredAt: target.registeredAt,
1469
+ },
1470
+ instructions: `To confirm: call overlay({ action: "remove-service", serviceId: "${serviceId}", confirmToken: "${token}" }). Token expires in 5 minutes.`,
1471
+ };
1472
+ }
1473
+
1474
+ // Step 2: Token provided → validate and execute
1475
+ const validation = validateConfirmToken(confirmToken, 'remove-service');
1476
+ if (!validation.valid) {
1477
+ throw new Error(validation.error!);
1478
+ }
1479
+
1480
+ // Execute the remove via CLI (which now does on-chain deletion)
1481
+ const result = await execFileAsync('node', [cliPath, 'remove', serviceId], { env, timeout: 60000 });
1482
+ const output = parseCliOutput(result.stdout);
1483
+
1484
+ if (!output.success) {
1485
+ throw new Error(`Remove service failed: ${output.error}`);
1486
+ }
1487
+
1488
+ writeActivityEvent({
1489
+ type: 'service_removed', emoji: '🗑️',
1490
+ serviceId, message: `Service '${serviceId}' removed from overlay network.`,
1491
+ });
1492
+
1493
+ return {
1494
+ status: 'removed',
1495
+ message: `Service '${serviceId}' has been removed from the overlay network and is no longer discoverable.`,
1496
+ ...output.data,
1497
+ };
1498
+ }
1499
+
1500
+ async function handleDiscover(params, env, cliPath) {
1501
+ const { service, agent } = params;
1502
+ const args = [cliPath, 'discover'];
1503
+
1504
+ if (service) {
1505
+ args.push('--service', service);
1506
+ }
1507
+ if (agent) {
1508
+ args.push('--agent', agent);
1509
+ }
1510
+
1511
+ const result = await execFileAsync('node', args, { env });
1512
+ const output = parseCliOutput(result.stdout);
1513
+
1514
+ if (!output.success) {
1515
+ throw new Error(`Discovery failed: ${output.error}`);
1516
+ }
1517
+
1518
+ return output.data;
1519
+ }
1520
+
1521
+ async function handleBalance(env, cliPath) {
1522
+ const result = await execFileAsync('node', [cliPath, 'balance'], { env });
1523
+ const output = parseCliOutput(result.stdout);
1524
+
1525
+ if (!output.success) {
1526
+ throw new Error(`Balance check failed: ${output.error}`);
1527
+ }
1528
+
1529
+ return output.data;
1530
+ }
1531
+
1532
+ async function handleStatus(env, cliPath) {
1533
+ try {
1534
+ // Get identity
1535
+ const identityResult = await execFileAsync('node', [cliPath, 'identity'], { env });
1536
+ const identity = parseCliOutput(identityResult.stdout);
1537
+
1538
+ // Get balance
1539
+ const balanceResult = await execFileAsync('node', [cliPath, 'balance'], { env });
1540
+ const balance = parseCliOutput(balanceResult.stdout);
1541
+
1542
+ // Get services
1543
+ const servicesResult = await execFileAsync('node', [cliPath, 'services'], { env });
1544
+ const services = parseCliOutput(servicesResult.stdout);
1545
+
1546
+ return {
1547
+ identity: identity.data,
1548
+ balance: balance.data,
1549
+ services: services.data
1550
+ };
1551
+ } catch (error) {
1552
+ throw new Error(`Status check failed: ${error.message}`);
1553
+ }
1554
+ }
1555
+
1556
+ async function handleDirectPay(params, env, cliPath, config) {
1557
+ const { identityKey, sats, description } = params;
1558
+ const walletDir = config?.walletDir || path.join(process.env.HOME || '', '.clawdbot', 'bsv-wallet');
1559
+
1560
+ if (!identityKey || !sats) {
1561
+ throw new Error("identityKey and sats are required for pay action");
1562
+ }
1563
+
1564
+ // Check daily budget
1565
+ const dailyLimit = config?.dailyBudgetSats || 1000;
1566
+ const budgetCheck = checkBudget(walletDir, sats, dailyLimit);
1567
+ if (!budgetCheck.allowed) {
1568
+ throw new Error(`Payment would exceed daily budget. Spent: ${budgetCheck.spent} sats, Remaining: ${budgetCheck.remaining} sats, Requested: ${sats} sats. Please confirm with user.`);
1569
+ }
1570
+
1571
+ const args = [cliPath, 'pay', identityKey, sats.toString()];
1572
+ if (description) {
1573
+ args.push(description);
1574
+ }
1575
+
1576
+ const result = await execFileAsync('node', args, { env });
1577
+ const output = parseCliOutput(result.stdout);
1578
+
1579
+ if (!output.success) {
1580
+ throw new Error(`Payment failed: ${output.error}`);
1581
+ }
1582
+
1583
+ // Record the spending
1584
+ recordSpend(walletDir, sats, 'direct-payment', identityKey);
1585
+ writeActivityEvent({ type: 'outgoing_payment', emoji: '💸', sats, service: 'direct-payment', provider: identityKey?.slice(0, 16), message: `Direct payment: ${sats} sats sent` });
1586
+
1587
+ return output.data;
1588
+ }
1589
+
1590
+ async function handleSetup(env, cliPath) {
1591
+ const result = await execFileAsync('node', [cliPath, 'setup'], { env });
1592
+ const output = parseCliOutput(result.stdout);
1593
+
1594
+ if (!output.success) {
1595
+ throw new Error(`Setup failed: ${output.error}`);
1596
+ }
1597
+
1598
+ return output.data;
1599
+ }
1600
+
1601
+ async function handleAddress(env, cliPath) {
1602
+ const result = await execFileAsync('node', [cliPath, 'address'], { env });
1603
+ const output = parseCliOutput(result.stdout);
1604
+
1605
+ if (!output.success) {
1606
+ throw new Error(`Address failed: ${output.error}`);
1607
+ }
1608
+
1609
+ return output.data;
1610
+ }
1611
+
1612
+ async function handleImport(params, env, cliPath) {
1613
+ const { txid, vout } = params;
1614
+
1615
+ if (!txid) {
1616
+ throw new Error("txid is required for import action");
1617
+ }
1618
+
1619
+ const args = [cliPath, 'import', txid];
1620
+ if (vout !== undefined) {
1621
+ args.push(vout.toString());
1622
+ }
1623
+
1624
+ // Import with extended timeout - the new import logic polls for tx if needed
1625
+ const result = await execFileAsync('node', args, { env, timeout: 90000 });
1626
+ const output = parseCliOutput(result.stdout);
1627
+
1628
+ if (!output.success) {
1629
+ throw new Error(`Import failed: ${output.error}`);
1630
+ }
1631
+
1632
+ // Check if we should auto-register after successful import
1633
+ const regPath = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay', 'registration.json');
1634
+ const isRegistered = fs.existsSync(regPath);
1635
+
1636
+ if (!isRegistered && output.data?.balance >= 1000) {
1637
+ // Auto-register immediately after funding
1638
+ try {
1639
+ const regResult = await execFileAsync('node', [cliPath, 'register'], { env, timeout: 60000 });
1640
+ const regOutput = parseCliOutput(regResult.stdout);
1641
+
1642
+ if (regOutput.success) {
1643
+ // Return combined result
1644
+ return {
1645
+ ...output.data,
1646
+ autoRegistered: true,
1647
+ registration: regOutput.data,
1648
+ message: `Funding imported and agent registered on the overlay network!`,
1649
+ };
1650
+ }
1651
+ } catch (regErr: any) {
1652
+ // Registration failed but import succeeded - still return success
1653
+ return {
1654
+ ...output.data,
1655
+ autoRegistered: false,
1656
+ registrationError: regErr.message,
1657
+ message: `Funding imported successfully. Registration failed: ${regErr.message}. Try: overlay({ action: "register" })`,
1658
+ };
1659
+ }
1660
+ }
1661
+
1662
+ return output.data;
1663
+ }
1664
+
1665
+ async function handleRegister(env, cliPath) {
1666
+ const result = await execFileAsync('node', [cliPath, 'register'], { env });
1667
+ const output = parseCliOutput(result.stdout);
1668
+
1669
+ if (!output.success) {
1670
+ throw new Error(`Registration failed: ${output.error}`);
1671
+ }
1672
+
1673
+ return {
1674
+ ...output.data,
1675
+ registered: true,
1676
+ availableServices: serviceManager.getAvailableServices().map(svc => ({
1677
+ serviceId: svc.id,
1678
+ name: svc.name,
1679
+ description: svc.description,
1680
+ suggestedPrice: svc.defaultPrice,
1681
+ category: svc.category,
1682
+ })),
1683
+ nextStep: "Choose which services to advertise. Call overlay({ action: 'advertise', ... }) for each."
1684
+ };
1685
+ }
1686
+
1687
+ async function handleAdvertise(params, env, cliPath) {
1688
+ const { serviceId, name, description, priceSats } = params;
1689
+
1690
+ if (!serviceId || !name || !description || priceSats === undefined) {
1691
+ throw new Error("serviceId, name, description, and priceSats are required for advertise action");
1692
+ }
1693
+
1694
+ const result = await execFileAsync('node', [cliPath, 'advertise', serviceId, name, description, priceSats.toString()], { env });
1695
+ const output = parseCliOutput(result.stdout);
1696
+
1697
+ if (!output.success) {
1698
+ throw new Error(`Advertise failed: ${output.error}`);
1699
+ }
1700
+
1701
+ return output.data;
1702
+ }
1703
+
1704
+ async function handleReadvertise(params, env, cliPath) {
1705
+ const { serviceId, newPrice, newName, newDesc } = params;
1706
+
1707
+ if (!serviceId || newPrice === undefined) {
1708
+ throw new Error("serviceId and newPrice are required for readvertise action");
1709
+ }
1710
+
1711
+ const args = [cliPath, 'readvertise', serviceId, newPrice.toString()];
1712
+ if (newName) {
1713
+ args.push(newName);
1714
+ }
1715
+ if (newDesc) {
1716
+ args.push(newDesc);
1717
+ }
1718
+
1719
+ const result = await execFileAsync('node', args, { env });
1720
+ const output = parseCliOutput(result.stdout);
1721
+
1722
+ if (!output.success) {
1723
+ throw new Error(`Readvertise failed: ${output.error}`);
1724
+ }
1725
+
1726
+ return output.data;
1727
+ }
1728
+
1729
+ async function handleRemove(params, env, cliPath) {
1730
+ const { serviceId } = params;
1731
+
1732
+ if (!serviceId) {
1733
+ throw new Error("serviceId is required for remove action");
1734
+ }
1735
+
1736
+ const result = await execFileAsync('node', [cliPath, 'remove', serviceId], { env });
1737
+ const output = parseCliOutput(result.stdout);
1738
+
1739
+ if (!output.success) {
1740
+ throw new Error(`Remove failed: ${output.error}`);
1741
+ }
1742
+
1743
+ return output.data;
1744
+ }
1745
+
1746
+ async function handleSend(params, env, cliPath) {
1747
+ const { identityKey, messageType, payload } = params;
1748
+
1749
+ if (!identityKey || !messageType || !payload) {
1750
+ throw new Error("identityKey, messageType, and payload are required for send action");
1751
+ }
1752
+
1753
+ const result = await execFileAsync('node', [cliPath, 'send', identityKey, messageType, JSON.stringify(payload)], { env });
1754
+ const output = parseCliOutput(result.stdout);
1755
+
1756
+ if (!output.success) {
1757
+ throw new Error(`Send failed: ${output.error}`);
1758
+ }
1759
+
1760
+ return output.data;
1761
+ }
1762
+
1763
+ async function handleInbox(env, cliPath) {
1764
+ const result = await execFileAsync('node', [cliPath, 'inbox'], { env });
1765
+ const output = parseCliOutput(result.stdout);
1766
+
1767
+ if (!output.success) {
1768
+ throw new Error(`Inbox failed: ${output.error}`);
1769
+ }
1770
+
1771
+ return output.data;
1772
+ }
1773
+
1774
+ async function handleServices(env, cliPath) {
1775
+ const result = await execFileAsync('node', [cliPath, 'services'], { env });
1776
+ const output = parseCliOutput(result.stdout);
1777
+
1778
+ if (!output.success) {
1779
+ throw new Error(`Services failed: ${output.error}`);
1780
+ }
1781
+
1782
+ return output.data;
1783
+ }
1784
+
1785
+ async function handleRefund(params, env, cliPath) {
1786
+ const { address } = params;
1787
+
1788
+ if (!address) {
1789
+ throw new Error("address is required for refund action");
1790
+ }
1791
+
1792
+ const result = await execFileAsync('node', [cliPath, 'refund', address], { env });
1793
+ const output = parseCliOutput(result.stdout);
1794
+
1795
+ if (!output.success) {
1796
+ throw new Error(`Refund failed: ${output.error}`);
1797
+ }
1798
+
1799
+ return output.data;
1800
+ }
1801
+
1802
+ async function handleOnboard(params, env, cliPath) {
1803
+ const { agentName, agentDescription } = params;
1804
+ const steps = [];
1805
+
1806
+ // Apply agent name/description to env if provided
1807
+ const onboardEnv = { ...env };
1808
+ if (agentName) onboardEnv.AGENT_NAME = agentName;
1809
+ if (agentDescription) onboardEnv.AGENT_DESCRIPTION = agentDescription;
1810
+
1811
+ // Step 1: Setup wallet
1812
+ try {
1813
+ const setup = await execFileAsync('node', [cliPath, 'setup'], { env: onboardEnv });
1814
+ const setupOutput = parseCliOutput(setup.stdout);
1815
+ steps.push({ step: 'setup', success: true, identityKey: setupOutput.data?.identityKey });
1816
+ } catch (err) {
1817
+ steps.push({ step: 'setup', success: false, error: err.message });
1818
+ return { steps, nextStep: 'Fix wallet setup error and try again' };
1819
+ }
1820
+
1821
+ // Step 2: Get address
1822
+ try {
1823
+ const addr = await execFileAsync('node', [cliPath, 'address'], { env: onboardEnv });
1824
+ const addrOutput = parseCliOutput(addr.stdout);
1825
+ steps.push({ step: 'address', success: true, address: addrOutput.data?.address });
1826
+ } catch (err) {
1827
+ steps.push({ step: 'address', success: false, error: err.message });
1828
+ }
1829
+
1830
+ // Step 3: Check balance
1831
+ try {
1832
+ const bal = await execFileAsync('node', [cliPath, 'balance'], { env: onboardEnv });
1833
+ const balOutput = parseCliOutput(bal.stdout);
1834
+ const balance = balOutput.data?.walletBalance || balOutput.data?.onChain?.confirmed || 0;
1835
+ steps.push({ step: 'balance', success: true, balance });
1836
+
1837
+ if (balance < 1000) {
1838
+ return {
1839
+ steps,
1840
+ funded: false,
1841
+ 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.`
1842
+ };
1843
+ }
1844
+ } catch (err) {
1845
+ steps.push({ step: 'balance', success: false, error: err.message });
1846
+ }
1847
+
1848
+ // Step 4: Register
1849
+ try {
1850
+ const reg = await execFileAsync('node', [cliPath, 'register'], { env: onboardEnv, timeout: 60000 });
1851
+ const regOutput = parseCliOutput(reg.stdout);
1852
+ steps.push({ step: 'register', success: regOutput.success, data: regOutput.data });
1853
+ } catch (err) {
1854
+ steps.push({ step: 'register', success: false, error: err.message });
1855
+ }
1856
+
1857
+ return {
1858
+ steps,
1859
+ funded: true,
1860
+ registered: true,
1861
+ agentName: onboardEnv.AGENT_NAME,
1862
+ agentDescription: onboardEnv.AGENT_DESCRIPTION,
1863
+ availableServices: serviceManager.getAvailableServices().map(svc => ({
1864
+ serviceId: svc.id,
1865
+ name: svc.name,
1866
+ description: svc.description,
1867
+ suggestedPrice: svc.defaultPrice,
1868
+ category: svc.category,
1869
+ })),
1870
+ nextStep: "Choose which services to advertise. Call overlay({ action: 'advertise', ... }) for each.",
1871
+ message: 'Onboarding complete! Your agent is registered on the BSV overlay network. The background service will handle incoming requests.'
1872
+ };
1873
+ }
1874
+
1875
+ async function handlePendingRequests(env, cliPath) {
1876
+ // Clean up old queue entries before checking pending requests
1877
+ try {
1878
+ const { cleanupServiceQueue } = await import('./src/scripts/utils/storage.js');
1879
+ cleanupServiceQueue();
1880
+ } catch (err) {
1881
+ console.error('Queue cleanup failed:', err.message);
1882
+ }
1883
+
1884
+ const result = await execFileAsync('node', [cliPath, 'service-queue'], { env });
1885
+ const output = parseCliOutput(result.stdout);
1886
+ if (!output.success) throw new Error(`Queue check failed: ${output.error}`);
1887
+
1888
+ // Clear the alert file since we're checking now
1889
+ const alertPath = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay', 'pending-alert.jsonl');
1890
+ try { if (fs.existsSync(alertPath)) fs.unlinkSync(alertPath); } catch {}
1891
+
1892
+ return output.data;
1893
+ }
1894
+
1895
+ function handleActivity() {
1896
+ const feedPath = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay', 'activity-feed.jsonl');
1897
+ if (!fs.existsSync(feedPath)) return { events: [], count: 0 };
1898
+
1899
+ const lines = fs.readFileSync(feedPath, 'utf-8').trim().split('\n').filter(Boolean);
1900
+ const events = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
1901
+
1902
+ // Clear the feed after reading
1903
+ fs.writeFileSync(feedPath, '');
1904
+
1905
+ return { events, count: events.length };
1906
+ }
1907
+
1908
+ async function handleFulfill(params, env, cliPath) {
1909
+ const { requestId, recipientKey, serviceId, result } = params;
1910
+ if (!requestId || !recipientKey || !serviceId || !result) {
1911
+ throw new Error("requestId, recipientKey, serviceId, and result are required");
1912
+ }
1913
+
1914
+ const cliResult = await execFileAsync('node', [
1915
+ cliPath, 'respond-service', requestId, recipientKey, serviceId, JSON.stringify(result)
1916
+ ], { env });
1917
+ const output = parseCliOutput(cliResult.stdout);
1918
+ if (!output.success) throw new Error(`Fulfill failed: ${output.error}`);
1919
+
1920
+ // Clean up the request ID from tracking since it's now fulfilled
1921
+ wokenRequests.delete(requestId);
1922
+
1923
+ writeActivityEvent({ type: 'service_fulfilled', emoji: '✅', serviceId, recipientKey: recipientKey?.slice(0, 16), message: `Fulfilled ${serviceId} request — response sent` });
1924
+
1925
+ return output.data;
1926
+ }
1927
+
1928
+ function buildEnvironment(config) {
1929
+ const env = { ...process.env };
1930
+
1931
+ if (config.walletDir) {
1932
+ env.BSV_WALLET_DIR = config.walletDir;
1933
+ }
1934
+ if (config.overlayUrl) {
1935
+ env.OVERLAY_URL = config.overlayUrl;
1936
+ } else if (!env.OVERLAY_URL) {
1937
+ env.OVERLAY_URL = 'https://clawoverlay.com';
1938
+ }
1939
+
1940
+ // Set defaults
1941
+ env.BSV_NETWORK = env.BSV_NETWORK || 'mainnet';
1942
+ if (config.agentName) {
1943
+ env.AGENT_NAME = config.agentName;
1944
+ } else if (!env.AGENT_NAME) {
1945
+ env.AGENT_NAME = 'clawdbot-agent';
1946
+ }
1947
+ if (config.agentDescription) {
1948
+ env.AGENT_DESCRIPTION = config.agentDescription;
1949
+ } else if (!env.AGENT_DESCRIPTION) {
1950
+ env.AGENT_DESCRIPTION = 'AI agent on the OpenClaw Overlay Network. Offers services for BSV micropayments.';
1951
+ }
1952
+ env.AGENT_ROUTED = 'true'; // Route service requests through the agent
1953
+
1954
+ return env;
1955
+ }
1956
+
1957
+ function parseCliOutput(stdout) {
1958
+ try {
1959
+ return JSON.parse(stdout.trim());
1960
+ } catch (error) {
1961
+ throw new Error(`Failed to parse CLI output: ${error.message}`);
1962
+ }
1963
+ }
1964
+
1965
+ // sleep() removed — no longer needed since polling loop was removed