@getalby/cli 0.2.4 → 0.4.0

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/README.md CHANGED
@@ -6,17 +6,64 @@ Built for agents - use with the [Alby Bitcoin Payments CLI Skill](https://github
6
6
 
7
7
  ## Usage
8
8
 
9
+ ### First-time setup
10
+
11
+ The CLI is an interface to a wallet and therefore needs a connection secret.
12
+
13
+ **Option 1: `auth` — for wallets that support it (e.g. Alby Hub)**
14
+
9
15
  ```bash
10
- # Pass a file path to a connection secret (preferred)
11
- npx @getalby/cli -c /path/to/secret.txt <command> [options]
16
+ # Step 1: generate a connection URL and open it in your wallet to approve
17
+ # --app-name is the name of the agent/app that will use the wallet via the CLI (e.g. "Claude Code", "OpenClaw")
18
+ npx @getalby/cli auth https://my.albyhub.com --app-name "Claude Code"
12
19
 
13
- # Or pass connection string directly
14
- npx @getalby/cli -c "nostr+walletconnect://..." <command> [options]
20
+ # Step 2: after approving in the wallet, complete the connection
21
+ npx @getalby/cli auth --complete
22
+ ```
23
+
24
+ **Option 2: `connect` — paste a NWC connection secret directly**
25
+
26
+ ```bash
27
+ npx @getalby/cli connect "nostr+walletconnect://..."
28
+ ```
29
+
30
+ ### Multiple wallets
31
+
32
+ Use `--wallet-name` when setting up to save named connections:
33
+
34
+ ```bash
35
+ npx @getalby/cli connect "nostr+walletconnect://..." --wallet-name work
36
+ npx @getalby/cli auth https://my.albyhub.com --app-name "Claude Code" --wallet-name personal
37
+ ```
38
+
39
+ Then pass `--wallet-name` to any command to use that wallet:
40
+
41
+ ```bash
42
+ npx @getalby/cli --wallet-name work get-balance
43
+ npx @getalby/cli --wallet-name personal pay-invoice --invoice lnbc...
44
+ ```
45
+
46
+ ### Connection secret resolution (in order of priority)
47
+
48
+ 1. `--connection-secret` flag (value or path to file)
49
+ 2. `--wallet-name` flag (`~/.alby-cli/connection-secret-<name>.key`)
50
+ 3. `NWC_URL` environment variable
51
+ 4. `~/.alby-cli/connection-secret.key` (default file location)
52
+
53
+ ```bash
54
+ # Use the default saved wallet connection (preferred)
55
+ npx @getalby/cli <command> [options]
56
+
57
+ # Use a named wallet
58
+ npx @getalby/cli --wallet-name alice <command> [options]
59
+
60
+ # Or pass a connection secret directly
61
+ npx @getalby/cli -c /path/to/secret.txt <command> [options]
15
62
  ```
16
63
 
17
64
  The `-c` option auto-detects whether you're passing a connection string or a file path. You can get a connection string from your NWC-compatible wallet (e.g., [Alby](https://getalby.com)).
18
65
 
19
- You can also pass a connection string via the `NWC_URL` environment variable instead of using the `-c` option:
66
+ You can also set the `NWC_URL` environment variable instead of using the `-c` option:
20
67
 
21
68
  ```txt
22
69
  NWC_URL="nostr+walletconnect://..."
@@ -42,44 +89,50 @@ curl -X POST "https://faucet.nwc.dev/wallets/<username>/topup?amount=5000"
42
89
 
43
90
  ### Wallet Commands
44
91
 
45
- These commands require `--connection-secret`:
92
+ These commands require a wallet connection - either default connection, or specify a custom connection with `-w`, '-c', or `NWC_URL` environment variable:
46
93
 
47
94
  ```bash
48
95
  # Get wallet balance
49
- npx @getalby/cli -c "nostr+walletconnect://..." get-balance
96
+ npx @getalby/cli get-balance
50
97
 
51
98
  # Get wallet info
52
- npx @getalby/cli -c "nostr+walletconnect://..." get-info
99
+ npx @getalby/cli get-info
53
100
 
54
101
  # Get wallet service capabilities
55
- npx @getalby/cli -c "nostr+walletconnect://..." get-wallet-service-info
102
+ npx @getalby/cli get-wallet-service-info
56
103
 
57
104
  # Create an invoice
58
- npx @getalby/cli -c "nostr+walletconnect://..." make-invoice --amount 1000 --description "Payment"
105
+ npx @getalby/cli make-invoice --amount 1000 --description "Payment"
59
106
 
60
107
  # Pay an invoice
61
- npx @getalby/cli -c "nostr+walletconnect://..." pay-invoice --invoice "lnbc..."
108
+ npx @getalby/cli pay-invoice --invoice "lnbc..."
62
109
 
63
110
  # Send a keysend payment
64
- npx @getalby/cli -c "nostr+walletconnect://..." pay-keysend --pubkey "02abc..." --amount 100
111
+ npx @getalby/cli pay-keysend --pubkey "02abc..." --amount 100
65
112
 
66
113
  # Look up an invoice by payment hash
67
- npx @getalby/cli -c "nostr+walletconnect://..." lookup-invoice --payment-hash "abc123..."
114
+ npx @getalby/cli lookup-invoice --payment-hash "abc123..."
68
115
 
69
116
  # List transactions
70
- npx @getalby/cli -c "nostr+walletconnect://..." list-transactions --limit 10
117
+ npx @getalby/cli list-transactions --limit 10
71
118
 
72
119
  # Get wallet budget
73
- npx @getalby/cli -c "nostr+walletconnect://..." get-budget
120
+ npx @getalby/cli get-budget
74
121
 
75
122
  # Sign a message
76
- npx @getalby/cli -c "nostr+walletconnect://..." sign-message --message "Hello, World!"
123
+ npx @getalby/cli sign-message --message "Hello, World!"
77
124
 
78
- # Fetch L402-protected resource
79
- npx @getalby/cli -c "nostr+walletconnect://..." fetch-l402 --url "https://example.com/api"
125
+ # Fetch a payment-protected resource (auto-detects L402, X402, MPP)
126
+ npx @getalby/cli fetch --url "https://example.com/api"
127
+
128
+ # Fetch with custom method, headers, and body
129
+ npx @getalby/cli fetch --url "https://example.com/api" --method POST --body '{"query":"hello"}' --headers '{"Accept":"application/json"}'
130
+
131
+ # Fetch with a custom max amount (default: 5000 sats, 0 = no limit)
132
+ npx @getalby/cli fetch --url "https://example.com/api" --max-amount 1000
80
133
 
81
134
  # Wait for a payment notification
82
- npx @getalby/cli -c "nostr+walletconnect://..." wait-for-payment --payment-hash "abc123..."
135
+ npx @getalby/cli wait-for-payment --payment-hash "abc123..."
83
136
  ```
84
137
 
85
138
  ### HOLD Invoices
@@ -88,13 +141,13 @@ HOLD invoices allow you to accept payments conditionally - the payment is held u
88
141
 
89
142
  ```bash
90
143
  # Create a HOLD invoice (you provide the payment hash)
91
- npx @getalby/cli -c "nostr+walletconnect://..." make-hold-invoice --amount 1000 --payment-hash "abc123..."
144
+ npx @getalby/cli make-hold-invoice --amount 1000 --payment-hash "abc123..."
92
145
 
93
146
  # Settle a HOLD invoice (claim the payment)
94
- npx @getalby/cli -c "nostr+walletconnect://..." settle-hold-invoice --preimage "def456..."
147
+ npx @getalby/cli settle-hold-invoice --preimage "def456..."
95
148
 
96
149
  # Cancel a HOLD invoice (reject the payment)
97
- npx @getalby/cli -c "nostr+walletconnect://..." cancel-hold-invoice --payment-hash "abc123..."
150
+ npx @getalby/cli cancel-hold-invoice --payment-hash "abc123..."
98
151
  ```
99
152
 
100
153
  ### Lightning Tools
@@ -120,46 +173,7 @@ npx @getalby/cli request-invoice-from-lightning-address --address "hello@getalby
120
173
 
121
174
  ## Command Reference
122
175
 
123
- ### Wallet Commands
124
-
125
- These require `-c` or `--connection-secret`:
126
-
127
- | Command | Description | Required Options |
128
- | ------------------------- | ------------------------------ | ------------------------------- |
129
- | `get-balance` | Get wallet balance | - |
130
- | `get-info` | Get wallet info | - |
131
- | `get-wallet-service-info` | Get wallet capabilities | - |
132
- | `get-budget` | Get wallet budget | - |
133
- | `make-invoice` | Create a lightning invoice | `--amount` |
134
- | `pay-invoice` | Pay a lightning invoice | `--invoice` |
135
- | `pay-keysend` | Send a keysend payment | `--pubkey`, `--amount` |
136
- | `lookup-invoice` | Look up an invoice | `--payment-hash` or `--invoice` |
137
- | `list-transactions` | List transactions | - |
138
- | `sign-message` | Sign a message with wallet key | `--message` |
139
- | `wait-for-payment` | Wait for payment notification | `--payment-hash` |
140
- | `fetch-l402` | Fetch L402-protected resource | `--url` |
141
-
142
- ### HOLD Invoice Commands
143
-
144
- These require `-c` or `--connection-secret`:
145
-
146
- | Command | Description | Required Options |
147
- | --------------------- | --------------------- | ---------------------------- |
148
- | `make-hold-invoice` | Create a HOLD invoice | `--amount`, `--payment-hash` |
149
- | `settle-hold-invoice` | Settle a HOLD invoice | `--preimage` |
150
- | `cancel-hold-invoice` | Cancel a HOLD invoice | `--payment-hash` |
151
-
152
- ### Lightning Tools
153
-
154
- These don't require a wallet connection:
155
-
156
- | Command | Description | Required Options |
157
- | ---------------------------------------- | -------------------------------------- | ------------------------- |
158
- | `fiat-to-sats` | Convert fiat to sats | `--currency`, `--amount` |
159
- | `sats-to-fiat` | Convert sats to fiat | `--amount`, `--currency` |
160
- | `parse-invoice` | Parse a BOLT-11 invoice | `--invoice` |
161
- | `verify-preimage` | Verify preimage against invoice | `--invoice`, `--preimage` |
162
- | `request-invoice-from-lightning-address` | Request invoice from lightning address | `--address`, `--amount` |
176
+ Run `npx @getalby/cli help` for a full list of commands and possible arguments.
163
177
 
164
178
  ## Output
165
179
 
@@ -0,0 +1,66 @@
1
+ import { NWCClient } from "@getalby/sdk";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { getConnectionSecretPath, getPendingConnectionRelayPath, getPendingConnectionSecretPath, handleError, } from "../utils.js";
6
+ import { generateSecretKey, getPublicKey } from "nostr-tools";
7
+ import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
8
+ export function registerAuthCommand(program) {
9
+ program
10
+ .command("auth <wallet-url>")
11
+ .description("Securely connect a wallet with human confirmation via the browser\n\n" +
12
+ " Step 1: npx @getalby/cli auth https://my.albyhub.com --app-name MyApp\n" +
13
+ " Step 2: after human confirmation, run any command to finalize the connection")
14
+ .option("--app-name <name>", 'Name of the agent or app that will use this wallet (e.g. "Claude Code")')
15
+ .option("--relay-url <url>", "Relay URL for the pending connection", "wss://relay.getalby.com/v1")
16
+ .option("--force", "Overwrite existing connection secret")
17
+ .option("--remove-pending", "Remove a pending connection and start fresh")
18
+ .action(async (walletUrl, options) => {
19
+ await handleError(async () => {
20
+ const walletName = program.opts().walletName;
21
+ const connectionSecretPath = getConnectionSecretPath(walletName);
22
+ const pendingSecretPath = getPendingConnectionSecretPath(walletName);
23
+ const pendingRelayPath = getPendingConnectionRelayPath(walletName);
24
+ // Remove pending connection
25
+ if (options.removePending) {
26
+ if (!existsSync(pendingSecretPath)) {
27
+ console.log(`No pending connection found at ${pendingSecretPath}`);
28
+ }
29
+ else {
30
+ rmSync(pendingSecretPath);
31
+ console.log(`Removed pending connection at ${pendingSecretPath}`);
32
+ }
33
+ if (existsSync(pendingRelayPath)) {
34
+ rmSync(pendingRelayPath);
35
+ }
36
+ return;
37
+ }
38
+ // Generate auth URL
39
+ if (!options.appName) {
40
+ console.error(`Error: No app name provided.\n` +
41
+ `Add --app-name <name> to identify the app in the wallet.`);
42
+ process.exit(1);
43
+ }
44
+ if (existsSync(connectionSecretPath) && !options.force) {
45
+ console.error(`Error: Already connected. Connection secret exists at ${connectionSecretPath}\n` +
46
+ `To overwrite, use --force.`);
47
+ process.exit(1);
48
+ }
49
+ const secret = bytesToHex(generateSecretKey());
50
+ const pubkey = getPublicKey(hexToBytes(secret));
51
+ const authUrl = NWCClient.getAuthorizationUrl(`${walletUrl}/apps/new`, { name: options.appName }, pubkey).toString();
52
+ const dir = join(homedir(), ".alby-cli");
53
+ if (!existsSync(dir)) {
54
+ mkdirSync(dir, { recursive: true });
55
+ }
56
+ writeFileSync(pendingSecretPath, secret, { mode: 0o600 });
57
+ writeFileSync(pendingRelayPath, options.relayUrl, { mode: 0o600 });
58
+ console.log("Click the following URL to approve the connection in your wallet:\n" +
59
+ authUrl);
60
+ const retryCmd = walletName
61
+ ? `npx @getalby/cli get-balance --wallet-name ${walletName}`
62
+ : `npx @getalby/cli get-balance`;
63
+ console.log(`\nOnce approved, run any command, e.g.:\n ${retryCmd}`);
64
+ });
65
+ });
66
+ }
@@ -7,7 +7,7 @@ export function registerCancelHoldInvoiceCommand(program) {
7
7
  .requiredOption("--payment-hash <hex>", "Payment hash (32 bytes hex)")
8
8
  .action(async (options) => {
9
9
  await handleError(async () => {
10
- const client = getClient(program);
10
+ const client = await getClient(program);
11
11
  const result = await cancelHoldInvoice(client, {
12
12
  payment_hash: options.paymentHash,
13
13
  });
@@ -0,0 +1,46 @@
1
+ import { NWCClient } from "@getalby/sdk";
2
+ import { existsSync } from "node:fs";
3
+ import { getConnectionSecretPath, handleError, saveConnectionSecret, testAndLogConnection, } from "../utils.js";
4
+ export function registerConnectCommand(program) {
5
+ program
6
+ .command('connect "[connection-secret]"')
7
+ .description("Connect to a Nostr Wallet Connect wallet")
8
+ .option("--force", "Overwrite existing connection secret")
9
+ .action(async (connectionSecret, options) => {
10
+ await handleError(async () => {
11
+ const connectionSecretPath = getConnectionSecretPath(program.opts().walletName);
12
+ if (existsSync(connectionSecretPath) && !options.force) {
13
+ console.error(`Error: Already connected. Connection secret exists at ${connectionSecretPath}\n` +
14
+ `To overwrite, use --force.`);
15
+ process.exit(1);
16
+ }
17
+ if (!connectionSecret) {
18
+ console.error(`Usage: npx @getalby/cli connect "<connection-secret>"\n` +
19
+ `Provide a NWC connection secret (nostr+walletconnect://...)`);
20
+ process.exit(1);
21
+ }
22
+ if (!connectionSecret.startsWith("nostr+walletconnect://")) {
23
+ console.error(`Error: Invalid connection secret. Expected format: nostr+walletconnect://...`);
24
+ process.exit(1);
25
+ }
26
+ const client = new NWCClient({
27
+ nostrWalletConnectUrl: connectionSecret,
28
+ });
29
+ if (!client.secret || !/^[0-9a-f]{64}$/i.test(client.secret)) {
30
+ console.error(`Error: Invalid connection secret. Missing or invalid secret key.`);
31
+ process.exit(1);
32
+ }
33
+ if (!client.walletPubkey ||
34
+ !/^[0-9a-f]{64}$/i.test(client.walletPubkey)) {
35
+ console.error(`Error: Invalid connection secret. Missing or invalid wallet pubkey.`);
36
+ process.exit(1);
37
+ }
38
+ if (!client.relayUrls?.length) {
39
+ console.error(`Error: Invalid connection secret. Missing relay URL.`);
40
+ process.exit(1);
41
+ }
42
+ await testAndLogConnection(client);
43
+ saveConnectionSecret(connectionSecretPath, connectionSecret, program.opts().verbose);
44
+ });
45
+ });
46
+ }
@@ -1,21 +1,23 @@
1
- import { fetchL402 } from "../tools/lightning/fetch_l402.js";
1
+ import { fetch402 } from "../tools/lightning/fetch.js";
2
2
  import { getClient, handleError, output } from "../utils.js";
