nostr-bray 1.28.0 → 1.29.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
@@ -221,6 +221,8 @@ Quick examples:
221
221
  nostr-bray shell # interactive mode
222
222
 
223
223
  Flags:
224
+ --bunker <uri> Use bunker:// URI (overrides env/config)
225
+ --key <nsec|hex|mnemonic> Use this secret key (overrides env/config)
224
226
  --json Output raw JSON (for piping/scripts)
225
227
  --human Force human-readable output
226
228
  --help Show help for a command
@@ -235,6 +237,20 @@ if (command === 'help' || command === '--help' || command === '-h') {
235
237
  console.log(HELP);
236
238
  process.exit(0);
237
239
  }
240
+ // Allow key/bunker overrides on the command line
241
+ // These are injected into env before loadConfig() so all loading logic stays centralised.
242
+ // Note: loadConfig() deletes BUNKER_URI / NOSTR_SECRET_KEY from env after reading them.
243
+ if (args.includes('--bunker')) {
244
+ process.env.BUNKER_URI = args[args.indexOf('--bunker') + 1];
245
+ // Remove from args so commands don't see it as a positional argument
246
+ const i = args.indexOf('--bunker');
247
+ args.splice(i, 2);
248
+ }
249
+ if (args.includes('--key')) {
250
+ process.env.NOSTR_SECRET_KEY = args[args.indexOf('--key') + 1];
251
+ const i = args.indexOf('--key');
252
+ args.splice(i, 2);
253
+ }
238
254
  // Commands that work purely offline — no relay connection needed
