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