nostr-bray 1.10.0 → 1.12.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
@@ -138,6 +138,7 @@ Environment:
138
138
  NOSTR_RELAYS Comma-separated relay URLs
139
139
  NWC_URI / NWC_URI_FILE Nostr Wallet Connect URI
140
140
  TOR_PROXY SOCKS5h proxy URL
141
+ NOSTR_BRAY_OUTPUT Default output: "human" (default) or "json"
141
142
 
142
143
  Quick examples:
143
144
  nostr-bray whoami # show your npub
@@ -149,7 +150,8 @@ Quick examples:
149
150
  nostr-bray shell # interactive mode
150
151
 
151
152
  Flags:
152
- --json Output raw JSON instead of human-readable
153
+ --json Output raw JSON (for piping/scripts)
154
+ --human Force human-readable output
153
155
  --help Show help for a command
154
156
 
155
157
  Use 'nostr-bray <command> --help' for detailed help on any command.
@@ -176,10 +178,21 @@ config.secretKey = '';
176
178
  config.nwcUri = undefined;
177
179
  const masterRelays = await nip65.loadForIdentity(ctx.activeNpub);
178
180
  pool.reconfigure(ctx.activeNpub, masterRelays);
179
- const jsonMode = args.includes('--json');
180
- /** Print JSON (always) or human-readable (default) */
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 */
181
194
  function out(data, humanFormatter) {
182
- if (jsonMode || !humanFormatter) {
195
+ if (currentOutputMode === 'json' || !humanFormatter) {
183
196
  console.log(JSON.stringify(data, null, 2));
184
197
  }
185
198
  else {
@@ -218,6 +231,7 @@ function parseShellLine(line) {
218
231
  return result;
219
232
  }
220
233
  async function run(cmdArgs) {
234
+ currentOutputMode = resolveOutputMode(cmdArgs);
221
235
  const command = cmdArgs[0];
222
236
  /** Require arg or throw */
223
237
  function req(index, usage) {
@@ -1,4 +1,6 @@
1
1
  import { z } from 'zod';
2
+ import { toolResponse } from '../tool-response.js';
3
+ import * as fmt from '../format.js';
2
4
  import { handleIdentityCreate, handleIdentityDerive, handleIdentityDerivePersona, handleIdentitySwitch, handleIdentityList, handleIdentityProve, } from './handlers.js';
3
5
  import { handleBackupShamir, handleRestoreShamir } from './shamir.js';
4
6
  import { hexId } from '../validation.js';
@@ -54,12 +56,13 @@ export function registerIdentityTools(server, deps) {
54
56
  });
55
57
  server.registerTool('identity_list', {
56
58
  description: 'List all known identities (master + all derived). Returns array of { npub, purpose, index, personaName }. Never includes private keys. Use this to see what identities are available before switching.',
59
+ inputSchema: {
60
+ output: z.enum(['json', 'human']).default('json').describe('Response format'),
61
+ },
57
62
  annotations: { readOnlyHint: true },
58
- }, async () => {
63
+ }, async ({ output }) => {
59
64
  const result = handleIdentityList(deps.ctx);
60
- return {
61
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
62
- };
65
+ return toolResponse(result, output, fmt.formatIdentityList);
63
66
  });
64
67
  server.registerTool('identity_prove', {
65
68
  description: 'Create a cryptographic linkage proof between the master key and the active identity. "blind" (default) proves they share a master without revealing how the child was derived. "full" also reveals the purpose and index. Returns a LinkageProof object that can be verified by anyone with nsec-tree. Use trust_proof_publish to make it permanent on relays.',
@@ -1,18 +1,19 @@
1
1
  import { z } from 'zod';
2
2
  import { relayUrl } from '../validation.js';
3
+ import { toolResponse } from '../tool-response.js';
4
+ import * as fmt from '../format.js';
3
5
  import { handleRelayList, handleRelaySet, handleRelayAdd, handleRelayInfo } from './handlers.js';
4
6
  export function registerRelayTools(server, deps) {
5
7
  server.registerTool('relay_list', {
6
8
  description: 'List the relay set (read/write) for the active identity. Optionally check for shared relays with another identity.',
7
9
  inputSchema: {
8
10
  compareWithNpub: z.string().optional().describe('Compare shared relays with this npub'),
11
+ output: z.enum(['json', 'human']).default('json').describe('Response format'),
9
12
  },
10
13
  annotations: { readOnlyHint: true },
11
- }, async ({ compareWithNpub }) => {
14
+ }, async ({ compareWithNpub, output }) => {
12
15
  const result = handleRelayList(deps.ctx, deps.pool, compareWithNpub);
13
- return {
14
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
15
- };
16
+ return toolResponse(result, output, fmt.formatRelays);
16
17
  });
17
18
  server.registerTool('relay_set', {
18
19
  description: 'Publish a kind 10002 relay list for the active identity. Warns if a relay list already exists.',
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { hexId } from '../validation.js';
3
+ import { toolResponse } from '../tool-response.js';
4
+ import * as fmt from '../format.js';
3
5
  import { handleSocialPost, handleSocialReply, handleSocialReact, handleSocialProfileGet, handleSocialProfileSet, handleContactsGet, handleContactsFollow, handleContactsUnfollow, handleSocialDelete, handleSocialRepost, } from './handlers.js';
4
6
  import { handleDmSend, handleDmRead } from './dm.js';
5
7
  import { handleNotifications, handleFeed } from './notifications.js';
@@ -90,14 +92,13 @@ export function registerSocialTools(server, deps) {
90
92
  inputSchema: {
91
93
  pubkeyHex: hexId.describe('Hex pubkey to fetch profile for'),
92
94
  npub: z.string().optional().describe('Bech32 npub (used for relay routing, defaults to active identity)'),
95
+ output: z.enum(['json', 'human']).default('json').describe('Response format'),
93
96
  },
94
97
  annotations: { readOnlyHint: true },
95
- }, async ({ pubkeyHex, npub }) => {
98
+ }, async ({ pubkeyHex, npub, output }) => {
96
99
  const resolvedNpub = npub ?? deps.ctx.activeNpub;
97
100
  const profile = await handleSocialProfileGet(deps.pool, resolvedNpub, pubkeyHex);
98
- return {
99
- content: [{ type: 'text', text: JSON.stringify(profile, null, 2) }],
100
- };
101
+ return toolResponse(profile, output, fmt.formatProfile);
101
102
  });
102
103
  server.registerTool('social_profile_set', {
103
104
  description: 'Set the kind 0 profile for the active identity. Warns if profile already exists — set confirm: true to overwrite.',
@@ -161,25 +162,25 @@ export function registerSocialTools(server, deps) {
161
162
  });
162
163
  server.registerTool('dm_read', {
163
164
  description: 'Read direct messages addressed to the active identity. Decrypts both NIP-17 (gift wrap) and NIP-04 (legacy). Each message includes { from, content, protocol, decrypted }. Gracefully handles decryption failures without crashing.',
165
+ inputSchema: {
166
+ output: z.enum(['json', 'human']).default('json').describe('Response format'),
167
+ },
164
168
  annotations: { readOnlyHint: true },
165
- }, async () => {
169
+ }, async ({ output }) => {
166
170
  const messages = await handleDmRead(deps.ctx, deps.pool);
167
- return {
168
- content: [{ type: 'text', text: JSON.stringify(messages, null, 2) }],
169
- };
171
+ return toolResponse(messages, output, fmt.formatDms);
170
172
  });
171
173
  server.registerTool('social_notifications', {
172
174
  description: 'Fetch notifications for the active identity — mentions, replies, reactions, and zap receipts.',
173
175
  inputSchema: {
174
176
  since: z.number().optional().describe('Unix timestamp — only fetch notifications after this time'),
175
177
  limit: z.number().int().min(1).max(200).default(50).describe('Max notifications to return'),
178
+ output: z.enum(['json', 'human']).default('json').describe('Response format'),
176
179
  },
177
180
  annotations: { readOnlyHint: true },
178
- }, async ({ since, limit }) => {
181
+ }, async ({ since, limit, output }) => {
179
182
  const notifications = await handleNotifications(deps.ctx, deps.pool, { since, limit });
180
- return {
181
- content: [{ type: 'text', text: JSON.stringify(notifications, null, 2) }],
182
- };
183
+ return toolResponse(notifications, output, fmt.formatNotifications);
183
184
  });
184
185
  server.registerTool('social_feed', {
185
186
  description: 'Fetch the kind 1 text note feed. Optionally filter by authors.',
@@ -187,26 +188,24 @@ export function registerSocialTools(server, deps) {
187
188
  authors: z.array(hexId).optional().describe('Hex pubkeys to filter by'),
188
189
  since: z.number().optional().describe('Unix timestamp — only fetch posts after this time'),
189
190
  limit: z.number().int().min(1).max(100).default(20).describe('Max posts to return'),
191
+ output: z.enum(['json', 'human']).default('json').describe('Response format'),
190
192
  },
191
193
  annotations: { readOnlyHint: true },
192
- }, async ({ authors, since, limit }) => {
194
+ }, async ({ authors, since, limit, output }) => {
193
195
  const feed = await handleFeed(deps.ctx, deps.pool, { authors, since, limit });
194
- return {
195
- content: [{ type: 'text', text: JSON.stringify(feed, null, 2) }],
196
- };
196
+ return toolResponse(feed, output, fmt.formatFeed);
197
197
  });
198
198
  server.registerTool('contacts_get', {
199
199
  description: 'Fetch the contact list (kind 3 follows) for a pubkey. Returns pubkeys, relay hints, and petnames.',
200
200
  inputSchema: {
201
201
  pubkeyHex: hexId.describe('Hex pubkey to fetch contacts for'),
202
202
  npub: z.string().optional().describe('Bech32 npub for relay routing (defaults to active identity)'),
203
+ output: z.enum(['json', 'human']).default('json').describe('Response format'),
203
204
  },
204
205
  annotations: { readOnlyHint: true },
205
- }, async ({ pubkeyHex, npub }) => {
206
+ }, async ({ pubkeyHex, npub, output }) => {
206
207
  const contacts = await handleContactsGet(deps.pool, npub ?? deps.ctx.activeNpub, pubkeyHex);
207
- return {
208
- content: [{ type: 'text', text: JSON.stringify(contacts, null, 2) }],
209
- };
208
+ return toolResponse(contacts, output, fmt.formatContacts);
210
209
  });
211
210
  server.registerTool('contacts_follow', {
212
211
  description: 'Follow a Nostr pubkey. Fetches current contact list, adds the pubkey, publishes updated kind 3.',
@@ -0,0 +1,19 @@
1
+ /** Standard output format parameter — add to any tool's inputSchema */
2
+ export declare const outputFormatParam: {
3
+ readonly description: "Response format: \"json\" (default, structured data) or \"human\" (readable text)";
4
+ };
5
+ /**
6
+ * Build an MCP tool response with optional human formatting.
7
+ *
8
+ * Usage in tool registrations:
9
+ * return toolResponse(data, args.output, formatFn)
10
+ *
11
+ * If output === 'human' and a formatter is provided, returns formatted text.
12
+ * Otherwise returns JSON (the default for MCP).
13
+ */
14
+ export declare function toolResponse(data: unknown, output: string | undefined, humanFormatter?: (d: any) => string): {
15
+ content: Array<{
16
+ type: 'text';
17
+ text: string;
18
+ }>;
19
+ };
@@ -0,0 +1,19 @@
1
+ /** Standard output format parameter — add to any tool's inputSchema */
2
+ export const outputFormatParam = {
3
+ description: 'Response format: "json" (default, structured data) or "human" (readable text)',
4
+ };
5
+ /**
6
+ * Build an MCP tool response with optional human formatting.
7
+ *
8
+ * Usage in tool registrations:
9
+ * return toolResponse(data, args.output, formatFn)
10
+ *
11
+ * If output === 'human' and a formatter is provided, returns formatted text.
12
+ * Otherwise returns JSON (the default for MCP).
13
+ */
14
+ export function toolResponse(data, output, humanFormatter) {
15
+ if (output === 'human' && humanFormatter) {
16
+ return { content: [{ type: 'text', text: humanFormatter(data) }] };
17
+ }
18
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nostr-bray",
3
- "version": "1.10.0",
3
+ "version": "1.12.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",