paytaca-cli 0.3.1 → 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.
Files changed (48) hide show
  1. package/README.md +23 -5
  2. package/dist/commands/chat.d.ts +3 -0
  3. package/dist/commands/chat.d.ts.map +1 -0
  4. package/dist/commands/chat.js +592 -0
  5. package/dist/commands/chat.js.map +1 -0
  6. package/dist/index.js +2 -4
  7. package/dist/index.js.map +1 -1
  8. package/dist/nostr/chat.d.ts +42 -0
  9. package/dist/nostr/chat.d.ts.map +1 -0
  10. package/dist/nostr/chat.js +72 -0
  11. package/dist/nostr/chat.js.map +1 -0
  12. package/dist/nostr/chat.test.d.ts +2 -0
  13. package/dist/nostr/chat.test.d.ts.map +1 -0
  14. package/dist/nostr/chat.test.js +121 -0
  15. package/dist/nostr/chat.test.js.map +1 -0
  16. package/dist/nostr/keys.d.ts +8 -0
  17. package/dist/nostr/keys.d.ts.map +1 -0
  18. package/dist/nostr/keys.js +22 -0
  19. package/dist/nostr/keys.js.map +1 -0
  20. package/dist/nostr/relay.d.ts +47 -0
  21. package/dist/nostr/relay.d.ts.map +1 -0
  22. package/dist/nostr/relay.js +370 -0
  23. package/dist/nostr/relay.js.map +1 -0
  24. package/dist/nostr/store.d.ts +75 -0
  25. package/dist/nostr/store.d.ts.map +1 -0
  26. package/dist/nostr/store.js +410 -0
  27. package/dist/nostr/store.js.map +1 -0
  28. package/dist/nostr/store.test.d.ts +2 -0
  29. package/dist/nostr/store.test.d.ts.map +1 -0
  30. package/dist/nostr/store.test.js +448 -0
  31. package/dist/nostr/store.test.js.map +1 -0
  32. package/package.json +2 -1
  33. package/skills/README.md +68 -0
  34. package/skills/paytaca-send/SKILL.md +159 -0
  35. package/skills/paytaca-wallet/SKILL.md +155 -0
  36. package/dist/commands/claude.d.ts +0 -9
  37. package/dist/commands/claude.d.ts.map +0 -1
  38. package/dist/commands/claude.js +0 -20
  39. package/dist/commands/claude.js.map +0 -1
  40. package/dist/commands/opencode.d.ts +0 -9
  41. package/dist/commands/opencode.d.ts.map +0 -1
  42. package/dist/commands/opencode.js +0 -20
  43. package/dist/commands/opencode.js.map +0 -1
  44. package/dist/utils/skill.d.ts +0 -17
  45. package/dist/utils/skill.d.ts.map +0 -1
  46. package/dist/utils/skill.js +0 -100
  47. package/dist/utils/skill.js.map +0 -1
  48. /package/skills/{paytaca → paytaca-x402}/SKILL.md +0 -0
package/README.md CHANGED
@@ -122,12 +122,32 @@ paytaca pay https://api.nanogpt.com/v1/complete --method POST --body '{"prompt":
122
122
 
123
123
  ### AI Agent Integration
124
124
 
125
+ Paytaca CLI provides AI agent skills for wallet operations. Install from GitHub (supports 45+ AI frameworks):
126
+
125
127
  ```bash
126
- paytaca opencode install # Install Paytaca x402 skill for OpenCode AI agents
127
- paytaca claude install # Install Paytaca x402 skill for Claude Code agents
128
+ # Install specific skill
129
+ npx skills add paytaca/paytaca-cli --skill <skill-name> -a <agent> -g
130
+
131
+ # Install all skills
132
+ npx skills add paytaca/paytaca-cli --all
133
+
134
+ # Examples
135
+ npx skills add paytaca/paytaca-cli --skill paytaca-x402 -a opencode -g
136
+ npx skills add paytaca/paytaca-cli --skill paytaca-wallet -a claude-code -g
137
+ npx skills add paytaca/paytaca-cli --skill paytaca-send -a openclaw -g
128
138
  ```
