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.
Files changed (2) hide show
  1. package/bin/nervepay-cli.js +483 -499
  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,71 +392,54 @@ 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
- const pubKeyB64 = Buffer.from(pubKeyBytes).toString('base64');
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(`Pairing approval timed out (waited ${options.timeout}s)`));
460
+ settle(reject, new Error(`Approval timed out after ${options.timeout}s`));
389
461
  }, timeoutMs + 15000);
390
462
 
391
- ws.on('error', (err) => {
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 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);
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 raw = data.toString();
411
- const frame = JSON.parse(raw);
474
+ const frame = JSON.parse(data.toString());
475
+ logDebug('<<', JSON.stringify(frame).slice(0, 300));
412
476
 
413
- // Step 5: Receive connect.challenge sign nonce
477
+ // Challenge build auth payload, sign, and connect
414
478
  if (frame.event === 'connect.challenge') {
415
479
  const nonce = frame.payload?.nonce || '';
416
- console.log(chalk.green('Challenge received'));
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
- const connectReq = {
509
+ ws.send(JSON.stringify({
425
510
  type: 'req',
426
511
  id: connectId,
427
512
  method: 'connect',
428
513
  params: {
429
- role: 'operator',
430
- minProtocol: 1,
431
- maxProtocol: 1,
432
- scopes: ['operator.read', 'operator.write'],
514
+ role,
515
+ minProtocol: 3,
516
+ maxProtocol: 3,
517
+ scopes,
433
518
  client: {
434
- id: 'openclaw',
435
- version: '1.2.6',
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: pubKeyB64,
442
- signature: signatureB64,
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
- console.log(chalk.green('Connected to gateway'));
539
+ logOk('Connected to gateway');
456
540
 
457
- // Step 7: Send node.pair.request
458
541
  pairId = crypto.randomUUID();
459
- const pairReq = {
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: 'cli',
548
+ platform: process.platform,
466
549
  },
467
- };
468
- ws.send(JSON.stringify(pairReq));
550
+ }));
469
551
  } 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));
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 request response → extract requestId
560
+ // Pair response
483
561
  if (frame.type === 'res' && frame.id === pairId) {
484
562
  if (frame.ok) {
485
563
  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`));
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
- 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));
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
- // Step 8: node.pair.resolved event
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, { token, nodeId, requestId });
514
- } else if (decision === 'rejected') {
515
- ws.close();
516
- settle(reject, new Error('Pairing request was rejected by the gateway owner'));
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(`Unexpected pairing decision: ${decision}`));
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
- // 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
- }
601
+ // Unhandled
602
+ logDebug('Unhandled frame:', JSON.stringify(frame).slice(0, 200));
603
+ } catch { /* non-JSON */ }
531
604
  });
532
605
  });
533
606
 
534
- // 9. Send device token to NervePay API
535
- console.log(chalk.green('\n✓ Pairing approved! Saving to NervePay...'));
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
- 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'));
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
- console.error(chalk.red('\n❌ Device pairing failed:'), error.message);
558
- process.exit(1);
633
+ die(`Device pairing failed: ${error.message}`);
559
634
  }
560
635
  }
561
636
 
562
- /**
563
- * Legacy code-based pairing flow (--code flag provided).
564
- */
637
+ // ---------------------------------------------------------------------------
638
+ // Legacy code pairing
639
+ // ---------------------------------------------------------------------------
640
+
565
641
  async function legacyCodePairing(options) {
566
- console.log(chalk.cyan('\n🔗 NervePay Code Pairing\n'));
567
- console.log(chalk.gray(' Code:'), chalk.white(options.code));
642
+ logInfo('Code pairing');
643
+ field(' Code', options.code);
644
+ console.log();
568
645
 
569
646
  try {
570
- // Step 1: Load existing credentials or create new identity
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
- console.log(chalk.green(' Using existing agent identity'));
578
- console.log(chalk.gray(' DID:'), agentDid);
653
+ logOk('Agent loaded');
654
+ field(' DID', agentDid);
579
655
  } catch {
580
- // No existing config — create a new pending identity
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
- // Show claim URL and offer to open browser
668
+ logOk('Agent created');
669
+ field(' DID', agentDid);
670
+
597
671
  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'));
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
- console.log(chalk.gray(' Opened in browser.'));
606
- } catch {
607
- console.log(chalk.gray(' Open the URL above in your browser.'));
608
- }
680
+ } catch { /* ok */ }
609
681
 
610
- // Poll for claim (30 seconds — quick pair shouldn't block too long)
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 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));
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(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'));
700
+ console.log();
701
+ log(warn('Recovery mnemonic (save this!):'));
702
+ console.log(bold(` ${mnemonic}`));
642
703
  }
643
704
 
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);
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
- // Step 2: Read gateway config (from flags, env, or config files)
688
- console.log(chalk.cyan('📡 Reading gateway config...'));
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 openclawConfig = await readOpenClawConfig();
696
- const gw = extractGatewayConfig(openclawConfig);
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
- 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
- }
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
- 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
- }
731
+ logOk(`Gateway: ${gatewayUrl}`);
718
732
 
719
- console.log(chalk.green('✓ Gateway found'));
720
- console.log(chalk.gray(' URL:'), gatewayUrl);
733
+ console.log();
734
+ logInfo('Completing pairing...');
721
735
 
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
- });
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
- 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'));
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
- console.error(chalk.red('\n❌ Pairing failed:'), error.message);
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
- 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()));
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
- console.error(chalk.red('❌ Error:'), error.message);
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
- 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
- });
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
- console.error(chalk.red('❌ Error:'), error.message);
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 in vault')
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
- 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
- });
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
- console.error(chalk.red('❌ Error:'), error.message);
834
- process.exit(1);
847
+ die(error.message);
835
848
  }
836
849
  });
837
850
 
838
851
  // ============================================================================
839
- // HELPER FUNCTIONS
852
+ // Helpers
840
853
  // ============================================================================
841
854
 
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');
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
- // RUN
864
+ // Run
881
865
  // ============================================================================
882
866
 
883
867
  program.parse();