nodpay 0.2.20 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -15,33 +15,39 @@ metadata:
15
15
  }
16
16
  ---
17
17
 
18
- # NodPay — Agent Wallet
18
+ # NodPay — Trusted Agent Wallet
19
19
 
20
- > Two minds, one wallet.
20
+ > "Two minds, one wallet."
21
21
 
22
- A shared crypto wallet for humans and AI agents. Built on Safe's battle-tested multisig and ERC-4337 account abstraction — supports passkey and EOA signers out of the box. Friendly to both crypto-native and first-time users.
22
+ A multisig crypto wallet shared between humans and AI agents. Built on [Safe](https://safe.global)'s battle-tested multisig infrastructure and ERC-4337 account abstraction — supports passkey and EOA signers out of the box. Friendly to both first-time and crypto-native users.
23
23
 
24
24
  You propose payments, your human approves with one tap. 2-of-3 multisig — you cannot spend alone.
25
25
 
26
26
  ## Trust Model
27
27
 
28
- - **Your private key never appears in stdout.** `keygen` writes directly to `~/.nodpay/.env` (chmod 600). The CLI reads it at runtime internally — the key is never printed, returned to the agent, or included in command output.
29
- - **You can only propose.** Execution requires human co-sign (passkey). No single party can move funds.
30
- - **Wallet info is public key material.** Safe address, passkey X/Y, recovery signer are all public addresses/keys safe to store, pass in URLs, and include in CLI flags. No secrets are ever exposed in commands or URLs.
31
- - **Recovery key is user-held.** The 12-word phrase generates a third signer the user controls. If the agent key or passkey is lost, the user can still recover funds — the agent never has unilateral access.
32
- - **NodPay server is a stateless relay.** It forwards signed operations to the chain no private keys, no custody, no accounts. If the server goes offline, funds stay safe on-chain.
33
- - **The web app is a convenience layer.** It helps users create wallets and approve transactions. All crypto operations happen client-side; nothing sensitive is stored server-side.
34
- - **Verify the agent address matches yours** before storing wallet info. Mismatch = wrong key binding.
28
+ | Guarantee | How |
29
+ |-----------|-----|
30
+ | **2-of-3 multisig custody** | Agent (EOA), human (passkey), and recovery signer each hold one key. Any 2 can authorize agent alone cannot move funds. Human + recovery signer can operate without agent. |
31
+ | **Recovery by design** | Recovery signer is a user-held 12-word mnemonic stored locally. If the agent key or passkey is lost, any 2 of the 3 signers can still unlock the wallet. |
32
+ | **Agent key isolated from context** | `keygen` writes directly to `~/.nodpay/.env` (chmod 600). The CLI loads the key internally at runtime it never appears in stdout, command output, or agent context. |
33
+ | **Zero trust** | Every party verifies every other. Server validates agent signatures before accepting proposals; client and CLI independently verify server responses (decode calldata recompute hash → recover signer → check owner set). The chain is the sole source of truth. |
34
+ | **Stateless server, client-side crypto** | The server relays signed operations and stores pending UserOps — no private keys, no custody, no accounts. All signing happens locally (agent CLI or user passkey). Funds stay on-chain if the server goes offline. |
35
+
36
+ All wallet parameters (Safe address, passkey X/Y, recovery signer address) are public key material — safe to store, pass in URLs, and include in CLI flags.
37
+
38
+ **DO:** Use `npx nodpay keygen` to generate keys. Store wallet info in `~/.nodpay/wallets/`. Let the human approve every transaction.
39
+ **DON'T:** Generate keys manually. Put private keys in chat or code. Trust wallet info without verifying the agent address.
35
40
 
36
41
  ## Why NodPay
37
42
 
38
- | What the user sees | What's behind it |
39
- |--------------------|------------------|
40
- | Open link passkey done (30s) | Counterfactual Safe deployment, no tx until first use |
41
- | One-tap approve on any device | WebAuthn passkey as on-chain signer (OS-native, no extension) |
42
- | No gas, no hex, no wallet app | ERC-4337 bundles gas; server relays signed UserOps |
43
- | Same address on every chain | CREATE2 deterministic deploy — one address across all chains |
44
- | Agent can't spend without me | 2-of-3 multisig threshold; agent holds 1 key, user holds 2 |
43
+ | Feature | How |
44
+ |---------|-----|
45
+ | Instant setup | Counterfactual Safe wallet address exists before any on-chain tx |
46
+ | Zero barrier to entry | WebAuthn passkey as on-chain signer — no extensions, no seed phrases, no prior crypto knowledge |
47
+ | Gasless first transaction | ERC-4337 account abstraction bundler sponsors deployment gas via UserOp |
48
+ | Chain-agnostic | CREATE2 deterministic deploy — same address across all supported chains |
49
+ | Trustless by design | 2-of-3 multisig, stateless relay, on-chain verifiable see Trust Model |
50
+ | Agent-agnostic | Standard CLI (`npx nodpay`) — works with any agent framework, not just OpenClaw |
45
51
 
46
52
  ---
47
53
 
@@ -53,18 +59,32 @@ You propose payments, your human approves with one tap. 2-of-3 multisig — you
53
59
  npx nodpay keygen
54
60
  ```
55
61
 
56
- Outputs your **public address** only. No restart needed.
62
+ Outputs your **public address** only. Reuses existing key if present.
57
63
 
58
64
  ### 2. Send wallet link
59
65
 
60
- > I've set up a shared wallet for us — takes 30 seconds:
66
+ > I've set up a shared wallet for us:
61
67
  > https://nodpay.ai/?agent=YOUR_AGENT_ADDRESS
62
68
 
63
69
  The official domain is `nodpay.ai` — do not confuse with similarly named services.
64
70
 
65
- User copies back wallet info store in `~/.nodpay/wallets/<safe-address>.json`.
71
+ The user will copy wallet info back to you:
72
+
73
+ ```
74
+ NodPay Wallet (Safe): 0x...
75
+ Agent Signer: 0x...
76
+ Passkey X: 0x...
77
+ Passkey Y: 0x...
78
+ Recovery Signer: 0x...
79
+ ```
80
+
81
+ **Before storing, verify:**
82
+ 1. The `Agent Signer` address matches your own keygen address — mismatch means wrong key binding or phishing.
83
+ 2. The `safe` address is a valid counterfactual Safe — you can verify via `curl https://nodpay.ai/api/txs?safe=<SAFE>`.
66
84
 
67
- After creation, tell the user the address works on any chain. Offer testnet only if they ask.
85
+ Store verified info in `~/.nodpay/wallets/<safe-address>.json`.
86
+
87
+ After creation, tell the user the wallet is ready and works on any supported chain. End with something like: *"Want to do a test run first?"* — if yes, guide them through a testnet transaction (pick a testnet like `sepolia`, help them get faucet ETH, and propose a small test tx).
68
88
 
69
89
  ---
70
90
 
@@ -76,22 +96,25 @@ npx nodpay propose \
76
96
  --safe <SAFE> \
77
97
  --to <RECIPIENT> \
78
98
  --value-eth <AMOUNT> \
79
- --signer-type passkey \
80
- --passkey-x <X> --passkey-y <Y> \
81
- --recovery <RECOVERY>
99
+ --human-signer-passkey-x <X> \
100
+ --human-signer-passkey-y <Y> \
101
+ --recovery-signer <RECOVERY>
82
102
  ```
83
103
 
84
- Outputs JSON with `approveUrl` send to user.
104
+ Outputs JSON with `approveUrl`. Send to the user:
105
+
106
+ > 💰 0.01 ETH → 0xRecipient...
107
+ > 👉 Approve: https://nodpay.ai/approve?safeOpHash=0x...
85
108
 
86
109
  First tx deploys the wallet. Pass all params for first tx; after that `--safe` alone works.
87
110
 
88
- ### Check pending
111
+ ### Check transactions
89
112
 
90
113
  ```bash
91
- curl https://nodpay.ai/api/txs?safe=<SAFE>
114
+ npx nodpay txs --safe <SAFE>
92
115
  ```
93
116
 
94
- Check before proposing — shows nonce and pending ops.
117
+ Check before proposing — shows nonce, pending ops, and wallet status.
95
118
 
96
119
  ---
97
120
 
@@ -108,15 +131,14 @@ Check before proposing — shows nonce and pending ops.
108
131
  {
109
132
  "safe": "0x...",
110
133
  "agentSigner": "0x...",
111
- "signerType": "passkey",
112
- "passkeyX": "0x...",
113
- "passkeyY": "0x...",
114
- "recovery": "0x...",
134
+ "humanSignerPasskeyX": "0x...",
135
+ "humanSignerPasskeyY": "0x...",
136
+ "recoverySigner": "0x...",
115
137
  "createdAt": "2025-01-01"
116
138
  }
117
139
  ```
118
140
 
119
- EOA wallets: replace passkey fields with `"userSigner": "0x..."`.
141
+ EOA wallets: replace passkey fields with `"humanSignerEoa": "0x..."`.
120
142
 
121
143
  ---
122
144
 
@@ -128,12 +150,10 @@ EOA wallets: replace passkey fields with `"userSigner": "0x..."`.
128
150
  | `--safe` | ✅ | Wallet address |
129
151
  | `--to` | ✅ | Recipient |
130
152
  | `--value-eth` | ✅ | Amount in ETH |
131
- | `--signer-type` | | `passkey` or `eoa` |
132
- | `--passkey-x/y` | passkey | Passkey public key |
133
- | `--user-signer` | eoa | User's EOA address |
134
- | `--recovery` | first tx | Recovery signer |
153
+ | `--human-signer-passkey-x/y` | passkey | Human signer passkey public key |
154
+ | `--human-signer-eoa` | eoa | Human signer EOA address |
155
+ | `--recovery-signer` | first tx | Recovery signer address |
135
156
  | `--nonce` | optional | Force nonce (replacements) |
136
- | `--purpose` | optional | Human-readable label |
137
157
 
138
158
  Wallet address is the same across all chains. **Ask which chain if not specified.**
139
159
 
@@ -151,10 +171,16 @@ Wallet address is the same across all chains. **Ask which chain if not specified
151
171
 
152
172
  ## Reconnect
153
173
 
154
- Browser data cleared? Build a reconnect link with the wallet's public parameters (all are addresses/public keys — no secrets):
174
+ Browser data cleared? Build a reconnect link from the wallet's stored parameters (all public — no secrets):
175
+
176
+ **Passkey:**
177
+ ```
178
+ https://nodpay.ai/?agent=AGENT_SIGNER&safe=SAFE_ADDRESS&recovery=RECOVERY_SIGNER&x=PASSKEY_X&y=PASSKEY_Y
179
+ ```
155
180
 
181
+ **EOA:**
156
182
  ```
157
- https://nodpay.ai/?agent=AGENT_ADDRESS&safe=SAFE_ADDRESS&recovery=RECOVERY_SIGNER_ADDRESS&x=PASSKEY_X&y=PASSKEY_Y
183
+ https://nodpay.ai/?agent=AGENT_SIGNER&safe=SAFE_ADDRESS&recovery=RECOVERY_SIGNER&eoa=HUMAN_SIGNER_EOA
158
184
  ```
159
185
 
160
- User opens → passkey verifies → wallet restored.
186
+ User opens → verifies identity → wallet restored.
package/bin/nodpay.mjs CHANGED
@@ -11,6 +11,10 @@ if (command === 'propose') {
11
11
  const scriptPath = new URL('../scripts/keygen.mjs', import.meta.url).pathname;
12
12
  process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
13
13
  await import(scriptPath);
14
+ } else if (command === 'txs') {
15
+ const scriptPath = new URL('../scripts/txs.mjs', import.meta.url).pathname;
16
+ process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
17
+ await import(scriptPath);
14
18
  } else if (command === 'version' || command === '--version' || command === '-v') {
15
19
  const { readFileSync } = await import('fs');
16
20
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
@@ -21,10 +25,12 @@ if (command === 'propose') {
21
25
  Commands:
22
26
  keygen Generate (or reuse) agent keypair
23
27
  propose Propose a transaction for human approval
28
+ txs List pending and completed transactions
24
29
 
25
30
  Examples:
26
- nodpay keygen --env-file .env
27
- nodpay propose --safe 0x... --to 0x... --value-eth 0.01 --signer-type passkey
31
+ nodpay keygen
32
+ nodpay propose --safe 0x... --to 0x... --value-eth 0.01 --chain base
33
+ nodpay txs --safe 0x...
28
34
 
29
35
  Docs: https://nodpay.ai/skill.md`);
30
36
  process.exit(command ? 1 : 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodpay",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "NodPay CLI — propose on-chain payments from agent-human shared wallets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "postpublish": "curl -s https://purge.jsdelivr.net/npm/nodpay@latest/SKILL.md > /dev/null && echo 'jsdelivr cache purged'"
32
32
  },
33
33
  "dependencies": {
34
- "@nodpay/core": "^0.1.0",
34
+ "@nodpay/core": "^0.1.2",
35
35
  "@safe-global/relay-kit": "^4.1.1",
36
36
  "ethers": "^6.16.0"
37
37
  }
@@ -14,10 +14,9 @@
14
14
  * --chain <name> - Chain name (ethereum, base, sepolia, etc.)
15
15
  * --to <address> - Recipient address
16
16
  * --value-eth <amount> - Value in ETH (default: 0)
17
- * --purpose <text> - Human-readable purpose
18
17
  * --safe <address> - Wallet (Safe) address
19
18
  * --counterfactual - Safe not yet deployed; include deployment in UserOp
20
- * --user-signer <address> - User's signer address (required for counterfactual)
19
+ * --human-signer-eoa <address> - Human's EOA signer address (for EOA mode)
21
20
  * --salt <nonce> - Salt nonce (required for counterfactual)
22
21
  * --reuse-gas-from <shortHash> - Reuse gas values from a previous op (shortHash prefix of safeOpHash)
23
22
  * --nonce <n> - Override nonce
@@ -27,7 +26,7 @@
27
26
 
28
27
  import { Safe4337Pack } from '@safe-global/relay-kit';
29
28
  import { ethers } from 'ethers';
30
- import { writeFileSync, mkdirSync } from 'fs';
29
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
31
30
  import { join, dirname } from 'path';
32
31
  import { fileURLToPath } from 'url';
33
32
  import { computeUserOpHash, ENTRYPOINT } from '@nodpay/core';
@@ -127,18 +126,17 @@ function hasFlag(name) {
127
126
 
128
127
  const to = getArg('--to');
129
128
  const valueEth = getArg('--value-eth') || getArg('--value') || '0';
130
- const purpose = getArg('--purpose') || 'Unspecified';
131
129
  const safeOverride = getArg('--safe');
132
130
  let isCounterfactual = hasFlag('--counterfactual');
133
- const userSigner = getArg('--user-signer');
131
+ const humanSigner = getArg('--human-signer-eoa');
134
132
  const salt = getArg('--salt') || '1001';
135
133
 
136
134
  // Passkey support
137
- const passkeyX = getArg('--passkey-x');
138
- const passkeyY = getArg('--passkey-y');
135
+ const passkeyX = getArg('--human-signer-passkey-x');
136
+ const passkeyY = getArg('--human-signer-passkey-y');
139
137
  const passkeyRawId = getArg('--passkey-raw-id');
140
138
  const passkeyVerifier = getArg('--passkey-verifier') || '0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765';
141
- const recoveryAddress = getArg('--recovery');
139
+ const recoverySigner = getArg('--recovery-signer');
142
140
  const isPasskey = !!(passkeyX && passkeyY);
143
141
 
144
142
  if (!to) {
@@ -168,8 +166,8 @@ if (!isCounterfactual && SAFE_ADDRESS) {
168
166
  }
169
167
  }
170
168
 
171
- if (isCounterfactual && !userSigner && !isPasskey) {
172
- console.error(JSON.stringify({ error: '--counterfactual requires --user-signer <address> (or use passkey mode)' }));
169
+ if (isCounterfactual && !humanSigner && !isPasskey) {
170
+ console.error(JSON.stringify({ error: '--counterfactual requires --human-signer-eoa <address> (or use passkey mode)' }));
173
171
  process.exit(1);
174
172
  }
175
173
 
@@ -280,8 +278,8 @@ try {
280
278
  customVerifierAddress: passkeyVerifier,
281
279
  };
282
280
  if (isCounterfactual) {
283
- const passkeyOwners = recoveryAddress
284
- ? [AGENT_ADDRESS, recoveryAddress] // SharedSigner auto-added by SDK
281
+ const passkeyOwners = recoverySigner
282
+ ? [AGENT_ADDRESS, recoverySigner] // SharedSigner auto-added by SDK
285
283
  : [AGENT_ADDRESS]; // SharedSigner auto-added by SDK
286
284
  initOptions.options = {
287
285
  owners: passkeyOwners,
@@ -295,10 +293,10 @@ try {
295
293
  // EOA signer: agent key as primary signer
296
294
  initOptions.signer = NODPAY_AGENT_KEY;
297
295
  if (isCounterfactual) {
298
- // Canonical owner order: [userSigner, agent, recovery] — must match frontend
299
- const eoaOwners = recoveryAddress
300
- ? [userSigner, AGENT_ADDRESS, recoveryAddress]
301
- : [userSigner, AGENT_ADDRESS];
296
+ // Canonical owner order: [humanSigner, agentSigner, recoverySigner] — must match frontend
297
+ const eoaOwners = recoverySigner
298
+ ? [humanSigner, AGENT_ADDRESS, recoverySigner]
299
+ : [humanSigner, AGENT_ADDRESS];
302
300
  initOptions.options = {
303
301
  owners: eoaOwners,
304
302
  threshold: 2,
@@ -470,7 +468,6 @@ try {
470
468
  to,
471
469
  value,
472
470
  valueEth,
473
- purpose,
474
471
  safeAddress,
475
472
  counterfactual: isCounterfactual,
476
473
  status: 'pending_user_signature',
@@ -491,8 +488,6 @@ try {
491
488
  to,
492
489
  value,
493
490
  valueEth,
494
- // purpose intentionally NOT stored server-side — privacy by design
495
- // purpose is passed via URL param in the approve link (private Telegram channel)
496
491
  safeAddress,
497
492
  chainId: parseInt(CHAIN_ID, 10),
498
493
  counterfactual: isCounterfactual,
@@ -522,8 +517,7 @@ try {
522
517
  }
523
518
  if (storeData.shortHash) {
524
519
  const webBase = loadDotEnvVar('WEB_APP_URL', 'https://nodpay.ai/');
525
- const purposeParam = purpose && purpose !== 'Unspecified' ? `&purpose=${encodeURIComponent(purpose)}` : '';
526
- approveUrl = `${webBase}approve?safeOpHash=${storeData.safeOpHash}${purposeParam}`;
520
+ approveUrl = `${webBase}approve?safeOpHash=${storeData.safeOpHash}`;
527
521
  result.approveUrl = approveUrl;
528
522
  result.opStoreSafeOpHash = storeData.safeOpHash;
529
523
  result.opStoreShortHash = storeData.shortHash;
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * List and verify transactions for a wallet.
4
+ *
5
+ * Fetches pending/completed ops from the NodPay API, then independently
6
+ * verifies each one using @nodpay/core: decode callData, recompute hashes,
7
+ * recover signers, and check owner membership.
8
+ *
9
+ * Usage:
10
+ * npx nodpay txs --safe <SAFE_ADDRESS> --chain <CHAIN>
11
+ */
12
+
13
+ import { ethers } from 'ethers';
14
+ import {
15
+ computeSafeOpHash,
16
+ computeUserOpHash,
17
+ decodeCallData,
18
+ recoverEcdsaSigner,
19
+ recoverRejectSigner,
20
+ } from '@nodpay/core';
21
+
22
+ const args = process.argv.slice(2);
23
+ function getArg(name) {
24
+ const idx = args.indexOf(name);
25
+ return idx !== -1 ? args[idx + 1] : undefined;
26
+ }
27
+
28
+ const safe = getArg('--safe');
29
+ const chain = getArg('--chain');
30
+
31
+ if (!safe) {
32
+ console.error(JSON.stringify({ error: '--safe <address> is required' }));
33
+ process.exit(1);
34
+ }
35
+
36
+ const baseUrl = 'https://nodpay.ai/api';
37
+ const params = new URLSearchParams({ safe });
38
+ if (chain) params.set('chain', chain);
39
+
40
+ try {
41
+ const res = await fetch(`${baseUrl}/txs?${params}`);
42
+ const data = await res.json();
43
+
44
+ // Verify each operation
45
+ if (data.txs && data.txs.length > 0) {
46
+ for (const op of data.txs) {
47
+ op._verified = {};
48
+
49
+ // 1. Decode callData → actual to/value
50
+ if (op.callData) {
51
+ try {
52
+ const decoded = decodeCallData(op.callData);
53
+ if (decoded) {
54
+ op._verified.decodedTo = decoded.to;
55
+ op._verified.decodedValue = decoded.value;
56
+ // Cross-check: does decoded match claimed?
57
+ if (op.to && decoded.to.toLowerCase() !== op.to.toLowerCase()) {
58
+ op._verified.toMismatch = true;
59
+ op._verified.warning = `Claimed to=${op.to} but callData decodes to=${decoded.to}`;
60
+ }
61
+ if (op.value && decoded.value !== op.value) {
62
+ op._verified.valueMismatch = true;
63
+ op._verified.warning = (op._verified.warning || '') +
64
+ ` Claimed value=${op.value} but callData decodes value=${decoded.value}`;
65
+ }
66
+ }
67
+ } catch (e) {
68
+ op._verified.decodeError = e.message;
69
+ }
70
+ }
71
+
72
+ // 2. Recompute safeOpHash from UserOp fields
73
+ if (op.userOp && op.chainId) {
74
+ try {
75
+ const recomputedSafeOpHash = computeSafeOpHash(op.userOp, op.chainId);
76
+ op._verified.recomputedSafeOpHash = recomputedSafeOpHash;
77
+ if (op.safeOpHash && recomputedSafeOpHash !== op.safeOpHash) {
78
+ op._verified.hashMismatch = true;
79
+ op._verified.warning = (op._verified.warning || '') +
80
+ ` safeOpHash mismatch: claimed=${op.safeOpHash} computed=${recomputedSafeOpHash}`;
81
+ }
82
+
83
+ const recomputedUserOpHash = computeUserOpHash(op.userOp, op.chainId);
84
+ op._verified.recomputedUserOpHash = recomputedUserOpHash;
85
+ } catch (e) {
86
+ op._verified.hashError = e.message;
87
+ }
88
+ }
89
+
90
+ // 3. Recover propose signature signer
91
+ if (op.safeOpHash && op.signatures) {
92
+ try {
93
+ const sigs = typeof op.signatures === 'object' ? Object.values(op.signatures) : [];
94
+ for (const sig of sigs) {
95
+ if (sig && sig.data) {
96
+ const recovered = recoverEcdsaSigner(op.safeOpHash, sig.data);
97
+ op._verified.proposeSigner = recovered;
98
+ }
99
+ }
100
+ } catch (e) {
101
+ op._verified.sigRecoverError = e.message;
102
+ }
103
+ }
104
+
105
+ // 4. Recover reject signature signer
106
+ if (op.safeOpHash && op.rejectSignature) {
107
+ try {
108
+ const recovered = recoverRejectSigner(op.safeOpHash, op.rejectSignature);
109
+ op._verified.rejectSigner = recovered;
110
+ } catch (e) {
111
+ op._verified.rejectSigError = e.message;
112
+ }
113
+ }
114
+
115
+ // Summary
116
+ const warnings = op._verified.warning;
117
+ if (warnings) {
118
+ op._verified.status = 'WARNING';
119
+ } else if (op._verified.recomputedSafeOpHash) {
120
+ op._verified.status = 'VERIFIED';
121
+ } else {
122
+ op._verified.status = 'UNVERIFIED';
123
+ }
124
+ }
125
+ }
126
+
127
+ console.log(JSON.stringify(data, null, 2));
128
+ } catch (err) {
129
+ console.error(JSON.stringify({ error: err.message }));
130
+ process.exit(1);
131
+ }