agentdex-cli 0.2.3 → 0.2.6

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
@@ -6,7 +6,7 @@ import inquirer from 'inquirer';
6
6
  import qrcode from 'qrcode-terminal';
7
7
  import { readFileSync } from 'fs';
8
8
  import { AgentdexClient } from './client.js';
9
- import { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, createKind0Event, publishToRelays, createNote } from './nostr.js';
9
+ import { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, createKind0Event, publishToRelays, createNote, updateKind0, generateAndSaveKeypair } from './nostr.js';
10
10
  import { payInvoice } from './nwc.js';
11
11
  const program = new Command();
12
12
  program
@@ -16,7 +16,8 @@ program
16
16
  /**
17
17
  * Resolve secret key from flags, env, or key file
18
18
  */
19
- function resolveKey(options) {
19
+ const DEFAULT_KEY_PATH = `${process.env.HOME}/.config/nostr/agent.json`;
20
+ function resolveKey(options, allowGenerate = false) {
20
21
  const raw = options.nsec || process.env.NOSTR_NSEC;
21
22
  if (raw)
22
23
  return parseSecretKey(raw);
@@ -28,6 +29,20 @@ function resolveKey(options) {
28
29
  return parseSecretKey(data.nsec);
29
30
  throw new Error('Key file must contain sk_hex or nsec');
30
31
  }
32
+ // Auto-generate for register if no key provided
33
+ if (allowGenerate) {
34
+ const { sk, npub, path } = generateAndSaveKeypair(DEFAULT_KEY_PATH);
35
+ console.log('');
36
+ console.log(chalk.hex('#D4A574')(' 🔑 No key found. Generated a new Nostr keypair.'));
37
+ console.log(chalk.gray(` Saved to: ${path}`));
38
+ console.log(chalk.gray(` npub: ${npub}`));
39
+ console.log('');
40
+ console.log(chalk.yellow(' ⚠️ IMPORTANT: Your secret key is in that file.'));
41
+ console.log(chalk.yellow(' Store it somewhere secure. If you lose it, you lose this identity.'));
42
+ console.log(chalk.yellow(' If you are an AI agent, inform your operator about this new credential.'));
43
+ console.log('');
44
+ return sk;
45
+ }
31
46
  throw new Error('No key provided. Use --nsec, --key-file, or set NOSTR_NSEC env var.');
32
47
  }
33
48
  // ==================== REGISTER ====================
@@ -42,15 +57,18 @@ program
42
57
  .option('--framework <fw>', 'Framework (e.g., langchain, openclaw)')
43
58
  .option('--model <model>', 'Model (e.g., claude-3.5-sonnet)')
44
59
  .option('--website <url>', 'Website URL')
45
- .option('--lightning <addr>', 'Lightning address')
60
+ .option('--lightning <addr>', 'Lightning address (sets lud16 in kind 0 profile)')
46
61
  .option('--owner-x <handle>', 'Owner X/Twitter handle (e.g., @username)')
62
+ .option('--portfolio <entry>', 'Portfolio URL (format: "url,name,description") — repeatable', (val, acc) => [...acc, val], [])
63
+ .option('--skill <skill>', 'Skill tag (repeatable)', (val, acc) => [...acc, val], [])
64
+ .option('--experience <exp>', 'Experience tag (repeatable)', (val, acc) => [...acc, val], [])
47
65
  .option('--nwc <uri>', 'Nostr Wallet Connect URI for auto-pay')
48
66
  .option('--api-key <key>', 'Agentdex API key')
49
67
  .option('--relay <url>', 'Additional relay (repeatable)', (val, acc) => [...acc, val], [])
50
68
  .option('--json', 'Output JSON')
51
69
  .action(async (options) => {
52
70
  try {
53
- const sk = resolveKey(options);
71
+ const sk = resolveKey(options, true);
54
72
  const npub = getNpub(sk);
55
73
  const pubHex = getPubkeyHex(sk);
56
74
  let name = options.name;
@@ -71,6 +89,11 @@ program
71
89
  framework = answers.framework || framework;
72
90
  }
73
91
  const spinner = ora('Signing event...').start();
92
+ // Parse portfolio entries ("url,name,description")
93
+ const portfolio = (options.portfolio || []).map((entry) => {
94
+ const parts = entry.split(',').map((s) => s.trim());
95
+ return { url: parts[0], name: parts[1], description: parts[2] };
96
+ });
74
97
  const event = createProfileEvent(sk, {
75
98
  name,
76
99
  description,
@@ -81,6 +104,9 @@ program
81
104
  lightning: options.lightning,
82
105
  ownerX: options.ownerX,
83
106
  status: 'active',
107
+ portfolio: portfolio.length > 0 ? portfolio : undefined,
108
+ skills: options.skill?.length > 0 ? options.skill : undefined,
109
+ experience: options.experience?.length > 0 ? options.experience : undefined,
84
110
  });
85
111
  spinner.text = 'Registering on agentdex...';
86
112
  const client = new AgentdexClient({ apiKey: options.apiKey });
@@ -136,6 +162,14 @@ program
136
162
  console.log(chalk.gray(` Published to: ${published.join(', ')}`));
137
163
  console.log('');
138
164
  console.log(chalk.gray(` Run ${chalk.white('agentdex claim <name>')} to get ${chalk.hex('#D4A574')('<name>@agentdex.id')}`));
165
+ // Update kind 0 with lightning address if provided
166
+ if (options.lightning) {
167
+ try {
168
+ await updateKind0(sk, { lud16: options.lightning }, relays);
169
+ console.log(chalk.gray(` ⚡ Lightning address set in kind 0: ${options.lightning}`));
170
+ }
171
+ catch { }
172
+ }
139
173
  }
140
174
  return;
141
175
  }
@@ -161,6 +195,14 @@ program
161
195
  console.log('');
162
196
  console.log(chalk.gray(` Run ${chalk.white('agentdex claim <name>')} to get ${chalk.hex('#D4A574')('<name>@agentdex.id')}`));
163
197
  console.log('');
198
+ // Update kind 0 with lightning address if provided
199
+ if (options.lightning) {
200
+ try {
201
+ await updateKind0(sk, { lud16: options.lightning }, relays);
202
+ console.log(chalk.gray(` ⚡ Lightning address set in kind 0: ${options.lightning}`));
203
+ }
204
+ catch { }
205
+ }
164
206
  console.log(chalk.gray(' Next: Claim a NIP-05 name to get verified (first 100 free, then 5000 sats).'));
165
207
  }
