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.
Files changed (2) hide show
  1. package/bin/nervepay-cli.js +450 -491
  2. package/package.json +1 -1
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * NervePay CLI - Standalone command-line tool
4
- * Works independently of OpenClaw plugin system
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
- const NERVEPAY_CREDS_PATH = join(homedir(), '.nervepay', 'credentials.json');
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
- * Find OpenClaw config path — checks OPENCLAW_CONFIG_PATH env var,
22
- * then standard locations, then legacy paths.
23
- */
24
- async function findOpenClawConfigPath() {
25
- const { existsSync } = await import('fs');
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
- // Check env var override first
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'), // Legacy "Moltbot" name
36
- join(homedir(), '.clawdbot', 'openclaw.json'), // Original "Clawdbot" name
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 = await findOpenClawConfigPath();
91
+ const configPath = findOpenClawConfigPath();
50
92
  if (!configPath) return {};
51
- try {
52
- return JSON.parse(await readFile(configPath, 'utf-8'));
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
- // Env var overrides (OpenClaw precedence: CLI flags > env vars > config)
81
- if (process.env.OPENCLAW_GATEWAY_TOKEN) {
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('NervePay CLI - Self-sovereign identity for AI agents')
97
- .version('1.2.0');
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('Interactive setup wizard - register identity, pair gateway')
106
- .option('--api-url <url>', 'NervePay API URL', 'https://api.nervepay.xyz')
107
- .option('--gateway-url <url>', 'Gateway URL (overrides config)')
108
- .option('--gateway-token <token>', 'Gateway token (overrides config)')
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
- console.log(chalk.cyan('\n🔐 NervePay Setup Wizard\n'));
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 = await findOpenClawConfigPath();
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
- console.log(chalk.green('✓ Found OpenClaw gateway:'), gatewayUrl);
269
+ logOk(`Found OpenClaw gateway: ${gatewayUrl}`);
124
270
  } else {
125
- console.log(chalk.yellow('⚠️ No OpenClaw gateway detected'));
126
- console.log(chalk.gray(' Gateway pairing will be skipped. You can pair later with:'));
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
- // Ask for agent details
275
+ console.log();
131
276
  const answers = await inquirer.prompt([
132
- {
133
- type: 'input',
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(chalk.cyan('\n📝 Registering agent identity...'));
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
- console.log(chalk.green('Agent identity created'));
158
- console.log(chalk.gray(' DID:'), registration.did);
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(chalk.cyan('\n👤 Please claim this agent in your dashboard:'));
163
- console.log(chalk.yellow(` ${registration.claim_url}`));
164
- console.log(chalk.gray(' (Opens in browser automatically if possible)\n'));
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(registration.claim_url);
300
+ await open.default(reg.claim_url);
301
+ log(dim('Opened in browser'));
170
302
  } catch {
171
- console.log(chalk.gray(' (Could not open browser automatically)'));
303
+ log(dim('Open the URL above in your browser'));
172
304
  }
173
305
 
174
- // Poll for claim
175
- console.log(chalk.cyan('Waiting for claim (checking every 5 seconds)...\n'));
306
+ console.log();
307
+ logInfo('Waiting for claim...');
176
308
 
177
309
  let claimed = false;
178
- const maxAttempts = 60; // 5 minutes
179
- let attempts = 0;
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 status = await fetch(`${options.apiUrl}/v1/agent-identity/register-pending/${registration.session_id}/status`);
187
- const data = await status.json();
188
-
189
- if (data.status === 'claimed') {
190
- claimed = true;
191
- console.log(chalk.green('\n✓ Agent claimed successfully!\n'));
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 (!claimed) {
204
- console.log(chalk.yellow('\n⚠️ Claim timeout — continuing setup.'));
205
- console.log(chalk.gray(' You can claim later at:'), chalk.yellow(registration.claim_url));
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
- // Update client with credentials
209
- client.updateConfig({
210
- agentDid: registration.did,
211
- privateKey: registration.private_key,
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 (!openclawConfig.plugins) openclawConfig.plugins = {};
236
- if (!openclawConfig.plugins.entries) openclawConfig.plugins.entries = {};
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(chalk.cyan('\n🔗 Pairing gateway...'));
256
-
257
- const pairingRequest = await gateway.createPairingRequest(client, {
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
- console.log(chalk.green(' Pairing request sent'));
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
- console.log(chalk.cyan('\n✅ Setup complete!'));
269
- console.log(chalk.gray('\nNext steps:'));
270
- if (gatewayUrl && gatewayToken) {
271
- console.log(chalk.gray(' 1. Approve the pairing request in Mission Control'));
272
- console.log(chalk.gray(' 2. Restart OpenClaw gateway:'), chalk.cyan('openclaw gateway restart'));
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
- console.log(chalk.gray(' 1. Pair your gateway:'), chalk.cyan('nervepay pair --code <CODE> --gateway-token <TOKEN>'));
361
+ log(dim('Next:'), info('nervepay whoami'));
275
362
  }
276
- console.log(chalk.gray(` ${gatewayUrl ? '3' : '2'}. Test:`), chalk.cyan('nervepay whoami'));
277
- console.log(chalk.gray('\n Mnemonic (backup phrase):'), chalk.yellow(registration.mnemonic));
278
- console.log(chalk.red(' ⚠️ Save this mnemonic - you can recover your keys with it!'));
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
- console.error(chalk.red('\n❌ Setup failed:'), error.message);
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('Pair gateway via device node protocol (default) or pairing code (--code)')
292
- .option('--code <code>', '6-character pairing code (legacy flow)')
293
- .option('--api-url <url>', 'NervePay API URL', 'https://api.nervepay.xyz')
294
- .option('--gateway-url <url>', 'Gateway URL (e.g. ws://127.0.0.1:18789)')
295
- .option('--gateway-token <token>', 'Gateway authentication token (only for --code flow)')
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 wait timeout in seconds', '300')
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
- * Device node pairing — connects to gateway via WebSocket, signs challenge
308
- * with the agent's DID Ed25519 key, and receives a scoped device token.
309
- */
395
+ // ---------------------------------------------------------------------------
396
+ // Device node pairing
397
+ // ---------------------------------------------------------------------------
398
+
310
399
  async function deviceNodePairing(options) {
311
- const { signRawBytes, deriveDeviceId, getPublicKeyFromPrivate } = await import('../dist/utils/crypto.js');
400
+ const { signRawBytes, deriveDeviceId, getPublicKeyFromPrivate, decodeBase58 } =
401
+ await import('../dist/utils/crypto.js');
312
402
 
313
- console.log(chalk.cyan('\n🔗 NervePay Device Node Pairing\n'));
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
- config = await loadConfig();
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
- console.log(chalk.green('Agent identity loaded'));
328
- console.log(chalk.gray(' DID:'), agentDid);
412
+ logOk('Agent loaded');
413
+ field(' DID', agentDid);
329
414
 
330
- // 2. Find gateway URL from config or --gateway-url flag
415
+ // Resolve gateway URL
331
416
  let gatewayUrl = options.gatewayUrl;
332
417
  if (!gatewayUrl) {
333
- const openclawConfig = await readOpenClawConfig();
334
- const gw = extractGatewayConfig(openclawConfig);
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
- // Normalize to ws:// URL
347
- let wsUrl = gatewayUrl.replace(/\/$/, '');
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
- // 3. Derive device ID from agent's public key
426
+ // Device identity
355
427
  const publicKeyBase58 = await getPublicKeyFromPrivate(privateKey);
356
428
  const deviceId = deriveDeviceId(publicKeyBase58);
357
- console.log(chalk.gray(' Device ID:'), deviceId.slice(0, 16) + '...');
429
+ field(' Device', deviceId.slice(0, 16) + '...');
358
430
 
359
- // 4. Connect to gateway via WebSocket
360
- console.log(chalk.cyan('\n📡 Connecting to gateway...'));
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 values before entering the WebSocket flow
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(`Pairing approval timed out (waited ${options.timeout}s)`));
458
+ settle(reject, new Error(`Approval timed out after ${options.timeout}s`));
389
459
  }, timeoutMs + 15000);
390
460
 
391
- ws.on('error', (err) => {
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 reasonStr = reason?.toString() || '';
399
- const detail = lastError
400
- ? lastError
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 raw = data.toString();
411
- const frame = JSON.parse(raw);
472
+ const frame = JSON.parse(data.toString());
473
+ logDebug('<<', JSON.stringify(frame).slice(0, 300));
412
474
 
413
- // Step 5: Receive connect.challenge → sign nonce
475
+ // Challenge → sign and connect
414
476
  if (frame.event === 'connect.challenge') {
415
477
  const nonce = frame.payload?.nonce || '';
416
- console.log(chalk.green('Challenge received'));
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
- const connectReq = {
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: 1,
431
- maxProtocol: 1,
490
+ minProtocol: 3,
491
+ maxProtocol: 3,
432
492
  scopes: ['operator.read', 'operator.write'],
433
493
  client: {
434
- id: 'openclaw',
435
- version: '1.2.6',
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
- console.log(chalk.green('Connected to gateway'));
514
+ logOk('Connected to gateway');
456
515
 
457
- // Step 7: Send node.pair.request
458
516
  pairId = crypto.randomUUID();
459
- const pairReq = {
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: 'cli',
523
+ platform: process.platform,
466
524
  },
467
- };
468
- ws.send(JSON.stringify(pairReq));
525
+ }));
469
526
  } else {
470
- const errDetail = typeof frame.error === 'object'
471
- ? JSON.stringify(frame.error)
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 request response → extract requestId
535
+ // Pair response
483
536
  if (frame.type === 'res' && frame.id === pairId) {
484
537
  if (frame.ok) {
485
538
  requestId = frame.payload?.requestId || '';
486
- console.log(chalk.green('Pairing request submitted'));
487
- console.log(chalk.cyan('\n⏳ Waiting for gateway owner approval...'));
488
- console.log(chalk.gray(` Approve with: openclaw devices approve ${requestId}`));
489
- console.log(chalk.gray(` Timeout: ${options.timeout}s\n`));
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
- const errDetail = typeof frame.error === 'object'
492
- ? JSON.stringify(frame.error)
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
- // Step 8: node.pair.resolved event
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(reject, new Error('Pairing request was rejected by the gateway owner'));
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(`Unexpected pairing decision: ${decision}`));
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
- // Log unhandled frames for debugging
525
- if (frame.type === 'res' || frame.error) {
526
- console.log(chalk.gray(' [debug] Unhandled frame:'), JSON.stringify(frame).slice(0, 200));
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
- // 9. Send device token to NervePay API
535
- console.log(chalk.green('\n✓ Pairing approved! Saving to NervePay...'));
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
- console.log(chalk.green('\n✅ Device pairing complete!'));
551
- console.log(chalk.gray(' Gateway ID:'), apiResult.gateway_id || 'created');
552
- console.log(chalk.gray(' Device ID: '), deviceId.slice(0, 16) + '...');
553
- console.log(chalk.gray(' Agent DID: '), agentDid);
554
- console.log(chalk.gray('\n Your gateway is now live in the NervePay dashboard.'));
555
- console.log(chalk.gray(' Go to Mission Control to spawn agents and manage tasks.\n'));
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
- console.error(chalk.red('\n❌ Device pairing failed:'), error.message);
558
- process.exit(1);
608
+ die(`Device pairing failed: ${error.message}`);
559
609
  }
560
610
  }
561
611
 
562
- /**
563
- * Legacy code-based pairing flow (--code flag provided).
564
- */
612
+ // ---------------------------------------------------------------------------
613
+ // Legacy code pairing
614
+ // ---------------------------------------------------------------------------
615
+
565
616
  async function legacyCodePairing(options) {
566
- console.log(chalk.cyan('\n🔗 NervePay Code Pairing\n'));
567
- console.log(chalk.gray(' Code:'), chalk.white(options.code));
617
+ logInfo('Code pairing');
618
+ field(' Code', options.code);
619
+ console.log();
568
620
 
569
621
  try {
570
- // Step 1: Load existing credentials or create new identity
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
- console.log(chalk.green(' Using existing agent identity'));
578
- console.log(chalk.gray(' DID:'), agentDid);
628
+ logOk('Agent loaded');
629
+ field(' DID', agentDid);
579
630
  } catch {
580
- // No existing config — create a new pending identity
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
- // Show claim URL and offer to open browser
643
+ logOk('Agent created');
644
+ field(' DID', agentDid);
645
+
597
646
  if (claimUrl) {
598
- console.log(chalk.cyan('\n 👤 Claim this agent to link it to your NervePay account:'));
599
- console.log(chalk.yellow(` ${claimUrl}`));
600
- console.log(chalk.gray(' (This links the agent to your dashboard for management)\n'));
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
- console.log(chalk.gray(' Opened in browser.'));
606
- } catch {
607
- console.log(chalk.gray(' Open the URL above in your browser.'));
608
- }
655
+ } catch { /* ok */ }
609
656
 
610
- // Poll for claim (30 seconds — quick pair shouldn't block too long)
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 status = await fetch(`${options.apiUrl}/v1/agent-identity/register-pending/${sessionId}/status`);
617
- const data = await status.json();
618
- if (data.status === 'claimed') {
619
- claimed = true;
620
- console.log(chalk.green(' ✓ Agent claimed successfully!\n'));
621
- break;
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(chalk.cyan('\n 🔐 Recovery mnemonic:'));
640
- console.log(chalk.yellow(` ${mnemonic}`));
641
- console.log(chalk.red(' ⚠️ Save this — you can recover your keys with it!\n'));
675
+ console.log();
676
+ log(warn('Recovery mnemonic (save this!):'));
677
+ console.log(bold(` ${mnemonic}`));
642
678
  }
643
679
 
644
- // Save credentials immediately so they persist even if pairing fails
645
- try {
646
- const { mkdir } = await import('fs/promises');
647
- await mkdir(join(homedir(), '.nervepay'), { recursive: true });
648
- await writeFile(
649
- NERVEPAY_CREDS_PATH,
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
- // Step 2: Read gateway config (from flags, env, or config files)
688
- console.log(chalk.cyan('📡 Reading gateway config...'));
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 openclawConfig = await readOpenClawConfig();
696
- const gw = extractGatewayConfig(openclawConfig);
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
- console.error(chalk.red('\n❌ Could not find gateway token'));
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
- if (!gatewayUrl) {
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(chalk.green('✓ Gateway found'));
720
- console.log(chalk.gray(' URL:'), gatewayUrl);
708
+ console.log();
709
+ logInfo('Completing pairing...');
721
710
 
722
- // Step 3: Complete pairing
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
- console.log(chalk.green('\n✅ Pairing complete!'));
738
- console.log(chalk.gray(' Gateway ID:'), result.gateway_id || 'created');
739
- console.log(chalk.gray(' Agent DID: '), agentDid);
740
- console.log(chalk.gray('\n Your gateway is now live in the NervePay dashboard.'));
741
- console.log(chalk.gray(' Go to Mission Control to spawn agents and manage tasks.\n'));
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
- console.error(chalk.red('\n❌ Pairing failed:'), error.message);
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
- console.log(chalk.cyan('\n🤖 Agent Identity\n'));
762
- console.log(chalk.gray('DID:'), chalk.white(result.agent_did));
763
- console.log(chalk.gray('Name:'), chalk.white(result.name));
764
- console.log(chalk.gray('Description:'), chalk.white(result.description));
765
- console.log(chalk.gray('Reputation:'), chalk.white(`${result.reputation_score}/100`));
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
- console.error(chalk.red('❌ Error:'), error.message);
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
- console.log(chalk.cyan('\n🌐 Connected Gateways\n'));
788
-
789
- if (!result.gateways || result.gateways.length === 0) {
790
- console.log(chalk.yellow('No gateways connected'));
791
- console.log(chalk.gray('Run:'), chalk.cyan('nervepay setup'), chalk.gray('to pair your gateway'));
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
- console.error(chalk.red('❌ Error:'), error.message);
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 in vault')
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
- console.log(chalk.cyan('\n🔐 Vault Secrets\n'));
820
-
821
- if (!result.secrets || result.secrets.length === 0) {
822
- console.log(chalk.yellow('No secrets stored'));
823
- console.log(chalk.gray('Add secrets in Mission Control > Agent Passports > Secrets tab'));
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
- console.error(chalk.red('❌ Error:'), error.message);
834
- process.exit(1);
822
+ die(error.message);
835
823
  }
836
824
  });
837
825
 
838
826
  // ============================================================================
839
- // HELPER FUNCTIONS
827
+ // Helpers
840
828
  // ============================================================================
841
829
 
842
- async function loadConfig() {
843
- // Try OpenClaw config first (checks env var, standard paths, legacy paths)
844
- try {
845
- const configPath = await findOpenClawConfigPath();
846
- if (configPath) {
847
- const openclawConfig = JSON.parse(await readFile(configPath, 'utf-8'));
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
- // RUN
839
+ // Run
881
840
  // ============================================================================
882
841
 
883
842
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nervepay",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "description": "NervePay plugin for OpenClaw - Self-sovereign identity, vault, and orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",