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