nervepay 1.3.4 → 1.3.6
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 +483 -499
- 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,71 +392,54 @@ 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
|
+
// OpenClaw expects base64url encoding (no padding) for public key
|
|
441
|
+
const pubKeyB64Url = Buffer.from(pubKeyBytes).toString('base64')
|
|
442
|
+
.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
371
443
|
|
|
372
444
|
const result = await new Promise((resolve, reject) => {
|
|
373
445
|
let settled = false;
|
|
@@ -385,154 +457,156 @@ async function deviceNodePairing(options) {
|
|
|
385
457
|
|
|
386
458
|
const timer = setTimeout(() => {
|
|
387
459
|
ws.close();
|
|
388
|
-
settle(reject, new Error(`
|
|
460
|
+
settle(reject, new Error(`Approval timed out after ${options.timeout}s`));
|
|
389
461
|
}, timeoutMs + 15000);
|
|
390
462
|
|
|
391
|
-
ws.on('error', (
|
|
392
|
-
settle(reject, new Error(`WebSocket error: ${err.message}`));
|
|
393
|
-
});
|
|
463
|
+
ws.on('error', (e) => settle(reject, new Error(`Connection failed: ${e.message}`)));
|
|
394
464
|
|
|
395
465
|
ws.on('close', (code, reason) => {
|
|
396
|
-
// Delay slightly to let any pending message handler finish
|
|
397
466
|
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);
|
|
467
|
+
const detail = lastError || (reason?.toString() ? `${reason}` : `code ${code}`);
|
|
468
|
+
settle(reject, new Error(detail));
|
|
469
|
+
}, 150);
|
|
406
470
|
});
|
|
407
471
|
|
|
408
472
|
ws.on('message', async (data) => {
|
|
409
473
|
try {
|
|
410
|
-
const
|
|
411
|
-
|
|
474
|
+
const frame = JSON.parse(data.toString());
|
|
475
|
+
logDebug('<<', JSON.stringify(frame).slice(0, 300));
|
|
412
476
|
|
|
413
|
-
//
|
|
477
|
+
// Challenge → build auth payload, sign, and connect
|
|
414
478
|
if (frame.event === 'connect.challenge') {
|
|
415
479
|
const nonce = frame.payload?.nonce || '';
|
|
416
|
-
|
|
480
|
+
logOk('Challenge received');
|
|
481
|
+
|
|
482
|
+
const signedAtMs = Date.now();
|
|
483
|
+
const role = 'operator';
|
|
484
|
+
const scopes = ['operator.read', 'operator.write'];
|
|
485
|
+
|
|
486
|
+
// Build structured payload per OpenClaw device-auth protocol (v2)
|
|
487
|
+
// Format: v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce
|
|
488
|
+
const payload = [
|
|
489
|
+
'v2',
|
|
490
|
+
deviceId,
|
|
491
|
+
'node-host',
|
|
492
|
+
'node',
|
|
493
|
+
role,
|
|
494
|
+
scopes.join(','),
|
|
495
|
+
String(signedAtMs),
|
|
496
|
+
'', // token (empty for initial connect)
|
|
497
|
+
nonce,
|
|
498
|
+
].join('|');
|
|
499
|
+
|
|
500
|
+
logDebug('Auth payload:', payload);
|
|
501
|
+
|
|
502
|
+
const payloadBytes = new TextEncoder().encode(payload);
|
|
503
|
+
const signatureB64 = await signRawBytes(privateKey, payloadBytes);
|
|
504
|
+
// Convert to base64url (no padding)
|
|
505
|
+
const signatureB64Url = signatureB64
|
|
506
|
+
.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
417
507
|
|
|
418
|
-
const nonceBytes = new TextEncoder().encode(nonce);
|
|
419
|
-
const signatureB64 = await signRawBytes(privateKey, nonceBytes);
|
|
420
|
-
const signedAt = Date.now();
|
|
421
|
-
|
|
422
|
-
// Step 6: Send connect request as operator with device identity
|
|
423
508
|
connectId = crypto.randomUUID();
|
|
424
|
-
|
|
509
|
+
ws.send(JSON.stringify({
|
|
425
510
|
type: 'req',
|
|
426
511
|
id: connectId,
|
|
427
512
|
method: 'connect',
|
|
428
513
|
params: {
|
|
429
|
-
role
|
|
430
|
-
minProtocol:
|
|
431
|
-
maxProtocol:
|
|
432
|
-
scopes
|
|
514
|
+
role,
|
|
515
|
+
minProtocol: 3,
|
|
516
|
+
maxProtocol: 3,
|
|
517
|
+
scopes,
|
|
433
518
|
client: {
|
|
434
|
-
id: '
|
|
435
|
-
version:
|
|
519
|
+
id: 'node-host',
|
|
520
|
+
version: VERSION,
|
|
436
521
|
platform: process.platform,
|
|
437
522
|
mode: 'node',
|
|
438
523
|
},
|
|
439
524
|
device: {
|
|
440
525
|
id: deviceId,
|
|
441
|
-
publicKey:
|
|
442
|
-
signature:
|
|
443
|
-
signedAt,
|
|
526
|
+
publicKey: pubKeyB64Url,
|
|
527
|
+
signature: signatureB64Url,
|
|
528
|
+
signedAt: signedAtMs,
|
|
444
529
|
nonce,
|
|
445
530
|
},
|
|
446
531
|
},
|
|
447
|
-
};
|
|
448
|
-
ws.send(JSON.stringify(connectReq));
|
|
532
|
+
}));
|
|
449
533
|
return;
|
|
450
534
|
}
|
|
451
535
|
|
|
452
536
|
// Connect response
|
|
453
537
|
if (frame.type === 'res' && frame.id === connectId) {
|
|
454
538
|
if (frame.ok) {
|
|
455
|
-
|
|
539
|
+
logOk('Connected to gateway');
|
|
456
540
|
|
|
457
|
-
// Step 7: Send node.pair.request
|
|
458
541
|
pairId = crypto.randomUUID();
|
|
459
|
-
|
|
542
|
+
ws.send(JSON.stringify({
|
|
460
543
|
type: 'req',
|
|
461
544
|
id: pairId,
|
|
462
545
|
method: 'node.pair.request',
|
|
463
546
|
params: {
|
|
464
547
|
displayName: `NervePay (${agentDid.split(':').pop().slice(0, 8)})`,
|
|
465
|
-
platform:
|
|
548
|
+
platform: process.platform,
|
|
466
549
|
},
|
|
467
|
-
};
|
|
468
|
-
ws.send(JSON.stringify(pairReq));
|
|
550
|
+
}));
|
|
469
551
|
} 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));
|
|
552
|
+
lastError = formatFrameError('Connect rejected', frame);
|
|
553
|
+
logErr(lastError);
|
|
476
554
|
ws.close();
|
|
477
555
|
settle(reject, new Error(lastError));
|
|
478
556
|
}
|
|
479
557
|
return;
|
|
480
558
|
}
|
|
481
559
|
|
|
482
|
-
// Pair
|
|
560
|
+
// Pair response
|
|
483
561
|
if (frame.type === 'res' && frame.id === pairId) {
|
|
484
562
|
if (frame.ok) {
|
|
485
563
|
requestId = frame.payload?.requestId || '';
|
|
486
|
-
|
|
487
|
-
console.log(
|
|
488
|
-
|
|
489
|
-
|
|
564
|
+
logOk('Pairing request submitted');
|
|
565
|
+
console.log();
|
|
566
|
+
logInfo('Waiting for gateway owner approval...');
|
|
567
|
+
log(dim(`Approve: openclaw devices approve ${requestId}`));
|
|
568
|
+
log(dim(`Timeout: ${options.timeout}s`));
|
|
569
|
+
console.log();
|
|
490
570
|
} 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));
|
|
571
|
+
lastError = formatFrameError('Pair rejected', frame);
|
|
572
|
+
logErr(lastError);
|
|
497
573
|
ws.close();
|
|
498
574
|
settle(reject, new Error(lastError));
|
|
499
575
|
}
|
|
500
576
|
return;
|
|
501
577
|
}
|
|
502
578
|
|
|
503
|
-
//
|
|
579
|
+
// Pair resolved
|
|
504
580
|
if (frame.event === 'node.pair.resolved') {
|
|
505
581
|
const rid = frame.payload?.requestId;
|
|
506
582
|
if (rid && rid !== requestId) return;
|
|
507
583
|
|
|
508
584
|
const decision = frame.payload?.decision;
|
|
509
585
|
if (decision === 'approved') {
|
|
510
|
-
const token = frame.payload?.token || '';
|
|
511
|
-
const nodeId = frame.payload?.nodeId || '';
|
|
512
586
|
ws.close();
|
|
513
|
-
settle(resolve, {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
587
|
+
settle(resolve, {
|
|
588
|
+
token: frame.payload?.token || '',
|
|
589
|
+
nodeId: frame.payload?.nodeId || '',
|
|
590
|
+
requestId,
|
|
591
|
+
});
|
|
517
592
|
} else {
|
|
518
593
|
ws.close();
|
|
519
|
-
settle(reject, new Error(
|
|
594
|
+
settle(reject, new Error(decision === 'rejected'
|
|
595
|
+
? 'Pairing rejected by gateway owner'
|
|
596
|
+
: `Unexpected decision: ${decision}`));
|
|
520
597
|
}
|
|
521
598
|
return;
|
|
522
599
|
}
|
|
523
600
|
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
} catch (err) {
|
|
529
|
-
// Ignore parse errors for non-JSON messages (ping, etc)
|
|
530
|
-
}
|
|
601
|
+
// Unhandled
|
|
602
|
+
logDebug('Unhandled frame:', JSON.stringify(frame).slice(0, 200));
|
|
603
|
+
} catch { /* non-JSON */ }
|
|
531
604
|
});
|
|
532
605
|
});
|
|
533
606
|
|
|
534
|
-
|
|
535
|
-
console.log(
|
|
607
|
+
logOk(bold('Approved!'));
|
|
608
|
+
console.log();
|
|
609
|
+
logInfo('Saving to NervePay...');
|
|
536
610
|
|
|
537
611
|
const client = new NervePayClient({
|
|
538
612
|
apiUrl: options.apiUrl || apiUrl,
|
|
@@ -547,38 +621,39 @@ async function deviceNodePairing(options) {
|
|
|
547
621
|
gateway_name: options.name,
|
|
548
622
|
});
|
|
549
623
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
console.log(
|
|
624
|
+
divider();
|
|
625
|
+
logOk(bold('Gateway paired'));
|
|
626
|
+
field(' Gateway ID', apiResult.gateway_id);
|
|
627
|
+
field(' Device ID', deviceId.slice(0, 16) + '...');
|
|
628
|
+
field(' Agent DID', agentDid);
|
|
629
|
+
console.log();
|
|
630
|
+
log(dim('View in dashboard:'), info('Mission Control > Task Board'));
|
|
631
|
+
console.log();
|
|
556
632
|
} catch (error) {
|
|
557
|
-
|
|
558
|
-
process.exit(1);
|
|
633
|
+
die(`Device pairing failed: ${error.message}`);
|
|
559
634
|
}
|
|
560
635
|
}
|
|
561
636
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
// Legacy code pairing
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
|
|
565
641
|
async function legacyCodePairing(options) {
|
|
566
|
-
|
|
567
|
-
|
|
642
|
+
logInfo('Code pairing');
|
|
643
|
+
field(' Code', options.code);
|
|
644
|
+
console.log();
|
|
568
645
|
|
|
569
646
|
try {
|
|
570
|
-
|
|
571
|
-
let agentDid, privateKey, mnemonic, claimUrl, sessionId, isNew = false;
|
|
647
|
+
let agentDid, privateKey, mnemonic, claimUrl, sessionId;
|
|
572
648
|
|
|
573
649
|
try {
|
|
574
650
|
const config = await loadConfig();
|
|
575
651
|
agentDid = config.agentDid;
|
|
576
652
|
privateKey = config.privateKey;
|
|
577
|
-
|
|
578
|
-
|
|
653
|
+
logOk('Agent loaded');
|
|
654
|
+
field(' DID', agentDid);
|
|
579
655
|
} catch {
|
|
580
|
-
|
|
581
|
-
console.log(chalk.cyan(' No existing identity found, creating one...\n'));
|
|
656
|
+
logInfo('Creating agent identity...');
|
|
582
657
|
const tempClient = new NervePayClient({ apiUrl: options.apiUrl });
|
|
583
658
|
const reg = await identity.registerPendingIdentity(tempClient, {
|
|
584
659
|
name: options.name,
|
|
@@ -589,143 +664,76 @@ async function legacyCodePairing(options) {
|
|
|
589
664
|
mnemonic = reg.mnemonic;
|
|
590
665
|
claimUrl = reg.claim_url;
|
|
591
666
|
sessionId = reg.session_id;
|
|
592
|
-
isNew = true;
|
|
593
|
-
console.log(chalk.green(' ✓ Agent identity created'));
|
|
594
|
-
console.log(chalk.gray(' DID:'), agentDid);
|
|
595
667
|
|
|
596
|
-
|
|
668
|
+
logOk('Agent created');
|
|
669
|
+
field(' DID', agentDid);
|
|
670
|
+
|
|
597
671
|
if (claimUrl) {
|
|
598
|
-
console.log(
|
|
599
|
-
|
|
600
|
-
console.log(
|
|
672
|
+
console.log();
|
|
673
|
+
logInfo('Claim in your browser:');
|
|
674
|
+
console.log(bold(` ${claimUrl}`));
|
|
675
|
+
console.log();
|
|
601
676
|
|
|
602
677
|
try {
|
|
603
678
|
const open = await import('open');
|
|
604
679
|
await open.default(claimUrl);
|
|
605
|
-
|
|
606
|
-
} catch {
|
|
607
|
-
console.log(chalk.gray(' Open the URL above in your browser.'));
|
|
608
|
-
}
|
|
680
|
+
} catch { /* ok */ }
|
|
609
681
|
|
|
610
|
-
|
|
611
|
-
console.log(chalk.cyan(' ⏳ Waiting for claim (30s timeout)...\n'));
|
|
682
|
+
logInfo('Waiting for claim (30s)...');
|
|
612
683
|
let claimed = false;
|
|
613
684
|
for (let i = 0; i < 6; i++) {
|
|
614
685
|
await new Promise((r) => setTimeout(r, 5000));
|
|
615
686
|
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));
|
|
687
|
+
const resp = await fetch(`${options.apiUrl}/v1/agent-identity/register-pending/${sessionId}/status`);
|
|
688
|
+
const data = await resp.json();
|
|
689
|
+
if (data.status === 'claimed') { claimed = true; break; }
|
|
690
|
+
if (data.status === 'expired') break;
|
|
691
|
+
process.stdout.write(dim('.'));
|
|
692
|
+
} catch { process.stdout.write(dim('.')); }
|
|
634
693
|
}
|
|
694
|
+
console.log();
|
|
695
|
+
if (claimed) logOk('Agent claimed');
|
|
696
|
+
else logWarn('Not claimed yet. Claim later at: ' + claimUrl);
|
|
635
697
|
}
|
|
636
698
|
|
|
637
|
-
// Show mnemonic
|
|
638
699
|
if (mnemonic) {
|
|
639
|
-
console.log(
|
|
640
|
-
|
|
641
|
-
console.log(
|
|
700
|
+
console.log();
|
|
701
|
+
log(warn('Recovery mnemonic (save this!):'));
|
|
702
|
+
console.log(bold(` ${mnemonic}`));
|
|
642
703
|
}
|
|
643
704
|
|
|
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);
|
|
705
|
+
console.log();
|
|
706
|
+
await saveCredentials(agentDid, privateKey, mnemonic, options.apiUrl);
|
|
707
|
+
logOk('Credentials saved');
|
|
708
|
+
|
|
709
|
+
if (await updateOpenClawConfig(agentDid, privateKey, options.apiUrl)) {
|
|
710
|
+
logOk('OpenClaw config updated');
|
|
684
711
|
}
|
|
685
712
|
}
|
|
686
713
|
|
|
687
|
-
//
|
|
688
|
-
console.log(
|
|
714
|
+
// Gateway config
|
|
715
|
+
console.log();
|
|
716
|
+
logInfo('Reading gateway config...');
|
|
689
717
|
|
|
690
718
|
let gatewayUrl = options.gatewayUrl;
|
|
691
719
|
let gatewayToken = options.gatewayToken;
|
|
692
720
|
|
|
693
|
-
// Try OpenClaw config if flags not provided
|
|
694
721
|
if (!gatewayUrl || !gatewayToken) {
|
|
695
|
-
const
|
|
696
|
-
const gw = extractGatewayConfig(
|
|
722
|
+
const oc = await readOpenClawConfig();
|
|
723
|
+
const gw = extractGatewayConfig(oc);
|
|
697
724
|
gatewayUrl = gatewayUrl || gw.url;
|
|
698
725
|
gatewayToken = gatewayToken || gw.token;
|
|
699
726
|
}
|
|
700
727
|
|
|
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
|
-
}
|
|
728
|
+
if (!gatewayToken) die('No gateway token found', 'Use: --gateway-token <token>');
|
|
729
|
+
if (!gatewayUrl) die('No gateway URL found', 'Use: --gateway-url <url>');
|
|
709
730
|
|
|
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
|
-
}
|
|
731
|
+
logOk(`Gateway: ${gatewayUrl}`);
|
|
718
732
|
|
|
719
|
-
console.log(
|
|
720
|
-
|
|
733
|
+
console.log();
|
|
734
|
+
logInfo('Completing pairing...');
|
|
721
735
|
|
|
722
|
-
|
|
723
|
-
console.log(chalk.cyan('\n🔐 Completing pairing...'));
|
|
724
|
-
const client = new NervePayClient({
|
|
725
|
-
apiUrl: options.apiUrl,
|
|
726
|
-
agentDid,
|
|
727
|
-
privateKey,
|
|
728
|
-
});
|
|
736
|
+
const client = new NervePayClient({ apiUrl: options.apiUrl, agentDid, privateKey });
|
|
729
737
|
|
|
730
738
|
const result = await gateway.completePairing(client, {
|
|
731
739
|
pairing_code: options.code,
|
|
@@ -734,14 +742,15 @@ async function legacyCodePairing(options) {
|
|
|
734
742
|
gateway_name: options.name,
|
|
735
743
|
});
|
|
736
744
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
console.log(
|
|
745
|
+
divider();
|
|
746
|
+
logOk(bold('Gateway paired'));
|
|
747
|
+
field(' Gateway ID', result.gateway_id || 'created');
|
|
748
|
+
field(' Agent DID', agentDid);
|
|
749
|
+
console.log();
|
|
750
|
+
log(dim('View in dashboard:'), info('Mission Control > Task Board'));
|
|
751
|
+
console.log();
|
|
742
752
|
} catch (error) {
|
|
743
|
-
|
|
744
|
-
process.exit(1);
|
|
753
|
+
die(`Pairing failed: ${error.message}`);
|
|
745
754
|
}
|
|
746
755
|
}
|
|
747
756
|
|
|
@@ -753,21 +762,21 @@ program
|
|
|
753
762
|
.command('whoami')
|
|
754
763
|
.description('Show current agent identity')
|
|
755
764
|
.action(async () => {
|
|
765
|
+
banner();
|
|
766
|
+
|
|
756
767
|
try {
|
|
757
768
|
const config = await loadConfig();
|
|
758
769
|
const client = new NervePayClient(config);
|
|
759
770
|
const result = await identity.whoami(client);
|
|
760
771
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
console.log(chalk.gray('Created:'), chalk.white(new Date(result.created_at).toLocaleString()));
|
|
772
|
+
field('Name', bold(result.name));
|
|
773
|
+
field('DID', result.agent_did);
|
|
774
|
+
field('Description', result.description || dim('none'));
|
|
775
|
+
field('Reputation', `${result.reputation_score}/100`);
|
|
776
|
+
field('Created', new Date(result.created_at).toLocaleDateString());
|
|
767
777
|
console.log();
|
|
768
778
|
} catch (error) {
|
|
769
|
-
|
|
770
|
-
process.exit(1);
|
|
779
|
+
die(error.message);
|
|
771
780
|
}
|
|
772
781
|
});
|
|
773
782
|
|
|
@@ -777,29 +786,33 @@ program
|
|
|
777
786
|
|
|
778
787
|
program
|
|
779
788
|
.command('gateways')
|
|
789
|
+
.alias('gw')
|
|
780
790
|
.description('List connected gateways')
|
|
781
791
|
.action(async () => {
|
|
792
|
+
banner();
|
|
793
|
+
|
|
782
794
|
try {
|
|
783
795
|
const config = await loadConfig();
|
|
784
796
|
const client = new NervePayClient(config);
|
|
785
797
|
const result = await gateway.listGateways(client);
|
|
786
798
|
|
|
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
|
-
});
|
|
799
|
+
if (!result.gateways?.length) {
|
|
800
|
+
logWarn('No gateways connected');
|
|
801
|
+
log(dim('Run:'), info('nervepay pair --gateway-url <url>'));
|
|
802
|
+
console.log();
|
|
803
|
+
return;
|
|
799
804
|
}
|
|
805
|
+
|
|
806
|
+
result.gateways.forEach((gw, i) => {
|
|
807
|
+
const status = gw.last_health_status === 'healthy' ? ok('healthy') : warn(gw.last_health_status || 'unknown');
|
|
808
|
+
log(bold(`${i + 1}. ${gw.name}`), dim('|'), status);
|
|
809
|
+
field(' URL', gw.url || gw.gateway_url);
|
|
810
|
+
if (gw.integration_source) field(' Source', gw.integration_source);
|
|
811
|
+
if (gw.linked_agent_did) field(' Agent', gw.linked_agent_did);
|
|
812
|
+
console.log();
|
|
813
|
+
});
|
|
800
814
|
} catch (error) {
|
|
801
|
-
|
|
802
|
-
process.exit(1);
|
|
815
|
+
die(error.message);
|
|
803
816
|
}
|
|
804
817
|
});
|
|
805
818
|
|
|
@@ -809,75 +822,46 @@ program
|
|
|
809
822
|
|
|
810
823
|
program
|
|
811
824
|
.command('secrets')
|
|
812
|
-
.description('List secrets
|
|
825
|
+
.description('List vault secrets')
|
|
813
826
|
.action(async () => {
|
|
827
|
+
banner();
|
|
828
|
+
|
|
814
829
|
try {
|
|
815
830
|
const config = await loadConfig();
|
|
816
831
|
const client = new NervePayClient(config);
|
|
817
832
|
const result = await vault.listSecrets(client);
|
|
818
833
|
|
|
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
|
-
});
|
|
834
|
+
if (!result.secrets?.length) {
|
|
835
|
+
logWarn('No secrets stored');
|
|
836
|
+
log(dim('Add secrets in dashboard: Agent Passports > Secrets'));
|
|
837
|
+
console.log();
|
|
838
|
+
return;
|
|
831
839
|
}
|
|
840
|
+
|
|
841
|
+
result.secrets.forEach((s, i) => {
|
|
842
|
+
log(bold(`${i + 1}. ${s.name}`), dim('|'), s.secret_type);
|
|
843
|
+
field(' Updated', new Date(s.updated_at).toLocaleDateString());
|
|
844
|
+
});
|
|
845
|
+
console.log();
|
|
832
846
|
} catch (error) {
|
|
833
|
-
|
|
834
|
-
process.exit(1);
|
|
847
|
+
die(error.message);
|
|
835
848
|
}
|
|
836
849
|
});
|
|
837
850
|
|
|
838
851
|
// ============================================================================
|
|
839
|
-
//
|
|
852
|
+
// Helpers
|
|
840
853
|
// ============================================================================
|
|
841
854
|
|
|
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');
|
|
855
|
+
function formatFrameError(prefix, frame) {
|
|
856
|
+
const e = frame.error;
|
|
857
|
+
const msg = typeof e === 'object' && e !== null
|
|
858
|
+
? (e.message || JSON.stringify(e))
|
|
859
|
+
: (e || 'Unknown error');
|
|
860
|
+
return `${prefix}: ${msg}`;
|
|
877
861
|
}
|
|
878
862
|
|
|
879
863
|
// ============================================================================
|
|
880
|
-
//
|
|
864
|
+
// Run
|
|
881
865
|
// ============================================================================
|
|
882
866
|
|
|
883
867
|
program.parse();
|