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 +16 -0
- package/dist/context.js +18 -3
- package/dist/index.js +33 -8
- package/dist/widgets/dm-thread.html +2 -2
- package/dist/widgets/identity-picker-handler.d.ts +1 -0
- package/dist/widgets/identity-picker-handler.js +1 -0
- package/dist/widgets/identity-picker.html +19 -8
- package/dist/widgets/social-feed.html +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
215
|
-
|
|
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
|
|
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
|
|
|
@@ -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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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.
|
|
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",
|