239
255
  const OFFLINE_COMMANDS = new Set([
240
256
  'whoami', 'create', 'list', 'derive', 'persona', 'switch', 'prove',
package/dist/context.js CHANGED
@@ -121,12 +121,27 @@ export class IdentityContext {
121
121
  return;
122
122
  }
123
123
  }
124
- // Not in cache — derive on the fly
125
- const identity = derive(this.root, purposeOrName, index ?? 0);
124
+ // Not in cache — derive on the fly.
125
+ // If no colon, treat as a persona name (nostr:persona:{name}); otherwise raw purpose.
126
+ const isPersonaName = !purposeOrName.includes(':');
127
+ let identity;
128
+ let purpose;
129
+ let personaName;
130
+ if (isPersonaName) {
131
+ const persona = derivePersona(this.root, purposeOrName, index ?? 0);
132
+ identity = persona.identity;
133
+ purpose = persona.identity.purpose;
134
+ personaName = purposeOrName;
135
+ }
136
+ else {
137
+ identity = derive(this.root, purposeOrName, index ?? 0);
138
+ purpose = purposeOrName;
139
+ }
126
140
  const entry = {
127
141
  identity,
128
- purpose: purposeOrName,
142
+ purpose,
129
143
  index: index ?? 0,
144
+ personaName,
130
145
  lastUsed: Date.now(),
131
146
  };
132
147
  this.putCache(identity.npub, entry);
package/dist/index.js CHANGED
@@ -142,12 +142,10 @@ if (config.transport === 'stdio') {
142
142
  else {
143
143
  const { createServer } = await import('node:http');
144
144
  const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
145
- const { randomUUID } = await import('node:crypto');
145
+ const { isInitializeRequest } = await import('@modelcontextprotocol/sdk/types.js');
146
+ const { randomUUID, timingSafeEqual } = await import('node:crypto');
146
147
  const token = process.env.BRAY_HTTP_TOKEN ?? randomUUID();
147
148
  console.error(`nostr-bray HTTP auth token: ${token}`);
148
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
149
- await server.connect(transport);
150
- const { timingSafeEqual } = await import('node:crypto');
151
149
  const expectedAuth = Buffer.from(`Bearer ${token}`);
152
150
  // Sliding window rate limiter (per-IP)
153
151
  const rateLimits = new Map();
@@ -163,6 +161,25 @@ else {
163
161
  entry.count++;
164
162
  return entry.count <= RATE_LIMIT;
165
163
  }
164
+ // Session map — each MCP session gets its own transport.
165
+ // The underlying McpServer is reused across sessions: when a session ends its
166
+ // transport closes, which sets server._transport = undefined, allowing the next
167
+ // session to call server.connect(newTransport).
168
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
169
+ const sessions = new Map();
170
+ async function createSession() {
171
+ // Close the current server connection so it can be reconnected below.
172
+ if (server.isConnected())
173
+ await server.close();
174
+ const t = new StreamableHTTPServerTransport({
175
+ sessionIdGenerator: () => randomUUID(),
176
+ onsessioninitialized: (id) => { sessions.set(id, t); },
177
+ });
178
+ t.onclose = () => { if (t.sessionId)
179
+ sessions.delete(t.sessionId); };
180
+ await server.connect(t);
181
+ return t;
182
+ }
166
183
  const httpServer = createServer(async (req, res) => {
167
184
  const clientIp = req.socket.remoteAddress ?? 'unknown';
168
185
  // Rate limiting
@@ -187,6 +204,7 @@ else {
187
204
  return;
188
205
  }
189
206
  // Parse body for POST requests (1MB limit)
207
+ let body;
190
208
  if (req.method === 'POST') {
191
209
  const MAX_BODY = 1_048_576;
192
210
  const chunks = [];
@@ -200,7 +218,6 @@ else {
200
218
  }
201
219
  chunks.push(chunk);
202
220
  }
203
- let body;
204
221
  try {
205
222
  body = JSON.parse(Buffer.concat(chunks).toString());
206
223
  }
@@ -209,11 +226,19 @@ else {
209
226
  res.end(JSON.stringify({ error: 'Invalid JSON' }));
210
227
  return;
211
228
  }
212
- await transport.handleRequest(req, res, body);
213
229
  }
214
- else {
215
- await transport.handleRequest(req, res);
230
+ // Route to existing session or create a new one for initialize requests
231
+ const sessionId = req.headers['mcp-session-id'];
232
+ let transport = sessionId ? sessions.get(sessionId) : undefined;
233
+ if (!transport) {
234
+ if (req.method !== 'POST' || !isInitializeRequest(body)) {
235
+ res.writeHead(400, { 'Content-Type': 'application/json' });
236
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: no active session' }, id: null }));
237
+ return;
238
+ }
239
+ transport = await createSession();
216
240
  }
241
+ await transport.handleRequest(req, res, body);
217
242
  });
218
243
  httpServer.listen(config.port, config.bindAddress, () => {
219
244
  console.error(`nostr-bray HTTP on ${config.bindAddress}:${config.port}`);
@@ -240,11 +240,11 @@ const { App } = globalThis.ExtApps;
240
240
  function sendReply() {
241
241
  const message = replyInput.value.trim();
242
242
  if (!message || !partnerPubkeyHex) return;
243
- app.callServerTool("dm-send", { recipientPubkeyHex: partnerPubkeyHex, message: message });
243
+ app.callServerTool({ name: "dm-send", arguments: { recipientPubkeyHex: partnerPubkeyHex, message } });
244
244
  replyInput.value = "";
245
245
  // Refresh conversation after short delay
246
246
  setTimeout(() => {
247
- app.callServerTool("dm-thread-widget", { withPubkeyHex: partnerPubkeyHex });
247
+ app.callServerTool({ name: "dm-thread-widget", arguments: { withPubkeyHex: partnerPubkeyHex } });
248
248
  }, 1500);
249
249
  }
250
250
 
@@ -6,6 +6,7 @@ import type { SigningContext } from '../signing-context.js';
6
6
  import type { RelayPool } from '../relay-pool.js';
7
7
  export interface IdentityPickerEntry {
8
8
  npub: string;
9
+ purpose: string;
9
10
  personaName?: string;
10
11
  displayName?: string;
11
12
  pictureDataUri?: string;
@@ -25,6 +25,7 @@ export async function handleIdentityPickerWidget(ctx, pool) {
25
25
  catch { /* profile lookup may fail for new identities */ }
26
26
  entries.push({
27
27
  npub: id.npub,
28
+ purpose: id.purpose ?? 'master',
28
29
  personaName: id.personaName,
29
30
  displayName,
30
31
  pictureDataUri,
@@ -110,7 +110,7 @@ const { App } = globalThis.ExtApps;
110
110
  app.ontoolresult = ({ content }) => {
111
111
  try {
112
112
  const data = JSON.parse(content[0].text);
113
- render(data);
113
+ if (data.identities) render(data);
114
114
  } catch (e) {
115
115
  clearAndShowMessage("Failed to parse identity data.");
116
116
  }
@@ -119,6 +119,10 @@ const { App } = globalThis.ExtApps;
119
119
  await app.connect();
120
120
  const hostCtx = app.getHostContext();
121
121
  if (hostCtx) applyTheme(hostCtx.theme);
122
+ const caps = app.getHostCapabilities();
123
+ const canCallTools = !!caps?.serverTools;
124
+ console.debug("[identity-picker] hostCapabilities:", JSON.stringify(caps));
125
+ console.debug("[identity-picker] canCallTools:", canCallTools);
122
126
 
123
127
  function clearAndShowMessage(msg) {
124
128
  while (root.firstChild) root.removeChild(root.firstChild);
@@ -179,14 +183,21 @@ const { App } = globalThis.ExtApps;
179
183
  card.appendChild(badge);
180
184
  }
181
185
 
182
- card.addEventListener("click", () => {
186
+ card.addEventListener("click", async () => {
183
187
  if (id.isActive) return;
184
- const target = id.personaName || "master";
185
- app.callServerTool("identity-switch", { target: target });
186
- // Re-fetch identities after switch
187
- setTimeout(() => {
188
- app.callServerTool("identity-picker-widget", {});
189
- }, 500);
188
+ if (!canCallTools) {
189
+ clearAndShowMessage(`Host doesn't support widget tool calls. Ask the AI to switch to "${id.personaName || id.purpose}".`);
190
+ return;
191
+ }
192
+ const target = id.personaName || id.purpose;
193
+ try {
194
+ await app.callServerTool({ name: "identity-switch", arguments: { target } });
195
+ const result = await app.callServerTool({ name: "identity-picker-widget", arguments: {} });
196
+ const data = JSON.parse(result.content[0].text);
197
+ render(data);
198
+ } catch (e) {
199
+ clearAndShowMessage("Switch failed: " + (e?.message ?? e));
200
+ }
190
201
  });
191
202
 
192
203
  grid.appendChild(card);
@@ -210,7 +210,7 @@ const { App } = globalThis.ExtApps;
210
210
  loadMore.className = "load-more";
211
211
  loadMore.textContent = "Load more";
212
212
  loadMore.addEventListener("click", () => {
213
- app.callServerTool("social-feed-widget", { since: lastSince });
213
+ app.callServerTool({ name: "social-feed-widget", arguments: { since: lastSince } });
214
214
  });
215
215
  root.appendChild(loadMore);
216
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nostr-bray",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "mcpName": "dev.forgesworn/bray",
5
5
  "description": "Trust-aware Nostr for AI and humans. Three dimensions of trust -- verification, proximity, and access -- woven into every interaction.",
6
6
  "type": "module",