3
- export function registerFetchL402Command(program) {
3
+ export function registerFetch402Command(program) {
4
4
  program
5
- .command("fetch-l402")
6
- .description("Fetch L402-protected resource")
5
+ .command("fetch")
6
+ .description("Fetch a payment-protected resource (auto-detects L402, X402, MPP)")
7
7
  .requiredOption("-u, --url <url>", "URL to fetch")
8
8
  .option("-m, --method <method>", "HTTP method (GET, POST, etc.)")
9
9
  .option("-b, --body <json>", "Request body (JSON string)")
10
10
  .option("-H, --headers <json>", "Additional headers (JSON string)")
11
+ .option("--max-amount <sats>", "Maximum amount in sats to pay per request. Aborts if the endpoint requests more. (default: 5000, 0 = no limit)", parseInt)
11
12
  .action(async (options) => {
12
13
  await handleError(async () => {
13
- const client = getClient(program);
14
- const result = await fetchL402(client, {
14
+ const client = await getClient(program);
15
+ const result = await fetch402(client, {
15
16
  url: options.url,
16
17
  method: options.method,
17
18
  body: options.body,
18
19
  headers: options.headers ? JSON.parse(options.headers) : undefined,
20
+ maxAmountSats: options.maxAmount,
19
21
  });
20
22
  output(result);
21
23
  });
@@ -6,7 +6,7 @@ export function registerGetBalanceCommand(program) {
6
6
  .description("Get wallet balance")
7
7
  .action(async () => {
8
8
  await handleError(async () => {
9
- const client = getClient(program);
9
+ const client = await getClient(program);
10
10
  const result = await getBalance(client);
11
11
  output(result);
12
12
  });
@@ -6,7 +6,7 @@ export function registerGetBudgetCommand(program) {
6
6
  .description("Get wallet budget information")
7
7
  .action(async () => {
8
8
  await handleError(async () => {
9
- const client = getClient(program);
9
+ const client = await getClient(program);
10
10
  const result = await getBudget(client);
11
11
  output(result);
12
12
  });
@@ -6,7 +6,7 @@ export function registerGetInfoCommand(program) {
6
6
  .description("Get wallet info")
7
7
  .action(async () => {
8
8
  await handleError(async () => {
9
- const client = getClient(program);
9
+ const client = await getClient(program);
10
10
  const result = await getInfo(client);
11
11
  output(result);
12
12
  });
@@ -6,7 +6,7 @@ export function registerGetWalletServiceInfoCommand(program) {
6
6
  .description("Get wallet service capabilities")
7
7
  .action(async () => {
8
8
  await handleError(async () => {
9
- const client = getClient(program);
9
+ const client = await getClient(program);
10
10
  const result = await getWalletServiceInfo(client);
11
11
  output(result);
12
12
  });
@@ -12,7 +12,7 @@ export function registerListTransactionsCommand(program) {
12
12
  .option("-t, --type <type>", "Filter by type (incoming|outgoing)")
13
13
  .action(async (options) => {
14
14
  await handleError(async () => {
15
- const client = getClient(program);
15
+ const client = await getClient(program);
16
16
  const result = await listTransactions(client, {
17
17
  from: options.from,
18
18
  until: options.until,
@@ -12,7 +12,7 @@ export function registerLookupInvoiceCommand(program) {
12
12
  console.error("Error: --payment-hash or --invoice is required");
13
13
  process.exit(1);
14
14
  }
15
- const client = getClient(program);
15
+ const client = await getClient(program);
16
16
  const result = await lookupInvoice(client, {
17
17
  payment_hash: options.paymentHash,
18
18
  invoice: options.invoice,
@@ -10,7 +10,7 @@ export function registerMakeHoldInvoiceCommand(program) {
10
10
  .option("-e, --expiry <seconds>", "Expiry time in seconds", parseInt)
11
11
  .action(async (options) => {
12
12
  await handleError(async () => {
13
- const client = getClient(program);
13
+ const client = await getClient(program);
14
14
  const result = await makeHoldInvoice(client, {
15
15
  amount_in_sats: options.amount,
16
16
  payment_hash: options.paymentHash,
@@ -9,7 +9,7 @@ export function registerMakeInvoiceCommand(program) {
9
9
  .option("-e, --expiry <seconds>", "Expiry time in seconds", parseInt)
10
10
  .action(async (options) => {
11
11
  await handleError(async () => {
12
- const client = getClient(program);
12
+ const client = await getClient(program);
13
13
  const result = await makeInvoice(client, {
14
14
  amount_in_sats: options.amount,
15
15
  description: options.description,
@@ -8,7 +8,7 @@ export function registerPayInvoiceCommand(program) {
8
8
  .option("-a, --amount <sats>", "Amount (for zero-amount invoices)", parseInt)
9
9
  .action(async (options) => {
10
10
  await handleError(async () => {
11
- const client = getClient(program);
11
+ const client = await getClient(program);
12
12
  const result = await payInvoice(client, {
13
13
  invoice: options.invoice,
14
14
  amount_in_sats: options.amount,
@@ -10,7 +10,7 @@ export function registerPayKeysendCommand(program) {
10
10
  .option("--tlv-records <json>", "TLV records as JSON array [{type, value}]")
11
11
  .action(async (options) => {
12
12
  await handleError(async () => {
13
- const client = getClient(program);
13
+ const client = await getClient(program);
14
14
  let tlvRecords;
15
15
  if (options.tlvRecords) {
16
16
  tlvRecords = JSON.parse(options.tlvRecords);
@@ -7,7 +7,7 @@ export function registerSettleHoldInvoiceCommand(program) {
7
7
  .requiredOption("--preimage <hex>", "Preimage (32 bytes hex)")
8
8
  .action(async (options) => {
9
9
  await handleError(async () => {
10
- const client = getClient(program);
10
+ const client = await getClient(program);
11
11
  const result = await settleHoldInvoice(client, {
12
12
  preimage: options.preimage,
13
13
  });
@@ -7,7 +7,7 @@ export function registerSignMessageCommand(program) {
7
7
  .requiredOption("-m, --message <text>", "Message to sign")
8
8
  .action(async (options) => {
9
9
  await handleError(async () => {
10
- const client = getClient(program);
10
+ const client = await getClient(program);
11
11
  const result = await signMessage(client, {
12
12
  message: options.message,
13
13
  });
@@ -9,7 +9,7 @@ export function registerWaitForPaymentCommand(program) {
9
9
  .option("--timeout <seconds>", "Timeout in seconds", parseInt)
10
10
  .action(async (options) => {
11
11
  await handleError(async () => {
12
- const client = getClient(program);
12
+ const client = await getClient(program);
13
13
  const result = await waitForPayment(client, {
14
14
  payment_hash: options.paymentHash,
15
15
  type: options.type,
package/build/index.js CHANGED
@@ -19,38 +19,64 @@ import { registerSatsToFiatCommand } from "./commands/sats-to-fiat.js";
19
19
  import { registerParseInvoiceCommand } from "./commands/parse-invoice.js";
20
20
  import { registerVerifyPreimageCommand } from "./commands/verify-preimage.js";
21
21
  import { registerRequestInvoiceFromLightningAddressCommand } from "./commands/request-invoice-from-lightning-address.js";
22
- import { registerFetchL402Command } from "./commands/fetch-l402.js";
22
+ import { registerFetch402Command } from "./commands/fetch.js";
23
+ import { registerConnectCommand } from "./commands/connect.js";
24
+ import { registerAuthCommand } from "./commands/auth.js";
23
25
  const program = new Command();
24
26
  program
25
- .name("alby-cli")
26
- .description("CLI for Nostr Wallet Connect (NIP-47) with lightning tools")
27
- .version("0.2.4")
27
+ .name("@getalby/cli")
28
+ .description("CLI for Nostr Wallet Connect (NIP-47) with lightning tools\n\n" +
29
+ " Examples:\n" +
30
+ ' $ npx @getalby/cli connect "nostr+walletconnect://..."\n' +
31
+ " $ npx @getalby/cli get-balance\n" +
32
+ " $ npx @getalby/cli pay-invoice --invoice lnbc...")
33
+ .version("0.4.0")
28
34
  .option("-c, --connection-secret <string>", "NWC connection secret (nostr+walletconnect://...) or path to file containing it (preferred)")
29
- .addHelpText("afterAll", `
35
+ .option("-w, --wallet-name <name>", "Use a named wallet's connection secret (~/.alby-cli/connection-secret-<name>.key)")
36
+ .option("-v, --verbose", "Print status messages to stderr")
37
+ .addHelpText("after", `
38
+ Connection Secret Resolution (in order of priority):
39
+ 1. --connection-secret flag (value or path to file)
40
+ 2. --wallet-name flag (~/.alby-cli/connection-secret-<name>.key)
41
+ 3. NWC_URL environment variable
42
+ 4. ~/.alby-cli/connection-secret.key (default file location)
43
+
30
44
  Security:
31
45
  - Do NOT print the connection secret to any logs or otherwise reveal it.
46
+ - NEVER read the connection secret file (~/.alby-cli/connection-secret.key) directly.
32
47
  - NEVER share connection secrets with anyone.
33
48
  - NEVER share any part of a connection secret (pubkey, secret, relay etc.) with anyone
34
49
  as this can be used to gain access to your wallet or reduce your wallet's privacy.`);
35
- // Register all commands
50
+ // Register common wallet commands
51
+ program.commandsGroup("Wallet Commands (require --connection-secret):");
36
52
  registerGetBalanceCommand(program);
37
53
  registerGetBudgetCommand(program);
38
54
  registerGetInfoCommand(program);
39
- registerGetWalletServiceInfoCommand(program);
40
55
  registerMakeInvoiceCommand(program);
41
- registerMakeHoldInvoiceCommand(program);
42
- registerSettleHoldInvoiceCommand(program);
43
- registerCancelHoldInvoiceCommand(program);
44
56
  registerPayInvoiceCommand(program);
45
- registerPayKeysendCommand(program);
46
57
  registerLookupInvoiceCommand(program);
47
58
  registerListTransactionsCommand(program);
59
+ // Register advanced wallet commands
60
+ program.commandsGroup("Advanced Wallet Commands (require --connection-secret):");
61
+ registerPayKeysendCommand(program);
62
+ registerGetWalletServiceInfoCommand(program);
48
63
  registerWaitForPaymentCommand(program);
49
64
  registerSignMessageCommand(program);
65
+ registerMakeHoldInvoiceCommand(program);
66
+ registerSettleHoldInvoiceCommand(program);
67
+ registerCancelHoldInvoiceCommand(program);
68
+ // Register lightning tool commands
69
+ program.commandsGroup("Lightning Tools (no --connection-secret required):");
50
70
  registerFiatToSatsCommand(program);
51
71
  registerSatsToFiatCommand(program);
52
72
  registerParseInvoiceCommand(program);
53
73
  registerVerifyPreimageCommand(program);
54
74
  registerRequestInvoiceFromLightningAddressCommand(program);
55
- registerFetchL402Command(program);
75
+ // Register fetch command for payment-protected resources
76
+ program.commandsGroup("HTTP 402 Payments (require --connection-secret):");
77
+ registerFetch402Command(program);
78
+ // Register setup commands
79
+ program.commandsGroup("Setup:");
80
+ registerAuthCommand(program);
81
+ registerConnectCommand(program);
56
82
  program.parse();
@@ -45,9 +45,12 @@ describe("Connection Secret Handling", () => {
45
45
  expect(result.output.error).toContain("Invalid connection secret");
46
46
  });
47
47
  test("errors when no connection secret provided", () => {
48
- const result = runCli("get-balance");
48
+ const result = runCli("get-balance", {
49
+ HOME: "/nonexistent-test-home",
50
+ NWC_URL: "",
51
+ });
49
52
  expect(result.success).toBe(false);
50
- expect(result.output.error).toContain("--connection-secret is required");
53
+ expect(result.output.error).toContain("No connection secret provided");
51
54
  });
52
55
  test("errors when connection string is malformed", () => {
53
56
  const result = runCli(`-c "nostr+walletconnect://asdf" get-balance`);
@@ -1,9 +1,11 @@
1
- import { fetchWithL402 } from "@getalby/lightning-tools";
2
- export async function fetchL402(client, params) {
1
+ import { fetch402 as fetch402Lib } from "@getalby/lightning-tools/402";
2
+ const DEFAULT_MAX_AMOUNT_SATS = 5000;
3
+ export async function fetch402(client, params) {
4
+ const method = params.method?.toUpperCase();
3
5
  const requestOptions = {
4
- method: params.method,
6
+ method,
5
7
  };
6
- if (params.method && params.method !== "GET" && params.method !== "HEAD") {
8
+ if (method && method !== "GET" && method !== "HEAD") {
7
9
  requestOptions.body = params.body;
8
10
  requestOptions.headers = {
9
11
  "Content-Type": "application/json",
@@ -13,13 +15,10 @@ export async function fetchL402(client, params) {
13
15
  else if (params.headers) {
14
16
  requestOptions.headers = params.headers;
15
17
  }
16
- const result = await fetchWithL402(params.url, requestOptions, {
17
- wallet: {
18
- sendPayment: async (invoice) => {
19
- const result = await client.payInvoice({ invoice });
20
- return { preimage: result.preimage };
21
- },
22
- },
18
+ const maxAmountSats = params.maxAmountSats ?? DEFAULT_MAX_AMOUNT_SATS;
19
+ const result = await fetch402Lib(params.url, requestOptions, {
20
+ wallet: client,
21
+ maxAmount: maxAmountSats || undefined,
23
22
  });
24
23
  const responseContent = await result.text();
25
24
  if (!result.ok) {
package/build/utils.js CHANGED
@@ -1,30 +1,119 @@
1
- import { NWCClient } from "@getalby/sdk";
2
- import { readFileSync } from "node:fs";
1
+ import { NWAClient, NWCClient } from "@getalby/sdk";
2
+ import { getInfo } from "./tools/nwc/get_info.js";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
3
4
  import { homedir } from "node:os";
4
5
  import { join } from "node:path";
5
- export function getClient(program) {
6
+ function sanitizeWalletName(name) {
7
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
8
+ }
9
+ export function getConnectionSecretPath(name) {
10
+ const filename = name
11
+ ? `connection-secret-${sanitizeWalletName(name)}.key`
12
+ : "connection-secret.key";
13
+ return join(homedir(), ".alby-cli", filename);
14
+ }
15
+ export function getPendingConnectionSecretPath(name) {
16
+ const filename = name
17
+ ? `pending-connection-secret-${sanitizeWalletName(name)}.key`
18
+ : "pending-connection-secret.key";
19
+ return join(homedir(), ".alby-cli", filename);
20
+ }
21
+ export function getPendingConnectionRelayPath(name) {
22
+ const filename = name
23
+ ? `pending-connection-relay-${sanitizeWalletName(name)}.txt`
24
+ : "pending-connection-relay.txt";
25
+ return join(homedir(), ".alby-cli", filename);
26
+ }
27
+ export function saveConnectionSecret(path, secret, verbose) {
28
+ const alreadyExists = existsSync(path);
29
+ const dir = join(homedir(), ".alby-cli");
30
+ if (!existsSync(dir)) {
31
+ mkdirSync(dir, { recursive: true });
32
+ }
33
+ writeFileSync(path, secret, { mode: 0o600 });
34
+ if (alreadyExists) {
35
+ chmodSync(path, 0o600);
36
+ }
37
+ if (verbose) {
38
+ console.error(`Connection saved to ${path}`);
39
+ }
40
+ }
41
+ export async function testAndLogConnection(client) {
42
+ console.log("Testing connection...");
43
+ const info = await getInfo(client);
44
+ console.log(`Connected to ${info.alias || "wallet"} (${info.network || "unknown network"})`);
45
+ }
46
+ export async function completePendingConnection(pendingSecretPath, connectionSecretPath, relayUrl, verbose, pendingRelayPath) {
47
+ const secret = readFileSync(pendingSecretPath, "utf-8").trim();
48
+ const DEFAULT_RELAY = "wss://relay.getalby.com/v1";
49
+ if (!relayUrl && pendingRelayPath && existsSync(pendingRelayPath)) {
50
+ relayUrl = readFileSync(pendingRelayPath, "utf-8").trim();
51
+ }
52
+ const resolvedRelay = relayUrl ?? DEFAULT_RELAY;
53
+ const nwaClient = new NWAClient({
54
+ appSecretKey: secret,
55
+ relayUrls: [resolvedRelay],
56
+ requestMethods: [],
57
+ });
58
+ return new Promise((resolve, reject) => {
59
+ let settled = false;
60
+ const timer = setTimeout(() => {
61
+ if (settled)
62
+ return;
63
+ settled = true;
64
+ unsub?.();
65
+ reject(new Error("Timed out waiting for wallet approval.\n\nTo retry, run the command again.\nTo cancel: npx @getalby/cli auth --remove-pending"));
66
+ }, 5000);
67
+ let unsub;
68
+ nwaClient
69
+ .subscribe({
70
+ onSuccess: async (nwcClient) => {
71
+ if (settled)
72
+ return;
73
+ settled = true;
74
+ clearTimeout(timer);
75
+ unsub?.();
76
+ saveConnectionSecret(connectionSecretPath, nwcClient.getNostrWalletConnectUrl(), verbose);
77
+ rmSync(pendingSecretPath);
78
+ if (pendingRelayPath && existsSync(pendingRelayPath)) {
79
+ rmSync(pendingRelayPath);
80
+ }
81
+ resolve(nwcClient);
82
+ },
83
+ })
84
+ .then(({ unsub: u }) => {
85
+ unsub = u;
86
+ });
87
+ });
88
+ }
89
+ export async function getClient(program) {
6
90
  const opts = program.opts();
7
91
  let connectionSecret = opts.connectionSecret;
8
- // Check environment variables if --connection-secret not provided
9
- if (!connectionSecret) {
92
+ const walletName = opts.walletName;
93
+ const connectionPath = getConnectionSecretPath(walletName);
94
+ const pendingPath = getPendingConnectionSecretPath(walletName);
95
+ if (!connectionSecret && !walletName) {
10
96
  connectionSecret = process.env.NWC_URL;
11
97
  }
12
98
  if (!connectionSecret) {
13
- const defaultPath = join(homedir(), ".alby-cli", "connection-secret.key");
14
99
  try {
15
- connectionSecret = readFileSync(defaultPath, "utf-8").trim();
100
+ connectionSecret = readFileSync(connectionPath, "utf-8").trim();
16
101
  }
17
102
  catch (error) {
18
103
  const err = error;
19
- if (err.code !== "ENOENT") {
20
- // only throw an error if it's not a file not found error
104
+ if (err.code !== "ENOENT")
21
105
  throw err;
22
- }
23
106
  }
24
107
  }
108
+ if (!connectionSecret && existsSync(pendingPath)) {
109
+ if (opts.verbose) {
110
+ console.error("Pending connection found. Waiting for wallet approval...");
111
+ }
112
+ const pendingRelayPath = getPendingConnectionRelayPath(walletName);
113
+ return await completePendingConnection(pendingPath, connectionPath, undefined, opts.verbose, pendingRelayPath);
114
+ }
25
115
  if (!connectionSecret) {
26
- console.error("Error: No connection secret found. Pass -c <secret>, set NWC_URL, or create ~/.alby-cli/connection-secret.key");
27
- process.exit(1);
116
+ throw new Error("No connection secret provided. Pass -c <secret or file path>, set NWC_URL, use --wallet-name <name>, or create ~/.alby-cli/connection-secret.key");
28
117
  }
29
118
  // Auto-detect: if it doesn't start with the protocol, treat as file path
30
119
  if (!connectionSecret.startsWith("nostr+walletconnect://")) {
package/package.json CHANGED
@@ -2,12 +2,11 @@
2
2
  "name": "@getalby/cli",
3
3
  "description": "CLI for Nostr Wallet Connect (NIP-47) with a few additional useful lightning tools",
4
4
  "repository": "https://github.com/getAlby/cli.git",
5
- "version": "0.2.4",
5
+ "version": "0.4.0",
6
6
  "type": "module",
7
7
  "main": "build/index.js",
8
8
  "bin": {
9
- "cli": "build/index.js",
10
- "alby-cli": "build/index.js"
9
+ "cli": "build/index.js"
11
10
  },
12
11
  "files": [
13
12
  "build/**/*"
@@ -33,10 +32,15 @@
33
32
  ],
34
33
  "author": "Alby contributors",
35
34
  "license": "MIT",
35
+ "engines": {
36
+ "node": ">=20"
37
+ },
36
38
  "dependencies": {
37
- "@getalby/lightning-tools": "^7.0.2",
39
+ "@getalby/lightning-tools": "^8.0.0",
38
40
  "@getalby/sdk": "^7.0.0",
39
- "commander": "^13.1.0"
41
+ "@noble/hashes": "^2.0.1",
42
+ "commander": "^14.0.3",
43
+ "nostr-tools": "^2.23.3"
40
44
  },
41
45
  "devDependencies": {
42
46
  "@types/node": "^25.2.0",