129
139
 
130
- This enables AI agents to autonomously handle HTTP 402 payment responses when calling x402-enabled APIs.
140
+ **Available Skills:**
141
+
142
+ | Skill | Purpose | Approval Needed |
143
+ |-------|---------|------------------|
144
+ | `paytaca-x402` | HTTP 402 payment handling | Yes (before payment) |
145
+ | `paytaca-wallet` | Balance, addresses, history, token info | No (read-only) |
146
+ | `paytaca-send` | Send BCH and CashTokens | Yes (before sending) |
147
+
148
+ **Supported frameworks:** OpenCode, Claude Code, OpenClaw, Cursor, Cline, Windsurf, Roo, Gemini CLI, Codex, GitHub Copilot, Goose, Trae, Kiro CLI, and 30+ more.
149
+
150
+ See [skills/README.md](skills/README.md) for detailed skill documentation.
131
151
 
132
152
  ## Network
133
153
 
@@ -163,8 +183,6 @@ src/
163
183
  token.ts CashToken commands (list, info, send, send-nft)
164
184
  pay.ts x402 BCH payment handler for HTTP requests
165
185
  check.ts Check if URL requires x402 payment
166
- opencode.ts Install x402 skill for OpenCode AI agents
167
- claude.ts Install x402 skill for Claude Code AI agents
168
186
  wallet/
169
187
  index.ts Wallet class, mnemonic gen/import/load
