paytaca-cli 0.3.2 → 0.4.1

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.
@@ -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,CA8rB3D"}
@@ -0,0 +1,632 @@
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('profile')
347
+ .description("Show your profile (display name, BCH address)")
348
+ .option('--json', 'Output as JSON')
349
+ .action(async (opts) => {
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 [displayName, bchAddress] = await Promise.all([
362
+ store.resolveDisplayName(store.keys.pubKeyHex),
363
+ store.resolveBchAddress(store.keys.pubKeyHex),
364
+ ]);
365
+ store.saveState();
366
+ if (opts.json) {
367
+ console.log(JSON.stringify({
368
+ npub: store.keys.npub,
369
+ pubKeyHex: store.keys.pubKeyHex,
370
+ displayName: displayName || null,
371
+ bchAddress: bchAddress || null,
372
+ }));
373
+ store.cleanup();
374
+ process.exit(0);
375
+ }
376
+ console.log();
377
+ console.log(` ${chalk.bold('displayName:')} ${displayName || chalk.dim('(not set)')}`);
378
+ console.log(` ${chalk.bold('bchAddress:')} ${bchAddress || chalk.dim('(not set)')}`);
379
+ console.log(` ${chalk.bold('npub:')} ${store.keys.npub}`);
380
+ console.log(` ${chalk.bold('hex:')} ${store.keys.pubKeyHex}`);
381
+ console.log();
382
+ store.cleanup();
383
+ process.exit(0);
384
+ });
385
+ chat
386
+ .command('set-display-name')
387
+ .description('Publish your display name to relays (NIP-78)')
388
+ .argument('<name>', 'Display name to publish')
389
+ .action(async (name) => {
390
+ const data = loadMnemonic();
391
+ if (!data) {
392
+ console.log(chalk.red('\nNo wallet found.\n'));
393
+ process.exit(1);
394
+ }
395
+ const store = new ChatStore();
396
+ await store.initialize(data.mnemonic);
397
+ if (!store.keys) {
398
+ console.log(chalk.red('\nFailed to derive Nostr keys.\n'));
399
+ process.exit(1);
400
+ }
401
+ const privKeyBytes = hexToBytes(store.keys.privKeyHex);
402
+ const event = finalizeEvent({
403
+ kind: 30078,
404
+ created_at: Math.floor(Date.now() / 1000),
405
+ tags: [
406
+ ['d', 'paytaca:display-name'],
407
+ ['p', store.keys.pubKeyHex],
408
+ ],
409
+ content: JSON.stringify({ name: 'Paytaca Display Name', data: { displayName: name.trim() } }),
410
+ }, privKeyBytes);
411
+ const { accepted, errors } = await relayService.publish(store.relays, event);
412
+ if (accepted.length === 0) {
413
+ const errorDetails = errors.map(e => `${e.relay}: ${e.reason}`).join('; ');
414
+ store.cleanup();
415
+ console.log(chalk.red(`\n Publish failed. ${errorDetails}\n`));
416
+ process.exit(1);
417
+ }
418
+ store.displayNameCache[store.keys.pubKeyHex] = name.trim();
419
+ store.saveState();
420
+ store.cleanup();
421
+ console.log(chalk.green(`\n Display name published: ${name.trim()}\n`));
422
+ process.exit(0);
423
+ });
424
+ chat
425
+ .command('remove-display-name')
426
+ .description('Remove your published display name from relays')
427
+ .action(async () => {
428
+ const data = loadMnemonic();
429
+ if (!data) {
430
+ console.log(chalk.red('\nNo wallet found.\n'));
431
+ process.exit(1);
432
+ }
433
+ const store = new ChatStore();
434
+ await store.initialize(data.mnemonic);
435
+ if (!store.keys) {
436
+ console.log(chalk.red('\nFailed to derive Nostr keys.\n'));
437
+ process.exit(1);
438
+ }
439
+ const privKeyBytes = hexToBytes(store.keys.privKeyHex);
440
+ const event = finalizeEvent({
441
+ kind: 30078,
442
+ created_at: Math.floor(Date.now() / 1000),
443
+ tags: [
444
+ ['d', 'paytaca:display-name'],
445
+ ['p', store.keys.pubKeyHex],
446
+ ],
447
+ content: JSON.stringify({ name: 'Paytaca Display Name', data: {} }),
448
+ }, privKeyBytes);
449
+ const { accepted, errors } = await relayService.publish(store.relays, event);
450
+ if (accepted.length === 0) {
451
+ const errorDetails = errors.map(e => `${e.relay}: ${e.reason}`).join('; ');
452
+ store.cleanup();
453
+ console.log(chalk.red(`\n Remove failed. ${errorDetails}\n`));
454
+ process.exit(1);
455
+ }
456
+ delete store.displayNameCache[store.keys.pubKeyHex];
457
+ store.saveState();
458
+ store.cleanup();
459
+ console.log(chalk.green('\n Display name removed.\n'));
460
+ process.exit(0);
461
+ });
462
+ chat
463
+ .command('set-bch-address')
464
+ .description('Publish your BCH address to relays (NIP-78)')
465
+ .argument('<address>', 'BCH address (cashaddr format)')
466
+ .action(async (address) => {
467
+ const trimmed = address.trim().toLowerCase();
468
+ if (!/^(bitcoincash|bchtest|bchreg):[qpzry9x8gf2tvdw0s3jn54khce6mua7l]+$/.test(trimmed)) {
469
+ console.log(chalk.red('\nInvalid BCH address. Must be cashaddr format (e.g. bitcoincash:...).\n'));
470
+ process.exit(1);
471
+ }
472
+ const data = loadMnemonic();
473
+ if (!data) {
474
+ console.log(chalk.red('\nNo wallet found.\n'));
475
+ process.exit(1);
476
+ }
477
+ const store = new ChatStore();
478
+ await store.initialize(data.mnemonic);
479
+ if (!store.keys) {
480
+ console.log(chalk.red('\nFailed to derive Nostr keys.\n'));
481
+ process.exit(1);
482
+ }
483
+ const privKeyBytes = hexToBytes(store.keys.privKeyHex);
484
+ const event = finalizeEvent({
485
+ kind: 30078,
486
+ created_at: Math.floor(Date.now() / 1000),
487
+ tags: [
488
+ ['d', 'paytaca:bch-address'],
489
+ ['p', store.keys.pubKeyHex],
490
+ ],
491
+ content: JSON.stringify({ name: 'Paytaca BCH Address', data: { address: trimmed } }),
492
+ }, privKeyBytes);
493
+ const { accepted, errors } = await relayService.publish(store.relays, event);
494
+ if (accepted.length === 0) {
495
+ const errorDetails = errors.map(e => `${e.relay}: ${e.reason}`).join('; ');
496
+ store.cleanup();
497
+ console.log(chalk.red(`\n Publish failed. ${errorDetails}\n`));
498
+ process.exit(1);
499
+ }
500
+ store.bchAddressCache[store.keys.pubKeyHex] = trimmed;
501
+ store.saveState();
502
+ store.cleanup();
503
+ console.log(chalk.green(`\n BCH address published: ${trimmed}\n`));
504
+ process.exit(0);
505
+ });
506
+ chat
507
+ .command('remove-bch-address')
508
+ .description('Remove your published BCH address from relays')
509
+ .action(async () => {
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
+ if (!store.keys) {
518
+ console.log(chalk.red('\nFailed to derive Nostr keys.\n'));
519
+ process.exit(1);
520
+ }
521
+ const privKeyBytes = hexToBytes(store.keys.privKeyHex);
522
+ const event = finalizeEvent({
523
+ kind: 30078,
524
+ created_at: Math.floor(Date.now() / 1000),
525
+ tags: [
526
+ ['d', 'paytaca:bch-address'],
527
+ ['p', store.keys.pubKeyHex],
528
+ ],
529
+ content: JSON.stringify({ name: 'Paytaca BCH Address', data: {} }),
530
+ }, privKeyBytes);
531
+ const { accepted, errors } = await relayService.publish(store.relays, event);
532
+ if (accepted.length === 0) {
533
+ const errorDetails = errors.map(e => `${e.relay}: ${e.reason}`).join('; ');
534
+ store.cleanup();
535
+ console.log(chalk.red(`\n Remove failed. ${errorDetails}\n`));
536
+ process.exit(1);
537
+ }
538
+ delete store.bchAddressCache[store.keys.pubKeyHex];
539
+ store.saveState();
540
+ store.cleanup();
541
+ console.log(chalk.green('\n BCH address removed.\n'));
542
+ process.exit(0);
543
+ });
544
+ chat
545
+ .command('listen')
546
+ .description('Subscribe to new messages (long-running)')
547
+ .option('--contact <npub|name>', 'Filter to conversations involving this contact')
548
+ .option('--json', 'Output new messages as JSON lines')
549
+ .action(async (opts) => {
550
+ const data = loadMnemonic();
551
+ if (!data) {
552
+ console.log(chalk.red('\nNo wallet found.\n'));
553
+ process.exit(1);
554
+ }
555
+ const store = new ChatStore();
556
+ await store.initialize(data.mnemonic);
557
+ let filterPubKey = store.keys?.pubKeyHex || null;
558
+ if (opts.contact) {
559
+ const contact = store.contacts.find(c => c.npub === opts.contact || c.name === opts.contact || c.pubKeyHex === opts.contact);
560
+ if (contact) {
561
+ filterPubKey = contact.pubKeyHex;
562
+ }
563
+ else if (opts.contact.startsWith('npub1')) {
564
+ try {
565
+ const decoded = nip19Decode(opts.contact);
566
+ filterPubKey = decoded.data;
567
+ }
568
+ catch {
569
+ console.log(chalk.red(`\n Invalid npub: ${opts.contact}\n`));
570
+ process.exit(1);
571
+ }
572
+ }
573
+ else {
574
+ console.log(chalk.yellow(`\n Contact not found: ${opts.contact}. Watching all conversations.\n`));
575
+ filterPubKey = null;
576
+ }
577
+ }
578
+ const isJson = Boolean(opts.json);
579
+ if (!isJson) {
580
+ const target = !filterPubKey
581
+ ? 'all conversations'
582
+ : filterPubKey === store.keys?.pubKeyHex
583
+ ? 'your conversations'
584
+ : `conversations with ${store.getContactName(filterPubKey)}`;
585
+ console.log(chalk.dim(`\n Listening for new messages in ${target}... (Ctrl+C to stop)\n`));
586
+ }
587
+ store.setOnNewMessage((room, message) => {
588
+ if (filterPubKey && !room.members.includes(filterPubKey))
589
+ return;
590
+ if (isJson) {
591
+ console.log(JSON.stringify({
592
+ type: 'message',
593
+ room: { id: room.id, name: room.name, members: room.members },
594
+ message: {
595
+ id: message.id,
596
+ content: message.content,
597
+ sender: message.sender,
598
+ created_at: message.created_at,
599
+ replyTo: message.replyTo || null,
600
+ editOf: message.editOf || null,
601
+ },
602
+ senderName: store.getContactName(message.sender),
603
+ }));
604
+ return;
605
+ }
606
+ const isMine = message.sender === store.keys?.pubKeyHex;
607
+ const sender = isMine
608
+ ? chalk.cyan('me')
609
+ : chalk.yellow(store.getContactName(message.sender));
610
+ const time = chalk.dim(formatTimestamp(message.created_at));
611
+ const roomLabel = chalk.blue(room.name);
612
+ console.log(` [${roomLabel}] ${sender} ${time}`);
613
+ console.log(` ${message.content}`);
614
+ if (message.editOf) {
615
+ console.log(chalk.dim(' (edited)'));
616
+ }
617
+ console.log();
618
+ });
619
+ store.subscribe();
620
+ const shutdown = () => {
621
+ store.unsubscribe();
622
+ if (!isJson) {
623
+ console.log(chalk.dim('\n Stopped.\n'));
624
+ }
625
+ process.exit(0);
626
+ };
627
+ process.on('SIGINT', shutdown);
628
+ process.on('SIGTERM', shutdown);
629
+ await new Promise(() => { });
630
+ });
631
+ }
632
+ //# sourceMappingURL=chat.js.map