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.
- package/README.md +23 -5
- package/dist/commands/chat.d.ts +3 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +592 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/nostr/chat.d.ts +42 -0
- package/dist/nostr/chat.d.ts.map +1 -0
- package/dist/nostr/chat.js +72 -0
- package/dist/nostr/chat.js.map +1 -0
- package/dist/nostr/chat.test.d.ts +2 -0
- package/dist/nostr/chat.test.d.ts.map +1 -0
- package/dist/nostr/chat.test.js +121 -0
- package/dist/nostr/chat.test.js.map +1 -0
- package/dist/nostr/keys.d.ts +8 -0
- package/dist/nostr/keys.d.ts.map +1 -0
- package/dist/nostr/keys.js +22 -0
- package/dist/nostr/keys.js.map +1 -0
- package/dist/nostr/relay.d.ts +47 -0
- package/dist/nostr/relay.d.ts.map +1 -0
- package/dist/nostr/relay.js +370 -0
- package/dist/nostr/relay.js.map +1 -0
- package/dist/nostr/store.d.ts +75 -0
- package/dist/nostr/store.d.ts.map +1 -0
- package/dist/nostr/store.js +410 -0
- package/dist/nostr/store.js.map +1 -0
- package/dist/nostr/store.test.d.ts +2 -0
- package/dist/nostr/store.test.d.ts.map +1 -0
- package/dist/nostr/store.test.js +448 -0
- package/dist/nostr/store.test.js.map +1 -0
- package/package.json +2 -1
- package/skills/README.md +68 -0
- package/skills/paytaca-send/SKILL.md +159 -0
- package/skills/paytaca-wallet/SKILL.md +155 -0
- package/dist/commands/claude.d.ts +0 -9
- package/dist/commands/claude.d.ts.map +0 -1
- package/dist/commands/claude.js +0 -20
- package/dist/commands/claude.js.map +0 -1
- package/dist/commands/opencode.d.ts +0 -9
- package/dist/commands/opencode.d.ts.map +0 -1
- package/dist/commands/opencode.js +0 -20
- package/dist/commands/opencode.js.map +0 -1
- package/dist/utils/skill.d.ts +0 -17
- package/dist/utils/skill.d.ts.map +0 -1
- package/dist/utils/skill.js +0 -100
- package/dist/utils/skill.js.map +0 -1
- /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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
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
|