agentdex-cli 0.4.2 → 0.4.4

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
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'url';
9
9
  import { dirname, join } from 'path';
10
10
  import { nip19 } from 'nostr-tools';
11
11
  import { AgentdexClient } from './client.js';
12
- import { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, createKind0Event, publishToRelays, createNote, generateAndSaveKeypair } from './nostr.js';
12
+ import { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, buildKind0Event, publishToRelays, createNote, generateAndSaveKeypair } from './nostr.js';
13
13
  import { payInvoice } from './nwc.js';
14
14
  const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = dirname(__filename);
@@ -70,7 +70,7 @@ program
70
70
  .option('--owner-type <type>', 'Owner type: human, agent, org (sets owner_type tag on kind 31339)')
71
71
  .option('--parent <npub-or-hex>', 'Parent/orchestrator agent pubkey (npub or hex)')
72
72
  .option('--bot', 'Add ["bot"] tag to kind 0 profile (declares this pubkey as automated)')
73
- .option('--portfolio <entry>', 'Portfolio URL (format: "url,name,description") — repeatable', (val, acc) => [...acc, val], [])
73
+ .option('--portfolio <entry>', 'Portfolio entry (format: "id,url,label,description") — repeatable', (val, acc) => [...acc, val], [])
74
74
  .option('--skill <skill>', 'Skill tag (repeatable)', (val, acc) => [...acc, val], [])
75
75
  .option('--experience <exp>', 'Experience tag (repeatable)', (val, acc) => [...acc, val], [])
76
76
  .option('--nwc <uri>', 'Nostr Wallet Connect URI for auto-pay')
@@ -100,10 +100,16 @@ program
100
100
  framework = answers.framework || framework;
101
101
  }
102
102
  const spinner = ora('Signing event...').start();
103
- // Parse portfolio entries ("url,name,description")
103
+ // Parse portfolio entries ("id,url,label,description")
104
104
  const portfolio = (options.portfolio || []).map((entry) => {
105
105
  const parts = entry.split(',').map((s) => s.trim());
106
- return { url: parts[0], name: parts[1], description: parts[2] };
106
+ // Support both: "id,url,label,desc" (new) and "url,label,desc" (old)
107
+ if (parts.length >= 2 && parts[1]?.startsWith('http')) {
108
+ return { id: parts[0], url: parts[1], name: parts[2], description: parts[3] };
109
+ }
110
+ // Old format fallback
111
+ const id = (parts[1] || parts[0]).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
112
+ return { id, url: parts[0], name: parts[1], description: parts[2] };
107
113
  });
108
114
  // Resolve owner pubkey hex from --owner flag (npub or hex)
109
115
  let ownerNpub = options.owner;
@@ -209,17 +215,17 @@ program
209
215
  console.log(chalk.gray(` Published to: ${published.join(', ')}`));
210
216
  console.log('');
211
217
  console.log(chalk.gray(` Run ${chalk.white('agentdex claim <name>')} to get ${chalk.hex('#D4A574')('<name>@agentdex.id')}`));
212
- // Publish kind 0 profile (name, about, avatar, website, lud16)
218
+ // Publish kind 0 profile (fetch existing, merge, republish)
213
219
  const k0Spinner = ora('Publishing kind 0 profile to Nostr relays...').start();
214
220
  try {
215
- const kind0 = createKind0Event(sk, {
221
+ const kind0 = await buildKind0Event(sk, {
216
222
  name,
217
223
  about: description || undefined,
218
224
  picture: options.avatar || undefined,
219
225
  lud16: options.lightning || undefined,
220
226
  ownerPubkeyHex,
221
227
  bot: !!options.bot,
222
- });
228
+ }, relays);
223
229
  await publishToRelays(kind0, relays);
224
230
  k0Spinner.succeed('Kind 0 published — visible on all Nostr clients');
225
231
  }
@@ -257,18 +263,18 @@ program
257
263
  console.log('');
258
264
  console.log(chalk.gray(` Run ${chalk.white('agentdex claim <name>')} to get ${chalk.hex('#D4A574')('<name>@agentdex.id')}`));
259
265
  console.log('');
260
- // Publish kind 0 profile (name, about, avatar, website, lud16)
266
+ // Publish kind 0 profile (fetch existing, merge, republish)
261
267
  // Kind 0 is canonical for basic profile; kind 31339 is agent-specific metadata
262
268
  const k0Spinner = ora('Publishing kind 0 profile to Nostr relays...').start();
