nostr-bray 1.19.0 → 1.21.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 CHANGED
@@ -8,7 +8,7 @@
8
8
  [![licence](https://img.shields.io/npm/l/nostr-bray)](./LICENSE)
9
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-ESM-blue)](./tsconfig.json)
10
10
 
11
- An MCP server that gives AI agents a full Nostr identity — not just a key pair, but a hierarchical identity tree with personas, attestations, ring signatures, encrypted DMs, and duress detection. 74 tools across 10 groups.
11
+ An MCP server that gives AI agents a full Nostr identity — not just a key pair, but a hierarchical identity tree with personas, attestations, ring signatures, encrypted DMs, and duress detection. 77 tools across 10 groups.
12
12
 
13
13
  ## The Problem
14
14
 
@@ -16,6 +16,41 @@ AI agents interacting with Nostr today are handed a single key pair with no sepa
16
16
 
17
17
  nostr-bray solves this with nsec-tree hierarchical derivation. A single master secret generates unlimited child identities, each with its own key pair, purpose, and relay set. Private keys are zeroed from memory on eviction. Agents can switch personas mid-conversation, prove they control the master without revealing the derivation path, and activate a duress identity if compromised.
18
18
 
19
+ ## How It Works
20
+
21
+ Every tool operates as the "active identity." Derive a persona, switch to it, and everything you do is signed by that persona's key — cryptographically unlinkable to the master unless you publish a proof.
22
+
23
+ ```
24
+ identity-derive-persona("work") → npub1abc...
25
+ identity-switch("work") → now operating as work persona
26
+ social-post("Hello from work!") → signed by npub1abc...
27
+ identity-switch("master") → back to master
28
+ social-post("Back to main") → signed by master npub
29
+ ```
30
+
31
+ This is not just key management — it is context isolation. Each persona has its own relay set, its own contact list, and its own attestation chain. Compromise one and the others remain intact.
32
+
33
+ ## Comparison
34
+
35
+ | Feature | nostr-bray | nak | nostr-mcp | nostr-tools |
36
+ |---------|-----------|-----|-----------|-------------|
37
+ | MCP server | 77 tools | [via `nak mcp`](https://github.com/fiatjaf/nak) | 5–23 tools | — |
38
+ | CLI | 77 commands | ✓ (mature) | — | — |
39
+ | Hierarchical identity (nsec-tree) | ✓ | — | — | — |
40
+ | Persona switching | ✓ | — | — | — |
41
+ | Ring signatures (anonymous proofs) | ✓ | — | — | — |
42
+ | Shamir backup (BIP-39 words) | ✓ | — | — | — |
43
+ | Duress detection | ✓ | — | — | — |
44
+ | NIP-VA attestations (kind 31000) | ✓ | — | — | — |
45
+ | Linkage proofs (blind/full) | ✓ | — | — | — |
46
+ | NIP-46 bunker (server + client) | ✓ | ✓ (client) | partial | — |
47
+ | NIP-17 encrypted DMs | ✓ | ✓ | ✓ | ✓ |
48
+ | NWC Lightning payments | ✓ | — | — | — |
49
+ | Blossom media | ✓ | ✓ | — | — |
50
+ | NIP-29 groups | ✓ | — | — | — |
51
+ | Tor routing (SOCKS5h) | ✓ | — | — | — |
52
+ | 329 tests, 96% coverage | ✓ | — | — | — |
53
+
19
54
  ## Quick Start — CLI
20
55
 
21
56
  ```bash
@@ -68,143 +103,133 @@ Or with a secret file (recommended):
68
103
 
69
104
  ## Tool Groups
70
105
 
71
- ### Identity (11 tools)
106
+ ### Identity (12 tools) — create, derive, switch, prove, backup, and migrate Nostr identities
72
107
 
73
108
  | Tool | Description |
74
109
  |------|-------------|
75
- | `identity_create` | Generate a fresh identity with BIP-39 mnemonic |
76
- | `identity_derive` | Derive a child identity by purpose and index |
77
- | `identity_derive_persona` | Derive a named persona (work, personal, anonymous) |
78
- | `identity_switch` | Switch active identity — all tools operate as the new identity |
79
- | `identity_list` | List all known identities (public info only) |
80
- | `identity_prove` | Create blind/full linkage proof |
81
- | `identity_backup_shamir` | Split master secret into Shamir shard files |
82
- | `identity_restore_shamir` | Reconstruct secret from shard files |
83
- | `identity_backup` | Fetch profile, contacts, relay list as portable bundle |
84
- | `identity_restore` | Re-sign migratable events under the active identity |
85
- | `identity_migrate` | Full migration with preview, confirmation, and linkage proof |
86
-
87
- ### Social (14 tools)
110
+ | `whoami` | Returns the active identity's npub |
111
+ | `identity-create` | Generate a fresh identity with BIP-39 mnemonic |
112
+ | `identity-derive` | Derive a child identity by purpose and index |
113
+ | `identity-derive-persona` | Derive a named persona (work, personal, anonymous) |
114
+ | `identity-switch` | Switch active identity — all tools operate as the new identity |
115
+ | `identity-list` | List all known identities (public info only) |
116
+ | `identity-prove` | Create blind/full linkage proof |
117
+ | `identity-backup-shamir` | Split master secret into Shamir shard files |
118
+ | `identity-restore-shamir` | Reconstruct secret from shard files |
119
+ | `identity-backup` | Fetch profile, contacts, relay list as portable bundle |
120
+ | `identity-restore` | Re-sign migratable events under the active identity |
121
+ | `identity-migrate` | Full migration with preview, confirmation, and linkage proof |
122
+
123
+ ### Social (14 tools) — post, reply, react, DM, follow, and read feeds
88
124
 
89
125
  | Tool | Description |
90
126
  |------|-------------|
91
- | `social_post` | Post a text note (kind 1) |
92
- | `social_reply` | Reply with correct e-tag and p-tag threading |
93
- | `social_react` | React to an event (kind 7) |
94
- | `social_delete` | Request deletion of your event (kind 5) |
95
- | `social_repost` | Repost/boost an event (kind 6) |
96
- | `social_profile_get` | Fetch and parse a kind 0 profile |
97
- | `social_profile_set` | Set profile with overwrite safety guard |
98
- | `dm_send` | Send encrypted DM (NIP-17 default, NIP-04 opt-in) |
99
- | `dm_read` | Read and decrypt received DMs |
100
- | `contacts_get` | Fetch contact list (kind 3 follows) |
101
- | `contacts_follow` | Follow a pubkey (publishes updated kind 3) |
102
- | `contacts_unfollow` | Unfollow a pubkey |
103
- | `social_notifications` | Fetch mentions, replies, reactions, zap receipts |
104
- | `social_feed` | Fetch kind 1 text note feed |
105
-
106
- ### Trust (11 tools)
127
+ | `social-post` | Post a text note (kind 1) |
128
+ | `social-reply` | Reply with correct e-tag and p-tag threading |
129
+ | `social-react` | React to an event (kind 7) |
130
+ | `social-delete` | Request deletion of your event (kind 5) |
131
+ | `social-repost` | Repost/boost an event (kind 6) |
132
+ | `social-profile-get` | Fetch and parse a kind 0 profile |
133
+ | `social-profile-set` | Set profile with overwrite safety guard |
134
+ | `dm-send` | Send encrypted DM (NIP-17 default, NIP-04 opt-in) |
135
+ | `dm-read` | Read and decrypt received DMs |
136
+ | `contacts-get` | Fetch contact list (kind 3 follows) |
137
+ | `contacts-follow` | Follow a pubkey (publishes updated kind 3) |
138
+ | `contacts-unfollow` | Unfollow a pubkey |
139
+ | `social-notifications` | Fetch mentions, replies, reactions, zap receipts |
140
+ | `social-feed` | Fetch kind 1 text note feed |
141
+
142
+ ### Trust (11 tools) — attestations, ring signatures, linkage proofs, spoken verification
107
143
 
108
144
  | Tool | Description |
109
145
  |------|-------------|
110
- | `trust_attest` | Create kind 31000 verifiable attestation |
111
- | `trust_read` | Read attestations by subject/type/attestor |
112
- | `trust_verify` | Validate attestation structure |
113
- | `trust_revoke` | Revoke an attestation (identity check) |
114
- | `trust_request` | Send attestation request via NIP-17 |
115
- | `trust_request_list` | Scan DMs for attestation requests |
116
- | `trust_proof_publish` | Publish linkage proof (kind 30078) with confirmation |
117
- | `trust_ring_prove` | Anonymous group membership proof (ring signature) |
118
- | `trust_ring_verify` | Verify ring signature proof |
119
- | `trust_spoken_challenge` | Generate spoken verification token |
120
- | `trust_spoken_verify` | Verify spoken token response |
121
-
122
- ### Relay (4 tools)
146
+ | `trust-attest` | Create kind 31000 verifiable attestation |
147
+ | `trust-read` | Read attestations by subject/type/attestor |
148
+ | `trust-verify` | Validate attestation structure |
149
+ | `trust-revoke` | Revoke an attestation (identity check) |
150
+ | `trust-request` | Send attestation request via NIP-17 |
151
+ | `trust-request-list` | Scan DMs for attestation requests |
152
+ | `trust-proof-publish` | Publish linkage proof (kind 30078) with confirmation |
153
+ | `trust-ring-prove` | Anonymous group membership proof (ring signature) |
154
+ | `trust-ring-verify` | Verify ring signature proof |
155
+ | `trust-spoken-challenge` | Generate spoken verification token |
156
+ | `trust-spoken-verify` | Verify spoken token response |
157
+
158
+ ### Relay (5 tools) — per-identity relay lists, NIP-65 management, direct queries
123
159
 
124
160
  | Tool | Description |
125
161
  |------|-------------|
126
- | `relay_list` | List relays with shared-relay warnings |
127
- | `relay_set` | Publish kind 10002 relay list |
128
- | `relay_add` | Add relay to active identity (in-memory) |
129
- | `relay_info` | Fetch NIP-11 relay information document |
162
+ | `relay-list` | List relays with shared-relay warnings |
163
+ | `relay-set` | Publish kind 10002 relay list |
164
+ | `relay-add` | Add relay to active identity (in-memory) |
165
+ | `relay-query` | Query events from relays by kind, author, tags, or time range |
166
+ | `relay-info` | Fetch NIP-11 relay information document |
130
167
 
131
- ### Zap (7 tools)
168
+ ### Zap (7 tools) — Lightning payments and invoices via Nostr Wallet Connect
132
169
 
133
170
  | Tool | Description |
134
171
  |------|-------------|
135
- | `zap_send` | Pay a Lightning invoice via NWC |
136
- | `zap_balance` | Request wallet balance via NWC |
137
- | `zap_make_invoice` | Generate a Lightning invoice via NWC |
138
- | `zap_lookup_invoice` | Check invoice payment status via NWC |
139
- | `zap_list_transactions` | List recent Lightning transactions |
140
- | `zap_receipts` | Parse zap receipts (amount, sender, message) |
141
- | `zap_decode` | Decode bolt11 invoice fields |
172
+ | `zap-send` | Pay a Lightning invoice via NWC |
173
+ | `zap-balance` | Request wallet balance via NWC |
174
+ | `zap-make-invoice` | Generate a Lightning invoice via NWC |
175
+ | `zap-lookup-invoice` | Check invoice payment status via NWC |
176
+ | `zap-list-transactions` | List recent Lightning transactions |
177
+ | `zap-receipts` | Parse zap receipts (amount, sender, message) |
178
+ | `zap-decode` | Decode bolt11 invoice fields |
142
179
 
143
- ### Safety (2 tools)
180
+ ### Safety (2 tools) — duress personas for coercion resistance
144
181
 
145
182
  | Tool | Description |
146
183
  |------|-------------|
147
- | `safety_configure` | Configure an alternative identity persona |
148
- | `safety_activate` | Switch to alternative identity |
184
+ | `safety-configure` | Configure an alternative identity persona |
185
+ | `safety-activate` | Switch to alternative identity |
149
186
 
150
- ### Blossom (3 tools)
187
+ ### Blossom (3 tools) — media uploads and management
151
188
 
152
189
  | Tool | Description |
153
190
  |------|-------------|
154
- | `blossom_upload` | Upload file to a blossom media server |
155
- | `blossom_list` | List blobs for a pubkey |
156
- | `blossom_delete` | Delete a blob by SHA-256 hash |
191
+ | `blossom-upload` | Upload file to a blossom media server |
192
+ | `blossom-list` | List blobs for a pubkey |
193
+ | `blossom-delete` | Delete a blob by SHA-256 hash |
157
194
 
158
- ### Groups — NIP-29 (4 tools)
195
+ ### Groups — NIP-29 (4 tools) — group chat, metadata, and membership
159
196
 
160
197
  | Tool | Description |
161
198
  |------|-------------|
162
- | `group_info` | Fetch group metadata |
163
- | `group_chat` | Read group chat messages |
164
- | `group_send` | Send message to a group |
165
- | `group_members` | List group members |
199
+ | `group-info` | Fetch group metadata |
200
+ | `group-chat` | Read group chat messages |
201
+ | `group-send` | Send message to a group |
202
+ | `group-members` | List group members |
166
203
 
167
- ### Community NIPs (2 tools)
204
+ ### Community NIPs (2 tools) — publish and read community-proposed NIPs
168
205
 
169
206
  | Tool | Description |
170
207
  |------|-------------|
171
- | `nip_publish` | Publish a community NIP (kind 30817) |
172
- | `nip_read` | Fetch community NIPs |
208
+ | `nip-publish` | Publish a community NIP (kind 30817) |
209
+ | `nip-read` | Fetch community NIPs |
173
210
 
174
- ### Utility (16 tools)
211
+ ### Utility (18 tools) — encode, decode, encrypt, verify, filter, fetch, browse NIPs
175
212
 
176
213
  | Tool | Description |
177
214
  |------|-------------|
178
215
  | `decode` | Decode npub/nsec/note/nevent/nprofile/naddr |
179
- | `encode_npub` | Encode hex pubkey as npub |
180
- | `encode_note` | Encode hex event ID as note |
181
- | `encode_nprofile` | Encode pubkey + relays as nprofile |
182
- | `encode_nevent` | Encode event ID + relays as nevent |
183
- | `encode_naddr` | Encode addressable event as naddr |
184
- | `encode_nsec` | Encode hex private key as nsec |
185
- | `key_public` | Derive pubkey from secret key |
186
- | `verify_event` | Verify event hash and signature |
216
+ | `encode-npub` | Encode hex pubkey as npub |
217
+ | `encode-note` | Encode hex event ID as note |
218
+ | `encode-nprofile` | Encode pubkey + relays as nprofile |
219
+ | `encode-nevent` | Encode event ID + relays as nevent |
220
+ | `encode-naddr` | Encode addressable event as naddr |
221
+ | `encode-nsec` | Encode hex private key as nsec |
222
+ | `key-public` | Derive pubkey from secret key |
223
+ | `key-encrypt` | Encrypt a secret key with a password (NIP-49 ncryptsec) |
224
+ | `key-decrypt` | Decrypt an ncryptsec (NIP-49) with a password |
225
+ | `verify-event` | Verify event hash and signature |
187
226
  | `filter` | Test if an event matches a filter |
188
- | `nip44_encrypt` | NIP-44 encrypt for a recipient |
189
- | `nip44_decrypt` | NIP-44 decrypt from a sender |
227
+ | `nip44-encrypt` | NIP-44 encrypt for a recipient |
228
+ | `nip44-decrypt` | NIP-44 decrypt from a sender |
190
229
  | `count` | Count events matching a filter |
191
230
  | `fetch` | Fetch events by nip19 code |
192
- | `nip_list` | List all official NIPs |
193
- | `nip_show` | Show a specific NIP's content |
194
-
195
- ## Identity Switching
196
-
197
- The killer feature. Every tool operates as the "active identity." Switch with a single call:
198
-
199
- ```
200
- identity_derive_persona("work") → npub1abc...
201
- identity_switch("work") → now operating as work persona
202
- social_post("Hello from work!") → signed by npub1abc...
203
- identity_switch("master") → back to master
204
- social_post("Back to main") → signed by master npub
205
- ```
206
-
207
- Derived identities are cryptographically unlinkable unless you publish a linkage proof.
231
+ | `nip-list` | List all official NIPs |
232
+ | `nip-show` | Show a specific NIP's content |
208
233
 
209
234
  ## Configuration
210
235
 
@@ -0,0 +1,35 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ type ToolHandler = (args: any) => Promise<{
3
+ content: Array<{
4
+ type: 'text';
5
+ text: string;
6
+ }>;
7
+ }>;
8
+ /**
9
+ * Holds non-promoted tool definitions and exposes them via search-actions
10
+ * and execute-action meta-tools. This keeps the primary tool list lean
11
+ * (context-window economics) while still making every action discoverable.
12
+ */
13
+ export declare class ActionCatalog {
14
+ private readonly entries;
15
+ add(name: string, definition: any, handler: ToolHandler): void;
16
+ get size(): number;
17
+ search(intent: string): Array<{
18
+ name: string;
19
+ description: string;
20
+ parameters?: Record<string, string>;
21
+ }>;
22
+ execute(name: string, params: Record<string, unknown>): Promise<{
23
+ content: Array<{
24
+ type: 'text';
25
+ text: string;
26
+ }>;
27
+ }>;
28
+ registerMetaTools(server: McpServer): void;
29
+ }
30
+ /**
31
+ * Create a proxy around McpServer that intercepts registerTool calls.
32
+ * Promoted tools go to the real server; everything else goes to the catalog.
33
+ */
34
+ export declare function createCatalogProxy(server: McpServer, catalog: ActionCatalog, promoted: Set<string>): McpServer;
35
+ export {};
@@ -0,0 +1,148 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Holds non-promoted tool definitions and exposes them via search-actions
4
+ * and execute-action meta-tools. This keeps the primary tool list lean
5
+ * (context-window economics) while still making every action discoverable.
6
+ */
7
+ export class ActionCatalog {
8
+ entries = new Map();
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ add(name, definition, handler) {
11
+ this.entries.set(name, {
12
+ name,
13
+ description: definition.description ?? '',
14
+ inputSchema: definition.inputSchema,
15
+ annotations: definition.annotations,
16
+ handler,
17
+ });
18
+ }
19
+ get size() {
20
+ return this.entries.size;
21
+ }
22
+ search(intent) {
23
+ const tokens = intent.toLowerCase().split(/[\s\-_]+/).filter(t => t.length > 1);
24
+ const scored = [];
25
+ for (const entry of this.entries.values()) {
26
+ const haystack = `${entry.name} ${entry.description}`.toLowerCase();
27
+ let score = 0;
28
+ for (const token of tokens) {
29
+ if (haystack.includes(token))
30
+ score++;
31
+ }
32
+ if (score > 0)
33
+ scored.push({ entry, score });
34
+ }
35
+ scored.sort((a, b) => b.score - a.score);
36
+ return scored.slice(0, 10).map(({ entry }) => ({
37
+ name: entry.name,
38
+ description: entry.description,
39
+ parameters: extractParams(entry.inputSchema),
40
+ }));
41
+ }
42
+ async execute(name, params) {
43
+ const entry = this.entries.get(name);
44
+ if (!entry) {
45
+ return {
46
+ content: [{
47
+ type: 'text',
48
+ text: JSON.stringify({
49
+ error: `Unknown action "${name}". Use search-actions to find available actions.`,
50
+ }),
51
+ }],
52
+ };
53
+ }
54
+ if (entry.inputSchema && Object.keys(entry.inputSchema).length > 0) {
55
+ const schema = z.object(entry.inputSchema);
56
+ const result = schema.safeParse(params);
57
+ if (!result.success) {
58
+ return {
59
+ content: [{
60
+ type: 'text',
61
+ text: JSON.stringify({
62
+ error: 'Invalid parameters',
63
+ action: name,
64
+ issues: result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`),
65
+ expected: extractParams(entry.inputSchema),
66
+ }, null, 2),
67
+ }],
68
+ };
69
+ }
70
+ return entry.handler(result.data);
71
+ }
72
+ return entry.handler(params);
73
+ }
74
+ registerMetaTools(server) {
75
+ const catalog = this;
76
+ server.registerTool('search-actions', {
77
+ description: `Search ${catalog.size} additional actions by describing what you want to do. ` +
78
+ 'Returns matching actions with names, descriptions, and parameter schemas. ' +
79
+ 'Use this when the promoted tools (whoami, social-post, social-reply, social-feed, ' +
80
+ 'dm-send, dm-read, zap-send, zap-balance, identity-switch, relay-query) ' +
81
+ 'do not cover what you need.',
82
+ inputSchema: {
83
+ intent: z.string().describe('Describe what you want to do (e.g. "upload a file", "verify an attestation", ' +
84
+ '"encrypt a message", "manage contacts", "ring signature")'),
85
+ },
86
+ annotations: { readOnlyHint: true },
87
+ }, async ({ intent }) => {
88
+ const matches = catalog.search(intent);
89
+ if (matches.length === 0) {
90
+ return {
91
+ content: [{
92
+ type: 'text',
93
+ text: JSON.stringify({
94
+ matches: [],
95
+ hint: 'No matches found. Try different keywords or broader terms.',
96
+ }, null, 2),
97
+ }],
98
+ };
99
+ }
100
+ return {
101
+ content: [{
102
+ type: 'text',
103
+ text: JSON.stringify({ matches }, null, 2),
104
+ }],
105
+ };
106
+ });
107
+ server.registerTool('execute-action', {
108
+ description: 'Execute an action found via search-actions. Pass the exact action name and its parameters.',
109
+ inputSchema: {
110
+ action: z.string().describe('Action name from search-actions results'),
111
+ params: z.record(z.string(), z.unknown()).default({}).describe('Parameters for the action (see search-actions results for expected params)'),
112
+ },
113
+ annotations: { openWorldHint: true },
114
+ }, async ({ action, params }) => {
115
+ return catalog.execute(action, params);
116
+ });
117
+ }
118
+ }
119
+ /**
120
+ * Create a proxy around McpServer that intercepts registerTool calls.
121
+ * Promoted tools go to the real server; everything else goes to the catalog.
122
+ */
123
+ export function createCatalogProxy(server, catalog, promoted) {
124
+ return new Proxy(server, {
125
+ get(target, prop, receiver) {
126
+ if (prop === 'registerTool') {
127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
128
+ return (name, definition, handler) => {
129
+ if (promoted.has(name)) {
130
+ return target.registerTool(name, definition, handler);
131
+ }
132
+ catalog.add(name, definition, handler);
133
+ };
134
+ }
135
+ return Reflect.get(target, prop, receiver);
136
+ },
137
+ });
138
+ }
139
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
140
+ function extractParams(schema) {
141
+ if (!schema || Object.keys(schema).length === 0)
142
+ return undefined;
143
+ const params = {};
144
+ for (const [key, zodType] of Object.entries(schema)) {
145
+ params[key] = zodType?.description ?? '(no description)';
146
+ }
147
+ return params;
148
+ }
package/dist/cli.js CHANGED
@@ -130,7 +130,8 @@ Social:
130
130
  group-members <group-id> List group members
