@johngalt5/bsv-overlay 0.2.0
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/README.md +339 -0
- package/SKILL.md +327 -0
- package/clawdbot.plugin.json +55 -0
- package/index.ts +1062 -0
- package/package.json +47 -0
- package/scripts/overlay-cli.mjs +4536 -0
- package/scripts/setup.sh +96 -0
package/index.ts
ADDED
|
@@ -0,0 +1,1062 @@
|
|
|
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
|
+
|
|
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
|
+
// Auto-import tracking
|
|
17
|
+
let autoImportInterval: any = null;
|
|
18
|
+
let knownTxids: Set<string> = new Set();
|
|
19
|
+
|
|
20
|
+
// Budget tracking
|
|
21
|
+
const BUDGET_FILE = 'daily-spending.json';
|
|
22
|
+
|
|
23
|
+
interface DailySpending {
|
|
24
|
+
date: string; // YYYY-MM-DD
|
|
25
|
+
totalSats: number;
|
|
26
|
+
transactions: Array<{ ts: number; sats: number; service: string; provider: string }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getBudgetPath(walletDir: string): string {
|
|
30
|
+
return path.join(walletDir, BUDGET_FILE);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadDailySpending(walletDir: string): DailySpending {
|
|
34
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
35
|
+
const budgetPath = getBudgetPath(walletDir);
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.parse(fs.readFileSync(budgetPath, 'utf-8'));
|
|
38
|
+
if (data.date === today) return data;
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore parse errors - return fresh daily spending for corrupted/missing file
|
|
41
|
+
}
|
|
42
|
+
return { date: today, totalSats: 0, transactions: [] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function recordSpend(walletDir: string, sats: number, service: string, provider: string) {
|
|
46
|
+
const spending = loadDailySpending(walletDir);
|
|
47
|
+
spending.totalSats += sats;
|
|
48
|
+
spending.transactions.push({ ts: Date.now(), sats, service, provider });
|
|
49
|
+
fs.writeFileSync(getBudgetPath(walletDir), JSON.stringify(spending, null, 2));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function checkBudget(walletDir: string, requestedSats: number, dailyLimit: number): { allowed: boolean; remaining: number; spent: number } {
|
|
53
|
+
const spending = loadDailySpending(walletDir);
|
|
54
|
+
const remaining = dailyLimit - spending.totalSats;
|
|
55
|
+
return {
|
|
56
|
+
allowed: remaining >= requestedSats,
|
|
57
|
+
remaining,
|
|
58
|
+
spent: spending.totalSats
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function startAutoImport(env, cliPath, logger) {
|
|
63
|
+
// Get our address
|
|
64
|
+
try {
|
|
65
|
+
const addrResult = await execFileAsync('node', [cliPath, 'address'], { env });
|
|
66
|
+
const addrOutput = parseCliOutput(addrResult.stdout);
|
|
67
|
+
if (!addrOutput.success) return;
|
|
68
|
+
const address = addrOutput.data?.address;
|
|
69
|
+
if (!address) return;
|
|
70
|
+
|
|
71
|
+
// Load known txids from wallet state
|
|
72
|
+
const balResult = await execFileAsync('node', [cliPath, 'balance'], { env });
|
|
73
|
+
const balOutput = parseCliOutput(balResult.stdout);
|
|
74
|
+
// Track what we already have
|
|
75
|
+
|
|
76
|
+
autoImportInterval = setInterval(async () => {
|
|
77
|
+
try {
|
|
78
|
+
const network = env.BSV_NETWORK === 'testnet' ? 'test' : 'main';
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
81
|
+
try {
|
|
82
|
+
const resp = await fetch(`https://api.whatsonchain.com/v1/bsv/${network}/address/${address}/unspent`, { signal: controller.signal });
|
|
83
|
+
if (!resp.ok) return;
|
|
84
|
+
const utxos = await resp.json();
|
|
85
|
+
|
|
86
|
+
for (const utxo of utxos) {
|
|
87
|
+
const key = `${utxo.tx_hash}:${utxo.tx_pos}`;
|
|
88
|
+
if (knownTxids.has(key)) continue;
|
|
89
|
+
if (utxo.value < 200) continue; // skip dust
|
|
90
|
+
|
|
91
|
+
logger?.info?.(`[bsv-overlay] Auto-importing UTXO: ${utxo.tx_hash}:${utxo.tx_pos} (${utxo.value} sats)`);
|
|
92
|
+
try {
|
|
93
|
+
const importResult = await execFileAsync('node', [cliPath, 'import', utxo.tx_hash, String(utxo.tx_pos)], { env });
|
|
94
|
+
const importOutput = parseCliOutput(importResult.stdout);
|
|
95
|
+
if (importOutput.success) {
|
|
96
|
+
knownTxids.add(key);
|
|
97
|
+
logger?.info?.(`[bsv-overlay] Auto-imported ${utxo.value} sats from ${utxo.tx_hash}`);
|
|
98
|
+
|
|
99
|
+
// Check if registered, auto-register if not
|
|
100
|
+
try {
|
|
101
|
+
const regPath = path.join(process.env.HOME || '', '.clawdbot', 'bsv-overlay', 'registration.json');
|
|
102
|
+
if (!fs.existsSync(regPath)) {
|
|
103
|
+
logger?.info?.('[bsv-overlay] Not yet registered — auto-registering...');
|
|
104
|
+
const regResult = await execFileAsync('node', [cliPath, 'register'], { env, timeout: 60000 });
|
|
105
|
+
const regOutput = parseCliOutput(regResult.stdout);
|
|
106
|
+
if (regOutput.success) {
|
|
107
|
+
logger?.info?.('[bsv-overlay] Auto-registered on overlay network!');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
logger?.warn?.('[bsv-overlay] Auto-registration failed:', err.message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
// Already imported or error — track it so we don't retry
|
|
116
|
+
knownTxids.add(key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// WoC API error — just skip this cycle
|
|
121
|
+
} finally {
|
|
122
|
+
clearTimeout(timeout);
|
|
123
|
+
}
|
|
124
|
+
}, 60000); // Check every 60 seconds
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger?.warn?.('[bsv-overlay] Auto-import setup failed:', err.message);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stopAutoImport() {
|
|
131
|
+
if (autoImportInterval) {
|
|
132
|
+
clearInterval(autoImportInterval);
|
|
133
|
+
autoImportInterval = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function startBackgroundService(env, cliPath, logger) {
|
|
138
|
+
if (backgroundProcess) return;
|
|
139
|
+
serviceRunning = true;
|
|
140
|
+
|
|
141
|
+
function spawnConnect() {
|
|
142
|
+
if (!serviceRunning) return;
|
|
143
|
+
|
|
144
|
+
const proc = spawn('node', [cliPath, 'connect'], {
|
|
145
|
+
env,
|
|
146
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
backgroundProcess = proc;
|
|
150
|
+
|
|
151
|
+
proc.stdout?.on('data', (data) => {
|
|
152
|
+
// Log incoming service fulfillments
|
|
153
|
+
const lines = data.toString().split('\n').filter(Boolean);
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
try {
|
|
156
|
+
const event = JSON.parse(line);
|
|
157
|
+
logger?.debug?.(`[bsv-overlay] ${event.event || event.type || 'message'}:`, JSON.stringify(event).slice(0, 200));
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
proc.stderr?.on('data', (data) => {
|
|
163
|
+
const lines = data.toString().split('\n').filter(Boolean);
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
try {
|
|
166
|
+
const event = JSON.parse(line);
|
|
167
|
+
if (event.event === 'connected') {
|
|
168
|
+
logger?.info?.('[bsv-overlay] WebSocket relay connected');
|
|
169
|
+
} else if (event.event === 'disconnected') {
|
|
170
|
+
logger?.warn?.('[bsv-overlay] WebSocket disconnected, reconnecting...');
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
logger?.debug?.(`[bsv-overlay] ${line}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
proc.on('exit', (code) => {
|
|
179
|
+
backgroundProcess = null;
|
|
180
|
+
if (serviceRunning) {
|
|
181
|
+
logger?.warn?.(`[bsv-overlay] Background service exited (code ${code}), restarting in 5s...`);
|
|
182
|
+
setTimeout(spawnConnect, 5000);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
spawnConnect();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function stopBackgroundService() {
|
|
191
|
+
serviceRunning = false;
|
|
192
|
+
if (backgroundProcess) {
|
|
193
|
+
backgroundProcess.kill('SIGTERM');
|
|
194
|
+
backgroundProcess = null;
|
|
195
|
+
}
|
|
196
|
+
stopAutoImport();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default async function register(api) {
|
|
200
|
+
// Capture config at registration time (api.getConfig may not be available later)
|
|
201
|
+
const pluginConfig = api.getConfig?.()?.plugins?.entries?.['bsv-overlay']?.config || api.config || {};
|
|
202
|
+
|
|
203
|
+
// Register the overlay agent tool
|
|
204
|
+
api.registerTool({
|
|
205
|
+
name: "overlay",
|
|
206
|
+
description: "Access the BSV agent marketplace - discover agents and exchange BSV micropayments for services",
|
|
207
|
+
parameters: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
action: {
|
|
211
|
+
type: "string",
|
|
212
|
+
enum: [
|
|
213
|
+
"request", "discover", "balance", "status", "pay",
|
|
214
|
+
"setup", "address", "import", "register", "advertise",
|
|
215
|
+
"readvertise", "remove", "send", "inbox", "services", "refund",
|
|
216
|
+
"onboard", "pending-requests", "fulfill"
|
|
217
|
+
],
|
|
218
|
+
description: "Action to perform"
|
|
219
|
+
},
|
|
220
|
+
service: {
|
|
221
|
+
type: "string",
|
|
222
|
+
description: "Service ID for request/discover"
|
|
223
|
+
},
|
|
224
|
+
input: {
|
|
225
|
+
type: "object",
|
|
226
|
+
description: "Service-specific input data"
|
|
227
|
+
},
|
|
228
|
+
maxPrice: {
|
|
229
|
+
type: "number",
|
|
230
|
+
description: "Max sats willing to pay"
|
|
231
|
+
},
|
|
232
|
+
identityKey: {
|
|
233
|
+
type: "string",
|
|
234
|
+
description: "Target agent key for direct pay/send"
|
|
235
|
+
},
|
|
236
|
+
sats: {
|
|
237
|
+
type: "number",
|
|
238
|
+
description: "Amount for direct pay"
|
|
239
|
+
},
|
|
240
|
+
description: {
|
|
241
|
+
type: "string"
|
|
242
|
+
},
|
|
243
|
+
agent: {
|
|
244
|
+
type: "string",
|
|
245
|
+
description: "Agent name filter for discover"
|
|
246
|
+
},
|
|
247
|
+
// Import parameters
|
|
248
|
+
txid: {
|
|
249
|
+
type: "string",
|
|
250
|
+
description: "Transaction ID for import"
|
|
251
|
+
},
|
|
252
|
+
vout: {
|
|
253
|
+
type: "number",
|
|
254
|
+
description: "Output index for import (optional)"
|
|
255
|
+
},
|
|
256
|
+
// Service management parameters
|
|
257
|
+
serviceId: {
|
|
258
|
+
type: "string",
|
|
259
|
+
description: "Service ID for advertise/readvertise/remove"
|
|
260
|
+
},
|
|
261
|
+
name: {
|
|
262
|
+
type: "string",
|
|
263
|
+
description: "Service name for advertise/readvertise"
|
|
264
|
+
},
|
|
265
|
+
priceSats: {
|
|
266
|
+
type: "number",
|
|
267
|
+
description: "Price in satoshis for advertise"
|
|
268
|
+
},
|
|
269
|
+
newPrice: {
|
|
270
|
+
type: "number",
|
|
271
|
+
description: "New price for readvertise"
|
|
272
|
+
},
|
|
273
|
+
newName: {
|
|
274
|
+
type: "string",
|
|
275
|
+
description: "New name for readvertise (optional)"
|
|
276
|
+
},
|
|
277
|
+
newDesc: {
|
|
278
|
+
type: "string",
|
|
279
|
+
description: "New description for readvertise (optional)"
|
|
280
|
+
},
|
|
281
|
+
// Messaging parameters
|
|
282
|
+
messageType: {
|
|
283
|
+
type: "string",
|
|
284
|
+
description: "Message type for send"
|
|
285
|
+
},
|
|
286
|
+
payload: {
|
|
287
|
+
type: "object",
|
|
288
|
+
description: "Message payload for send"
|
|
289
|
+
},
|
|
290
|
+
// Refund parameters
|
|
291
|
+
address: {
|
|
292
|
+
type: "string",
|
|
293
|
+
description: "Destination address for refund"
|
|
294
|
+
},
|
|
295
|
+
// Fulfill parameters
|
|
296
|
+
requestId: {
|
|
297
|
+
type: "string",
|
|
298
|
+
description: "Request ID for fulfill"
|
|
299
|
+
},
|
|
300
|
+
recipientKey: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "Recipient identity key for fulfill"
|
|
303
|
+
},
|
|
304
|
+
result: {
|
|
305
|
+
type: "object",
|
|
306
|
+
description: "Service result for fulfill"
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
required: ["action"]
|
|
310
|
+
},
|
|
311
|
+
async execute(id, params) {
|
|
312
|
+
const config = pluginConfig;
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const result = await executeOverlayAction(params, config, api);
|
|
316
|
+
return {
|
|
317
|
+
content: [{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
320
|
+
}]
|
|
321
|
+
};
|
|
322
|
+
} catch (error) {
|
|
323
|
+
return {
|
|
324
|
+
content: [{
|
|
325
|
+
type: "text",
|
|
326
|
+
text: `Error: ${error.message}`
|
|
327
|
+
}]
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Register background service for WebSocket relay
|
|
334
|
+
api.registerService({
|
|
335
|
+
id: "bsv-overlay-relay",
|
|
336
|
+
start: async () => {
|
|
337
|
+
api.logger.info("Starting BSV overlay WebSocket relay...");
|
|
338
|
+
try {
|
|
339
|
+
const config = pluginConfig;
|
|
340
|
+
const env = buildEnvironment(config);
|
|
341
|
+
const cliPath = path.join(__dirname, 'scripts', 'overlay-cli.mjs');
|
|
342
|
+
|
|
343
|
+
// Use the improved background service
|
|
344
|
+
startBackgroundService(env, cliPath, api.logger);
|
|
345
|
+
|
|
346
|
+
// Start auto-import
|
|
347
|
+
startAutoImport(env, cliPath, api.logger);
|
|
348
|
+
|
|
349
|
+
api.logger.info("BSV overlay WebSocket relay started");
|
|
350
|
+
} catch (error) {
|
|
351
|
+
api.logger.error(`Failed to start BSV overlay relay: ${error.message}`);
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
stop: async () => {
|
|
355
|
+
api.logger.info("Stopping BSV overlay WebSocket relay...");
|
|
356
|
+
stopBackgroundService();
|
|
357
|
+
api.logger.info("BSV overlay WebSocket relay stopped");
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Register CLI commands
|
|
362
|
+
api.registerCli(({ program }) => {
|
|
363
|
+
const overlay = program.command("overlay").description("BSV Overlay Network commands");
|
|
364
|
+
|
|
365
|
+
overlay.command("status")
|
|
366
|
+
.description("Show identity, balance, registration, and services")
|
|
367
|
+
.action(async () => {
|
|
368
|
+
try {
|
|
369
|
+
const config = pluginConfig;
|
|
370
|
+
const result = await handleStatus(buildEnvironment(config), path.join(__dirname, 'scripts', 'overlay-cli.mjs'));
|
|
371
|
+
console.log("BSV Overlay Status:");
|
|
372
|
+
console.log("Identity:", result.identity);
|
|
373
|
+
console.log("Balance:", result.balance);
|
|
374
|
+
console.log("Services:", result.services);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error("Error:", error.message);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
overlay.command("balance")
|
|
381
|
+
.description("Show wallet balance")
|
|
382
|
+
.action(async () => {
|
|
383
|
+
try {
|
|
384
|
+
const config = pluginConfig;
|
|
385
|
+
const result = await handleBalance(buildEnvironment(config), path.join(__dirname, 'scripts', 'overlay-cli.mjs'));
|
|
386
|
+
console.log("Balance:", result);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error("Error:", error.message);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
overlay.command("address")
|
|
393
|
+
.description("Show receive address")
|
|
394
|
+
.action(async () => {
|
|
395
|
+
try {
|
|
396
|
+
const config = pluginConfig;
|
|
397
|
+
const result = await handleAddress(buildEnvironment(config), path.join(__dirname, 'scripts', 'overlay-cli.mjs'));
|
|
398
|
+
console.log("Address:", result);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error("Error:", error.message);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
overlay.command("discover")
|
|
405
|
+
.description("List agents and services on the network")
|
|
406
|
+
.option("--service <type>", "Filter by service type")
|
|
407
|
+
.option("--agent <name>", "Filter by agent name")
|
|
408
|
+
.action(async (options) => {
|
|
409
|
+
try {
|
|
410
|
+
const config = pluginConfig;
|
|
411
|
+
const result = await handleDiscover(options, buildEnvironment(config), path.join(__dirname, 'scripts', 'overlay-cli.mjs'));
|
|
412
|
+
console.log("Discovery results:");
|
|
413
|
+
console.log(`Overlay URL: ${result.overlayUrl}`);
|
|
414
|
+
console.log(`Agents: ${result.agentCount}, Services: ${result.serviceCount}`);
|
|
415
|
+
if (result.agents?.length > 0) {
|
|
416
|
+
console.log("\nAgents:");
|
|
417
|
+
result.agents.forEach(agent => {
|
|
418
|
+
console.log(` ${agent.agentName} (${agent.identityKey})`);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
if (result.services?.length > 0) {
|
|
422
|
+
console.log("\nServices:");
|
|
423
|
+
result.services.forEach(service => {
|
|
424
|
+
console.log(` ${service.serviceId} - ${service.name} (${service.pricing?.amountSats || 0} sats) by ${service.agentName}`);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
} catch (error) {
|
|
428
|
+
console.error("Error:", error.message);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
overlay.command("services")
|
|
433
|
+
.description("List our advertised services")
|
|
434
|
+
.action(async () => {
|
|
435
|
+
try {
|
|
436
|
+
const config = pluginConfig;
|
|
437
|
+
const result = await handleServices(buildEnvironment(config), path.join(__dirname, 'scripts', 'overlay-cli.mjs'));
|
|
438
|
+
console.log("Our services:", result);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.error("Error:", error.message);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
overlay.command("setup")
|
|
445
|
+
.description("Run initial wallet setup")
|
|
446
|
+
.action(async () => {
|
|
447
|
+
try {
|
|
448
|
+
const config = pluginConfig;
|
|
449
|
+
const env = buildEnvironment(config);
|
|
450
|
+
const cliPath = path.join(__dirname, 'scripts', 'overlay-cli.mjs');
|
|
451
|
+
|
|
452
|
+
const result = await execFileAsync('node', [cliPath, 'setup'], { env });
|
|
453
|
+
const output = parseCliOutput(result.stdout);
|
|
454
|
+
console.log("Setup result:", output);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.error("Error:", error.message);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
overlay.command("register")
|
|
461
|
+
.description("Register with the overlay network")
|
|
462
|
+
.action(async () => {
|
|
463
|
+
try {
|
|
464
|
+
const config = pluginConfig;
|
|
465
|
+
const env = buildEnvironment(config);
|
|
466
|
+
const cliPath = path.join(__dirname, 'scripts', 'overlay-cli.mjs');
|
|
467
|
+
|
|
468
|
+
const result = await execFileAsync('node', [cliPath, 'register'], { env });
|
|
469
|
+
const output = parseCliOutput(result.stdout);
|
|
470
|
+
console.log("Registration result:", output);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error("Error:", error.message);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
}, { commands: ["overlay"] });
|
|
476
|
+
|
|
477
|
+
// Auto-setup: ensure wallet exists (best-effort, non-fatal)
|
|
478
|
+
try {
|
|
479
|
+
const config = pluginConfig;
|
|
480
|
+
const walletDir = config?.walletDir || path.join(process.env.HOME || '', '.clawdbot', 'bsv-wallet');
|
|
481
|
+
const identityFile = path.join(walletDir, 'wallet-identity.json');
|
|
482
|
+
if (!fs.existsSync(identityFile)) {
|
|
483
|
+
api.log?.info?.('[bsv-overlay] No wallet found — running auto-setup...');
|
|
484
|
+
try {
|
|
485
|
+
const env = buildEnvironment(config || {});
|
|
486
|
+
const cliPath = path.join(__dirname, 'scripts', 'overlay-cli.mjs');
|
|
487
|
+
await execFileAsync('node', [cliPath, 'setup'], { env });
|
|
488
|
+
api.log?.info?.('[bsv-overlay] Wallet initialized. Fund it and run: overlay({ action: "register" })');
|
|
489
|
+
} catch (err: any) {
|
|
490
|
+
api.log?.warn?.('[bsv-overlay] Auto-setup failed:', err.message);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
} catch (err: any) {
|
|
494
|
+
// Non-fatal — plugin still loads if auto-setup fails
|
|
495
|
+
api.log?.debug?.('[bsv-overlay] Auto-setup skipped:', err.message);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function executeOverlayAction(params, config, api) {
|
|
500
|
+
const { action } = params;
|
|
501
|
+
const env = buildEnvironment(config);
|
|
502
|
+
const cliPath = path.join(__dirname, 'scripts', 'overlay-cli.mjs');
|
|
503
|
+
|
|
504
|
+
switch (action) {
|
|
505
|
+
case "request":
|
|
506
|
+
return await handleServiceRequest(params, env, cliPath, config, api);
|
|
507
|
+
|
|
508
|
+
case "discover":
|
|
509
|
+
return await handleDiscover(params, env, cliPath);
|
|
510
|
+
|
|
511
|
+
case "balance":
|
|
512
|
+
return await handleBalance(env, cliPath);
|
|
513
|
+
|
|
514
|
+
case "status":
|
|
515
|
+
return await handleStatus(env, cliPath);
|
|
516
|
+
|
|
517
|
+
case "pay":
|
|
518
|
+
return await handleDirectPay(params, env, cliPath, config);
|
|
519
|
+
|
|
520
|
+
case "setup":
|
|
521
|
+
return await handleSetup(env, cliPath);
|
|
522
|
+
|
|
523
|
+
case "address":
|
|
524
|
+
return await handleAddress(env, cliPath);
|
|
525
|
+
|
|
526
|
+
case "import":
|
|
527
|
+
return await handleImport(params, env, cliPath);
|
|
528
|
+
|
|
529
|
+
case "register":
|
|
530
|
+
return await handleRegister(env, cliPath);
|
|
531
|
+
|
|
532
|
+
case "advertise":
|
|
533
|
+
return await handleAdvertise(params, env, cliPath);
|
|
534
|
+
|
|
535
|
+
case "readvertise":
|
|
536
|
+
return await handleReadvertise(params, env, cliPath);
|
|
537
|
+
|
|
538
|
+
case "remove":
|
|
539
|
+
return await handleRemove(params, env, cliPath);
|
|
540
|
+
|
|
541
|
+
case "send":
|
|
542
|
+
return await handleSend(params, env, cliPath);
|
|
543
|
+
|
|
544
|
+
case "inbox":
|
|
545
|
+
return await handleInbox(env, cliPath);
|
|
546
|
+
|
|
547
|
+
case "services":
|
|
548
|
+
return await handleServices(env, cliPath);
|
|
549
|
+
|
|
550
|
+
case "refund":
|
|
551
|
+
return await handleRefund(params, env, cliPath);
|
|
552
|
+
|
|
553
|
+
case "onboard":
|
|
554
|
+
return await handleOnboard(env, cliPath);
|
|
555
|
+
|
|
556
|
+
case "pending-requests":
|
|
557
|
+
return await handlePendingRequests(env, cliPath);
|
|
558
|
+
|
|
559
|
+
case "fulfill":
|
|
560
|
+
return await handleFulfill(params, env, cliPath);
|
|
561
|
+
|
|
562
|
+
default:
|
|
563
|
+
throw new Error(`Unknown action: ${action}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function handleServiceRequest(params, env, cliPath, config, api) {
|
|
568
|
+
const { service, input, maxPrice } = params;
|
|
569
|
+
const walletDir = config?.walletDir || path.join(process.env.HOME || '', '.clawdbot', 'bsv-wallet');
|
|
570
|
+
|
|
571
|
+
if (!service) {
|
|
572
|
+
throw new Error("Service is required for request action");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 1. Discover providers for the service
|
|
576
|
+
const discoverResult = await execFileAsync('node', [cliPath, 'discover', '--service', service], { env });
|
|
577
|
+
const discoverOutput = parseCliOutput(discoverResult.stdout);
|
|
578
|
+
|
|
579
|
+
if (!discoverOutput.success) {
|
|
580
|
+
throw new Error(`Discovery failed: ${discoverOutput.error}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// FIX: Use discoverOutput.data.services instead of treating data as flat array
|
|
584
|
+
const providers = discoverOutput.data.services;
|
|
585
|
+
if (!providers || providers.length === 0) {
|
|
586
|
+
throw new Error(`No providers found for service: ${service}`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// 2. Filter out our own identity key
|
|
590
|
+
const identityResult = await execFileAsync('node', [cliPath, 'identity'], { env });
|
|
591
|
+
const identityOutput = parseCliOutput(identityResult.stdout);
|
|
592
|
+
const ourKey = identityOutput.data?.identityKey;
|
|
593
|
+
|
|
594
|
+
const externalProviders = providers.filter(p => p.identityKey !== ourKey);
|
|
595
|
+
if (externalProviders.length === 0) {
|
|
596
|
+
throw new Error("No external providers available (only found our own services)");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// 3. Sort by price - FIX: Use pricing.amountSats instead of pricingSats
|
|
600
|
+
externalProviders.sort((a, b) => (a.pricing?.amountSats || 0) - (b.pricing?.amountSats || 0));
|
|
601
|
+
|
|
602
|
+
const bestProvider = externalProviders[0];
|
|
603
|
+
const price = bestProvider.pricing?.amountSats || 0;
|
|
604
|
+
|
|
605
|
+
// 4. Check price limits
|
|
606
|
+
const maxAutoPaySats = config.maxAutoPaySats || 200;
|
|
607
|
+
const userMaxPrice = maxPrice || maxAutoPaySats;
|
|
608
|
+
|
|
609
|
+
if (price > userMaxPrice) {
|
|
610
|
+
throw new Error(`Service price (${price} sats) exceeds limit (${userMaxPrice} sats)`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 5. Check daily budget
|
|
614
|
+
const dailyLimit = config.dailyBudgetSats || 1000;
|
|
615
|
+
const budgetCheck = checkBudget(walletDir, price, dailyLimit);
|
|
616
|
+
if (!budgetCheck.allowed) {
|
|
617
|
+
throw new Error(`Service request would exceed daily budget. Spent: ${budgetCheck.spent} sats, Remaining: ${budgetCheck.remaining} sats, Requested: ${price} sats. Please confirm with user.`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
api.logger.info(`Requesting service ${service} from ${bestProvider.agentName} for ${price} sats`);
|
|
621
|
+
|
|
622
|
+
// 6. Request the service
|
|
623
|
+
const requestArgs = [cliPath, 'request-service', bestProvider.identityKey, service, price.toString()];
|
|
624
|
+
if (input) {
|
|
625
|
+
requestArgs.push(JSON.stringify(input));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const requestResult = await execFileAsync('node', requestArgs, { env });
|
|
629
|
+
const requestOutput = parseCliOutput(requestResult.stdout);
|
|
630
|
+
|
|
631
|
+
if (!requestOutput.success) {
|
|
632
|
+
throw new Error(`Service request failed: ${requestOutput.error}`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// 7. Poll for response
|
|
636
|
+
const maxPollAttempts = 12; // ~60 seconds with 5 second intervals
|
|
637
|
+
let attempts = 0;
|
|
638
|
+
|
|
639
|
+
while (attempts < maxPollAttempts) {
|
|
640
|
+
await sleep(5000); // Wait 5 seconds
|
|
641
|
+
attempts++;
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
const pollResult = await execFileAsync('node', [cliPath, 'poll'], { env });
|
|
645
|
+
const pollOutput = parseCliOutput(pollResult.stdout);
|
|
646
|
+
|
|
647
|
+
if (pollOutput.success && pollOutput.data) {
|
|
648
|
+
// FIX: Check pollOutput.data.messages array for service-response
|
|
649
|
+
const messages = pollOutput.data.messages || [];
|
|
650
|
+
for (const msg of messages) {
|
|
651
|
+
if (msg.type === 'service-response' && msg.from === bestProvider.identityKey) {
|
|
652
|
+
api.logger.info(`Received response from ${bestProvider.agentName}`);
|
|
653
|
+
// Record the spending
|
|
654
|
+
recordSpend(walletDir, price, service, bestProvider.agentName);
|
|
655
|
+
return {
|
|
656
|
+
provider: bestProvider.agentName,
|
|
657
|
+
cost: price,
|
|
658
|
+
result: msg.payload
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} catch (pollError) {
|
|
664
|
+
// Continue polling even if one poll fails
|
|
665
|
+
api.logger.warn(`Poll attempt ${attempts} failed: ${pollError.message}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
throw new Error(`Service request timed out after ${maxPollAttempts * 5} seconds`);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function handleDiscover(params, env, cliPath) {
|
|
673
|
+
const { service, agent } = params;
|
|
674
|
+
const args = [cliPath, 'discover'];
|
|
675
|
+
|
|
676
|
+
if (service) {
|
|
677
|
+
args.push('--service', service);
|
|
678
|
+
}
|
|
679
|
+
if (agent) {
|
|
680
|
+
args.push('--agent', agent);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const result = await execFileAsync('node', args, { env });
|
|
684
|
+
const output = parseCliOutput(result.stdout);
|
|
685
|
+
|
|
686
|
+
if (!output.success) {
|
|
687
|
+
throw new Error(`Discovery failed: ${output.error}`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return output.data;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function handleBalance(env, cliPath) {
|
|
694
|
+
const result = await execFileAsync('node', [cliPath, 'balance'], { env });
|
|
695
|
+
const output = parseCliOutput(result.stdout);
|
|
696
|
+
|
|
697
|
+
if (!output.success) {
|
|
698
|
+
throw new Error(`Balance check failed: ${output.error}`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return output.data;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function handleStatus(env, cliPath) {
|
|
705
|
+
try {
|
|
706
|
+
// Get identity
|
|
707
|
+
const identityResult = await execFileAsync('node', [cliPath, 'identity'], { env });
|
|
708
|
+
const identity = parseCliOutput(identityResult.stdout);
|
|
709
|
+
|
|
710
|
+
// Get balance
|
|
711
|
+
const balanceResult = await execFileAsync('node', [cliPath, 'balance'], { env });
|
|
712
|
+
const balance = parseCliOutput(balanceResult.stdout);
|
|
713
|
+
|
|
714
|
+
// Get services
|
|
715
|
+
const servicesResult = await execFileAsync('node', [cliPath, 'services'], { env });
|
|
716
|
+
const services = parseCliOutput(servicesResult.stdout);
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
identity: identity.data,
|
|
720
|
+
balance: balance.data,
|
|
721
|
+
services: services.data
|
|
722
|
+
};
|
|
723
|
+
} catch (error) {
|
|
724
|
+
throw new Error(`Status check failed: ${error.message}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function handleDirectPay(params, env, cliPath, config) {
|
|
729
|
+
const { identityKey, sats, description } = params;
|
|
730
|
+
const walletDir = config?.walletDir || path.join(process.env.HOME || '', '.clawdbot', 'bsv-wallet');
|
|
731
|
+
|
|
732
|
+
if (!identityKey || !sats) {
|
|
733
|
+
throw new Error("identityKey and sats are required for pay action");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Check daily budget
|
|
737
|
+
const dailyLimit = config?.dailyBudgetSats || 1000;
|
|
738
|
+
const budgetCheck = checkBudget(walletDir, sats, dailyLimit);
|
|
739
|
+
if (!budgetCheck.allowed) {
|
|
740
|
+
throw new Error(`Payment would exceed daily budget. Spent: ${budgetCheck.spent} sats, Remaining: ${budgetCheck.remaining} sats, Requested: ${sats} sats. Please confirm with user.`);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const args = [cliPath, 'pay', identityKey, sats.toString()];
|
|
744
|
+
if (description) {
|
|
745
|
+
args.push(description);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const result = await execFileAsync('node', args, { env });
|
|
749
|
+
const output = parseCliOutput(result.stdout);
|
|
750
|
+
|
|
751
|
+
if (!output.success) {
|
|
752
|
+
throw new Error(`Payment failed: ${output.error}`);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Record the spending
|
|
756
|
+
recordSpend(walletDir, sats, 'direct-payment', identityKey);
|
|
757
|
+
|
|
758
|
+
return output.data;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function handleSetup(env, cliPath) {
|
|
762
|
+
const result = await execFileAsync('node', [cliPath, 'setup'], { env });
|
|
763
|
+
const output = parseCliOutput(result.stdout);
|
|
764
|
+
|
|
765
|
+
if (!output.success) {
|
|
766
|
+
throw new Error(`Setup failed: ${output.error}`);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return output.data;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function handleAddress(env, cliPath) {
|
|
773
|
+
const result = await execFileAsync('node', [cliPath, 'address'], { env });
|
|
774
|
+
const output = parseCliOutput(result.stdout);
|
|
775
|
+
|
|
776
|
+
if (!output.success) {
|
|
777
|
+
throw new Error(`Address failed: ${output.error}`);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return output.data;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function handleImport(params, env, cliPath) {
|
|
784
|
+
const { txid, vout } = params;
|
|
785
|
+
|
|
786
|
+
if (!txid) {
|
|
787
|
+
throw new Error("txid is required for import action");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const args = [cliPath, 'import', txid];
|
|
791
|
+
if (vout !== undefined) {
|
|
792
|
+
args.push(vout.toString());
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const result = await execFileAsync('node', args, { env });
|
|
796
|
+
const output = parseCliOutput(result.stdout);
|
|
797
|
+
|
|
798
|
+
if (!output.success) {
|
|
799
|
+
throw new Error(`Import failed: ${output.error}`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return output.data;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async function handleRegister(env, cliPath) {
|
|
806
|
+
const result = await execFileAsync('node', [cliPath, 'register'], { env });
|
|
807
|
+
const output = parseCliOutput(result.stdout);
|
|
808
|
+
|
|
809
|
+
if (!output.success) {
|
|
810
|
+
throw new Error(`Registration failed: ${output.error}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return {
|
|
814
|
+
...output.data,
|
|
815
|
+
registered: true,
|
|
816
|
+
availableServices: [
|
|
817
|
+
{ serviceId: "tell-joke", name: "Random Joke", description: "Get a random joke", suggestedPrice: 5 },
|
|
818
|
+
{ serviceId: "code-review", name: "Code Review", description: "Review code for bugs, security, and style", suggestedPrice: 50 },
|
|
819
|
+
{ serviceId: "web-research", name: "Web Research", description: "Research a topic using web sources", suggestedPrice: 50 },
|
|
820
|
+
{ serviceId: "translate", name: "Translation", description: "Translate text between languages", suggestedPrice: 20 },
|
|
821
|
+
{ serviceId: "api-proxy", name: "API Proxy", description: "Proxy requests to public APIs", suggestedPrice: 15 },
|
|
822
|
+
{ serviceId: "roulette", name: "Roulette", description: "Casino-style roulette game", suggestedPrice: 10 },
|
|
823
|
+
{ serviceId: "memory-store", name: "Memory Store", description: "Key-value storage for agents", suggestedPrice: 10 },
|
|
824
|
+
{ serviceId: "code-develop", name: "Code Development", description: "Generate code from requirements", suggestedPrice: 100 }
|
|
825
|
+
],
|
|
826
|
+
nextStep: "Choose which services to advertise. Call overlay({ action: 'advertise', ... }) for each."
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async function handleAdvertise(params, env, cliPath) {
|
|
831
|
+
const { serviceId, name, description, priceSats } = params;
|
|
832
|
+
|
|
833
|
+
if (!serviceId || !name || !description || priceSats === undefined) {
|
|
834
|
+
throw new Error("serviceId, name, description, and priceSats are required for advertise action");
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const result = await execFileAsync('node', [cliPath, 'advertise', serviceId, name, description, priceSats.toString()], { env });
|
|
838
|
+
const output = parseCliOutput(result.stdout);
|
|
839
|
+
|
|
840
|
+
if (!output.success) {
|
|
841
|
+
throw new Error(`Advertise failed: ${output.error}`);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return output.data;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function handleReadvertise(params, env, cliPath) {
|
|
848
|
+
const { serviceId, newPrice, newName, newDesc } = params;
|
|
849
|
+
|
|
850
|
+
if (!serviceId || newPrice === undefined) {
|
|
851
|
+
throw new Error("serviceId and newPrice are required for readvertise action");
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const args = [cliPath, 'readvertise', serviceId, newPrice.toString()];
|
|
855
|
+
if (newName) {
|
|
856
|
+
args.push(newName);
|
|
857
|
+
}
|
|
858
|
+
if (newDesc) {
|
|
859
|
+
args.push(newDesc);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const result = await execFileAsync('node', args, { env });
|
|
863
|
+
const output = parseCliOutput(result.stdout);
|
|
864
|
+
|
|
865
|
+
if (!output.success) {
|
|
866
|
+
throw new Error(`Readvertise failed: ${output.error}`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return output.data;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async function handleRemove(params, env, cliPath) {
|
|
873
|
+
const { serviceId } = params;
|
|
874
|
+
|
|
875
|
+
if (!serviceId) {
|
|
876
|
+
throw new Error("serviceId is required for remove action");
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const result = await execFileAsync('node', [cliPath, 'remove', serviceId], { env });
|
|
880
|
+
const output = parseCliOutput(result.stdout);
|
|
881
|
+
|
|
882
|
+
if (!output.success) {
|
|
883
|
+
throw new Error(`Remove failed: ${output.error}`);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return output.data;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function handleSend(params, env, cliPath) {
|
|
890
|
+
const { identityKey, messageType, payload } = params;
|
|
891
|
+
|
|
892
|
+
if (!identityKey || !messageType || !payload) {
|
|
893
|
+
throw new Error("identityKey, messageType, and payload are required for send action");
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const result = await execFileAsync('node', [cliPath, 'send', identityKey, messageType, JSON.stringify(payload)], { env });
|
|
897
|
+
const output = parseCliOutput(result.stdout);
|
|
898
|
+
|
|
899
|
+
if (!output.success) {
|
|
900
|
+
throw new Error(`Send failed: ${output.error}`);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return output.data;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function handleInbox(env, cliPath) {
|
|
907
|
+
const result = await execFileAsync('node', [cliPath, 'inbox'], { env });
|
|
908
|
+
const output = parseCliOutput(result.stdout);
|
|
909
|
+
|
|
910
|
+
if (!output.success) {
|
|
911
|
+
throw new Error(`Inbox failed: ${output.error}`);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return output.data;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async function handleServices(env, cliPath) {
|
|
918
|
+
const result = await execFileAsync('node', [cliPath, 'services'], { env });
|
|
919
|
+
const output = parseCliOutput(result.stdout);
|
|
920
|
+
|
|
921
|
+
if (!output.success) {
|
|
922
|
+
throw new Error(`Services failed: ${output.error}`);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return output.data;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function handleRefund(params, env, cliPath) {
|
|
929
|
+
const { address } = params;
|
|
930
|
+
|
|
931
|
+
if (!address) {
|
|
932
|
+
throw new Error("address is required for refund action");
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const result = await execFileAsync('node', [cliPath, 'refund', address], { env });
|
|
936
|
+
const output = parseCliOutput(result.stdout);
|
|
937
|
+
|
|
938
|
+
if (!output.success) {
|
|
939
|
+
throw new Error(`Refund failed: ${output.error}`);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return output.data;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function handleOnboard(env, cliPath) {
|
|
946
|
+
const steps = [];
|
|
947
|
+
|
|
948
|
+
// Step 1: Setup wallet
|
|
949
|
+
try {
|
|
950
|
+
const setup = await execFileAsync('node', [cliPath, 'setup'], { env });
|
|
951
|
+
const setupOutput = parseCliOutput(setup.stdout);
|
|
952
|
+
steps.push({ step: 'setup', success: true, identityKey: setupOutput.data?.identityKey });
|
|
953
|
+
} catch (err) {
|
|
954
|
+
steps.push({ step: 'setup', success: false, error: err.message });
|
|
955
|
+
return { steps, nextStep: 'Fix wallet setup error and try again' };
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Step 2: Get address
|
|
959
|
+
try {
|
|
960
|
+
const addr = await execFileAsync('node', [cliPath, 'address'], { env });
|
|
961
|
+
const addrOutput = parseCliOutput(addr.stdout);
|
|
962
|
+
steps.push({ step: 'address', success: true, address: addrOutput.data?.address });
|
|
963
|
+
} catch (err) {
|
|
964
|
+
steps.push({ step: 'address', success: false, error: err.message });
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Step 3: Check balance
|
|
968
|
+
try {
|
|
969
|
+
const bal = await execFileAsync('node', [cliPath, 'balance'], { env });
|
|
970
|
+
const balOutput = parseCliOutput(bal.stdout);
|
|
971
|
+
const balance = balOutput.data?.walletBalance || balOutput.data?.onChain?.confirmed || 0;
|
|
972
|
+
steps.push({ step: 'balance', success: true, balance });
|
|
973
|
+
|
|
974
|
+
if (balance < 1000) {
|
|
975
|
+
return {
|
|
976
|
+
steps,
|
|
977
|
+
funded: false,
|
|
978
|
+
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.`
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
} catch (err) {
|
|
982
|
+
steps.push({ step: 'balance', success: false, error: err.message });
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Step 4: Register
|
|
986
|
+
try {
|
|
987
|
+
const reg = await execFileAsync('node', [cliPath, 'register'], { env, timeout: 60000 });
|
|
988
|
+
const regOutput = parseCliOutput(reg.stdout);
|
|
989
|
+
steps.push({ step: 'register', success: regOutput.success, data: regOutput.data });
|
|
990
|
+
} catch (err) {
|
|
991
|
+
steps.push({ step: 'register', success: false, error: err.message });
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return {
|
|
995
|
+
steps,
|
|
996
|
+
funded: true,
|
|
997
|
+
registered: true,
|
|
998
|
+
availableServices: [
|
|
999
|
+
{ serviceId: "tell-joke", name: "Random Joke", description: "Get a random joke", suggestedPrice: 5 },
|
|
1000
|
+
{ serviceId: "code-review", name: "Code Review", description: "Review code for bugs, security, and style", suggestedPrice: 50 },
|
|
1001
|
+
{ serviceId: "web-research", name: "Web Research", description: "Research a topic using web sources", suggestedPrice: 50 },
|
|
1002
|
+
{ serviceId: "translate", name: "Translation", description: "Translate text between languages", suggestedPrice: 20 },
|
|
1003
|
+
{ serviceId: "api-proxy", name: "API Proxy", description: "Proxy requests to public APIs", suggestedPrice: 15 },
|
|
1004
|
+
{ serviceId: "roulette", name: "Roulette", description: "Casino-style roulette game", suggestedPrice: 10 },
|
|
1005
|
+
{ serviceId: "memory-store", name: "Memory Store", description: "Key-value storage for agents", suggestedPrice: 10 },
|
|
1006
|
+
{ serviceId: "code-develop", name: "Code Development", description: "Generate code from requirements", suggestedPrice: 100 }
|
|
1007
|
+
],
|
|
1008
|
+
nextStep: "Choose which services to advertise. Call overlay({ action: 'advertise', ... }) for each.",
|
|
1009
|
+
message: 'Onboarding complete! Your agent is registered on the BSV overlay network. The background service will handle incoming requests.'
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async function handlePendingRequests(env, cliPath) {
|
|
1014
|
+
const result = await execFileAsync('node', [cliPath, 'service-queue'], { env });
|
|
1015
|
+
const output = parseCliOutput(result.stdout);
|
|
1016
|
+
if (!output.success) throw new Error(`Queue check failed: ${output.error}`);
|
|
1017
|
+
return output.data;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async function handleFulfill(params, env, cliPath) {
|
|
1021
|
+
const { requestId, recipientKey, serviceId, result } = params;
|
|
1022
|
+
if (!requestId || !recipientKey || !serviceId || !result) {
|
|
1023
|
+
throw new Error("requestId, recipientKey, serviceId, and result are required");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const cliResult = await execFileAsync('node', [
|
|
1027
|
+
cliPath, 'respond-service', requestId, recipientKey, serviceId, JSON.stringify(result)
|
|
1028
|
+
], { env });
|
|
1029
|
+
const output = parseCliOutput(cliResult.stdout);
|
|
1030
|
+
if (!output.success) throw new Error(`Fulfill failed: ${output.error}`);
|
|
1031
|
+
return output.data;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function buildEnvironment(config) {
|
|
1035
|
+
const env = { ...process.env };
|
|
1036
|
+
|
|
1037
|
+
if (config.walletDir) {
|
|
1038
|
+
env.BSV_WALLET_DIR = config.walletDir;
|
|
1039
|
+
}
|
|
1040
|
+
if (config.overlayUrl) {
|
|
1041
|
+
env.OVERLAY_URL = config.overlayUrl;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Set defaults
|
|
1045
|
+
env.BSV_NETWORK = env.BSV_NETWORK || 'mainnet';
|
|
1046
|
+
env.AGENT_NAME = env.AGENT_NAME || 'clawdbot-agent';
|
|
1047
|
+
env.AGENT_ROUTED = 'true'; // Route service requests through the agent
|
|
1048
|
+
|
|
1049
|
+
return env;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function parseCliOutput(stdout) {
|
|
1053
|
+
try {
|
|
1054
|
+
return JSON.parse(stdout.trim());
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
throw new Error(`Failed to parse CLI output: ${error.message}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function sleep(ms) {
|
|
1061
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1062
|
+
}
|