263
269
  try {
264
- const kind0 = createKind0Event(sk, {
270
+ const kind0 = await buildKind0Event(sk, {
265
271
  name,
266
272
  about: description || undefined,
267
273
  picture: options.avatar || undefined,
268
274
  lud16: options.lightning || undefined,
269
275
  ownerPubkeyHex,
270
276
  bot: !!options.bot,
271
- });
277
+ }, relays);
272
278
  await publishToRelays(kind0, relays);
273
279
  k0Spinner.succeed('Kind 0 published — visible on all Nostr clients');
274
280
  }
@@ -325,14 +331,14 @@ program
325
331
  if (!options.skipKind0) {
326
332
  const k0Spinner = ora('Publishing kind 0 profile to Nostr relays...').start();
327
333
  try {
328
- const kind0 = createKind0Event(sk, {
334
+ const relays = ['wss://nos.lol', 'wss://relay.damus.io', ...(options.relay || [])];
335
+ const kind0 = await buildKind0Event(sk, {
329
336
  name: claim.agent?.name || name,
330
337
  about: claim.agent?.description || undefined,
331
338
  picture: claim.agent?.avatarUrl || undefined,
332
339
  nip05: `${name}@agentdex.id`,
333
340
  lud16: options.lightning || undefined,
334
- });
335
- const relays = ['wss://nos.lol', 'wss://relay.damus.io', ...(options.relay || [])];
341
+ }, relays);
336
342
  const published = await publishToRelays(kind0, relays);
337
343
  k0Spinner.succeed(`Kind 0 published to ${published.join(', ')}`);
338
344
  console.log(chalk.gray(' NIP-05 will appear on njump/Damus/Primal once relays propagate (~30s)'));
@@ -392,8 +398,8 @@ program
392
398
  if (!options.skipKind0) {
393
399
  const k0Spinner = ora('Publishing kind 0 profile to Nostr relays...').start();
394
400
  try {
395
- const kind0 = createKind0Event(sk, { name, nip05: `${name}@agentdex.id` });
396
401
  const relays = ['wss://nos.lol', 'wss://relay.damus.io', ...(options.relay || [])];
402
+ const kind0 = await buildKind0Event(sk, { name, nip05: `${name}@agentdex.id` }, relays);
397
403
  await publishToRelays(kind0, relays);
398
404
  k0Spinner.succeed('Kind 0 published — NIP-05 active on all Nostr clients');
399
405
  }
package/dist/index.d.ts CHANGED
@@ -11,5 +11,5 @@
11
11
  */
12
12
  export { AgentdexClient } from './client.js';
13
13
  export type { AgentdexConfig, RegisterOptions, VerifyResult, ClaimResult, ClaimStatus, SearchOptions, } from './client.js';
14
- export { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, createKind0Event, publishToRelays, createNote, } from './nostr.js';
14
+ export { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, buildKind0Event, publishToRelays, createNote, } from './nostr.js';
15
15
  export type { AgentProfile } from './nostr.js';
package/dist/index.js CHANGED
@@ -10,4 +10,4 @@
10
10
  * ```
11
11
  */
12
12
  export { AgentdexClient } from './client.js';
13
- export { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, createKind0Event, publishToRelays, createNote, } from './nostr.js';
13
+ export { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, buildKind0Event, publishToRelays, createNote, } from './nostr.js';
package/dist/nostr.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * Nostr utilities — event creation, signing, publishing
3
3
  */
4
4
  export interface PortfolioItem {
5
+ id: string;
5
6
  url: string;
6
7
  name?: string;
7
8
  description?: string;
@@ -64,15 +65,20 @@ export declare function updateKind0(sk: Uint8Array, updates: {
64
65
  * After claiming a NIP-05 name, publish this to relays so Nostr clients
65
66
  * (njump, Damus, Primal) can verify the identity.
66
67
  */
67
- export declare function createKind0Event(sk: Uint8Array, profile: {
68
- name: string;
68
+ /**
69
+ * Fetch existing kind 0, merge explicit updates, and return signed event.
70
+ * Safe for existing Nostr users — only overwrites fields explicitly passed.
71
+ */
72
+ export declare function buildKind0Event(sk: Uint8Array, updates: {
73
+ name?: string;
69
74
  about?: string;
70
75
  nip05?: string;
71
76
  picture?: string;
72
77
  lud16?: string;
78
+ website?: string;
73
79
  ownerPubkeyHex?: string;
74
80
  bot?: boolean;
75
- }): import("nostr-tools").VerifiedEvent;
81
+ }, relays?: string[]): Promise<import("nostr-tools").VerifiedEvent>;
76
82
  /**
77
83
  * Create and sign a kind 1 note tagged #agentdex
78
84
  */
package/dist/nostr.js CHANGED
@@ -93,7 +93,8 @@ export function createProfileEvent(sk, profile) {
93
93
  tags.push(['messaging_fee', String(profile.messagingFee)]);
94
94
  if (profile.portfolio) {
95
95
  for (const item of profile.portfolio) {
96
- const tag = ['portfolio', item.url];
96
+ // New format: ["portfolio", id, url, label, description]
97
+ const tag = ['portfolio', item.id, item.url];
97
98
  if (item.name)
98
99
  tag.push(item.name);
99
100
  if (item.description)
@@ -185,24 +186,79 @@ export async function updateKind0(sk, updates, relays = DEFAULT_RELAYS) {
185
186
  * After claiming a NIP-05 name, publish this to relays so Nostr clients
186
187
  * (njump, Damus, Primal) can verify the identity.
187
188
  */
188
- export function createKind0Event(sk, profile) {
189
+ /**
190
+ * Fetch existing kind 0, merge explicit updates, and return signed event.
191
+ * Safe for existing Nostr users — only overwrites fields explicitly passed.
192
+ */
193
+ export async function buildKind0Event(sk, updates, relays = DEFAULT_RELAYS) {
194
+ const pool = new SimplePool();
195
+ const pubkey = getPublicKey(sk);
196
+ // Fetch existing kind 0 from relays
197
+ let existing = {};
198
+ let existingTags = [];
199
+ try {
200
+ const events = await Promise.race([
201
+ pool.querySync(relays, { kinds: [0], authors: [pubkey] }),
202
+ new Promise((resolve) => setTimeout(() => resolve([]), 5000)),
203
+ ]);
204
+ if (events.length > 0) {
205
+ // Get newest event
206
+ const newest = events.reduce((a, b) => a.created_at > b.created_at ? a : b);
207
+ existing = JSON.parse(newest.content);
208
+ existingTags = newest.tags || [];
209
+ }
210
+ }
211
+ catch { }
212
+ pool.close(relays);
213
+ // Merge content fields — only overwrite if explicitly provided (not undefined)
214
+ if (updates.name !== undefined)
215
+ existing.name = updates.name;
216
+ if (updates.about !== undefined)
217
+ existing.about = updates.about;
218
+ if (updates.nip05 !== undefined)
219
+ existing.nip05 = updates.nip05;
220
+ if (updates.picture !== undefined)
221
+ existing.picture = updates.picture;
222
+ if (updates.lud16 !== undefined)
223
+ existing.lud16 = updates.lud16;
224
+ if (updates.website !== undefined)
225
+ existing.website = updates.website;
226
+ // Merge tags
189
227
  const tags = [];
190
- if (profile.bot)
191
- tags.push(["bot"]);
192
- if (profile.ownerPubkeyHex) {
193
- tags.push(["p", profile.ownerPubkeyHex, "", "owner"]);
228
+ // Preserve existing tags that we don't manage (e.g., user's own tags)
229
+ for (const tag of existingTags) {
230
+ // Skip tags we manage — we'll re-add them below
231
+ if (tag[0] === 'p' && tag[3] === 'owner')
232
+ continue;
233
+ if (tag[0] === 'bot')
234
+ continue;
235
+ tags.push(tag);
236
+ }
237
+ // Add/update owner p tag
238
+ if (updates.ownerPubkeyHex) {
239
+ tags.push(['p', updates.ownerPubkeyHex, '', 'owner']);
240
+ }
241
+ else {
242
+ // Preserve existing owner p tag if we're not updating it
243
+ const existingOwner = existingTags.find(t => t[0] === 'p' && t[3] === 'owner');
244
+ if (existingOwner)
245
+ tags.push(existingOwner);
246
+ }
247
+ // Add bot tag only if explicitly requested
248
+ if (updates.bot) {
249
+ tags.push(['bot']);
250
+ }
251
+ else {
252
+ // Preserve existing bot tag if we're not changing it
253
+ const existingBot = existingTags.find(t => t[0] === 'bot');
254
+ if (existingBot)
255
+ tags.push(existingBot);
194
256
  }
195
257
  return finalizeEvent({
196
258
  kind: 0,
197
259
  created_at: Math.floor(Date.now() / 1000),
198
260
  tags,
199
- content: JSON.stringify({
200
- name: profile.name,
201
- ...(profile.about && { about: profile.about }),
202
- ...(profile.nip05 && { nip05: profile.nip05 }),
203
- ...(profile.picture && { picture: profile.picture }),
204
- ...(profile.lud16 && { lud16: profile.lud16 }),
205
- }),
261
+ content: JSON.stringify(existing),
206
262
  }, sk);
207
263
  }
208
264
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentdex-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "CLI and SDK for the agentdex AI agent directory",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'url';
10
10
  import { dirname, join } from 'path';
11
11
  import { nip19 } from 'nostr-tools';
12
12
  import { AgentdexClient } from './client.js';
13
- import { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, createKind0Event, publishToRelays, createNote, updateKind0, generateAndSaveKeypair } from './nostr.js';
13
+ import { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, buildKind0Event, publishToRelays, createNote, updateKind0, generateAndSaveKeypair } from './nostr.js';
14
14
  import { payInvoice } from './nwc.js';
15
15
 
16
16
  const __filename = fileURLToPath(import.meta.url);
@@ -78,7 +78,7 @@ program
78
78
  .option('--owner-type <type>', 'Owner type: human, agent, org (sets owner_type tag on kind 31339)')
79
79
  .option('--parent <npub-or-hex>', 'Parent/orchestrator agent pubkey (npub or hex)')
80
80
  .option('--bot', 'Add ["bot"] tag to kind 0 profile (declares this pubkey as automated)')
81
- .option('--portfolio <entry>', 'Portfolio URL (format: "url,name,description") — repeatable', (val: string, acc: string[]) => [...acc, val], [])
81
+ .option('--portfolio <entry>', 'Portfolio entry (format: "id,url,label,description") — repeatable', (val: string, acc: string[]) => [...acc, val], [])
82
82
  .option('--skill <skill>', 'Skill tag (repeatable)', (val: string, acc: string[]) => [...acc, val], [])
83
83
  .option('--experience <exp>', 'Experience tag (repeatable)', (val: string, acc: string[]) => [...acc, val], [])
84
84
  .option('--nwc <uri>', 'Nostr Wallet Connect URI for auto-pay')
@@ -112,10 +112,16 @@ program
112
112
 
113
113
  const spinner = ora('Signing event...').start();
114
114
 
115
- // Parse portfolio entries ("url,name,description")
115
+ // Parse portfolio entries ("id,url,label,description")
116
116
  const portfolio = (options.portfolio || []).map((entry: string) => {
117
117
  const parts = entry.split(',').map((s: string) => s.trim());
118
- return { url: parts[0], name: parts[1], description: parts[2] };
118
+ // Support both: "id,url,label,desc" (new) and "url,label,desc" (old)
119
+ if (parts.length >= 2 && parts[1]?.startsWith('http')) {
120
+ return { id: parts[0], url: parts[1], name: parts[2], description: parts[3] };
121
+ }
122
+ // Old format fallback
123
+ const id = (parts[1] || parts[0]).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
124
+ return { id, url: parts[0], name: parts[1], description: parts[2] };
119
125
  });
120
126
 
121
127
  // Resolve owner pubkey hex from --owner flag (npub or hex)
@@ -227,17 +233,17 @@ program
227
233
  console.log('');
228
234
  console.log(chalk.gray(` Run ${chalk.white('agentdex claim <name>')} to get ${chalk.hex('#D4A574')('<name>@agentdex.id')}`));
229
235
 
230
- // Publish kind 0 profile (name, about, avatar, website, lud16)
236
+ // Publish kind 0 profile (fetch existing, merge, republish)
231
237
  const k0Spinner = ora('Publishing kind 0 profile to Nostr relays...').start();
232
238
  try {
233
- const kind0 = createKind0Event(sk, {
239
+ const kind0 = await buildKind0Event(sk, {
234
240
  name,
235
241
  about: description || undefined,
236
242
  picture: options.avatar || undefined,
237
243
  lud16: options.lightning || undefined,
238
244
  ownerPubkeyHex,
239
245
  bot: !!options.bot,
240
- });
246
+ }, relays);
241
247
  await publishToRelays(kind0, relays);
242
248
  k0Spinner.succeed('Kind 0 published — visible on all Nostr clients');
243
249
  } catch {
@@ -277,18 +283,18 @@ program
277
283
  console.log('');
278
284
  console.log(chalk.gray(` Run ${chalk.white('agentdex claim <name>')} to get ${chalk.hex('#D4A574')('<name>@agentdex.id')}`));
279
285
  console.log('');
280
- // Publish kind 0 profile (name, about, avatar, website, lud16)
286
+ // Publish kind 0 profile (fetch existing, merge, republish)
281
287
  // Kind 0 is canonical for basic profile; kind 31339 is agent-specific metadata
282
288
  const k0Spinner = ora('Publishing kind 0 profile to Nostr relays...').start();
283
289
  try {
284
- const kind0 = createKind0Event(sk, {
290
+ const kind0 = await buildKind0Event(sk, {
285
291
  name,
286
292
  about: description || undefined,
287
293
  picture: options.avatar || undefined,
288
294
  lud16: options.lightning || undefined,
289
295
  ownerPubkeyHex,
290
296
  bot: !!options.bot,
291
- });
297
+ }, relays);
292
298
  await publishToRelays(kind0, relays);
293
299
  k0Spinner.succeed('Kind 0 published — visible on all Nostr clients');
294
300
  } catch {
@@ -349,14 +355,14 @@ program
349
355
  if (!options.skipKind0) {
350
356
  const k0Spinner = ora('Publishing kind 0 profile to Nostr relays...').start();
351
357
  try {
352
- const kind0 = createKind0Event(sk, {
358
+ const relays = ['wss://nos.lol', 'wss://relay.damus.io', ...(options.relay || [])];
359
+ const kind0 = await buildKind0Event(sk, {
353
360
  name: claim.agent?.name || name,
354
361
  about: claim.agent?.description || undefined,
355
362
  picture: claim.agent?.avatarUrl || undefined,
356
363
  nip05: `${name}@agentdex.id`,
357
364
  lud16: options.lightning || undefined,
358
- });
359
- const relays = ['wss://nos.lol', 'wss://relay.damus.io', ...(options.relay || [])];
365
+ }, relays);
360
366
  const published = await publishToRelays(kind0, relays);
361
367
  k0Spinner.succeed(`Kind 0 published to ${published.join(', ')}`);
362
368
  console.log(chalk.gray(' NIP-05 will appear on njump/Damus/Primal once relays propagate (~30s)'));
@@ -419,8 +425,8 @@ program
419
425
  if (!options.skipKind0) {
420
426
  const k0Spinner = ora('Publishing kind 0 profile to Nostr relays...').start();
421
427
  try {
422
- const kind0 = createKind0Event(sk, { name, nip05: `${name}@agentdex.id` });
423
428
  const relays = ['wss://nos.lol', 'wss://relay.damus.io', ...(options.relay || [])];
429
+ const kind0 = await buildKind0Event(sk, { name, nip05: `${name}@agentdex.id` }, relays);
424
430
  await publishToRelays(kind0, relays);
425
431
  k0Spinner.succeed('Kind 0 published — NIP-05 active on all Nostr clients');
426
432
  } catch {
package/src/index.ts CHANGED
@@ -25,7 +25,7 @@ export {
25
25
  getNpub,
26
26
  getPubkeyHex,
27
27
  createProfileEvent,
28
- createKind0Event,
28
+ buildKind0Event,
29
29
  publishToRelays,
30
30
  createNote,
31
31
  } from './nostr.js';
package/src/nostr.ts CHANGED
@@ -10,6 +10,7 @@ import { mkdirSync, writeFileSync } from 'fs';
10
10
  const DEFAULT_RELAYS = ['wss://nos.lol', 'wss://relay.damus.io'];
11
11
 
12
12
  export interface PortfolioItem {
13
+ id: string;
13
14
  url: string;
14
15
  name?: string;
15
16
  description?: string;
@@ -115,7 +116,8 @@ export function createProfileEvent(sk: Uint8Array, profile: AgentProfile) {
115
116
  if (profile.messagingFee) tags.push(['messaging_fee', String(profile.messagingFee)]);
116
117
  if (profile.portfolio) {
117
118
  for (const item of profile.portfolio) {
118
- const tag = ['portfolio', item.url];
119
+ // New format: ["portfolio", id, url, label, description]
120
+ const tag = ['portfolio', item.id, item.url];
119
121
  if (item.name) tag.push(item.name);
120
122
  if (item.description) tag.push(item.description);
121
123
  tags.push(tag);
@@ -215,23 +217,82 @@ export async function updateKind0(sk: Uint8Array, updates: { lud16?: string }, r
215
217
  * After claiming a NIP-05 name, publish this to relays so Nostr clients
216
218
  * (njump, Damus, Primal) can verify the identity.
217
219
  */
218
- export function createKind0Event(sk: Uint8Array, profile: { name: string; about?: string; nip05?: string; picture?: string; lud16?: string; ownerPubkeyHex?: string; bot?: boolean }) {
220
+ /**
221
+ * Fetch existing kind 0, merge explicit updates, and return signed event.
222
+ * Safe for existing Nostr users — only overwrites fields explicitly passed.
223
+ */
224
+ export async function buildKind0Event(sk: Uint8Array, updates: {
225
+ name?: string;
226
+ about?: string;
227
+ nip05?: string;
228
+ picture?: string;
229
+ lud16?: string;
230
+ website?: string;
231
+ ownerPubkeyHex?: string;
232
+ bot?: boolean;
233
+ }, relays: string[] = DEFAULT_RELAYS) {
234
+ const pool = new SimplePool();
235
+ const pubkey = getPublicKey(sk);
236
+
237
+ // Fetch existing kind 0 from relays
238
+ let existing: Record<string, unknown> = {};
239
+ let existingTags: string[][] = [];
240
+ try {
241
+ const events = await Promise.race([
242
+ pool.querySync(relays, { kinds: [0], authors: [pubkey] }),
243
+ new Promise<any[]>((resolve) => setTimeout(() => resolve([]), 5000)),
244
+ ]);
245
+ if (events.length > 0) {
246
+ // Get newest event
247
+ const newest = events.reduce((a: any, b: any) => a.created_at > b.created_at ? a : b);
248
+ existing = JSON.parse(newest.content);
249
+ existingTags = newest.tags || [];
250
+ }
251
+ } catch {}
252
+ pool.close(relays);
253
+
254
+ // Merge content fields — only overwrite if explicitly provided (not undefined)
255
+ if (updates.name !== undefined) existing.name = updates.name;
256
+ if (updates.about !== undefined) existing.about = updates.about;
257
+ if (updates.nip05 !== undefined) existing.nip05 = updates.nip05;
258
+ if (updates.picture !== undefined) existing.picture = updates.picture;
259
+ if (updates.lud16 !== undefined) existing.lud16 = updates.lud16;
260
+ if (updates.website !== undefined) existing.website = updates.website;
261
+
262
+ // Merge tags
219
263
  const tags: string[][] = [];
220
- if (profile.bot) tags.push(["bot"]);
221
- if (profile.ownerPubkeyHex) {
222
- tags.push(["p", profile.ownerPubkeyHex, "", "owner"]);
264
+
265
+ // Preserve existing tags that we don't manage (e.g., user's own tags)
266
+ for (const tag of existingTags) {
267
+ // Skip tags we manage — we'll re-add them below
268
+ if (tag[0] === 'p' && tag[3] === 'owner') continue;
269
+ if (tag[0] === 'bot') continue;
270
+ tags.push(tag);
271
+ }
272
+
273
+ // Add/update owner p tag
274
+ if (updates.ownerPubkeyHex) {
275
+ tags.push(['p', updates.ownerPubkeyHex, '', 'owner']);
276
+ } else {
277
+ // Preserve existing owner p tag if we're not updating it
278
+ const existingOwner = existingTags.find(t => t[0] === 'p' && t[3] === 'owner');
279
+ if (existingOwner) tags.push(existingOwner);
280
+ }
281
+
282
+ // Add bot tag only if explicitly requested
283
+ if (updates.bot) {
284
+ tags.push(['bot']);
285
+ } else {
286
+ // Preserve existing bot tag if we're not changing it
287
+ const existingBot = existingTags.find(t => t[0] === 'bot');
288
+ if (existingBot) tags.push(existingBot);
223
289
  }
290
+
224
291
  return finalizeEvent({
225
292
  kind: 0,
226
293
  created_at: Math.floor(Date.now() / 1000),
227
294
  tags,
228
- content: JSON.stringify({
229
- name: profile.name,
230
- ...(profile.about && { about: profile.about }),
231
- ...(profile.nip05 && { nip05: profile.nip05 }),
232
- ...(profile.picture && { picture: profile.picture }),
233
- ...(profile.lud16 && { lud16: profile.lud16 }),
234
- }),
295
+ content: JSON.stringify(existing),
235
296
  }, sk);
236
297
  }
237
298