131
131
 
132
132
  Trust:
133
- attest <type> <identifier> [subject] Create kind 31000 attestation
133
+ attest <event-id> Verify someone's assertion (assertion-first)
134
+ claim <type> Make a direct claim (endorsement, vouch, review)
134
135
  trust-read [--subject X] [--type X] Read attestations
135
136
  trust-verify <event-json> Validate attestation structure
136
137
  trust-revoke <type> <identifier> Revoke an attestation
@@ -473,13 +474,27 @@ async function run(cmdArgs) {
473
474
  out(await handleNotifications(ctx, pool, { limit: parseInt(flag('limit', '50'), 10) }), fmt.formatNotifications);
474
475
  break;
475
476
  // === Trust ===
476
- case 'attest':
477
+ case 'attest': {
478
+ const assertionId = req(1, 'attest <assertion-event-id> [--subject <hex>] [--type <type>] [--summary <text>]');
477
479
  out(await handleTrustAttest(ctx, pool, {
478
- type: req(1, 'attest <type> <identifier> [subject]'),
479
- identifier: cmdArgs[2],
480
- subject: cmdArgs[3],
480
+ assertionId,
481
+ subject: flag('subject'),
482
+ type: flag('type'),
483
+ summary: flag('summary'),
484
+ assertionRelay: flag('relay'),
481
485
  }));
482
486
  break;
487
+ }
488
+ case 'claim': {
489
+ const type = req(1, 'claim <type> [--subject <hex>] [--identifier <string>] [--summary <text>]');
490
+ out(await handleTrustAttest(ctx, pool, {
491
+ type,
492
+ subject: flag('subject'),
493
+ identifier: flag('identifier'),
494
+ summary: flag('summary'),
495
+ }));
496
+ break;
497
+ }
483
498
  case 'trust-read':
484
499
  out(await handleTrustRead(pool, ctx.activeNpub, {
485
500
  subject: flag('subject'),
@@ -14,7 +14,7 @@ export function registerIdentityTools(server, deps) {
14
14
  content: [{ type: 'text', text: deps.ctx.activeNpub }],
15
15
  };
16
16
  });
17
- server.registerTool('identity_create', {
17
+ server.registerTool('identity-create', {
18
18
  description: 'Generate a fresh Nostr identity with a BIP-39 mnemonic seed. Returns { npub, mnemonic }. Store the mnemonic securely — it will not be shown again. Use this when the user needs a brand new identity unrelated to the current one.',
19
19
  annotations: { readOnlyHint: false },
20
20
  }, async () => {
@@ -23,7 +23,7 @@ export function registerIdentityTools(server, deps) {
23
23
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
24
24
  };
25
25
  });
26
- server.registerTool('identity_derive', {
26
+ server.registerTool('identity-derive', {
27
27
  description: 'Derive a child Nostr identity from the master key by purpose and index. Deterministic — same inputs always produce the same npub. Returns { npub, purpose, index }. Use identity_switch after deriving to operate as the new identity.',
28
28
  inputSchema: {
29
29
  purpose: z.string().describe('Purpose string for derivation (e.g. "messaging", "signing")'),
@@ -36,7 +36,7 @@ export function registerIdentityTools(server, deps) {
36
36
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
37
37
  };
38
38
  });
39
- server.registerTool('identity_derive_persona', {
39
+ server.registerTool('identity-derive-persona', {
40
40
  description: 'Derive a named persona (e.g. "work", "personal", "anonymous"). Like identity_derive but uses a human-readable name. Returns { npub, personaName, index }. Follow with identity_switch to activate.',
41
41
  inputSchema: {
42
42
  name: z.string().describe('Persona name (e.g. "work", "personal", "anonymous")'),
@@ -49,7 +49,7 @@ export function registerIdentityTools(server, deps) {
49
49
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
50
50
  };
51
51
  });
52
- server.registerTool('identity_switch', {
52
+ server.registerTool('identity-switch', {
53
53
  description: 'Switch the active Nostr identity. ALL subsequent tool calls will sign events and query relays as the new identity. Pass "master" to return to root. Returns { npub } of the now-active identity. This is the key tool for multi-persona workflows.',
54
54
  inputSchema: {
55
55
  target: z.string().describe('Purpose, persona name, or "master" to switch to'),
@@ -62,7 +62,7 @@ export function registerIdentityTools(server, deps) {
62
62
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
63
63
  };
64
64
  });
65
- server.registerTool('identity_list', {
65
+ server.registerTool('identity-list', {
66
66
  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.',
67
67
  inputSchema: {
68
68
  output: z.enum(['json', 'human']).default('human').describe('Response format'),
@@ -72,7 +72,7 @@ export function registerIdentityTools(server, deps) {
72
72
  const result = handleIdentityList(deps.ctx);
73
73
  return toolResponse(result, output, fmt.formatIdentityList);
74
74
  });
75
- server.registerTool('identity_prove', {
75
+ server.registerTool('identity-prove', {
76
76
  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.',
77
77
  inputSchema: {
78
78
  mode: z.enum(['blind', 'full']).default('blind').describe('Proof mode: "blind" hides derivation path, "full" reveals purpose and index'),
@@ -84,7 +84,7 @@ export function registerIdentityTools(server, deps) {
84
84
  content: [{ type: 'text', text: JSON.stringify(proof, null, 2) }],
85
85
  };
86
86
  });
87
- server.registerTool('identity_backup_shamir', {
87
+ server.registerTool('identity-backup-shamir', {
88
88
  description: 'Split the active identity\'s private key into Shamir shard files using threshold secret sharing. Any "threshold" shards can reconstruct the key. Files written with 0600 permissions via atomic rename. Returns file paths only — shard content never appears in the response.',
89
89
  inputSchema: {
90
90
  threshold: z.number().int().min(2).describe('Minimum shards needed to reconstruct'),
@@ -106,7 +106,7 @@ export function registerIdentityTools(server, deps) {
106
106
  }, null, 2) }],
107
107
  };
108
108
  });
109
- server.registerTool('identity_restore_shamir', {
109
+ server.registerTool('identity-restore-shamir', {
110
110
  description: 'Reconstruct a secret from Shamir shard files. Returns the restored npub for verification.',
111
111
  inputSchema: {
112
112
  files: z.array(z.string()).describe('Paths to shard files'),
@@ -129,7 +129,7 @@ export function registerIdentityTools(server, deps) {
129
129
  }, null, 2) }],
130
130
  };
131
131
  });
132
- server.registerTool('identity_backup', {
132
+ server.registerTool('identity-backup', {
133
133
  description: 'Fetch profile, contacts, relay list, and attestations for a pubkey. Returns a portable JSON bundle (no private keys).',
134
134
  inputSchema: {
135
135
  pubkeyHex: hexId.describe('Hex pubkey to back up'),
@@ -147,7 +147,7 @@ export function registerIdentityTools(server, deps) {
147
147
  }, null, 2) }],
148
148
  };
149
149
  });
150
- server.registerTool('identity_restore', {
150
+ server.registerTool('identity-restore', {
151
151
  description: 'Re-sign migratable events (profile, contacts, relay list) under the active identity. Skips attestations (trust chain protection).',
152
152
  inputSchema: {
153
153
  pubkeyHex: hexId.describe('Hex pubkey of the original identity to restore from'),
@@ -162,7 +162,7 @@ export function registerIdentityTools(server, deps) {
162
162
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
163
163
  };
164
164
  });
165
- server.registerTool('identity_migrate', {
165
+ server.registerTool('identity-migrate', {
166
166
  description: 'Migrate from an old identity to the active one. Shows preview first — set confirm: true to execute. Publishes linkage proof and re-signs migratable events.',
167
167
  inputSchema: {
168
168
  oldPubkeyHex: z.string().describe('Hex pubkey of the old identity'),