nostr-bray 1.9.2 → 1.11.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/dist/cli.js CHANGED
@@ -20,6 +20,7 @@ import { handleDuressConfigure, handleDuressActivate } from './safety/handlers.j
20
20
  import { handleZapSend, handleZapBalance, handleZapMakeInvoice, handleZapLookupInvoice, handleZapListTransactions, handleZapReceipts, handleZapDecode } from './zap/handlers.js';
21
21
  import { handleDecode, handleEncodeNpub, handleEncodeNote, handleEncodeNprofile, handleEncodeNevent, handleVerify, handleEncrypt, handleDecrypt, handleCount, handleFetch, handleKeyPublic, handleEncodeNsec, handleFilter, handleNipList, handleNipShow } from './util/handlers.js';
22
22
  import { getCommandHelp } from './help.js';
23
+ import * as fmt from './format.js';
23
24
  const args = process.argv.slice(2);
24
25
  const command = args[0];
25
26
  // No command = start MCP server
@@ -137,6 +138,7 @@ Environment:
137
138
  NOSTR_RELAYS Comma-separated relay URLs
138
139
  NWC_URI / NWC_URI_FILE Nostr Wallet Connect URI
139
140
  TOR_PROXY SOCKS5h proxy URL
141
+ NOSTR_BRAY_OUTPUT Default output: "human" (default) or "json"
140
142
 
141
143
  Quick examples:
142
144
  nostr-bray whoami # show your npub
@@ -147,6 +149,11 @@ Quick examples:
147
149
  nostr-bray nips # browse official NIPs
148
150
  nostr-bray shell # interactive mode
149
151
 
152
+ Flags:
153
+ --json Output raw JSON (for piping/scripts)
154
+ --human Force human-readable output
155
+ --help Show help for a command
156
+
150
157
  Use 'nostr-bray <command> --help' for detailed help on any command.
151
158
 
152
159
  Learn more:
@@ -171,8 +178,27 @@ config.secretKey = '';
171
178
  config.nwcUri = undefined;
172
179
  const masterRelays = await nip65.loadForIdentity(ctx.activeNpub);
173
180
  pool.reconfigure(ctx.activeNpub, masterRelays);
174
- /** Helper: print JSON result */
175
- function out(data) { console.log(JSON.stringify(data, null, 2)); }
181
+ // Default output mode from env (CLI-level --json/--human handled per-command)
182
+ const envDefault = process.env.NOSTR_BRAY_OUTPUT === 'json' ? 'json' : 'human';
183
+ /** Resolve output mode for a given set of args */
184
+ function resolveOutputMode(cmdArgs) {
185
+ if (cmdArgs.includes('--json'))
186
+ return 'json';
187
+ if (cmdArgs.includes('--human'))
188
+ return 'human';
189
+ return envDefault;
190
+ }
191
+ // Top-level output mode (for single-command invocations)
192
+ let currentOutputMode = resolveOutputMode(args);
193
+ /** Print JSON or human-readable depending on output mode */
194
+ function out(data, humanFormatter) {
195
+ if (currentOutputMode === 'json' || !humanFormatter) {
196
+ console.log(JSON.stringify(data, null, 2));
197
+ }
198
+ else {
199
+ console.log(humanFormatter(data));
200
+ }
201
+ }
176
202
  /** Parse a shell line into args, respecting quotes */
