nervepay 1.3.4 → 1.3.5
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/bin/nervepay-cli.js +450 -491
- package/package.json +1 -1
package/bin/nervepay-cli.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* NervePay CLI -
|
|
4
|
-
*
|
|
3
|
+
* NervePay CLI - Self-sovereign identity for AI agents
|
|
4
|
+
* https://nervepay.xyz
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { readFile, writeFile } from 'fs/promises';
|
|
7
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
8
9
|
import { homedir } from 'os';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
10
12
|
import { Command } from 'commander';
|
|
11
13
|
import inquirer from 'inquirer';
|
|
12
14
|
import chalk from 'chalk';
|
|
@@ -15,86 +17,231 @@ import * as identity from '../dist/tools/identity.js';
|
|
|
15
17
|
import * as gateway from '../dist/tools/gateway.js';
|
|
16
18
|
import * as vault from '../dist/tools/vault.js';
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Constants
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const PKG = JSON.parse(await readFile(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
26
|
+
const VERSION = PKG.version;
|
|
27
|
+
|
|
28
|
+
const NERVEPAY_DIR = join(homedir(), '.nervepay');
|
|
29
|
+
const NERVEPAY_CREDS_PATH = join(NERVEPAY_DIR, 'credentials.json');
|
|
30
|
+
const DEFAULT_API_URL = 'https://api.nervepay.xyz';
|
|
31
|
+
|
|
32
|
+
let verbose = false;
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Formatting helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
const dim = chalk.gray;
|
|
39
|
+
const ok = chalk.green;
|
|
40
|
+
const warn = chalk.yellow;
|
|
41
|
+
const err = chalk.red;
|
|
42
|
+
const info = chalk.cyan;
|
|
43
|
+
const bold = chalk.bold;
|
|
44
|
+
const muted = chalk.dim;
|
|
45
|
+
|
|
46
|
+
function banner() {
|
|
47
|
+
console.log();
|
|
48
|
+
console.log(bold.cyan(` NervePay CLI v${VERSION}`));
|
|
49
|
+
console.log(dim(' Self-sovereign identity for AI agents'));
|
|
50
|
+
console.log();
|
|
51
|
+
}
|
|
19
52
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
53
|
+
function log(...args) { console.log(' ', ...args); }
|
|
54
|
+
function logOk(msg) { log(ok('~'), msg); }
|
|
55
|
+
function logErr(msg) { log(err('!'), msg); }
|
|
56
|
+
function logWarn(msg) { log(warn('~'), msg); }
|
|
57
|
+
function logInfo(msg) { log(info('>'), msg); }
|
|
58
|
+
function logDebug(...args) { if (verbose) log(dim('[debug]'), ...args); }
|
|
59
|
+
function field(label, value) { log(dim(`${label}:`), value); }
|
|
60
|
+
|
|
61
|
+
function die(msg, hint) {
|
|
62
|
+
logErr(msg);
|
|
63
|
+
if (hint) log(dim(hint));
|
|
64
|
+
console.log();
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function divider() { console.log(dim(' ' + '-'.repeat(50))); }
|
|
26
69
|
|
|
27
|
-
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Config discovery
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
function findOpenClawConfigPath() {
|
|
28
75
|
if (process.env.OPENCLAW_CONFIG_PATH && existsSync(process.env.OPENCLAW_CONFIG_PATH)) {
|
|
29
76
|
return process.env.OPENCLAW_CONFIG_PATH;
|
|
30
77
|
}
|
|
31
|
-
|
|
32
78
|
const candidates = [
|
|
33
79
|
join(homedir(), '.openclaw', 'openclaw.json'),
|
|
34
80
|
join(homedir(), '.config', 'openclaw', 'openclaw.json'),
|
|
35
|
-
join(homedir(), '.moltbot', 'openclaw.json'),
|
|
36
|
-
join(homedir(), '.clawdbot', 'openclaw.json'),
|
|
81
|
+
join(homedir(), '.moltbot', 'openclaw.json'),
|
|
82
|
+
join(homedir(), '.clawdbot', 'openclaw.json'),
|
|
37
83
|
];
|
|
38
|
-
|
|
39
84
|
for (const p of candidates) {
|
|
40
85
|
if (existsSync(p)) return p;
|
|
41
86
|
}
|
|
42
87
|
return null;
|
|
43
88
|
}
|
|
44
89
|
|
|
45
|
-
/**
|
|
46
|
-
* Read OpenClaw config, returns empty object if not found.
|
|
47
|
-
*/
|
|
48
90
|
async function readOpenClawConfig() {
|
|
49
|
-
const configPath =
|
|
91
|
+
const configPath = findOpenClawConfigPath();
|
|
50
92
|
if (!configPath) return {};
|
|
51
|
-
try {
|
|
52
|
-
|
|
53
|
-
} catch {
|
|
54
|
-
return {};
|
|
55
|
-
}
|
|
93
|
+
try { return JSON.parse(await readFile(configPath, 'utf-8')); }
|
|
94
|
+
catch { return {}; }
|
|
56
95
|
}
|
|
57
96
|
|
|
58
|
-
/**
|
|
59
|
-
* Extract gateway URL and token from OpenClaw config.
|
|
60
|
-
* Follows OpenClaw's config schema:
|
|
61
|
-
* - Remote mode: gateway.remote.url + gateway.remote.token
|
|
62
|
-
* - Local mode: gateway.auth.token + gateway.port (default 18789)
|
|
63
|
-
* - Env vars: OPENCLAW_GATEWAY_TOKEN, OPENCLAW_GATEWAY_PORT
|
|
64
|
-
*/
|
|
65
97
|
function extractGatewayConfig(config) {
|
|
66
98
|
let url = null;
|
|
67
99
|
let token = null;
|
|
68
100
|
|
|
69
101
|
if (config.gateway?.mode === 'remote' && config.gateway?.remote?.url) {
|
|
70
|
-
// Remote mode — CLI connects to a remote gateway
|
|
71
102
|
url = config.gateway.remote.url;
|
|
72
103
|
token = config.gateway.remote.token;
|
|
73
104
|
} else if (config.gateway?.auth?.token) {
|
|
74
|
-
// Local mode — gateway runs on this machine
|
|
75
105
|
const port = config.gateway?.port || 18789;
|
|
76
106
|
url = `http://127.0.0.1:${port}`;
|
|
77
107
|
token = config.gateway.auth.token;
|
|
78
108
|
}
|
|
79
109
|
|
|
80
|
-
|
|
81
|
-
if (process.env.
|
|
82
|
-
token = token || process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
83
|
-
}
|
|
84
|
-
const envPort = process.env.OPENCLAW_GATEWAY_PORT;
|
|
85
|
-
if (envPort && !url) {
|
|
86
|
-
url = `http://127.0.0.1:${envPort}`;
|
|
87
|
-
}
|
|
110
|
+
if (process.env.OPENCLAW_GATEWAY_TOKEN) token = token || process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
111
|
+
if (process.env.OPENCLAW_GATEWAY_PORT && !url) url = `http://127.0.0.1:${process.env.OPENCLAW_GATEWAY_PORT}`;
|
|
88
112
|
|
|
89
113
|
return { url, token };
|
|
90
114
|
}
|
|
91
115
|
|
|
116
|
+
async function loadConfig() {
|
|
117
|
+
try {
|
|
118
|
+
const configPath = findOpenClawConfigPath();
|
|
119
|
+
if (configPath) {
|
|
120
|
+
const oc = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
121
|
+
const pc = oc.plugins?.entries?.nervepay?.config || {};
|
|
122
|
+
if (pc.agentDid && pc.privateKey) {
|
|
123
|
+
return { apiUrl: pc.apiUrl || DEFAULT_API_URL, agentDid: pc.agentDid, privateKey: pc.privateKey };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch { /* fall through */ }
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const creds = JSON.parse(await readFile(NERVEPAY_CREDS_PATH, 'utf-8'));
|
|
130
|
+
if (creds.agent_did && creds.private_key) {
|
|
131
|
+
return { apiUrl: creds.api_url || DEFAULT_API_URL, agentDid: creds.agent_did, privateKey: creds.private_key };
|
|
132
|
+
}
|
|
133
|
+
} catch { /* fall through */ }
|
|
134
|
+
|
|
135
|
+
throw new Error('No credentials found. Run: nervepay setup');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function httpToWs(url) {
|
|
139
|
+
let u = url.replace(/\/$/, '');
|
|
140
|
+
if (u.startsWith('https://')) return 'wss://' + u.slice(8);
|
|
141
|
+
if (u.startsWith('http://')) return 'ws://' + u.slice(7);
|
|
142
|
+
if (!u.startsWith('ws://') && !u.startsWith('wss://')) return 'ws://' + u;
|
|
143
|
+
return u;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function saveCredentials(agentDid, privateKey, mnemonic, apiUrl) {
|
|
147
|
+
await mkdir(NERVEPAY_DIR, { recursive: true });
|
|
148
|
+
await writeFile(NERVEPAY_CREDS_PATH, JSON.stringify({
|
|
149
|
+
agent_did: agentDid,
|
|
150
|
+
private_key: privateKey,
|
|
151
|
+
...(mnemonic && { mnemonic }),
|
|
152
|
+
api_url: apiUrl,
|
|
153
|
+
created_at: new Date().toISOString(),
|
|
154
|
+
}, null, 2));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function updateOpenClawConfig(agentDid, privateKey, apiUrl) {
|
|
158
|
+
const configPath = findOpenClawConfigPath();
|
|
159
|
+
if (!configPath) return false;
|
|
160
|
+
try {
|
|
161
|
+
const config = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
162
|
+
if (!config.plugins) config.plugins = {};
|
|
163
|
+
if (!config.plugins.entries) config.plugins.entries = {};
|
|
164
|
+
if (!config.plugins.entries.nervepay) config.plugins.entries.nervepay = { enabled: true, config: {} };
|
|
165
|
+
config.plugins.entries.nervepay.config = {
|
|
166
|
+
...config.plugins.entries.nervepay.config,
|
|
167
|
+
apiUrl, agentDid, privateKey,
|
|
168
|
+
};
|
|
169
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
170
|
+
return true;
|
|
171
|
+
} catch { return false; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// CLI program
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
92
178
|
const program = new Command();
|
|
93
179
|
|
|
94
180
|
program
|
|
95
181
|
.name('nervepay')
|
|
96
|
-
.description('
|
|
97
|
-
.version('
|
|
182
|
+
.description('Self-sovereign identity for AI agents')
|
|
183
|
+
.version(VERSION, '-v, --version')
|
|
184
|
+
.option('--verbose', 'Show debug output')
|
|
185
|
+
.hook('preAction', (thisCommand) => {
|
|
186
|
+
verbose = !!thisCommand.opts().verbose;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// STATUS COMMAND
|
|
191
|
+
// ============================================================================
|
|
192
|
+
|
|
193
|
+
program
|
|
194
|
+
.command('status')
|
|
195
|
+
.description('Show current configuration and connection status')
|
|
196
|
+
.action(async () => {
|
|
197
|
+
banner();
|
|
198
|
+
|
|
199
|
+
// Credentials
|
|
200
|
+
let config;
|
|
201
|
+
try {
|
|
202
|
+
config = await loadConfig();
|
|
203
|
+
logOk('Agent identity configured');
|
|
204
|
+
field(' DID', config.agentDid);
|
|
205
|
+
field(' API', config.apiUrl);
|
|
206
|
+
} catch {
|
|
207
|
+
logWarn('No agent identity configured');
|
|
208
|
+
log(dim('Run'), info('nervepay setup'), dim('to get started'));
|
|
209
|
+
console.log();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// OpenClaw config
|
|
214
|
+
const ocPath = findOpenClawConfigPath();
|
|
215
|
+
if (ocPath) {
|
|
216
|
+
logOk(`OpenClaw config found`);
|
|
217
|
+
field(' Path', ocPath);
|
|
218
|
+
const oc = await readOpenClawConfig();
|
|
219
|
+
const gw = extractGatewayConfig(oc);
|
|
220
|
+
if (gw.url) field(' Gateway', gw.url);
|
|
221
|
+
if (gw.token) field(' Token', gw.token.slice(0, 12) + '...');
|
|
222
|
+
} else {
|
|
223
|
+
logWarn('No OpenClaw config found');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Credentials file
|
|
227
|
+
if (existsSync(NERVEPAY_CREDS_PATH)) {
|
|
228
|
+
logOk(`Credentials saved`);
|
|
229
|
+
field(' Path', NERVEPAY_CREDS_PATH);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// API connectivity
|
|
233
|
+
try {
|
|
234
|
+
const client = new NervePayClient(config);
|
|
235
|
+
const result = await identity.whoami(client);
|
|
236
|
+
logOk('API connection verified');
|
|
237
|
+
field(' Name', result.name);
|
|
238
|
+
field(' Reputation', `${result.reputation_score}/100`);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
logWarn(`API unreachable: ${e.message}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log();
|
|
244
|
+
});
|
|
98
245
|
|
|
99
246
|
// ============================================================================
|
|
100
247
|
// SETUP COMMAND
|
|
@@ -102,183 +249,124 @@ program
|
|
|
102
249
|
|
|
103
250
|
program
|
|
104
251
|
.command('setup')
|
|
105
|
-
.description('
|
|
106
|
-
.option('--api-url <url>', 'NervePay API URL',
|
|
107
|
-
.option('--gateway-url <url>', 'Gateway URL
|
|
108
|
-
.option('--gateway-token <token>', 'Gateway token
|
|
252
|
+
.description('Create agent identity and pair gateway')
|
|
253
|
+
.option('--api-url <url>', 'NervePay API URL', DEFAULT_API_URL)
|
|
254
|
+
.option('--gateway-url <url>', 'Gateway URL override')
|
|
255
|
+
.option('--gateway-token <token>', 'Gateway token override')
|
|
109
256
|
.action(async (options) => {
|
|
110
|
-
|
|
257
|
+
banner();
|
|
258
|
+
logInfo('Setup wizard');
|
|
259
|
+
console.log();
|
|
111
260
|
|
|
112
261
|
try {
|
|
113
|
-
// Read OpenClaw config (gracefully handles missing file)
|
|
114
262
|
const openclawConfig = await readOpenClawConfig();
|
|
115
|
-
const configPath =
|
|
116
|
-
|
|
117
|
-
// Extract gateway config from OpenClaw config + env vars
|
|
263
|
+
const configPath = findOpenClawConfigPath();
|
|
118
264
|
const gw = extractGatewayConfig(openclawConfig);
|
|
119
265
|
let gatewayUrl = options.gatewayUrl || gw.url;
|
|
120
266
|
let gatewayToken = options.gatewayToken || gw.token;
|
|
121
267
|
|
|
122
268
|
if (gatewayUrl) {
|
|
123
|
-
|
|
269
|
+
logOk(`Found OpenClaw gateway: ${gatewayUrl}`);
|
|
124
270
|
} else {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
console.log(chalk.cyan(' nervepay pair --code <CODE> --gateway-token <TOKEN>\n'));
|
|
271
|
+
logWarn('No OpenClaw gateway detected');
|
|
272
|
+
log(dim('You can pair later with:'), info('nervepay pair'));
|
|
128
273
|
}
|
|
129
274
|
|
|
130
|
-
|
|
275
|
+
console.log();
|
|
131
276
|
const answers = await inquirer.prompt([
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
name: 'name',
|
|
135
|
-
message: 'Agent name:',
|
|
136
|
-
default: 'My AI Agent',
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
type: 'input',
|
|
140
|
-
name: 'description',
|
|
141
|
-
message: 'Agent description:',
|
|
142
|
-
default: 'AI agent with self-sovereign identity',
|
|
143
|
-
},
|
|
277
|
+
{ type: 'input', name: 'name', message: 'Agent name:', default: 'My AI Agent' },
|
|
278
|
+
{ type: 'input', name: 'description', message: 'Description:', default: 'AI agent with self-sovereign identity' },
|
|
144
279
|
]);
|
|
145
280
|
|
|
146
|
-
console.log(
|
|
281
|
+
console.log();
|
|
282
|
+
logInfo('Registering agent identity...');
|
|
147
283
|
|
|
148
|
-
// Create client (without credentials initially)
|
|
149
284
|
const client = new NervePayClient({ apiUrl: options.apiUrl });
|
|
150
|
-
|
|
151
|
-
// Register pending identity
|
|
152
|
-
const registration = await identity.registerPendingIdentity(client, {
|
|
285
|
+
const reg = await identity.registerPendingIdentity(client, {
|
|
153
286
|
name: answers.name,
|
|
154
287
|
description: answers.description,
|
|
155
288
|
});
|
|
156
289
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
console.log(chalk.gray(' Session ID:'), registration.session_id);
|
|
160
|
-
console.log(chalk.gray(' Claim URL:'), registration.claim_url);
|
|
290
|
+
logOk('Agent identity created');
|
|
291
|
+
field(' DID', reg.did);
|
|
161
292
|
|
|
162
|
-
console.log(
|
|
163
|
-
|
|
164
|
-
console.log(
|
|
293
|
+
console.log();
|
|
294
|
+
logInfo('Claim this agent in your browser:');
|
|
295
|
+
console.log(bold(` ${reg.claim_url}`));
|
|
296
|
+
console.log();
|
|
165
297
|
|
|
166
|
-
// Try to open in browser
|
|
167
298
|
try {
|
|
168
299
|
const open = await import('open');
|
|
169
|
-
await open.default(
|
|
300
|
+
await open.default(reg.claim_url);
|
|
301
|
+
log(dim('Opened in browser'));
|
|
170
302
|
} catch {
|
|
171
|
-
|
|
303
|
+
log(dim('Open the URL above in your browser'));
|
|
172
304
|
}
|
|
173
305
|
|
|
174
|
-
|
|
175
|
-
|
|
306
|
+
console.log();
|
|
307
|
+
logInfo('Waiting for claim...');
|
|
176
308
|
|
|
177
309
|
let claimed = false;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
while (!claimed && attempts < maxAttempts) {
|
|
182
|
-
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
183
|
-
attempts++;
|
|
184
|
-
|
|
310
|
+
for (let i = 0; i < 60; i++) {
|
|
311
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
185
312
|
try {
|
|
186
|
-
const
|
|
187
|
-
const data = await
|
|
188
|
-
|
|
189
|
-
if (data.status === '
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
} else if (data.status === 'expired') {
|
|
193
|
-
console.error(chalk.red('❌ Claim session expired'));
|
|
194
|
-
process.exit(1);
|
|
195
|
-
} else {
|
|
196
|
-
process.stdout.write(chalk.gray('.'));
|
|
197
|
-
}
|
|
198
|
-
} catch (e) {
|
|
199
|
-
process.stdout.write(chalk.gray('.'));
|
|
200
|
-
}
|
|
313
|
+
const resp = await fetch(`${options.apiUrl}/v1/agent-identity/register-pending/${reg.session_id}/status`);
|
|
314
|
+
const data = await resp.json();
|
|
315
|
+
if (data.status === 'claimed') { claimed = true; break; }
|
|
316
|
+
if (data.status === 'expired') { break; }
|
|
317
|
+
process.stdout.write(dim('.'));
|
|
318
|
+
} catch { process.stdout.write(dim('.')); }
|
|
201
319
|
}
|
|
202
320
|
|
|
203
|
-
if (
|
|
204
|
-
console.log(
|
|
205
|
-
|
|
321
|
+
if (claimed) {
|
|
322
|
+
console.log();
|
|
323
|
+
logOk('Agent claimed');
|
|
324
|
+
} else {
|
|
325
|
+
console.log();
|
|
326
|
+
logWarn('Not claimed yet — you can claim later');
|
|
327
|
+
log(dim(reg.claim_url));
|
|
206
328
|
}
|
|
207
329
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
});
|
|
330
|
+
client.updateConfig({ agentDid: reg.did, privateKey: reg.private_key });
|
|
331
|
+
|
|
332
|
+
await saveCredentials(reg.did, reg.private_key, reg.mnemonic, options.apiUrl);
|
|
333
|
+
logOk('Credentials saved');
|
|
213
334
|
|
|
214
|
-
// Save credentials to ~/.nervepay/
|
|
215
|
-
const { mkdir } = await import('fs/promises');
|
|
216
|
-
await mkdir(join(homedir(), '.nervepay'), { recursive: true });
|
|
217
|
-
await writeFile(
|
|
218
|
-
NERVEPAY_CREDS_PATH,
|
|
219
|
-
JSON.stringify(
|
|
220
|
-
{
|
|
221
|
-
agent_did: registration.did,
|
|
222
|
-
private_key: registration.private_key,
|
|
223
|
-
mnemonic: registration.mnemonic,
|
|
224
|
-
api_url: options.apiUrl,
|
|
225
|
-
created_at: new Date().toISOString(),
|
|
226
|
-
},
|
|
227
|
-
null,
|
|
228
|
-
2
|
|
229
|
-
)
|
|
230
|
-
);
|
|
231
|
-
console.log(chalk.green('✓ Credentials saved to ~/.nervepay/credentials.json'));
|
|
232
|
-
|
|
233
|
-
// Update OpenClaw config if it exists
|
|
234
335
|
if (configPath) {
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
if (!openclawConfig.plugins.entries.nervepay) {
|
|
238
|
-
openclawConfig.plugins.entries.nervepay = { enabled: true, config: {} };
|
|
336
|
+
if (await updateOpenClawConfig(reg.did, reg.private_key, options.apiUrl)) {
|
|
337
|
+
logOk('OpenClaw config updated');
|
|
239
338
|
}
|
|
240
|
-
|
|
241
|
-
openclawConfig.plugins.entries.nervepay.config = {
|
|
242
|
-
...openclawConfig.plugins.entries.nervepay.config,
|
|
243
|
-
apiUrl: options.apiUrl,
|
|
244
|
-
agentDid: registration.did,
|
|
245
|
-
privateKey: registration.private_key,
|
|
246
|
-
enableOrchestration: true,
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
await writeFile(configPath, JSON.stringify(openclawConfig, null, 2));
|
|
250
|
-
console.log(chalk.green('✓ OpenClaw config updated'));
|
|
251
339
|
}
|
|
252
340
|
|
|
253
|
-
// Send pairing request if gateway available
|
|
254
341
|
if (gatewayUrl && gatewayToken) {
|
|
255
|
-
console.log(
|
|
256
|
-
|
|
257
|
-
|
|
342
|
+
console.log();
|
|
343
|
+
logInfo('Sending gateway pairing request...');
|
|
344
|
+
await gateway.createPairingRequest(client, {
|
|
258
345
|
gateway_name: 'OpenClaw Gateway',
|
|
259
346
|
gateway_url: gatewayUrl,
|
|
260
347
|
max_concurrent_agents: 8,
|
|
261
348
|
default_timeout_seconds: 3600,
|
|
262
349
|
});
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
console.log(chalk.yellow(' Approve in Mission Control > Task Board > Incoming tab'));
|
|
350
|
+
logOk('Pairing request sent');
|
|
351
|
+
log(dim('Approve in Mission Control > Task Board > Incoming'));
|
|
266
352
|
}
|
|
267
353
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
354
|
+
divider();
|
|
355
|
+
logOk(bold('Setup complete'));
|
|
356
|
+
console.log();
|
|
357
|
+
|
|
358
|
+
if (!gatewayUrl) {
|
|
359
|
+
log(dim('Next:'), info('nervepay pair --gateway-url <url>'));
|
|
273
360
|
} else {
|
|
274
|
-
|
|
361
|
+
log(dim('Next:'), info('nervepay whoami'));
|
|
275
362
|
}
|
|
276
|
-
|
|
277
|
-
console.log(
|
|
278
|
-
|
|
363
|
+
|
|
364
|
+
console.log();
|
|
365
|
+
log(warn('Recovery mnemonic (save this!):'));
|
|
366
|
+
console.log(bold(` ${reg.mnemonic}`));
|
|
367
|
+
console.log();
|
|
279
368
|
} catch (error) {
|
|
280
|
-
|
|
281
|
-
process.exit(1);
|
|
369
|
+
die(`Setup failed: ${error.message}`);
|
|
282
370
|
}
|
|
283
371
|
});
|
|
284
372
|
|
|
@@ -288,14 +376,15 @@ program
|
|
|
288
376
|
|
|
289
377
|
program
|
|
290
378
|
.command('pair')
|
|
291
|
-
.description('
|
|
292
|
-
.option('--code <code>', '
|
|
293
|
-
.option('--api-url <url>', 'NervePay API URL',
|
|
294
|
-
.option('--gateway-url <url>', 'Gateway URL
|
|
295
|
-
.option('--gateway-token <token>', 'Gateway
|
|
379
|
+
.description('Connect gateway via device protocol or pairing code')
|
|
380
|
+
.option('--code <code>', 'Pairing code from dashboard (legacy flow)')
|
|
381
|
+
.option('--api-url <url>', 'NervePay API URL', DEFAULT_API_URL)
|
|
382
|
+
.option('--gateway-url <url>', 'Gateway URL')
|
|
383
|
+
.option('--gateway-token <token>', 'Gateway token (only for --code flow)')
|
|
296
384
|
.option('--name <name>', 'Gateway display name', 'OpenClaw Gateway')
|
|
297
|
-
.option('--timeout <seconds>', 'Approval
|
|
385
|
+
.option('--timeout <seconds>', 'Approval timeout in seconds', '300')
|
|
298
386
|
.action(async (options) => {
|
|
387
|
+
banner();
|
|
299
388
|
if (options.code) {
|
|
300
389
|
await legacyCodePairing(options);
|
|
301
390
|
} else {
|
|
@@ -303,69 +392,50 @@ program
|
|
|
303
392
|
}
|
|
304
393
|
});
|
|
305
394
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// Device node pairing
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
310
399
|
async function deviceNodePairing(options) {
|
|
311
|
-
const { signRawBytes, deriveDeviceId, getPublicKeyFromPrivate } =
|
|
400
|
+
const { signRawBytes, deriveDeviceId, getPublicKeyFromPrivate, decodeBase58 } =
|
|
401
|
+
await import('../dist/utils/crypto.js');
|
|
312
402
|
|
|
313
|
-
|
|
403
|
+
logInfo('Device node pairing');
|
|
404
|
+
console.log();
|
|
314
405
|
|
|
315
406
|
try {
|
|
316
|
-
// 1. Load agent credentials (must exist — run setup first)
|
|
317
407
|
let config;
|
|
318
|
-
try {
|
|
319
|
-
|
|
320
|
-
} catch {
|
|
321
|
-
console.error(chalk.red('❌ No agent credentials found.'));
|
|
322
|
-
console.error(chalk.yellow(' Run:'), chalk.cyan('nervepay setup'), chalk.yellow('first'));
|
|
323
|
-
process.exit(1);
|
|
324
|
-
}
|
|
408
|
+
try { config = await loadConfig(); }
|
|
409
|
+
catch { die('No agent credentials found', 'Run: nervepay setup'); }
|
|
325
410
|
|
|
326
411
|
const { agentDid, privateKey, apiUrl } = config;
|
|
327
|
-
|
|
328
|
-
|
|
412
|
+
logOk('Agent loaded');
|
|
413
|
+
field(' DID', agentDid);
|
|
329
414
|
|
|
330
|
-
//
|
|
415
|
+
// Resolve gateway URL
|
|
331
416
|
let gatewayUrl = options.gatewayUrl;
|
|
332
417
|
if (!gatewayUrl) {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
gatewayUrl = gw.url;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (!gatewayUrl) {
|
|
339
|
-
console.error(chalk.red('\n❌ Could not find gateway URL'));
|
|
340
|
-
console.error(chalk.yellow(' Provide it via one of:'));
|
|
341
|
-
console.error(chalk.gray(' --gateway-url <url> (e.g. ws://127.0.0.1:18789)'));
|
|
342
|
-
console.error(chalk.gray(' ~/.openclaw/openclaw.json'));
|
|
343
|
-
process.exit(1);
|
|
418
|
+
const oc = await readOpenClawConfig();
|
|
419
|
+
gatewayUrl = extractGatewayConfig(oc).url;
|
|
344
420
|
}
|
|
421
|
+
if (!gatewayUrl) die('No gateway URL found', 'Use: nervepay pair --gateway-url <url>');
|
|
345
422
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (wsUrl.startsWith('https://')) wsUrl = 'wss://' + wsUrl.slice('https://'.length);
|
|
349
|
-
else if (wsUrl.startsWith('http://')) wsUrl = 'ws://' + wsUrl.slice('http://'.length);
|
|
350
|
-
else if (!wsUrl.startsWith('ws://') && !wsUrl.startsWith('wss://')) wsUrl = 'ws://' + wsUrl;
|
|
351
|
-
|
|
352
|
-
console.log(chalk.gray(' Gateway:'), wsUrl);
|
|
423
|
+
const wsUrl = httpToWs(gatewayUrl);
|
|
424
|
+
field(' Gateway', wsUrl);
|
|
353
425
|
|
|
354
|
-
//
|
|
426
|
+
// Device identity
|
|
355
427
|
const publicKeyBase58 = await getPublicKeyFromPrivate(privateKey);
|
|
356
428
|
const deviceId = deriveDeviceId(publicKeyBase58);
|
|
357
|
-
|
|
429
|
+
field(' Device', deviceId.slice(0, 16) + '...');
|
|
358
430
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const WebSocket = (await import('ws')).default;
|
|
431
|
+
console.log();
|
|
432
|
+
logInfo('Connecting...');
|
|
362
433
|
|
|
434
|
+
const WebSocket = (await import('ws')).default;
|
|
363
435
|
const ws = new WebSocket(wsUrl, { rejectUnauthorized: false });
|
|
364
436
|
const timeoutMs = parseInt(options.timeout, 10) * 1000;
|
|
365
437
|
|
|
366
|
-
// Pre-compute crypto
|
|
367
|
-
// (avoids async gaps in the message handler that cause race conditions)
|
|
368
|
-
const { decodeBase58 } = await import('../dist/utils/crypto.js');
|
|
438
|
+
// Pre-compute crypto outside WS handler to avoid async race conditions
|
|
369
439
|
const pubKeyBytes = decodeBase58(publicKeyBase58);
|
|
370
440
|
const pubKeyB64 = Buffer.from(pubKeyBytes).toString('base64');
|
|
371
441
|
|
|
@@ -385,54 +455,44 @@ async function deviceNodePairing(options) {
|
|
|
385
455
|
|
|
386
456
|
const timer = setTimeout(() => {
|
|
387
457
|
ws.close();
|
|
388
|
-
settle(reject, new Error(`
|
|
458
|
+
settle(reject, new Error(`Approval timed out after ${options.timeout}s`));
|
|
389
459
|
}, timeoutMs + 15000);
|
|
390
460
|
|
|
391
|
-
ws.on('error', (
|
|
392
|
-
settle(reject, new Error(`WebSocket error: ${err.message}`));
|
|
393
|
-
});
|
|
461
|
+
ws.on('error', (e) => settle(reject, new Error(`Connection failed: ${e.message}`)));
|
|
394
462
|
|
|
395
463
|
ws.on('close', (code, reason) => {
|
|
396
|
-
// Delay slightly to let any pending message handler finish
|
|
397
464
|
setTimeout(() => {
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
: reasonStr
|
|
402
|
-
? `code=${code} reason=${reasonStr}`
|
|
403
|
-
: `code=${code}`;
|
|
404
|
-
settle(reject, new Error(`Gateway closed connection (${detail})`));
|
|
405
|
-
}, 100);
|
|
465
|
+
const detail = lastError || (reason?.toString() ? `${reason}` : `code ${code}`);
|
|
466
|
+
settle(reject, new Error(detail));
|
|
467
|
+
}, 150);
|
|
406
468
|
});
|
|
407
469
|
|
|
408
470
|
ws.on('message', async (data) => {
|
|
409
471
|
try {
|
|
410
|
-
const
|
|
411
|
-
|
|
472
|
+
const frame = JSON.parse(data.toString());
|
|
473
|
+
logDebug('<<', JSON.stringify(frame).slice(0, 300));
|
|
412
474
|
|
|
413
|
-
//
|
|
475
|
+
// Challenge → sign and connect
|
|
414
476
|
if (frame.event === 'connect.challenge') {
|
|
415
477
|
const nonce = frame.payload?.nonce || '';
|
|
416
|
-
|
|
478
|
+
logOk('Challenge received');
|
|
417
479
|
|
|
418
480
|
const nonceBytes = new TextEncoder().encode(nonce);
|
|
419
481
|
const signatureB64 = await signRawBytes(privateKey, nonceBytes);
|
|
420
|
-
const signedAt = Date.now();
|
|
421
482
|
|
|
422
|
-
// Step 6: Send connect request as operator with device identity
|
|
423
483
|
connectId = crypto.randomUUID();
|
|
424
|
-
|
|
484
|
+
ws.send(JSON.stringify({
|
|
425
485
|
type: 'req',
|
|
426
486
|
id: connectId,
|
|
427
487
|
method: 'connect',
|
|
428
488
|
params: {
|
|
429
489
|
role: 'operator',
|
|
430
|
-
minProtocol:
|
|
431
|
-
maxProtocol:
|
|
490
|
+
minProtocol: 3,
|
|
491
|
+
maxProtocol: 3,
|
|
432
492
|
scopes: ['operator.read', 'operator.write'],
|
|
433
493
|
client: {
|
|
434
|
-
id: '
|
|
435
|
-
version:
|
|
494
|
+
id: 'node-host',
|
|
495
|
+
version: VERSION,
|
|
436
496
|
platform: process.platform,
|
|
437
497
|
mode: 'node',
|
|
438
498
|
},
|
|
@@ -440,99 +500,88 @@ async function deviceNodePairing(options) {
|
|
|
440
500
|
id: deviceId,
|
|
441
501
|
publicKey: pubKeyB64,
|
|
442
502
|
signature: signatureB64,
|
|
443
|
-
signedAt,
|
|
503
|
+
signedAt: Date.now(),
|
|
444
504
|
nonce,
|
|
445
505
|
},
|
|
446
506
|
},
|
|
447
|
-
};
|
|
448
|
-
ws.send(JSON.stringify(connectReq));
|
|
507
|
+
}));
|
|
449
508
|
return;
|
|
450
509
|
}
|
|
451
510
|
|
|
452
511
|
// Connect response
|
|
453
512
|
if (frame.type === 'res' && frame.id === connectId) {
|
|
454
513
|
if (frame.ok) {
|
|
455
|
-
|
|
514
|
+
logOk('Connected to gateway');
|
|
456
515
|
|
|
457
|
-
// Step 7: Send node.pair.request
|
|
458
516
|
pairId = crypto.randomUUID();
|
|
459
|
-
|
|
517
|
+
ws.send(JSON.stringify({
|
|
460
518
|
type: 'req',
|
|
461
519
|
id: pairId,
|
|
462
520
|
method: 'node.pair.request',
|
|
463
521
|
params: {
|
|
464
522
|
displayName: `NervePay (${agentDid.split(':').pop().slice(0, 8)})`,
|
|
465
|
-
platform:
|
|
523
|
+
platform: process.platform,
|
|
466
524
|
},
|
|
467
|
-
};
|
|
468
|
-
ws.send(JSON.stringify(pairReq));
|
|
525
|
+
}));
|
|
469
526
|
} else {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
: (frame.error || 'Unknown error');
|
|
473
|
-
lastError = `Connect rejected: ${errDetail}`;
|
|
474
|
-
console.error(chalk.red(` ${lastError}`));
|
|
475
|
-
if (frame.payload) console.error(chalk.gray(' Payload:'), JSON.stringify(frame.payload));
|
|
527
|
+
lastError = formatFrameError('Connect rejected', frame);
|
|
528
|
+
logErr(lastError);
|
|
476
529
|
ws.close();
|
|
477
530
|
settle(reject, new Error(lastError));
|
|
478
531
|
}
|
|
479
532
|
return;
|
|
480
533
|
}
|
|
481
534
|
|
|
482
|
-
// Pair
|
|
535
|
+
// Pair response
|
|
483
536
|
if (frame.type === 'res' && frame.id === pairId) {
|
|
484
537
|
if (frame.ok) {
|
|
485
538
|
requestId = frame.payload?.requestId || '';
|
|
486
|
-
|
|
487
|
-
console.log(
|
|
488
|
-
|
|
489
|
-
|
|
539
|
+
logOk('Pairing request submitted');
|
|
540
|
+
console.log();
|
|
541
|
+
logInfo('Waiting for gateway owner approval...');
|
|
542
|
+
log(dim(`Approve: openclaw devices approve ${requestId}`));
|
|
543
|
+
log(dim(`Timeout: ${options.timeout}s`));
|
|
544
|
+
console.log();
|
|
490
545
|
} else {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
: (frame.error || 'Unknown error');
|
|
494
|
-
lastError = `Pair request rejected: ${errDetail}`;
|
|
495
|
-
console.error(chalk.red(` ${lastError}`));
|
|
496
|
-
if (frame.payload) console.error(chalk.gray(' Payload:'), JSON.stringify(frame.payload));
|
|
546
|
+
lastError = formatFrameError('Pair rejected', frame);
|
|
547
|
+
logErr(lastError);
|
|
497
548
|
ws.close();
|
|
498
549
|
settle(reject, new Error(lastError));
|
|
499
550
|
}
|
|
500
551
|
return;
|
|
501
552
|
}
|
|
502
553
|
|
|
503
|
-
//
|
|
554
|
+
// Pair resolved
|
|
504
555
|
if (frame.event === 'node.pair.resolved') {
|
|
505
556
|
const rid = frame.payload?.requestId;
|
|
506
557
|
if (rid && rid !== requestId) return;
|
|
507
558
|
|
|
508
559
|
const decision = frame.payload?.decision;
|
|
509
560
|
if (decision === 'approved') {
|
|
510
|
-
const token = frame.payload?.token || '';
|
|
511
|
-
const nodeId = frame.payload?.nodeId || '';
|
|
512
|
-
ws.close();
|
|
513
|
-
settle(resolve, { token, nodeId, requestId });
|
|
514
|
-
} else if (decision === 'rejected') {
|
|
515
561
|
ws.close();
|
|
516
|
-
settle(
|
|
562
|
+
settle(resolve, {
|
|
563
|
+
token: frame.payload?.token || '',
|
|
564
|
+
nodeId: frame.payload?.nodeId || '',
|
|
565
|
+
requestId,
|
|
566
|
+
});
|
|
517
567
|
} else {
|
|
518
568
|
ws.close();
|
|
519
|
-
settle(reject, new Error(
|
|
569
|
+
settle(reject, new Error(decision === 'rejected'
|
|
570
|
+
? 'Pairing rejected by gateway owner'
|
|
571
|
+
: `Unexpected decision: ${decision}`));
|
|
520
572
|
}
|
|
521
573
|
return;
|
|
522
574
|
}
|
|
523
575
|
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
} catch (err) {
|
|
529
|
-
// Ignore parse errors for non-JSON messages (ping, etc)
|
|
530
|
-
}
|
|
576
|
+
// Unhandled
|
|
577
|
+
logDebug('Unhandled frame:', JSON.stringify(frame).slice(0, 200));
|
|
578
|
+
} catch { /* non-JSON */ }
|
|
531
579
|
});
|
|
532
580
|
});
|
|
533
581
|
|
|
534
|
-
|
|
535
|
-
console.log(
|
|
582
|
+
logOk(bold('Approved!'));
|
|
583
|
+
console.log();
|
|
584
|
+
logInfo('Saving to NervePay...');
|
|
536
585
|
|
|
537
586
|
const client = new NervePayClient({
|
|
538
587
|
apiUrl: options.apiUrl || apiUrl,
|
|
@@ -547,38 +596,39 @@ async function deviceNodePairing(options) {
|
|
|
547
596
|
gateway_name: options.name,
|
|
548
597
|
});
|
|
549
598
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
console.log(
|
|
599
|
+
divider();
|
|
600
|
+
logOk(bold('Gateway paired'));
|
|
601
|
+
field(' Gateway ID', apiResult.gateway_id);
|
|
602
|
+
field(' Device ID', deviceId.slice(0, 16) + '...');
|
|
603
|
+
field(' Agent DID', agentDid);
|
|
604
|
+
console.log();
|
|
605
|
+
log(dim('View in dashboard:'), info('Mission Control > Task Board'));
|
|
606
|
+
console.log();
|
|
556
607
|
} catch (error) {
|
|
557
|
-
|
|
558
|
-
process.exit(1);
|
|
608
|
+
die(`Device pairing failed: ${error.message}`);
|
|
559
609
|
}
|
|
560
610
|
}
|
|
561
611
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
612
|
+
// ---------------------------------------------------------------------------
|
|
613
|
+
// Legacy code pairing
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
|
|
565
616
|
async function legacyCodePairing(options) {
|
|
566
|
-
|
|
567
|
-
|
|
617
|
+
logInfo('Code pairing');
|
|
618
|
+
field(' Code', options.code);
|
|
619
|
+
console.log();
|
|
568
620
|
|
|
569
621
|
try {
|
|
570
|
-
|
|
571
|
-
let agentDid, privateKey, mnemonic, claimUrl, sessionId, isNew = false;
|
|
622
|
+
let agentDid, privateKey, mnemonic, claimUrl, sessionId;
|
|
572
623
|
|
|
573
624
|
try {
|
|
574
625
|
const config = await loadConfig();
|
|
575
626
|
agentDid = config.agentDid;
|
|
576
627
|
privateKey = config.privateKey;
|
|
577
|
-
|
|
578
|
-
|
|
628
|
+
logOk('Agent loaded');
|
|
629
|
+
field(' DID', agentDid);
|
|
579
630
|
} catch {
|
|
580
|
-
|
|
581
|
-
console.log(chalk.cyan(' No existing identity found, creating one...\n'));
|
|
631
|
+
logInfo('Creating agent identity...');
|
|
582
632
|
const tempClient = new NervePayClient({ apiUrl: options.apiUrl });
|
|
583
633
|
const reg = await identity.registerPendingIdentity(tempClient, {
|
|
584
634
|
name: options.name,
|
|
@@ -589,143 +639,76 @@ async function legacyCodePairing(options) {
|
|
|
589
639
|
mnemonic = reg.mnemonic;
|
|
590
640
|
claimUrl = reg.claim_url;
|
|
591
641
|
sessionId = reg.session_id;
|
|
592
|
-
isNew = true;
|
|
593
|
-
console.log(chalk.green(' ✓ Agent identity created'));
|
|
594
|
-
console.log(chalk.gray(' DID:'), agentDid);
|
|
595
642
|
|
|
596
|
-
|
|
643
|
+
logOk('Agent created');
|
|
644
|
+
field(' DID', agentDid);
|
|
645
|
+
|
|
597
646
|
if (claimUrl) {
|
|
598
|
-
console.log(
|
|
599
|
-
|
|
600
|
-
console.log(
|
|
647
|
+
console.log();
|
|
648
|
+
logInfo('Claim in your browser:');
|
|
649
|
+
console.log(bold(` ${claimUrl}`));
|
|
650
|
+
console.log();
|
|
601
651
|
|
|
602
652
|
try {
|
|
603
653
|
const open = await import('open');
|
|
604
654
|
await open.default(claimUrl);
|
|
605
|
-
|
|
606
|
-
} catch {
|
|
607
|
-
console.log(chalk.gray(' Open the URL above in your browser.'));
|
|
608
|
-
}
|
|
655
|
+
} catch { /* ok */ }
|
|
609
656
|
|
|
610
|
-
|
|
611
|
-
console.log(chalk.cyan(' ⏳ Waiting for claim (30s timeout)...\n'));
|
|
657
|
+
logInfo('Waiting for claim (30s)...');
|
|
612
658
|
let claimed = false;
|
|
613
659
|
for (let i = 0; i < 6; i++) {
|
|
614
660
|
await new Promise((r) => setTimeout(r, 5000));
|
|
615
661
|
try {
|
|
616
|
-
const
|
|
617
|
-
const data = await
|
|
618
|
-
if (data.status === 'claimed') {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
} else if (data.status === 'expired') {
|
|
623
|
-
break;
|
|
624
|
-
}
|
|
625
|
-
process.stdout.write(chalk.gray('.'));
|
|
626
|
-
} catch {
|
|
627
|
-
process.stdout.write(chalk.gray('.'));
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (!claimed) {
|
|
632
|
-
console.log(chalk.yellow('\n ⚠️ Not claimed yet — continuing with pairing.'));
|
|
633
|
-
console.log(chalk.gray(' You can claim later at:'), chalk.yellow(claimUrl));
|
|
662
|
+
const resp = await fetch(`${options.apiUrl}/v1/agent-identity/register-pending/${sessionId}/status`);
|
|
663
|
+
const data = await resp.json();
|
|
664
|
+
if (data.status === 'claimed') { claimed = true; break; }
|
|
665
|
+
if (data.status === 'expired') break;
|
|
666
|
+
process.stdout.write(dim('.'));
|
|
667
|
+
} catch { process.stdout.write(dim('.')); }
|
|
634
668
|
}
|
|
669
|
+
console.log();
|
|
670
|
+
if (claimed) logOk('Agent claimed');
|
|
671
|
+
else logWarn('Not claimed yet. Claim later at: ' + claimUrl);
|
|
635
672
|
}
|
|
636
673
|
|
|
637
|
-
// Show mnemonic
|
|
638
674
|
if (mnemonic) {
|
|
639
|
-
console.log(
|
|
640
|
-
|
|
641
|
-
console.log(
|
|
675
|
+
console.log();
|
|
676
|
+
log(warn('Recovery mnemonic (save this!):'));
|
|
677
|
+
console.log(bold(` ${mnemonic}`));
|
|
642
678
|
}
|
|
643
679
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
JSON.stringify({
|
|
651
|
-
agent_did: agentDid,
|
|
652
|
-
private_key: privateKey,
|
|
653
|
-
mnemonic: mnemonic || undefined,
|
|
654
|
-
api_url: options.apiUrl,
|
|
655
|
-
created_at: new Date().toISOString(),
|
|
656
|
-
}, null, 2)
|
|
657
|
-
);
|
|
658
|
-
console.log(chalk.green(' ✓ Credentials saved to ~/.nervepay/credentials.json'));
|
|
659
|
-
|
|
660
|
-
// Update OpenClaw config if it exists
|
|
661
|
-
try {
|
|
662
|
-
const ocConfigPath = await findOpenClawConfigPath();
|
|
663
|
-
if (ocConfigPath) {
|
|
664
|
-
const existingConfig = JSON.parse(await readFile(ocConfigPath, 'utf-8'));
|
|
665
|
-
if (!existingConfig.plugins) existingConfig.plugins = {};
|
|
666
|
-
if (!existingConfig.plugins.entries) existingConfig.plugins.entries = {};
|
|
667
|
-
if (!existingConfig.plugins.entries.nervepay) {
|
|
668
|
-
existingConfig.plugins.entries.nervepay = { enabled: true, config: {} };
|
|
669
|
-
}
|
|
670
|
-
existingConfig.plugins.entries.nervepay.config = {
|
|
671
|
-
...existingConfig.plugins.entries.nervepay.config,
|
|
672
|
-
apiUrl: options.apiUrl,
|
|
673
|
-
agentDid,
|
|
674
|
-
privateKey,
|
|
675
|
-
};
|
|
676
|
-
await writeFile(ocConfigPath, JSON.stringify(existingConfig, null, 2));
|
|
677
|
-
console.log(chalk.green(' ✓ OpenClaw config updated'));
|
|
678
|
-
}
|
|
679
|
-
} catch {
|
|
680
|
-
// Non-fatal
|
|
681
|
-
}
|
|
682
|
-
} catch (e) {
|
|
683
|
-
console.log(chalk.yellow(' ⚠️ Could not save credentials:'), e.message);
|
|
680
|
+
console.log();
|
|
681
|
+
await saveCredentials(agentDid, privateKey, mnemonic, options.apiUrl);
|
|
682
|
+
logOk('Credentials saved');
|
|
683
|
+
|
|
684
|
+
if (await updateOpenClawConfig(agentDid, privateKey, options.apiUrl)) {
|
|
685
|
+
logOk('OpenClaw config updated');
|
|
684
686
|
}
|
|
685
687
|
}
|
|
686
688
|
|
|
687
|
-
//
|
|
688
|
-
console.log(
|
|
689
|
+
// Gateway config
|
|
690
|
+
console.log();
|
|
691
|
+
logInfo('Reading gateway config...');
|
|
689
692
|
|
|
690
693
|
let gatewayUrl = options.gatewayUrl;
|
|
691
694
|
let gatewayToken = options.gatewayToken;
|
|
692
695
|
|
|
693
|
-
// Try OpenClaw config if flags not provided
|
|
694
696
|
if (!gatewayUrl || !gatewayToken) {
|
|
695
|
-
const
|
|
696
|
-
const gw = extractGatewayConfig(
|
|
697
|
+
const oc = await readOpenClawConfig();
|
|
698
|
+
const gw = extractGatewayConfig(oc);
|
|
697
699
|
gatewayUrl = gatewayUrl || gw.url;
|
|
698
700
|
gatewayToken = gatewayToken || gw.token;
|
|
699
701
|
}
|
|
700
702
|
|
|
701
|
-
if (!gatewayToken)
|
|
702
|
-
|
|
703
|
-
console.error(chalk.yellow(' Provide it via one of:'));
|
|
704
|
-
console.error(chalk.gray(' --gateway-token <token>'));
|
|
705
|
-
console.error(chalk.gray(' ~/.openclaw/openclaw.json (gateway.auth.token)'));
|
|
706
|
-
console.error(chalk.gray(' OPENCLAW_GATEWAY_TOKEN env var'));
|
|
707
|
-
process.exit(1);
|
|
708
|
-
}
|
|
703
|
+
if (!gatewayToken) die('No gateway token found', 'Use: --gateway-token <token>');
|
|
704
|
+
if (!gatewayUrl) die('No gateway URL found', 'Use: --gateway-url <url>');
|
|
709
705
|
|
|
710
|
-
|
|
711
|
-
console.error(chalk.red('\n❌ Could not find gateway URL'));
|
|
712
|
-
console.error(chalk.yellow(' Provide it via one of:'));
|
|
713
|
-
console.error(chalk.gray(' --gateway-url <url> (e.g. ws://your-server:18789)'));
|
|
714
|
-
console.error(chalk.gray(' ~/.openclaw/openclaw.json'));
|
|
715
|
-
console.error(chalk.gray(' OPENCLAW_GATEWAY_URL env var'));
|
|
716
|
-
process.exit(1);
|
|
717
|
-
}
|
|
706
|
+
logOk(`Gateway: ${gatewayUrl}`);
|
|
718
707
|
|
|
719
|
-
console.log(
|
|
720
|
-
|
|
708
|
+
console.log();
|
|
709
|
+
logInfo('Completing pairing...');
|
|
721
710
|
|
|
722
|
-
|
|
723
|
-
console.log(chalk.cyan('\n🔐 Completing pairing...'));
|
|
724
|
-
const client = new NervePayClient({
|
|
725
|
-
apiUrl: options.apiUrl,
|
|
726
|
-
agentDid,
|
|
727
|
-
privateKey,
|
|
728
|
-
});
|
|
711
|
+
const client = new NervePayClient({ apiUrl: options.apiUrl, agentDid, privateKey });
|
|
729
712
|
|
|
730
713
|
const result = await gateway.completePairing(client, {
|
|
731
714
|
pairing_code: options.code,
|
|
@@ -734,14 +717,15 @@ async function legacyCodePairing(options) {
|
|
|
734
717
|
gateway_name: options.name,
|
|
735
718
|
});
|
|
736
719
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
console.log(
|
|
720
|
+
divider();
|
|
721
|
+
logOk(bold('Gateway paired'));
|
|
722
|
+
field(' Gateway ID', result.gateway_id || 'created');
|
|
723
|
+
field(' Agent DID', agentDid);
|
|
724
|
+
console.log();
|
|
725
|
+
log(dim('View in dashboard:'), info('Mission Control > Task Board'));
|
|
726
|
+
console.log();
|
|
742
727
|
} catch (error) {
|
|
743
|
-
|
|
744
|
-
process.exit(1);
|
|
728
|
+
die(`Pairing failed: ${error.message}`);
|
|
745
729
|
}
|
|
746
730
|
}
|
|
747
731
|
|
|
@@ -753,21 +737,21 @@ program
|
|
|
753
737
|
.command('whoami')
|
|
754
738
|
.description('Show current agent identity')
|
|
755
739
|
.action(async () => {
|
|
740
|
+
banner();
|
|
741
|
+
|
|
756
742
|
try {
|
|
757
743
|
const config = await loadConfig();
|
|
758
744
|
const client = new NervePayClient(config);
|
|
759
745
|
const result = await identity.whoami(client);
|
|
760
746
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
console.log(chalk.gray('Created:'), chalk.white(new Date(result.created_at).toLocaleString()));
|
|
747
|
+
field('Name', bold(result.name));
|
|
748
|
+
field('DID', result.agent_did);
|
|
749
|
+
field('Description', result.description || dim('none'));
|
|
750
|
+
field('Reputation', `${result.reputation_score}/100`);
|
|
751
|
+
field('Created', new Date(result.created_at).toLocaleDateString());
|
|
767
752
|
console.log();
|
|
768
753
|
} catch (error) {
|
|
769
|
-
|
|
770
|
-
process.exit(1);
|
|
754
|
+
die(error.message);
|
|
771
755
|
}
|
|
772
756
|
});
|
|
773
757
|
|
|
@@ -777,29 +761,33 @@ program
|
|
|
777
761
|
|
|
778
762
|
program
|
|
779
763
|
.command('gateways')
|
|
764
|
+
.alias('gw')
|
|
780
765
|
.description('List connected gateways')
|
|
781
766
|
.action(async () => {
|
|
767
|
+
banner();
|
|
768
|
+
|
|
782
769
|
try {
|
|
783
770
|
const config = await loadConfig();
|
|
784
771
|
const client = new NervePayClient(config);
|
|
785
772
|
const result = await gateway.listGateways(client);
|
|
786
773
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
console.log(
|
|
791
|
-
|
|
792
|
-
} else {
|
|
793
|
-
result.gateways.forEach((gw, i) => {
|
|
794
|
-
console.log(chalk.white(`${i + 1}. ${gw.name}`));
|
|
795
|
-
console.log(chalk.gray(' URL:'), gw.url);
|
|
796
|
-
console.log(chalk.gray(' Status:'), gw.last_health_status === 'healthy' ? chalk.green('healthy') : chalk.yellow(gw.last_health_status));
|
|
797
|
-
console.log();
|
|
798
|
-
});
|
|
774
|
+
if (!result.gateways?.length) {
|
|
775
|
+
logWarn('No gateways connected');
|
|
776
|
+
log(dim('Run:'), info('nervepay pair --gateway-url <url>'));
|
|
777
|
+
console.log();
|
|
778
|
+
return;
|
|
799
779
|
}
|
|
780
|
+
|
|
781
|
+
result.gateways.forEach((gw, i) => {
|
|
782
|
+
const status = gw.last_health_status === 'healthy' ? ok('healthy') : warn(gw.last_health_status || 'unknown');
|
|
783
|
+
log(bold(`${i + 1}. ${gw.name}`), dim('|'), status);
|
|
784
|
+
field(' URL', gw.url || gw.gateway_url);
|
|
785
|
+
if (gw.integration_source) field(' Source', gw.integration_source);
|
|
786
|
+
if (gw.linked_agent_did) field(' Agent', gw.linked_agent_did);
|
|
787
|
+
console.log();
|
|
788
|
+
});
|
|
800
789
|
} catch (error) {
|
|
801
|
-
|
|
802
|
-
process.exit(1);
|
|
790
|
+
die(error.message);
|
|
803
791
|
}
|
|
804
792
|
});
|
|
805
793
|
|
|
@@ -809,75 +797,46 @@ program
|
|
|
809
797
|
|
|
810
798
|
program
|
|
811
799
|
.command('secrets')
|
|
812
|
-
.description('List secrets
|
|
800
|
+
.description('List vault secrets')
|
|
813
801
|
.action(async () => {
|
|
802
|
+
banner();
|
|
803
|
+
|
|
814
804
|
try {
|
|
815
805
|
const config = await loadConfig();
|
|
816
806
|
const client = new NervePayClient(config);
|
|
817
807
|
const result = await vault.listSecrets(client);
|
|
818
808
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
console.log(
|
|
823
|
-
|
|
824
|
-
} else {
|
|
825
|
-
result.secrets.forEach((secret, i) => {
|
|
826
|
-
console.log(chalk.white(`${i + 1}. ${secret.name}`));
|
|
827
|
-
console.log(chalk.gray(' Type:'), secret.secret_type);
|
|
828
|
-
console.log(chalk.gray(' Updated:'), new Date(secret.updated_at).toLocaleString());
|
|
829
|
-
console.log();
|
|
830
|
-
});
|
|
809
|
+
if (!result.secrets?.length) {
|
|
810
|
+
logWarn('No secrets stored');
|
|
811
|
+
log(dim('Add secrets in dashboard: Agent Passports > Secrets'));
|
|
812
|
+
console.log();
|
|
813
|
+
return;
|
|
831
814
|
}
|
|
815
|
+
|
|
816
|
+
result.secrets.forEach((s, i) => {
|
|
817
|
+
log(bold(`${i + 1}. ${s.name}`), dim('|'), s.secret_type);
|
|
818
|
+
field(' Updated', new Date(s.updated_at).toLocaleDateString());
|
|
819
|
+
});
|
|
820
|
+
console.log();
|
|
832
821
|
} catch (error) {
|
|
833
|
-
|
|
834
|
-
process.exit(1);
|
|
822
|
+
die(error.message);
|
|
835
823
|
}
|
|
836
824
|
});
|
|
837
825
|
|
|
838
826
|
// ============================================================================
|
|
839
|
-
//
|
|
827
|
+
// Helpers
|
|
840
828
|
// ============================================================================
|
|
841
829
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const pluginConfig = openclawConfig.plugins?.entries?.nervepay?.config || {};
|
|
849
|
-
|
|
850
|
-
if (pluginConfig.agentDid && pluginConfig.privateKey) {
|
|
851
|
-
return {
|
|
852
|
-
apiUrl: pluginConfig.apiUrl || 'https://api.nervepay.xyz',
|
|
853
|
-
agentDid: pluginConfig.agentDid,
|
|
854
|
-
privateKey: pluginConfig.privateKey,
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
} catch {
|
|
859
|
-
// Config not found or invalid — fall through to backup
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// Try NervePay credentials backup
|
|
863
|
-
try {
|
|
864
|
-
const creds = JSON.parse(await readFile(NERVEPAY_CREDS_PATH, 'utf-8'));
|
|
865
|
-
if (creds.agent_did && creds.private_key) {
|
|
866
|
-
return {
|
|
867
|
-
apiUrl: creds.api_url || 'https://api.nervepay.xyz',
|
|
868
|
-
agentDid: creds.agent_did,
|
|
869
|
-
privateKey: creds.private_key,
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
} catch {
|
|
873
|
-
// No backup either
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
throw new Error('No NervePay credentials found. Run: nervepay setup');
|
|
830
|
+
function formatFrameError(prefix, frame) {
|
|
831
|
+
const e = frame.error;
|
|
832
|
+
const msg = typeof e === 'object' && e !== null
|
|
833
|
+
? (e.message || JSON.stringify(e))
|
|
834
|
+
: (e || 'Unknown error');
|
|
835
|
+
return `${prefix}: ${msg}`;
|
|
877
836
|
}
|
|
878
837
|
|
|
879
838
|
// ============================================================================
|
|
880
|
-
//
|
|
839
|
+
// Run
|
|
881
840
|
// ============================================================================
|
|
882
841
|
|
|
883
842
|
program.parse();
|