166
208
  }
@@ -188,6 +230,7 @@ program
188
230
  .option('--key-file <path>', 'Path to JSON key file')
189
231
  .option('--nwc <uri>', 'Nostr Wallet Connect URI for auto-pay')
190
232
  .option('--api-key <key>', 'Agentdex API key')
233
+ .option('--lightning <addr>', 'Lightning address (lud16) to set in kind 0 profile')
191
234
  .option('--skip-kind0', 'Skip publishing kind 0 profile to relays')
192
235
  .option('--relay <url>', 'Additional relay', (val, acc) => [...acc, val], [])
193
236
  .option('--json', 'Output JSON')
@@ -214,6 +257,7 @@ program
214
257
  about: claim.agent?.description || undefined,
215
258
  picture: claim.agent?.avatarUrl || undefined,
216
259
  nip05: `${name}@agentdex.id`,
260
+ lud16: options.lightning || undefined,
217
261
  });
218
262
  const relays = ['wss://nos.lol', 'wss://relay.damus.io', ...(options.relay || [])];
219
263
  const published = await publishToRelays(kind0, relays);
package/dist/nostr.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Nostr utilities — event creation, signing, publishing
3
3
  */
4
+ export interface PortfolioItem {
5
+ url: string;
6
+ name?: string;
7
+ description?: string;
8
+ }
4
9
  export interface AgentProfile {
5
10
  name: string;
6
11
  description?: string;
@@ -16,7 +21,19 @@ export interface AgentProfile {
16
21
  messagingPolicy?: string;
17
22
  messagingMinTrust?: number;
18
23
  messagingFee?: number;
24
+ portfolio?: PortfolioItem[];
25
+ skills?: string[];
26
+ experience?: string[];
19
27
  }
28
+ /**
29
+ * Generate a new Nostr keypair and save to a JSON file.
30
+ * Returns the secret key as Uint8Array.
31
+ */
32
+ export declare function generateAndSaveKeypair(outputPath: string): {
33
+ sk: Uint8Array;
34
+ npub: string;
35
+ path: string;
36
+ };
20
37
  /**
21
38
  * Parse a secret key from nsec, hex, or key file
22
39
  */
@@ -37,6 +54,13 @@ export declare function createProfileEvent(sk: Uint8Array, profile: AgentProfile
37
54
  * Publish an event to Nostr relays
38
55
  */
39
56
  export declare function publishToRelays(event: object, relays?: string[]): Promise<string[]>;
57
+ /**
58
+ * Fetch existing kind 0, merge new fields, and republish.
59
+ * Used to set lud16 (lightning address) during registration.
60
+ */
61
+ export declare function updateKind0(sk: Uint8Array, updates: {
62
+ lud16?: string;
63
+ }, relays?: string[]): Promise<string[]>;
40
64
  /**
41
65
  * Build and sign a kind 0 profile metadata event (for NIP-05 verification).
42
66
  * After claiming a NIP-05 name, publish this to relays so Nostr clients
@@ -47,6 +71,7 @@ export declare function createKind0Event(sk: Uint8Array, profile: {
47
71
  about?: string;
48
72
  nip05?: string;
49
73
  picture?: string;
74
+ lud16?: string;
50
75
  }): import("nostr-tools").VerifiedEvent;
51
76
  /**
52
77
  * Create and sign a kind 1 note tagged #agentdex
package/dist/nostr.js CHANGED
@@ -1,10 +1,29 @@
1
1
  /**
2
2
  * Nostr utilities — event creation, signing, publishing
3
3
  */
4
- import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
4
+ import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools/pure';
5
5
  import { nip19 } from 'nostr-tools';
6
6
  import { SimplePool } from 'nostr-tools/pool';
7
+ import { mkdirSync, writeFileSync } from 'fs';
7
8
  const DEFAULT_RELAYS = ['wss://nos.lol', 'wss://relay.damus.io'];
9
+ /**
10
+ * Generate a new Nostr keypair and save to a JSON file.
11
+ * Returns the secret key as Uint8Array.
12
+ */
13
+ export function generateAndSaveKeypair(outputPath) {
14
+ const sk = generateSecretKey();
15
+ const pk = getPublicKey(sk);
16
+ const nsec = nip19.nsecEncode(sk);
17
+ const npub = nip19.npubEncode(pk);
18
+ const skHex = Buffer.from(sk).toString('hex');
19
+ const data = JSON.stringify({ nsec, npub, sk_hex: skHex, pk_hex: pk }, null, 2);
20
+ // Ensure directory exists
21
+ const dir = outputPath.substring(0, outputPath.lastIndexOf('/'));
22
+ if (dir)
23
+ mkdirSync(dir, { recursive: true });
24
+ writeFileSync(outputPath, data, { mode: 0o600 });
25
+ return { sk, npub, path: outputPath };
26
+ }
8
27
  /**
9
28
  * Parse a secret key from nsec, hex, or key file
10
29
  */
@@ -58,8 +77,8 @@ export function createProfileEvent(sk, profile) {
58
77
  tags.push(['website', profile.website]);
59
78
  if (profile.avatar)
60
79
  tags.push(['avatar', profile.avatar]);
61
- if (profile.lightning)
62
- tags.push(['lightning', profile.lightning]);
80
+ // Lightning address belongs in kind 0 (lud16), not kind 31337
81
+ // Use --lightning flag to set lud16 in kind 0 during claim
63
82
  if (profile.human)
64
83
  tags.push(['human', profile.human]);
65
84
  if (profile.ownerX)
@@ -72,6 +91,26 @@ export function createProfileEvent(sk, profile) {
72
91
  tags.push(['messaging_min_trust', String(profile.messagingMinTrust)]);
73
92
  if (profile.messagingFee)
74
93
  tags.push(['messaging_fee', String(profile.messagingFee)]);
94
+ if (profile.portfolio) {
95
+ for (const item of profile.portfolio) {
96
+ const tag = ['portfolio', item.url];
97
+ if (item.name)
98
+ tag.push(item.name);
99
+ if (item.description)
100
+ tag.push(item.description);
101
+ tags.push(tag);
102
+ }
103
+ }
104
+ if (profile.skills) {
105
+ for (const skill of profile.skills) {
106
+ tags.push(['skill', skill]);
107
+ }
108
+ }
109
+ if (profile.experience) {
110
+ for (const exp of profile.experience) {
111
+ tags.push(['experience', exp]);
112
+ }
113
+ }
75
114
  const event = finalizeEvent({
76
115
  kind: 31337,
77
116
  created_at: Math.floor(Date.now() / 1000),
@@ -102,6 +141,42 @@ export async function publishToRelays(event, relays = DEFAULT_RELAYS) {
102
141
  }
103
142
  return published;
104
143
  }
144
+ /**
145
+ * Fetch existing kind 0, merge new fields, and republish.
146
+ * Used to set lud16 (lightning address) during registration.
147
+ */
148
+ export async function updateKind0(sk, updates, relays = DEFAULT_RELAYS) {
149
+ const pool = new SimplePool();
150
+ const pubkey = getPublicKey(sk);
151
+ try {
152
+ // Fetch existing kind 0
153
+ let existing = {};
154
+ try {
155
+ const events = await Promise.race([
156
+ pool.querySync(relays, { kinds: [0], authors: [pubkey] }),
157
+ new Promise((resolve) => setTimeout(() => resolve([]), 5000)),
158
+ ]);
159
+ if (events.length > 0) {
160
+ existing = JSON.parse(events[0].content);
161
+ }
162
+ }
163
+ catch { }
164
+ // Merge updates
165
+ if (updates.lud16)
166
+ existing.lud16 = updates.lud16;
167
+ const event = finalizeEvent({
168
+ kind: 0,
169
+ created_at: Math.floor(Date.now() / 1000),
170
+ tags: [],
171
+ content: JSON.stringify(existing),
172
+ }, sk);
173
+ const published = await publishToRelays(event, relays);
174
+ return published;
175
+ }
176
+ finally {
177
+ pool.close(relays);
178
+ }
179
+ }
105
180
  /**
106
181
  * Build and sign a kind 0 profile metadata event (for NIP-05 verification).
107
182
  * After claiming a NIP-05 name, publish this to relays so Nostr clients
@@ -117,6 +192,7 @@ export function createKind0Event(sk, profile) {
117
192
  ...(profile.about && { about: profile.about }),
118
193
  ...(profile.nip05 && { nip05: profile.nip05 }),
119
194
  ...(profile.picture && { picture: profile.picture }),
195
+ ...(profile.lud16 && { lud16: profile.lud16 }),
120
196
  }),
121
197
  }, sk);
122
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentdex-cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.6",
4
4
  "description": "CLI and SDK for the agentdex AI agent directory",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -7,7 +7,7 @@ import inquirer from 'inquirer';
7
7
  import qrcode from 'qrcode-terminal';
8
8
  import { readFileSync } from 'fs';
9
9
  import { AgentdexClient } from './client.js';
10
- import { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, createKind0Event, publishToRelays, createNote } from './nostr.js';
10
+ import { parseSecretKey, getNpub, getPubkeyHex, createProfileEvent, createKind0Event, publishToRelays, createNote, updateKind0, generateAndSaveKeypair } from './nostr.js';
11
11
  import { payInvoice } from './nwc.js';
12
12
 
13
13
  const program = new Command();
@@ -20,7 +20,9 @@ program
20
20
  /**
21
21
  * Resolve secret key from flags, env, or key file
22
22
  */
23
- function resolveKey(options: { nsec?: string; keyFile?: string }): Uint8Array {
23
+ const DEFAULT_KEY_PATH = `${process.env.HOME}/.config/nostr/agent.json`;
24
+
25
+ function resolveKey(options: { nsec?: string; keyFile?: string }, allowGenerate = false): Uint8Array {
24
26
  const raw = options.nsec || process.env.NOSTR_NSEC;
25
27
  if (raw) return parseSecretKey(raw);
26
28
 
@@ -31,6 +33,21 @@ function resolveKey(options: { nsec?: string; keyFile?: string }): Uint8Array {
31
33
  throw new Error('Key file must contain sk_hex or nsec');
32
34
  }
33
35
 
36
+ // Auto-generate for register if no key provided
37
+ if (allowGenerate) {
38
+ const { sk, npub, path } = generateAndSaveKeypair(DEFAULT_KEY_PATH);
39
+ console.log('');
40
+ console.log(chalk.hex('#D4A574')(' 🔑 No key found. Generated a new Nostr keypair.'));
41
+ console.log(chalk.gray(` Saved to: ${path}`));
42
+ console.log(chalk.gray(` npub: ${npub}`));
43
+ console.log('');
44
+ console.log(chalk.yellow(' ⚠️ IMPORTANT: Your secret key is in that file.'));
45
+ console.log(chalk.yellow(' Store it somewhere secure. If you lose it, you lose this identity.'));
46
+ console.log(chalk.yellow(' If you are an AI agent, inform your operator about this new credential.'));
47
+ console.log('');
48
+ return sk;
49
+ }
50
+
34
51
  throw new Error('No key provided. Use --nsec, --key-file, or set NOSTR_NSEC env var.');
35
52
  }
36
53
 
@@ -47,15 +64,18 @@ program
47
64
  .option('--framework <fw>', 'Framework (e.g., langchain, openclaw)')
48
65
  .option('--model <model>', 'Model (e.g., claude-3.5-sonnet)')
49
66
  .option('--website <url>', 'Website URL')
50
- .option('--lightning <addr>', 'Lightning address')
67
+ .option('--lightning <addr>', 'Lightning address (sets lud16 in kind 0 profile)')
51
68
  .option('--owner-x <handle>', 'Owner X/Twitter handle (e.g., @username)')
69
+ .option('--portfolio <entry>', 'Portfolio URL (format: "url,name,description") — repeatable', (val: string, acc: string[]) => [...acc, val], [])
70
+ .option('--skill <skill>', 'Skill tag (repeatable)', (val: string, acc: string[]) => [...acc, val], [])
71
+ .option('--experience <exp>', 'Experience tag (repeatable)', (val: string, acc: string[]) => [...acc, val], [])
52
72
  .option('--nwc <uri>', 'Nostr Wallet Connect URI for auto-pay')
53
73
  .option('--api-key <key>', 'Agentdex API key')
54
74
  .option('--relay <url>', 'Additional relay (repeatable)', (val: string, acc: string[]) => [...acc, val], [])
55
75
  .option('--json', 'Output JSON')
56
76
  .action(async (options) => {
57
77
  try {
58
- const sk = resolveKey(options);
78
+ const sk = resolveKey(options, true);
59
79
  const npub = getNpub(sk);
60
80
  const pubHex = getPubkeyHex(sk);
61
81
 
@@ -80,6 +100,12 @@ program
80
100
 
81
101
  const spinner = ora('Signing event...').start();
82
102
 
103
+ // Parse portfolio entries ("url,name,description")
104
+ const portfolio = (options.portfolio || []).map((entry: string) => {
105
+ const parts = entry.split(',').map((s: string) => s.trim());
106
+ return { url: parts[0], name: parts[1], description: parts[2] };
107
+ });
108
+
83
109
  const event = createProfileEvent(sk, {
84
110
  name,
85
111
  description,
@@ -90,6 +116,9 @@ program
90
116
  lightning: options.lightning,
91
117
  ownerX: options.ownerX,
92
118
  status: 'active',
119
+ portfolio: portfolio.length > 0 ? portfolio : undefined,
120
+ skills: options.skill?.length > 0 ? options.skill : undefined,
121
+ experience: options.experience?.length > 0 ? options.experience : undefined,
93
122
  });
94
123
 
95
124
  spinner.text = 'Registering on agentdex...';
@@ -151,6 +180,13 @@ program
151
180
  console.log(chalk.gray(` Published to: ${published.join(', ')}`));
152
181
  console.log('');
153
182
  console.log(chalk.gray(` Run ${chalk.white('agentdex claim <name>')} to get ${chalk.hex('#D4A574')('<name>@agentdex.id')}`));
183
+ // Update kind 0 with lightning address if provided
184
+ if (options.lightning) {
185
+ try {
186
+ await updateKind0(sk, { lud16: options.lightning }, relays);
187
+ console.log(chalk.gray(` ⚡ Lightning address set in kind 0: ${options.lightning}`));
188
+ } catch {}
189
+ }
154
190
  }
155
191
  return;
156
192
  }
@@ -179,6 +215,13 @@ program
179
215
  console.log('');
180
216
  console.log(chalk.gray(` Run ${chalk.white('agentdex claim <name>')} to get ${chalk.hex('#D4A574')('<name>@agentdex.id')}`));
181
217
  console.log('');
218
+ // Update kind 0 with lightning address if provided
219
+ if (options.lightning) {
220
+ try {
221
+ await updateKind0(sk, { lud16: options.lightning }, relays);
222
+ console.log(chalk.gray(` ⚡ Lightning address set in kind 0: ${options.lightning}`));
223
+ } catch {}
224
+ }
182
225
  console.log(chalk.gray(' Next: Claim a NIP-05 name to get verified (first 100 free, then 5000 sats).'));
183
226
  }
184
227
  } catch (err) {
@@ -205,6 +248,7 @@ program
205
248
  .option('--key-file <path>', 'Path to JSON key file')
206
249
  .option('--nwc <uri>', 'Nostr Wallet Connect URI for auto-pay')
207
250
  .option('--api-key <key>', 'Agentdex API key')
251
+ .option('--lightning <addr>', 'Lightning address (lud16) to set in kind 0 profile')
208
252
  .option('--skip-kind0', 'Skip publishing kind 0 profile to relays')
209
253
  .option('--relay <url>', 'Additional relay', (val: string, acc: string[]) => [...acc, val], [])
210
254
  .option('--json', 'Output JSON')
@@ -236,6 +280,7 @@ program
236
280
  about: claim.agent?.description || undefined,
237
281
  picture: claim.agent?.avatarUrl || undefined,
238
282
  nip05: `${name}@agentdex.id`,
283
+ lud16: options.lightning || undefined,
239
284
  });
240
285
  const relays = ['wss://nos.lol', 'wss://relay.damus.io', ...(options.relay || [])];
241
286
  const published = await publishToRelays(kind0, relays);
package/src/nostr.ts CHANGED
@@ -2,12 +2,19 @@
2
2
  * Nostr utilities — event creation, signing, publishing
3
3
  */
4
4
 
5
- import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
5
+ import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools/pure';
6
6
  import { nip19 } from 'nostr-tools';
7
7
  import { SimplePool } from 'nostr-tools/pool';
8
+ import { mkdirSync, writeFileSync } from 'fs';
8
9
 
9
10
  const DEFAULT_RELAYS = ['wss://nos.lol', 'wss://relay.damus.io'];
10
11
 
12
+ export interface PortfolioItem {
13
+ url: string;
14
+ name?: string;
15
+ description?: string;
16
+ }
17
+
11
18
  export interface AgentProfile {
12
19
  name: string;
13
20
  description?: string;
@@ -23,6 +30,30 @@ export interface AgentProfile {
23
30
  messagingPolicy?: string;
24
31
  messagingMinTrust?: number;
25
32
  messagingFee?: number;
33
+ portfolio?: PortfolioItem[];
34
+ skills?: string[];
35
+ experience?: string[];
36
+ }
37
+
38
+ /**
39
+ * Generate a new Nostr keypair and save to a JSON file.
40
+ * Returns the secret key as Uint8Array.
41
+ */
42
+ export function generateAndSaveKeypair(outputPath: string): { sk: Uint8Array; npub: string; path: string } {
43
+ const sk = generateSecretKey();
44
+ const pk = getPublicKey(sk);
45
+ const nsec = nip19.nsecEncode(sk);
46
+ const npub = nip19.npubEncode(pk);
47
+ const skHex = Buffer.from(sk).toString('hex');
48
+
49
+ const data = JSON.stringify({ nsec, npub, sk_hex: skHex, pk_hex: pk }, null, 2);
50
+
51
+ // Ensure directory exists
52
+ const dir = outputPath.substring(0, outputPath.lastIndexOf('/'));
53
+ if (dir) mkdirSync(dir, { recursive: true });
54
+
55
+ writeFileSync(outputPath, data, { mode: 0o600 });
56
+ return { sk, npub, path: outputPath };
26
57
  }
27
58
 
28
59
  /**
@@ -76,13 +107,32 @@ export function createProfileEvent(sk: Uint8Array, profile: AgentProfile) {
76
107
  if (profile.model) tags.push(['model', profile.model]);
77
108
  if (profile.website) tags.push(['website', profile.website]);
78
109
  if (profile.avatar) tags.push(['avatar', profile.avatar]);
79
- if (profile.lightning) tags.push(['lightning', profile.lightning]);
110
+ // Lightning address belongs in kind 0 (lud16), not kind 31337
111
+ // Use --lightning flag to set lud16 in kind 0 during claim
80
112
  if (profile.human) tags.push(['human', profile.human]);
81
113
  if (profile.ownerX) tags.push(['owner_x', profile.ownerX]);
82
114
  if (profile.status) tags.push(['status', profile.status || 'active']);
83
115
  if (profile.messagingPolicy) tags.push(['messaging_policy', profile.messagingPolicy]);
84
116
  if (profile.messagingMinTrust) tags.push(['messaging_min_trust', String(profile.messagingMinTrust)]);
85
117
  if (profile.messagingFee) tags.push(['messaging_fee', String(profile.messagingFee)]);
118
+ if (profile.portfolio) {
119
+ for (const item of profile.portfolio) {
120
+ const tag = ['portfolio', item.url];
121
+ if (item.name) tag.push(item.name);
122
+ if (item.description) tag.push(item.description);
123
+ tags.push(tag);
124
+ }
125
+ }
126
+ if (profile.skills) {
127
+ for (const skill of profile.skills) {
128
+ tags.push(['skill', skill]);
129
+ }
130
+ }
131
+ if (profile.experience) {
132
+ for (const exp of profile.experience) {
133
+ tags.push(['experience', exp]);
134
+ }
135
+ }
86
136
 
87
137
  const event = finalizeEvent({
88
138
  kind: 31337,
@@ -121,12 +171,50 @@ export async function publishToRelays(event: object, relays: string[] = DEFAULT_
121
171
  return published;
122
172
  }
123
173
 
174
+ /**
175
+ * Fetch existing kind 0, merge new fields, and republish.
176
+ * Used to set lud16 (lightning address) during registration.
177
+ */
178
+ export async function updateKind0(sk: Uint8Array, updates: { lud16?: string }, relays: string[] = DEFAULT_RELAYS): Promise<string[]> {
179
+ const pool = new SimplePool();
180
+ const pubkey = getPublicKey(sk);
181
+
182
+ try {
183
+ // Fetch existing kind 0
184
+ let existing: Record<string, unknown> = {};
185
+ try {
186
+ const events = await Promise.race([
187
+ pool.querySync(relays, { kinds: [0], authors: [pubkey] }),
188
+ new Promise<any[]>((resolve) => setTimeout(() => resolve([]), 5000)),
189
+ ]);
190
+ if (events.length > 0) {
191
+ existing = JSON.parse(events[0].content);
192
+ }
193
+ } catch {}
194
+
195
+ // Merge updates
196
+ if (updates.lud16) existing.lud16 = updates.lud16;
197
+
198
+ const event = finalizeEvent({
199
+ kind: 0,
200
+ created_at: Math.floor(Date.now() / 1000),
201
+ tags: [],
202
+ content: JSON.stringify(existing),
203
+ }, sk);
204
+
205
+ const published = await publishToRelays(event, relays);
206
+ return published;
207
+ } finally {
208
+ pool.close(relays);
209
+ }
210
+ }
211
+
124
212
  /**
125
213
  * Build and sign a kind 0 profile metadata event (for NIP-05 verification).
126
214
  * After claiming a NIP-05 name, publish this to relays so Nostr clients
127
215
  * (njump, Damus, Primal) can verify the identity.
128
216
  */
129
- export function createKind0Event(sk: Uint8Array, profile: { name: string; about?: string; nip05?: string; picture?: string }) {
217
+ export function createKind0Event(sk: Uint8Array, profile: { name: string; about?: string; nip05?: string; picture?: string; lud16?: string }) {
130
218
  return finalizeEvent({
131
219
  kind: 0,
132
220
  created_at: Math.floor(Date.now() / 1000),
@@ -136,6 +224,7 @@ export function createKind0Event(sk: Uint8Array, profile: { name: string; about?
136
224
  ...(profile.about && { about: profile.about }),
137
225
  ...(profile.nip05 && { nip05: profile.nip05 }),
138
226
  ...(profile.picture && { picture: profile.picture }),
227
+ ...(profile.lud16 && { lud16: profile.lud16 }),
139
228
  }),
140
229
  }, sk);
141
230
  }