177
203
  function parseShellLine(line) {
178
204
  const result = [];
@@ -205,6 +231,7 @@ function parseShellLine(line) {
205
231
  return result;
206
232
  }
207
233
  async function run(cmdArgs) {
234
+ currentOutputMode = resolveOutputMode(cmdArgs);
208
235
  const command = cmdArgs[0];
209
236
  /** Require arg or throw */
210
237
  function req(index, usage) {
@@ -232,7 +259,7 @@ async function run(cmdArgs) {
232
259
  out(handleIdentityCreate());
233
260
  break;
234
261
  case 'list':
235
- out(handleIdentityList(ctx));
262
+ out(handleIdentityList(ctx), fmt.formatIdentityList);
236
263
  break;
237
264
  case 'derive':
238
265
  out(ctx.derive(req(1, 'derive <purpose> [index]'), parseInt(cmdArgs[2] ?? '0', 10)));
@@ -284,7 +311,7 @@ async function run(cmdArgs) {
284
311
  break;
285
312
  // === Social ===
286
313
  case 'post':
287
- out(await handleSocialPost(ctx, pool, { content: req(1, 'post "message"') }));
314
+ out(await handleSocialPost(ctx, pool, { content: req(1, 'post "message"') }), fmt.formatPost);
288
315
  break;
289
316
  case 'reply':
290
317
  out(await handleSocialReply(ctx, pool, {
@@ -301,7 +328,7 @@ async function run(cmdArgs) {
301
328
  }));
302
329
  break;
303
330
  case 'profile':
304
- out(await handleSocialProfileGet(pool, ctx.activeNpub, req(1, 'profile <pubkey-hex>')));
331
+ out(await handleSocialProfileGet(pool, ctx.activeNpub, req(1, 'profile <pubkey-hex>')), fmt.formatProfile);
305
332
  break;
306
333
  case 'profile-set': {
307
334
  const profile = JSON.parse(req(1, 'profile-set \'{"name":"..."}\''));
@@ -321,7 +348,7 @@ async function run(cmdArgs) {
321
348
  }));
322
349
  break;
323
350
  case 'contacts':
324
- out(await handleContactsGet(pool, ctx.activeNpub, req(1, 'contacts <pubkey-hex>')));
351
+ out(await handleContactsGet(pool, ctx.activeNpub, req(1, 'contacts <pubkey-hex>')), fmt.formatContacts);
325
352
  break;
326
353
  case 'follow':
327
354
  out(await handleContactsFollow(ctx, pool, {
@@ -344,10 +371,10 @@ async function run(cmdArgs) {
344
371
  }));
345
372
  break;
346
373
  case 'dm-read':
347
- out(await handleDmRead(ctx, pool));
374
+ out(await handleDmRead(ctx, pool), fmt.formatDms);
348
375
  break;
349
376
  case 'feed':
350
- out(await handleFeed(ctx, pool, { limit: parseInt(flag('limit', '20'), 10) }));
377
+ out(await handleFeed(ctx, pool, { limit: parseInt(flag('limit', '20'), 10) }), fmt.formatFeed);
351
378
  break;
352
379
  case 'nip-publish': {
353
380
  const id = req(1, 'nip-publish <identifier> <title> <content-or-file>');
@@ -370,7 +397,7 @@ async function run(cmdArgs) {
370
397
  }));
371
398
  break;
372
399
  case 'notifications':
373
- out(await handleNotifications(ctx, pool, { limit: parseInt(flag('limit', '50'), 10) }));
400
+ out(await handleNotifications(ctx, pool, { limit: parseInt(flag('limit', '50'), 10) }), fmt.formatNotifications);
374
401
  break;
375
402
  // === Trust ===
376
403
  case 'attest':
@@ -434,7 +461,7 @@ async function run(cmdArgs) {
434
461
  break;
435
462
  // === Relay ===
436
463
  case 'relay-list':
437
- out(handleRelayList(ctx, pool, flag('compare')));
464
+ out(handleRelayList(ctx, pool, flag('compare')), fmt.formatRelays);
438
465
  break;
439
466
  case 'relay-set': {
440
467
  const urls = args.slice(1).filter(a => !a.startsWith('--'));
@@ -477,7 +504,7 @@ async function run(cmdArgs) {
477
504
  }));
478
505
  break;
479
506
  case 'zap-receipts':
480
- out(await handleZapReceipts(ctx, pool, { limit: parseInt(flag('limit', '20'), 10) }));
507
+ out(await handleZapReceipts(ctx, pool, { limit: parseInt(flag('limit', '20'), 10) }), fmt.formatZapReceipts);
481
508
  break;
482
509
  case 'zap-decode':
483
510
  out(handleZapDecode(req(1, 'zap-decode <bolt11>')));
@@ -520,7 +547,7 @@ async function run(cmdArgs) {
520
547
  out(await handleGroupChat(pool, ctx.activeNpub, {
521
548
  groupId: req(1, 'group-chat <group-id>'),
522
549
  limit: parseInt(flag('limit', '20'), 10),
523
- }));
550
+ }), fmt.formatGroupChat);
524
551
  break;
525
552
  case 'group-send':
526
553
  out(await handleGroupSend(ctx, pool, {
@@ -535,7 +562,7 @@ async function run(cmdArgs) {
535
562
  break;
536
563
  // === Utility ===
537
564
  case 'decode':
538
- out(handleDecode(req(1, 'decode <nip19>')));
565
+ out(handleDecode(req(1, 'decode <nip19>')), fmt.formatDecode);
539
566
  break;
540
567
  case 'encode-npub':
541
568
  console.log(handleEncodeNpub(req(1, 'encode-npub <hex>')));
@@ -563,7 +590,7 @@ async function run(cmdArgs) {
563
590
  out(handleFilter(JSON.parse(req(1, 'filter <event-json> <filter-json>')), JSON.parse(req(2, 'filter <event-json> <filter-json>'))));
564
591
  break;
565
592
  case 'nips':
566
- out(await handleNipList());
593
+ out(await handleNipList(), fmt.formatNipList);
567
594
  break;
568
595
  case 'nip': {
569
596
  const num = parseInt(req(1, 'nip <number>'), 10);
@@ -0,0 +1,30 @@
1
+ /** Human-readable formatters for CLI output */
2
+ import type { PublicIdentity } from './types.js';
3
+ export declare function formatIdentity(id: PublicIdentity): string;
4
+ export declare function formatIdentityList(ids: PublicIdentity[]): string;
5
+ export declare function formatPost(result: any): string;
6
+ export declare function formatPublish(result: any): string;
7
+ export declare function formatProfile(profile: Record<string, unknown>): string;
8
+ export declare function formatContacts(contacts: Array<{
9
+ pubkey: string;
10
+ relay?: string;
11
+ petname?: string;
12
+ }>): string;
13
+ export declare function formatNotifications(notifications: any[]): string;
14
+ export declare function formatFeed(events: any[]): string;
15
+ export declare function formatDms(dms: any[]): string;
16
+ export declare function formatRelays(relays: {
17
+ read: string[];
18
+ write: string[];
19
+ sharedWarning?: string;
20
+ }): string;
21
+ export declare function formatZapReceipts(receipts: any[]): string;
22
+ export declare function formatGroupChat(messages: any[]): string;
23
+ export declare function formatNipList(nips: Array<{
24
+ number: number;
25
+ title: string;
26
+ }>): string;
27
+ export declare function formatDecode(result: {
28
+ type: string;
29
+ data: unknown;
30
+ }): string;
package/dist/format.js ADDED
@@ -0,0 +1,153 @@
1
+ /** Human-readable formatters for CLI output */
2
+ export function formatIdentity(id) {
3
+ let line = `${id.npub}`;
4
+ if (id.personaName)
5
+ line += ` (persona: ${id.personaName})`;
6
+ else if (id.purpose)
7
+ line += ` (${id.purpose}${id.index ? ` #${id.index}` : ''})`;
8
+ return line;
9
+ }
10
+ export function formatIdentityList(ids) {
11
+ if (ids.length === 0)
12
+ return 'No identities found.';
13
+ const lines = ids.map((id, i) => {
14
+ const marker = i === 0 ? '* ' : ' ';
15
+ return `${marker}${formatIdentity(id)}`;
16
+ });
17
+ return lines.join('\n');
18
+ }
19
+ export function formatPost(result) {
20
+ const status = result.publish?.success ? '✓' : '✗';
21
+ return `${status} ${result.event?.id?.slice(0, 12)}... published to ${result.publish?.accepted?.length ?? 0} relay(s)`;
22
+ }
23
+ export function formatPublish(result) {
24
+ const status = result.publish?.success ? '✓ Published' : '✗ Failed';
25
+ const relays = result.publish?.accepted?.length ?? 0;
26
+ return `${status} (${relays} relay${relays !== 1 ? 's' : ''})`;
27
+ }
28
+ export function formatProfile(profile) {
29
+ if (Object.keys(profile).length === 0)
30
+ return 'No profile found.';
31
+ const lines = [];
32
+ if (profile.name)
33
+ lines.push(`Name: ${profile.name}`);
34
+ if (profile.display_name)
35
+ lines.push(`Display: ${profile.display_name}`);
36
+ if (profile.about)
37
+ lines.push(`About: ${profile.about}`);
38
+ if (profile.nip05)
39
+ lines.push(`NIP-05: ${profile.nip05}`);
40
+ if (profile.lud16)
41
+ lines.push(`LN: ${profile.lud16}`);
42
+ if (profile.picture)
43
+ lines.push(`Picture: ${profile.picture}`);
44
+ if (profile.banner)
45
+ lines.push(`Banner: ${profile.banner}`);
46
+ return lines.join('\n');
47
+ }
48
+ export function formatContacts(contacts) {
49
+ if (contacts.length === 0)
50
+ return 'No contacts found.';
51
+ const lines = contacts.map(c => {
52
+ let line = c.pubkey.slice(0, 12) + '...';
53
+ if (c.petname)
54
+ line += ` ${c.petname}`;
55
+ if (c.relay)
56
+ line += ` (${c.relay})`;
57
+ return line;
58
+ });
59
+ return `${contacts.length} contacts:\n${lines.join('\n')}`;
60
+ }
61
+ export function formatNotifications(notifications) {
62
+ if (notifications.length === 0)
63
+ return 'No notifications.';
64
+ return notifications.map(n => {
65
+ const time = new Date(n.createdAt * 1000).toLocaleString();
66
+ const from = n.from?.slice(0, 12) + '...';
67
+ switch (n.type) {
68
+ case 'reply':
69
+ return `💬 ${from} replied: ${n.content?.slice(0, 80)} (${time})`;
70
+ case 'reaction':
71
+ return `${n.content === '+' ? '❤️' : n.content} ${from} reacted (${time})`;
72
+ case 'mention':
73
+ return `@ ${from} mentioned you: ${n.content?.slice(0, 80)} (${time})`;
74
+ case 'zap':
75
+ const sats = n.amountMsats ? `${Math.round(n.amountMsats / 1000)} sats` : 'zap';
76
+ return `⚡ ${n.zapSender?.slice(0, 12) ?? from}... zapped ${sats}${n.zapMessage ? `: ${n.zapMessage}` : ''} (${time})`;
77
+ default:
78
+ return `${n.type} from ${from} (${time})`;
79
+ }
80
+ }).join('\n');
81
+ }
82
+ export function formatFeed(events) {
83
+ if (events.length === 0)
84
+ return 'No posts found.';
85
+ return events.map(e => {
86
+ const time = new Date(e.createdAt * 1000).toLocaleString();
87
+ const author = e.pubkey?.slice(0, 12) + '...';
88
+ return `${author} ${time}\n ${e.content?.slice(0, 200)}\n`;
89
+ }).join('\n');
90
+ }
91
+ export function formatDms(dms) {
92
+ if (dms.length === 0)
93
+ return 'No messages.';
94
+ return dms.map(dm => {
95
+ const time = new Date(dm.createdAt * 1000).toLocaleString();
96
+ const from = dm.from?.slice(0, 12) + '...';
97
+ const proto = dm.protocol === 'nip04-deprecated' ? ' [NIP-04]' : '';
98
+ if (!dm.decrypted)
99
+ return `✗ ${from} Could not decrypt${proto} (${time})`;
100
+ return `${from}${proto} ${time}\n ${dm.content}`;
101
+ }).join('\n\n');
102
+ }
103
+ export function formatRelays(relays) {
104
+ const lines = [];
105
+ if (relays.read.length > 0) {
106
+ lines.push('Read:');
107
+ relays.read.forEach(r => lines.push(` ${r}`));
108
+ }
109
+ if (relays.write.length > 0) {
110
+ lines.push('Write:');
111
+ relays.write.forEach(r => lines.push(` ${r}`));
112
+ }
113
+ if (relays.sharedWarning)
114
+ lines.push(`\n⚠️ ${relays.sharedWarning}`);
115
+ return lines.join('\n');
116
+ }
117
+ export function formatZapReceipts(receipts) {
118
+ if (receipts.length === 0)
119
+ return 'No zap receipts.';
120
+ return receipts.map(r => {
121
+ const time = new Date(r.createdAt * 1000).toLocaleString();
122
+ const sats = r.amountMsats ? `${Math.round(r.amountMsats / 1000)} sats` : '? sats';
123
+ const from = r.sender ? r.sender.slice(0, 12) + '...' : 'unknown';
124
+ return `⚡ ${sats} from ${from}${r.message ? ` — ${r.message}` : ''} (${time})`;
125
+ }).join('\n');
126
+ }
127
+ export function formatGroupChat(messages) {
128
+ if (messages.length === 0)
129
+ return 'No messages.';
130
+ return messages.map(m => {
131
+ const time = new Date(m.createdAt * 1000).toLocaleString();
132
+ const author = m.pubkey?.slice(0, 12) + '...';
133
+ return `${author} ${time}\n ${m.content}`;
134
+ }).join('\n\n');
135
+ }
136
+ export function formatNipList(nips) {
137
+ return nips.map(n => `NIP-${String(n.number).padStart(2, '0')} ${n.title}`).join('\n');
138
+ }
139
+ export function formatDecode(result) {
140
+ const lines = [`Type: ${result.type}`];
141
+ if (typeof result.data === 'string') {
142
+ lines.push(`Data: ${result.data}`);
143
+ }
144
+ else if (typeof result.data === 'object' && result.data !== null) {
145
+ for (const [k, v] of Object.entries(result.data)) {
146
+ if (Array.isArray(v))
147
+ lines.push(`${k}: ${v.join(', ')}`);
148
+ else
149
+ lines.push(`${k}: ${v}`);
150
+ }
151
+ }
152
+ return lines.join('\n');
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nostr-bray",
3
- "version": "1.9.2",
3
+ "version": "1.11.0",
4
4
  "description": "MCP server giving AI agents sovereign Nostr identities — key derivation, personas, attestations, duress detection, zaps",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",