170
188
  bch.ts BchWallet (balance, send, history, CashTokens)
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerChatCommands(program: Command): void;
3
+ //# sourceMappingURL=chat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAoBnC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgpB3D"}
@@ -0,0 +1,592 @@
1
+ import chalk from 'chalk';
2
+ import { finalizeEvent } from 'nostr-tools';
3
+ import { hexToBytes } from 'nostr-tools/utils';
4
+ import { decode as nip19Decode } from 'nostr-tools/nip19';
5
+ import { loadMnemonic } from '../wallet/index.js';
6
+ import { ChatStore } from '../nostr/store.js';
7
+ import { relayService } from '../nostr/relay.js';
8
+ function formatTimestamp(unix) {
9
+ const d = new Date(unix * 1000);
10
+ const now = new Date();
11
+ const isToday = d.toDateString() === now.toDateString();
12
+ if (isToday) {
13
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
14
+ }
15
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) +
16
+ ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
17
+ }
18
+ export function registerChatCommands(program) {
19
+ const chat = program
20
+ .command('chat')
21
+ .description('Nostr-based chat');
22
+ chat
23
+ .command('list')
24
+ .description('List conversations')
25
+ .option('--json', 'Output as JSON')
26
+ .action(async (opts) => {
27
+ const data = loadMnemonic();
28
+ if (!data) {
29
+ console.log(chalk.red('\nNo wallet found. Run `paytaca wallet create` or `paytaca wallet import` first.\n'));
30
+ process.exit(1);
31
+ }
32
+ const store = new ChatStore();
33
+ await store.initialize(data.mnemonic);
34
+ const rooms = store.getRooms();
35
+ if (rooms.length === 0) {
36
+ console.log(chalk.dim('\n No conversations yet.\n'));
37
+ store.cleanup();
38
+ process.exit(0);
39
+ }
40
+ // Resolve display names and BCH addresses for all rooms' other members
41
+ const otherPubKeys = [...new Set(rooms.map(r => store.getOtherMember(r)).filter(Boolean))];
42
+ await Promise.allSettled(otherPubKeys.map(async (pk) => {
43
+ await store.resolveDisplayName(pk);
44
+ await store.resolveBchAddress(pk);
45
+ }));
46
+ if (opts.json) {
47
+ console.log(JSON.stringify(rooms.map(r => {
48
+ const otherPk = store.getOtherMember(r);
49
+ const msgs = store.getMessages(r.id);
50
+ const readIds = store.readMessageIds[r.id] || {};
51
+ const unreadCount = msgs.filter(m => {
52
+ return m.sender !== store.keys?.pubKeyHex && !readIds[m.id];
53
+ }).length;
54
+ return {
55
+ id: r.id,
56
+ name: otherPk ? store.getContactName(otherPk) : r.name,
57
+ displayName: otherPk ? (store.displayNameCache[otherPk] || null) : null,
58
+ bchAddress: otherPk ? (store.bchAddressCache[otherPk] || null) : null,
59
+ type: r.type,
60
+ members: r.members,
61
+ subject: r.subject,
62
+ createdAt: r.createdAt,
63
+ updatedAt: r.updatedAt,
64
+ messageCount: msgs.length,
65
+ unreadCount,
66
+ };
67
+ })));
68
+ store.cleanup();
69
+ process.exit(0);
70
+ }
71
+ console.log();
72
+ for (const room of rooms) {
73
+ const otherPubKey = store.getOtherMember(room);
74
+ const displayName = otherPubKey
75
+ ? store.getContactName(otherPubKey)
76
+ : room.name;
77
+ const msgs = store.getMessages(room.id);
78
+ const lastMsg = msgs.length > 0 ? msgs[msgs.length - 1] : null;
79
+ const preview = lastMsg
80
+ ? (lastMsg.content.length > 50 ? lastMsg.content.slice(0, 50) + '...' : lastMsg.content)
81
+ : chalk.dim('(no messages)');
82
+ const time = lastMsg ? formatTimestamp(lastMsg.created_at) : '';
83
+ const unread = msgs.filter(m => {
84
+ const readIds = store.readMessageIds[room.id] || {};
85
+ return m.sender !== store.keys?.pubKeyHex && !readIds[m.id];
86
+ }).length;
87
+ const unreadBadge = unread > 0 ? ` [${unread}]` : '';
88
+ console.log(` ${chalk.bold(displayName)}${unreadBadge}`);
89
+ console.log(` ${room.id}`);
90
+ console.log(` ${preview} ${chalk.dim(time)}`);
91
+ console.log();
92
+ }
93
+ store.cleanup();
94
+ process.exit(0);
95
+ });
96
+ chat
97
+ .command('open')
98
+ .description('Open a conversation and show messages')
99
+ .argument('<room-id>', 'Room ID (full or prefix)')
100
+ .option('--tail <count>', 'Show only last N messages', '20')
101
+ .option('--json', 'Output as JSON')
102
+ .action(async (roomId, opts) => {
103
+ const data = loadMnemonic();
104
+ if (!data) {
105
+ console.log(chalk.red('\nNo wallet found.\n'));
106
+ process.exit(1);
107
+ }
108
+ const store = new ChatStore();
109
+ await store.initialize(data.mnemonic);
110
+ const room = store.getRoom(roomId);
111
+ if (!room) {
112
+ console.log(chalk.red(`\nRoom not found: ${roomId}\n`));
113
+ process.exit(1);
114
+ }
115
+ const allMsgs = store.getMessages(room.id);
116
+ const tail = Number.isNaN(parseInt(opts.tail, 10)) ? 20 : parseInt(opts.tail, 10);
117
+ const msgs = tail === 0 ? [] : allMsgs.slice(-tail);
118
+ // Resolve display names for all unique message senders
119
+ const senderPubKeys = [...new Set(msgs.map(m => m.sender).filter(Boolean))];
120
+ await Promise.allSettled(senderPubKeys.map(pk => store.resolveDisplayName(pk)));
121
+ // Mark viewed messages as read (do this before JSON exit too)
122
+ store.readMessageIds[room.id] = store.readMessageIds[room.id] || {};
123
+ for (const msg of msgs) {
124
+ store.readMessageIds[room.id][msg.id] = true;
125
+ }
126
+ store.saveState();
127
+ if (opts.json) {
128
+ console.log(JSON.stringify({
129
+ room: {
130
+ id: room.id,
131
+ name: room.name,
132
+ type: room.type,
133
+ members: room.members,
134
+ },
135
+ messages: msgs.map(m => ({
136
+ id: m.id,
137
+ content: m.content,
138
+ sender: m.sender,
139
+ senderName: store.getContactName(m.sender),
140
+ created_at: m.created_at,
141
+ replyTo: m.replyTo,
142
+ editOf: m.editOf,
143
+ })),
144
+ }));
145
+ store.cleanup();
146
+ process.exit(0);
147
+ }
148
+ const otherPubKey = store.getOtherMember(room);
149
+ const roomDisplayName = otherPubKey
150
+ ? store.getContactName(otherPubKey)
151
+ : room.name;
152
+ console.log(chalk.bold(`\n ${roomDisplayName}`));
153
+ console.log(chalk.dim(` ${room.id} (${room.type})`));
154
+ if (room.subject) {
155
+ console.log(chalk.dim(` Subject: ${room.subject}`));
156
+ }
157
+ console.log();
158
+ for (const msg of msgs) {
159
+ const isMine = msg.sender === store.keys?.pubKeyHex;
160
+ const sender = isMine
161
+ ? chalk.cyan('me')
162
+ : chalk.yellow(store.getContactName(msg.sender));
163
+ const time = chalk.dim(formatTimestamp(msg.created_at));
164
+ console.log(` ${sender} ${time}`);
165
+ console.log(` ${msg.content}`);
166
+ if (msg.editOf) {
167
+ console.log(chalk.dim(' (edited)'));
168
+ }
169
+ console.log();
170
+ }
171
+ if (msgs.length < allMsgs.length) {
172
+ console.log(chalk.dim(` Showing last ${msgs.length} of ${allMsgs.length} messages.\n`));
173
+ }
174
+ store.cleanup();
175
+ process.exit(0);
176
+ });
177
+ chat
178
+ .command('send')
179
+ .description('Send a message to a conversation')
180
+ .argument('<room-id>', 'Room ID (full or prefix)')
181
+ .argument('<text>', 'Message text')
182
+ .action(async (roomId, text) => {
183
+ const data = loadMnemonic();
184
+ if (!data) {
185
+ console.log(chalk.red('\nNo wallet found.\n'));
186
+ process.exit(1);
187
+ }
188
+ const store = new ChatStore();
189
+ await store.initialize(data.mnemonic);
190
+ const room = store.getRoom(roomId);
191
+ if (!room) {
192
+ console.log(chalk.red(`\nRoom not found: ${roomId}\n`));
193
+ process.exit(1);
194
+ }
195
+ const { giftWraps, message } = await store.sendMessage(room.id, text);
196
+ const { accepted, errors } = await store.publishGiftWraps(giftWraps);
197
+ store.saveState();
198
+ store.cleanup();
199
+ if (accepted.length === 0 && errors.length > 0) {
200
+ console.log(chalk.red('\n Publish failed: no relay accepted the event.\n'));
201
+ for (const e of errors) {
202
+ console.log(chalk.dim(` ${e.relay}: ${e.reason || 'unknown error'}`));
203
+ }
204
+ console.log();
205
+ process.exit(1);
206
+ }
207
+ console.log(chalk.green(`\n Message sent! (accepted by ${accepted.length}/${accepted.length + errors.length} relays)\n`));
208
+ process.exit(0);
209
+ });
210
+ chat
211
+ .command('start')
212
+ .description('Start a new 1:1 conversation with a contact')
213
+ .argument('<npub>', "Contact's npub (e.g., npub1...)")
214
+ .action(async (npub) => {
215
+ const data = loadMnemonic();
216
+ if (!data) {
217
+ console.log(chalk.red('\nNo wallet found.\n'));
218
+ process.exit(1);
219
+ }
220
+ const store = new ChatStore();
221
+ await store.initialize(data.mnemonic);
222
+ try {
223
+ const room = store.startConversation(npub);
224
+ await store.resolveDisplayName(store.getOtherMember(room));
225
+ console.log(chalk.green(`\n Conversation started: ${room.name}\n`));
226
+ console.log(chalk.dim(` Room ID: ${room.id}\n`));
227
+ }
228
+ catch (err) {
229
+ console.log(chalk.red(`\n Error: ${err.message || err}\n`));
230
+ process.exit(1);
231
+ }
232
+ finally {
233
+ store.cleanup();
234
+ }
235
+ });
236
+ chat
237
+ .command('add-contact')
238
+ .description('Add a contact by npub')
239
+ .argument('<npub>', "Contact's npub (e.g., npub1...)")
240
+ .argument('[name]', 'Optional display name')
241
+ .action(async (npub, name) => {
242
+ const data = loadMnemonic();
243
+ if (!data) {
244
+ console.log(chalk.red('\nNo wallet found.\n'));
245
+ process.exit(1);
246
+ }
247
+ const store = new ChatStore();
248
+ await store.initialize(data.mnemonic);
249
+ try {
250
+ const contact = store.addContact(npub, name);
251
+ // Try to resolve display name and BCH address from relays
252
+ const [resolvedName, bchAddr] = await Promise.all([
253
+ store.resolveDisplayName(contact.pubKeyHex),
254
+ store.resolveBchAddress(contact.pubKeyHex),
255
+ ]);
256
+ if (resolvedName) {
257
+ contact.name = resolvedName;
258
+ store.saveState();
259
+ }
260
+ store.cleanup();
261
+ console.log(chalk.green(`\n Added contact: ${contact.name}\n`));
262
+ console.log(chalk.dim(` npub: ${contact.npub}`));
263
+ console.log(chalk.dim(` hex: ${contact.pubKeyHex}`));
264
+ if (bchAddr) {
265
+ console.log(chalk.dim(` bch: ${bchAddr}`));
266
+ }
267
+ console.log();
268
+ process.exit(0);
269
+ }
270
+ catch (err) {
271
+ store.cleanup();
272
+ console.log(chalk.red(`\n Error: ${err.message || err}\n`));
273
+ process.exit(1);
274
+ }
275
+ });
276
+ chat
277
+ .command('contacts')
278
+ .description('List contacts')
279
+ .option('--json', 'Output as JSON')
280
+ .action(async (opts) => {
281
+ const data = loadMnemonic();
282
+ if (!data) {
283
+ console.log(chalk.red('\nNo wallet found.\n'));
284
+ process.exit(1);
285
+ }
286
+ const store = new ChatStore();
287
+ await store.initialize(data.mnemonic);
288
+ if (store.contacts.length === 0) {
289
+ console.log(chalk.dim('\n No contacts. Use `paytaca chat add-contact <npub>` to add one.\n'));
290
+ store.cleanup();
291
+ process.exit(0);
292
+ }
293
+ // Resolve display names and BCH addresses for all contacts
294
+ await Promise.allSettled(store.contacts.map(async (c) => {
295
+ const name = await store.resolveDisplayName(c.pubKeyHex);
296
+ if (name)
297
+ c.name = name;
298
+ await store.resolveBchAddress(c.pubKeyHex);
299
+ }));
300
+ store.saveState();
301
+ if (opts.json) {
302
+ console.log(JSON.stringify(store.contacts.map(c => ({
303
+ ...c,
304
+ bchAddress: store.bchAddressCache[c.pubKeyHex] || null,
305
+ }))));
306
+ store.cleanup();
307
+ process.exit(0);
308
+ }
309
+ console.log();
310
+ for (const c of store.contacts) {
311
+ const bchAddr = store.bchAddressCache[c.pubKeyHex] || null;
312
+ console.log(` ${chalk.bold(c.name)}`);
313
+ console.log(chalk.dim(` npub: ${c.npub}`));
314
+ console.log(chalk.dim(` hex: ${c.pubKeyHex}`));
315
+ if (bchAddr) {
316
+ console.log(chalk.dim(` bch: ${bchAddr}`));
317
+ }
318
+ console.log();
319
+ }
320
+ store.cleanup();
321
+ process.exit(0);
322
+ });
323
+ chat
324
+ .command('identity')
325
+ .description("Show your Nostr identity (npub, pubkey)")
326
+ .action(async () => {
327
+ const data = loadMnemonic();
328
+ if (!data) {
329
+ console.log(chalk.red('\nNo wallet found.\n'));
330
+ process.exit(1);
331
+ }
332
+ const store = new ChatStore();
333
+ await store.initialize(data.mnemonic);
334
+ if (!store.keys) {
335
+ console.log(chalk.red('\nFailed to derive Nostr keys.\n'));
336
+ process.exit(1);
337
+ }
338
+ console.log();
339
+ console.log(` ${chalk.bold('npub:')} ${store.keys.npub}`);
340
+ console.log(` ${chalk.bold('hex:')} ${store.keys.pubKeyHex}`);
341
+ console.log();
342
+ store.cleanup();
343
+ process.exit(0);
344
+ });
345
+ chat
346
+ .command('set-display-name')
347
+ .description('Publish your display name to relays (NIP-78)')
348
+ .argument('<name>', 'Display name to publish')
349
+ .action(async (name) => {
350
+ const data = loadMnemonic();
351
+ if (!data) {
352
+ console.log(chalk.red('\nNo wallet found.\n'));
353
+ process.exit(1);
354
+ }
355
+ const store = new ChatStore();
356
+ await store.initialize(data.mnemonic);
357
+ if (!store.keys) {
358
+ console.log(chalk.red('\nFailed to derive Nostr keys.\n'));
359
+ process.exit(1);
360
+ }
361
+ const privKeyBytes = hexToBytes(store.keys.privKeyHex);
362
+ const event = finalizeEvent({
363
+ kind: 30078,
364
+ created_at: Math.floor(Date.now() / 1000),
365
+ tags: [
366
+ ['d', 'paytaca:display-name'],
367
+ ['p', store.keys.pubKeyHex],
368
+ ],
369
+ content: JSON.stringify({ name: 'Paytaca Display Name', data: { displayName: name.trim() } }),
370
+ }, privKeyBytes);
371
+ const { accepted, errors } = await relayService.publish(store.relays, event);
372
+ if (accepted.length === 0) {
373
+ const errorDetails = errors.map(e => `${e.relay}: ${e.reason}`).join('; ');
374
+ store.cleanup();
375
+ console.log(chalk.red(`\n Publish failed. ${errorDetails}\n`));
376
+ process.exit(1);
377
+ }
378
+ store.displayNameCache[store.keys.pubKeyHex] = name.trim();
379
+ store.saveState();
380
+ store.cleanup();
381
+ console.log(chalk.green(`\n Display name published: ${name.trim()}\n`));
382
+ process.exit(0);
383
+ });
384
+ chat
385
+ .command('remove-display-name')
386
+ .description('Remove your published display name from relays')
387
+ .action(async () => {
388
+ const data = loadMnemonic();
389
+ if (!data) {
390
+ console.log(chalk.red('\nNo wallet found.\n'));
391
+ process.exit(1);
392
+ }
393
+ const store = new ChatStore();
394
+ await store.initialize(data.mnemonic);
395
+ if (!store.keys) {
396
+ console.log(chalk.red('\nFailed to derive Nostr keys.\n'));
397
+ process.exit(1);
398
+ }
399
+ const privKeyBytes = hexToBytes(store.keys.privKeyHex);
400
+ const event = finalizeEvent({
401
+ kind: 30078,
402
+ created_at: Math.floor(Date.now() / 1000),
403
+ tags: [
404
+ ['d', 'paytaca:display-name'],
405
+ ['p', store.keys.pubKeyHex],
406
+ ],
407
+ content: JSON.stringify({ name: 'Paytaca Display Name', data: {} }),
408
+ }, privKeyBytes);
409
+ const { accepted, errors } = await relayService.publish(store.relays, event);
410
+ if (accepted.length === 0) {
411
+ const errorDetails = errors.map(e => `${e.relay}: ${e.reason}`).join('; ');
412
+ store.cleanup();
413
+ console.log(chalk.red(`\n Remove failed. ${errorDetails}\n`));
414
+ process.exit(1);
415
+ }
416
+ delete store.displayNameCache[store.keys.pubKeyHex];
417
+ store.saveState();
418
+ store.cleanup();
419
+ console.log(chalk.green('\n Display name removed.\n'));
420
+ process.exit(0);
421
+ });
422
+ chat
423
+ .command('set-bch-address')
424
+ .description('Publish your BCH address to relays (NIP-78)')
425
+ .argument('<address>', 'BCH address (cashaddr format)')
426
+ .action(async (address) => {
427
+ const trimmed = address.trim().toLowerCase();
428
+ if (!/^(bitcoincash|bchtest|bchreg):[qpzry9x8gf2tvdw0s3jn54khce6mua7l]+$/.test(trimmed)) {
429
+ console.log(chalk.red('\nInvalid BCH address. Must be cashaddr format (e.g. bitcoincash:...).\n'));
430
+ process.exit(1);
431
+ }
432
+ const data = loadMnemonic();
433
+ if (!data) {
434
+ console.log(chalk.red('\nNo wallet found.\n'));
435
+ process.exit(1);
436
+ }
437
+ const store = new ChatStore();
438
+ await store.initialize(data.mnemonic);
439
+ if (!store.keys) {
440
+ console.log(chalk.red('\nFailed to derive Nostr keys.\n'));
441
+ process.exit(1);
442
+ }
443
+ const privKeyBytes = hexToBytes(store.keys.privKeyHex);
444
+ const event = finalizeEvent({
445
+ kind: 30078,
446
+ created_at: Math.floor(Date.now() / 1000),
447
+ tags: [
448
+ ['d', 'paytaca:bch-address'],
449
+ ['p', store.keys.pubKeyHex],
450
+ ],
451
+ content: JSON.stringify({ name: 'Paytaca BCH Address', data: { address: trimmed } }),
452
+ }, privKeyBytes);
453
+ const { accepted, errors } = await relayService.publish(store.relays, event);
454
+ if (accepted.length === 0) {
455
+ const errorDetails = errors.map(e => `${e.relay}: ${e.reason}`).join('; ');
456
+ store.cleanup();
457
+ console.log(chalk.red(`\n Publish failed. ${errorDetails}\n`));
458
+ process.exit(1);
459
+ }
460
+ store.bchAddressCache[store.keys.pubKeyHex] = trimmed;
461
+ store.saveState();
462
+ store.cleanup();
463
+ console.log(chalk.green(`\n BCH address published: ${trimmed}\n`));
464
+ process.exit(0);
465
+ });
466
+ chat
467
+ .command('remove-bch-address')
468
+ .description('Remove your published BCH address from relays')
469
+ .action(async () => {
470
+ const data = loadMnemonic();
471
+ if (!data) {
472
+ console.log(chalk.red('\nNo wallet found.\n'));
473
+ process.exit(1);
474
+ }
475
+ const store = new ChatStore();
476
+ await store.initialize(data.mnemonic);
477
+ if (!store.keys) {
478
+ console.log(chalk.red('\nFailed to derive Nostr keys.\n'));
479
+ process.exit(1);
480
+ }
481
+ const privKeyBytes = hexToBytes(store.keys.privKeyHex);
482
+ const event = finalizeEvent({
483
+ kind: 30078,
484
+ created_at: Math.floor(Date.now() / 1000),
485
+ tags: [
486
+ ['d', 'paytaca:bch-address'],
487
+ ['p', store.keys.pubKeyHex],
488
+ ],
489
+ content: JSON.stringify({ name: 'Paytaca BCH Address', data: {} }),
490
+ }, privKeyBytes);
491
+ const { accepted, errors } = await relayService.publish(store.relays, event);
492
+ if (accepted.length === 0) {
493
+ const errorDetails = errors.map(e => `${e.relay}: ${e.reason}`).join('; ');
494
+ store.cleanup();
495
+ console.log(chalk.red(`\n Remove failed. ${errorDetails}\n`));
496
+ process.exit(1);
497
+ }
498
+ delete store.bchAddressCache[store.keys.pubKeyHex];
499
+ store.saveState();
500
+ store.cleanup();
501
+ console.log(chalk.green('\n BCH address removed.\n'));
502
+ process.exit(0);
503
+ });
504
+ chat
505
+ .command('listen')
506
+ .description('Subscribe to new messages (long-running)')
507
+ .option('--contact <npub|name>', 'Filter to conversations involving this contact')
508
+ .option('--json', 'Output new messages as JSON lines')
509
+ .action(async (opts) => {
510
+ const data = loadMnemonic();
511
+ if (!data) {
512
+ console.log(chalk.red('\nNo wallet found.\n'));
513
+ process.exit(1);
514
+ }
515
+ const store = new ChatStore();
516
+ await store.initialize(data.mnemonic);
517
+ let filterPubKey = store.keys?.pubKeyHex || null;
518
+ if (opts.contact) {
519
+ const contact = store.contacts.find(c => c.npub === opts.contact || c.name === opts.contact || c.pubKeyHex === opts.contact);
520
+ if (contact) {
521
+ filterPubKey = contact.pubKeyHex;
522
+ }
523
+ else if (opts.contact.startsWith('npub1')) {
524
+ try {
525
+ const decoded = nip19Decode(opts.contact);
526
+ filterPubKey = decoded.data;
527
+ }
528
+ catch {
529
+ console.log(chalk.red(`\n Invalid npub: ${opts.contact}\n`));
530
+ process.exit(1);
531
+ }
532
+ }
533
+ else {
534
+ console.log(chalk.yellow(`\n Contact not found: ${opts.contact}. Watching all conversations.\n`));
535
+ filterPubKey = null;
536
+ }
537
+ }
538
+ const isJson = Boolean(opts.json);
539
+ if (!isJson) {
540
+ const target = !filterPubKey
541
+ ? 'all conversations'
542
+ : filterPubKey === store.keys?.pubKeyHex
543
+ ? 'your conversations'
544
+ : `conversations with ${store.getContactName(filterPubKey)}`;
545
+ console.log(chalk.dim(`\n Listening for new messages in ${target}... (Ctrl+C to stop)\n`));
546
+ }
547
+ store.setOnNewMessage((room, message) => {
548
+ if (filterPubKey && !room.members.includes(filterPubKey))
549
+ return;
550
+ if (isJson) {
551
+ console.log(JSON.stringify({
552
+ type: 'message',
553
+ room: { id: room.id, name: room.name, members: room.members },
554
+ message: {
555
+ id: message.id,
556
+ content: message.content,
557
+ sender: message.sender,
558
+ created_at: message.created_at,
559
+ replyTo: message.replyTo || null,
560
+ editOf: message.editOf || null,
561
+ },
562
+ senderName: store.getContactName(message.sender),
563
+ }));
564
+ return;
565
+ }
566
+ const isMine = message.sender === store.keys?.pubKeyHex;
567
+ const sender = isMine
568
+ ? chalk.cyan('me')
569
+ : chalk.yellow(store.getContactName(message.sender));
570
+ const time = chalk.dim(formatTimestamp(message.created_at));
571
+ const roomLabel = chalk.blue(room.name);
572
+ console.log(` [${roomLabel}] ${sender} ${time}`);
573
+ console.log(` ${message.content}`);
574
+ if (message.editOf) {
575
+ console.log(chalk.dim(' (edited)'));
576
+ }
577
+ console.log();
578
+ });
579
+ store.subscribe();
580
+ const shutdown = () => {
581
+ store.unsubscribe();
582
+ if (!isJson) {
583
+ console.log(chalk.dim('\n Stopped.\n'));
584
+ }
585
+ process.exit(0);
586
+ };
587
+ process.on('SIGINT', shutdown);
588
+ process.on('SIGTERM', shutdown);
589
+ await new Promise(() => { });
590
+ });
591
+ }
592
+ //# sourceMappingURL=chat.js.map