agent-bootstrap 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeletor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # agent-bootstrap
2
+
3
+ One command to bootstrap an AI agent with Nostr identity, Lightning wallet, ai.wot trust, and DVM service announcement.
4
+
5
+ The "create-react-app" for the agent economy.
6
+
7
+ ## What it does
8
+
9
+ ```
10
+ agent-bootstrap init --name "My Agent" --nwc "nostr+walletconnect://..." --dvm
11
+ ```
12
+
13
+ In one command:
14
+ 1. šŸ”‘ **Generates Nostr keys** (nsec/npub) with secure file permissions (600)
15
+ 2. šŸ‘¤ **Publishes profile** to 4 relays (damus, nos.lol, primal, snort)
16
+ 3. ⚔ **Configures Lightning wallet** via NWC (Nostr Wallet Connect)
17
+ 4. šŸ¤ **Sets up ai.wot trust** — ready to receive attestations
18
+ 5. šŸ¤– **Registers as a DVM** (kind 31990 service announcement)
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install -g agent-bootstrap
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Bootstrap a new agent
29
+
30
+ ```bash
31
+ # Minimal — just identity
32
+ agent-bootstrap init --name "My Agent"
33
+
34
+ # With wallet
35
+ agent-bootstrap init --name "My Agent" --nwc "nostr+walletconnect://relay.example?secret=..."
36
+
37
+ # Full stack — identity + wallet + DVM
38
+ agent-bootstrap init --name "My Agent" \
39
+ --about "A helpful AI agent" \
40
+ --nwc "nostr+walletconnect://relay.example?secret=..." \
41
+ --dvm \
42
+ --dvm-price 21
43
+
44
+ # Custom relays
45
+ agent-bootstrap init --name "My Agent" \
46
+ --relays "wss://relay.damus.io,wss://nos.lol"
47
+
48
+ # Generate keys without publishing
49
+ agent-bootstrap init --name "My Agent" --skip-publish
50
+ ```
51
+
52
+ ### Check status
53
+
54
+ ```bash
55
+ agent-bootstrap status ./agent-identity
56
+ ```
57
+
58
+ ### Verify everything works
59
+
60
+ ```bash
61
+ agent-bootstrap verify ./agent-identity
62
+ ```
63
+
64
+ Checks:
65
+ - āœ… Keys exist and are valid
66
+ - āœ… Key file permissions are secure (600)
67
+ - āœ… Profile found on relays
68
+ - āœ… Wallet config present
69
+ - āœ… Trust score reachable via ai.wot API
70
+
71
+ ### JSON output
72
+
73
+ ```bash
74
+ agent-bootstrap init --name "My Agent" --json
75
+ agent-bootstrap status ./agent-identity --json
76
+ ```
77
+
78
+ ## What it creates
79
+
80
+ ```
81
+ agent-identity/
82
+ ā”œā”€ā”€ nostr-keys.json # Nostr keypair (600 permissions)
83
+ ā”œā”€ā”€ wallet-config.json # NWC connection (600 permissions)
84
+ └── bootstrap-manifest.json # Full bootstrap record
85
+ ```
86
+
87
+ ## Programmatic API
88
+
89
+ ```javascript
90
+ const { bootstrap, status, verify, generateKeys } = require('agent-bootstrap');
91
+
92
+ // Generate keys only
93
+ const keys = generateKeys();
94
+ console.log(keys.npub, keys.pubkey);
95
+
96
+ // Full bootstrap
97
+ const result = await bootstrap({
98
+ name: 'My Agent',
99
+ about: 'A helpful AI',
100
+ nwcUrl: 'nostr+walletconnect://...',
101
+ dir: './my-agent',
102
+ relays: ['wss://relay.damus.io', 'wss://nos.lol'],
103
+ dvm: true,
104
+ dvmKind: 5050,
105
+ dvmPrice: 21,
106
+ });
107
+
108
+ // Check status
109
+ const s = await status('./my-agent');
110
+
111
+ // Verify
112
+ const v = await verify('./my-agent');
113
+ console.log(`${v.passed}/${v.total} checks passed`);
114
+ ```
115
+
116
+ ## The Agent Economy Stack
117
+
118
+ agent-bootstrap is the entry point. Once bootstrapped, your agent can use:
119
+
120
+ | Package | Purpose |
121
+ |---------|---------|
122
+ | [ai-wot](https://github.com/jeletor/ai-wot) | Trust — earn reputation through attestations |
123
+ | [agent-discovery](https://github.com/jeletor/agent-discovery) | Discovery — publish and find agent services |
124
+ | [lightning-agent](https://github.com/jeletor/lightning-agent) | Payments — send and receive Lightning |
125
+ | [login-with-lightning](https://github.com/jeletor/login-with-lightning) | Auth — LNURL-auth for agents |
126
+ | [lightning-toll](https://github.com/jeletor/lightning-toll) | Monetize — L402 paywalls for APIs |
127
+ | [agent-escrow](https://github.com/jeletor/agent-escrow) | Escrow — safe agent-to-agent transactions |
128
+ | [agent-test-kit](https://github.com/jeletor/agent-test-kit) | Testing — mock relays and wallets |
129
+
130
+ The flow: **bootstrap** → discover → trust → pay → deliver → attest → repeat.
131
+
132
+ ## Dependencies
133
+
134
+ - `nostr-tools` — Nostr key generation and event signing
135
+ - `ws` — WebSocket for relay connections
136
+
137
+ Zero other dependencies.
138
+
139
+ ## License
140
+
141
+ MIT
142
+
143
+ ## Author
144
+
145
+ Built by [Jeletor](https://jeletor.com) šŸŒ€
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "agent-bootstrap",
3
+ "version": "0.1.0",
4
+ "description": "One command to bootstrap an AI agent with Nostr identity, Lightning wallet, ai.wot trust, DVM announcement, and monitoring",
5
+ "main": "src/index.cjs",
6
+ "bin": {
7
+ "agent-bootstrap": "src/cli.cjs"
8
+ },
9
+ "scripts": {
10
+ "test": "node test/run.cjs"
11
+ },
12
+ "keywords": [
13
+ "ai",
14
+ "agent",
15
+ "nostr",
16
+ "lightning",
17
+ "bootstrap",
18
+ "identity",
19
+ "wallet",
20
+ "trust",
21
+ "dvm"
22
+ ],
23
+ "author": "Jeletor <jeletor@jeletor.com>",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "nostr-tools": "^2.11.0",
27
+ "ws": "^8.18.0"
28
+ }
29
+ }
package/src/cli.cjs ADDED
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ // agent-bootstrap CLI — one command to set up an AI agent on Nostr + Lightning
3
+
4
+ const { bootstrap, status, verify } = require('./index.cjs');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+
8
+ const HELP = `
9
+ agent-bootstrap — Set up an AI agent with identity, wallet, trust, and services
10
+
11
+ Usage:
12
+ agent-bootstrap init [options] Bootstrap a new agent
13
+ agent-bootstrap status [dir] Check bootstrap status
14
+ agent-bootstrap verify [dir] Verify all components work
15
+ agent-bootstrap help Show this help
16
+
17
+ Init options:
18
+ --name <name> Agent display name (required)
19
+ --about <bio> Agent bio/description
20
+ --nwc <url> NWC connection string (for Lightning wallet)
21
+ --dir <path> Output directory (default: ./agent-identity)
22
+ --relays <urls> Comma-separated relay URLs
23
+ --dvm Register as a DVM (kind 5050 text generation)
24
+ --dvm-kind <kind> DVM kind number (default: 5050)
25
+ --dvm-price <sats> DVM price in sats (default: 21)
26
+ --skip-publish Generate keys but don't publish to relays
27
+ --json Output results as JSON
28
+
29
+ Examples:
30
+ agent-bootstrap init --name "My Agent" --about "A helpful AI"
31
+ agent-bootstrap init --name "My Agent" --nwc "nostr+walletconnect://..." --dvm
32
+ agent-bootstrap status ./agent-identity
33
+ agent-bootstrap verify ./agent-identity
34
+ `;
35
+
36
+ async function main() {
37
+ const args = process.argv.slice(2);
38
+ const cmd = args[0];
39
+
40
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
41
+ console.log(HELP);
42
+ process.exit(0);
43
+ }
44
+
45
+ // Parse flags
46
+ function getFlag(name) {
47
+ const idx = args.indexOf('--' + name);
48
+ if (idx === -1) return undefined;
49
+ return args[idx + 1];
50
+ }
51
+ function hasFlag(name) {
52
+ return args.includes('--' + name);
53
+ }
54
+
55
+ if (cmd === 'init') {
56
+ const name = getFlag('name');
57
+ if (!name) {
58
+ console.error('Error: --name is required');
59
+ process.exit(1);
60
+ }
61
+
62
+ const opts = {
63
+ name,
64
+ about: getFlag('about') || `${name} — AI agent on Nostr`,
65
+ nwcUrl: getFlag('nwc'),
66
+ dir: getFlag('dir') || './agent-identity',
67
+ relays: getFlag('relays')
68
+ ? getFlag('relays').split(',').map(s => s.trim())
69
+ : ['wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net', 'wss://relay.snort.social'],
70
+ dvm: hasFlag('dvm'),
71
+ dvmKind: parseInt(getFlag('dvm-kind') || '5050', 10),
72
+ dvmPrice: parseInt(getFlag('dvm-price') || '21', 10),
73
+ skipPublish: hasFlag('skip-publish'),
74
+ json: hasFlag('json'),
75
+ };
76
+
77
+ try {
78
+ const result = await bootstrap(opts);
79
+ if (opts.json) {
80
+ console.log(JSON.stringify(result, null, 2));
81
+ } else {
82
+ printResult(result);
83
+ }
84
+ } catch (err) {
85
+ console.error('Bootstrap failed:', err.message);
86
+ process.exit(1);
87
+ }
88
+ } else if (cmd === 'status') {
89
+ const dir = args[1] || './agent-identity';
90
+ const result = await status(dir);
91
+ if (hasFlag('json')) {
92
+ console.log(JSON.stringify(result, null, 2));
93
+ } else {
94
+ printStatus(result);
95
+ }
96
+ } else if (cmd === 'verify') {
97
+ const dir = args[1] || './agent-identity';
98
+ const result = await verify(dir);
99
+ if (hasFlag('json')) {
100
+ console.log(JSON.stringify(result, null, 2));
101
+ } else {
102
+ printVerify(result);
103
+ }
104
+ } else {
105
+ console.error(`Unknown command: ${cmd}`);
106
+ console.log(HELP);
107
+ process.exit(1);
108
+ }
109
+ }
110
+
111
+ function printResult(r) {
112
+ console.log('');
113
+ console.log('╔══════════════════════════════════════════════════════╗');
114
+ console.log('ā•‘ agent-bootstrap — Setup Complete ā•‘');
115
+ console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
116
+ console.log('');
117
+ console.log(' šŸ”‘ Identity');
118
+ console.log(` Name: ${r.identity.name}`);
119
+ console.log(` npub: ${r.identity.npub}`);
120
+ console.log(` Pubkey: ${r.identity.pubkeyHex}`);
121
+ console.log(` Keys: ${r.identity.keysFile}`);
122
+ console.log('');
123
+
124
+ if (r.profile) {
125
+ console.log(' šŸ‘¤ Profile');
126
+ console.log(` Published to ${r.profile.relays} relay(s)`);
127
+ console.log('');
128
+ }
129
+
130
+ if (r.wallet) {
131
+ console.log(' ⚔ Wallet');
132
+ console.log(` NWC: Connected`);
133
+ console.log(` Config: ${r.wallet.configFile}`);
134
+ console.log('');
135
+ }
136
+
137
+ if (r.trust) {
138
+ console.log(' šŸ¤ Trust (ai.wot)');
139
+ console.log(` Ready for attestations`);
140
+ console.log(` Score: https://wot.jeletor.cc/v1/score/${r.identity.pubkeyHex}`);
141
+ console.log('');
142
+ }
143
+
144
+ if (r.dvm) {
145
+ console.log(' šŸ¤– DVM');
146
+ console.log(` Kind: ${r.dvm.kind}`);
147
+ console.log(` Price: ${r.dvm.price} sats`);
148
+ console.log(` Published to ${r.dvm.relays} relay(s)`);
149
+ console.log('');
150
+ }
151
+
152
+ console.log(' šŸ“ Files saved to: ' + r.dir);
153
+ console.log('');
154
+ console.log(' Next steps:');
155
+ console.log(' 1. Keep your keys file safe (it contains your secret key)');
156
+ if (!r.wallet) {
157
+ console.log(' 2. Connect a wallet: agent-bootstrap init --nwc "nostr+walletconnect://..."');
158
+ }
159
+ console.log(' 3. Start receiving attestations: share your npub with other agents');
160
+ console.log(' 4. Check your trust score: curl https://wot.jeletor.cc/v1/score/' + r.identity.pubkeyHex);
161
+ console.log('');
162
+ }
163
+
164
+ function printStatus(s) {
165
+ console.log('');
166
+ console.log(' Agent Status: ' + s.dir);
167
+ console.log(' ─────────────────────────────');
168
+ console.log(` šŸ”‘ Identity: ${s.identity ? 'āœ… ' + s.identity.npub : 'āŒ Not found'}`);
169
+ console.log(` šŸ‘¤ Profile: ${s.profile ? 'āœ… Published' : 'āŒ Not published'}`);
170
+ console.log(` ⚔ Wallet: ${s.wallet ? 'āœ… Configured' : 'āŒ Not configured'}`);
171
+ console.log(` šŸ¤– DVM: ${s.dvm ? 'āœ… Registered' : '⬜ Not registered'}`);
172
+ console.log('');
173
+ }
174
+
175
+ function printVerify(v) {
176
+ console.log('');
177
+ console.log(' Verification Results');
178
+ console.log(' ─────────────────────────────');
179
+ for (const check of v.checks) {
180
+ const icon = check.ok ? 'āœ…' : 'āŒ';
181
+ console.log(` ${icon} ${check.name}: ${check.message}`);
182
+ }
183
+ console.log('');
184
+ console.log(` ${v.passed}/${v.total} checks passed`);
185
+ console.log('');
186
+ }
187
+
188
+ main().catch(err => {
189
+ console.error('Fatal:', err.message);
190
+ process.exit(1);
191
+ });
package/src/index.cjs ADDED
@@ -0,0 +1,352 @@
1
+ // agent-bootstrap — core library
2
+ // Generates Nostr identity, publishes profile, configures wallet, registers DVM, sets up ai.wot
3
+
4
+ const crypto = require('crypto');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ // ─── Key Generation ─────────────────────────────────────────────
9
+
10
+ function generateKeys() {
11
+ const { generateSecretKey, getPublicKey } = require('nostr-tools/pure');
12
+ const { nip19 } = require('nostr-tools');
13
+
14
+ const secretKey = generateSecretKey();
15
+ const pubkey = getPublicKey(secretKey);
16
+ const secretKeyHex = Buffer.from(secretKey).toString('hex');
17
+ const nsec = nip19.nsecEncode(secretKey);
18
+ const npub = nip19.npubEncode(pubkey);
19
+
20
+ return { secretKey, secretKeyHex, pubkey, nsec, npub };
21
+ }
22
+
23
+ // ─── Profile Publishing ─────────────────────────────────────────
24
+
25
+ async function publishProfile(keys, opts) {
26
+ const { Relay, useWebSocketImplementation } = require('nostr-tools/relay');
27
+ const { finalizeEvent } = require('nostr-tools/pure');
28
+ const WebSocket = require('ws');
29
+ useWebSocketImplementation(WebSocket);
30
+
31
+ const metadata = {
32
+ name: opts.name,
33
+ about: opts.about || '',
34
+ picture: opts.picture || '',
35
+ nip05: opts.nip05 || '',
36
+ };
37
+
38
+ if (opts.lightningAddress) {
39
+ metadata.lud16 = opts.lightningAddress;
40
+ }
41
+
42
+ const event = finalizeEvent({
43
+ kind: 0,
44
+ created_at: Math.floor(Date.now() / 1000),
45
+ tags: [],
46
+ content: JSON.stringify(metadata),
47
+ }, keys.secretKey);
48
+
49
+ let published = 0;
50
+ for (const url of opts.relays) {
51
+ try {
52
+ const relay = await Relay.connect(url);
53
+ await relay.publish(event);
54
+ relay.close();
55
+ published++;
56
+ } catch (e) {
57
+ // relay failed, continue
58
+ }
59
+ }
60
+
61
+ return { eventId: event.id, published, total: opts.relays.length };
62
+ }
63
+
64
+ // ─── DVM Announcement ───────────────────────────────────────────
65
+
66
+ async function publishDVMAnnouncement(keys, opts) {
67
+ const { Relay, useWebSocketImplementation } = require('nostr-tools/relay');
68
+ const { finalizeEvent } = require('nostr-tools/pure');
69
+ const WebSocket = require('ws');
70
+ useWebSocketImplementation(WebSocket);
71
+
72
+ const kind = opts.dvmKind || 5050;
73
+ const price = opts.dvmPrice || 21;
74
+
75
+ // Kind 31990 — DVM service announcement (parameterized replaceable)
76
+ const event = finalizeEvent({
77
+ kind: 31990,
78
+ created_at: Math.floor(Date.now() / 1000),
79
+ tags: [
80
+ ['d', `${opts.name}-dvm`],
81
+ ['k', String(kind)],
82
+ ['t', 'ai'],
83
+ ['t', 'agent'],
84
+ ['amount', String(price * 1000), 'msats'], // amount in msats
85
+ ],
86
+ content: JSON.stringify({
87
+ name: `${opts.name} DVM`,
88
+ about: opts.about || `${opts.name} — AI agent DVM service`,
89
+ kind,
90
+ price: { amount: price, unit: 'sats' },
91
+ }),
92
+ }, keys.secretKey);
93
+
94
+ let published = 0;
95
+ for (const url of opts.relays) {
96
+ try {
97
+ const relay = await Relay.connect(url);
98
+ await relay.publish(event);
99
+ relay.close();
100
+ published++;
101
+ } catch (e) {
102
+ // relay failed, continue
103
+ }
104
+ }
105
+
106
+ return { eventId: event.id, kind, price, published, total: opts.relays.length };
107
+ }
108
+
109
+ // ─── File Management ────────────────────────────────────────────
110
+
111
+ function saveKeys(dir, keys) {
112
+ const keysFile = path.join(dir, 'nostr-keys.json');
113
+ const data = {
114
+ publicKeyHex: keys.pubkey,
115
+ secretKeyHex: keys.secretKeyHex,
116
+ npub: keys.npub,
117
+ nsec: keys.nsec,
118
+ createdAt: new Date().toISOString(),
119
+ };
120
+ fs.writeFileSync(keysFile, JSON.stringify(data, null, 2));
121
+ fs.chmodSync(keysFile, 0o600); // owner read/write only
122
+ return keysFile;
123
+ }
124
+
125
+ function saveWalletConfig(dir, nwcUrl) {
126
+ const configFile = path.join(dir, 'wallet-config.json');
127
+ const data = {
128
+ nwcUrl,
129
+ createdAt: new Date().toISOString(),
130
+ };
131
+ fs.writeFileSync(configFile, JSON.stringify(data, null, 2));
132
+ fs.chmodSync(configFile, 0o600);
133
+ return configFile;
134
+ }
135
+
136
+ function saveBootstrapManifest(dir, result) {
137
+ const manifestFile = path.join(dir, 'bootstrap-manifest.json');
138
+ fs.writeFileSync(manifestFile, JSON.stringify(result, null, 2));
139
+ return manifestFile;
140
+ }
141
+
142
+ // ─── Bootstrap ──────────────────────────────────────────────────
143
+
144
+ async function bootstrap(opts) {
145
+ const dir = path.resolve(opts.dir || './agent-identity');
146
+
147
+ // Create directory
148
+ if (!fs.existsSync(dir)) {
149
+ fs.mkdirSync(dir, { recursive: true });
150
+ }
151
+
152
+ const result = { dir };
153
+
154
+ // 1. Generate keys
155
+ console.log('šŸ”‘ Generating Nostr identity...');
156
+ const keys = generateKeys();
157
+ const keysFile = saveKeys(dir, keys);
158
+ result.identity = {
159
+ name: opts.name,
160
+ npub: keys.npub,
161
+ pubkeyHex: keys.pubkey,
162
+ keysFile,
163
+ };
164
+
165
+ // 2. Publish profile
166
+ if (!opts.skipPublish) {
167
+ console.log('šŸ‘¤ Publishing profile to relays...');
168
+ const profile = await publishProfile(keys, {
169
+ name: opts.name,
170
+ about: opts.about,
171
+ relays: opts.relays,
172
+ lightningAddress: opts.lightningAddress,
173
+ });
174
+ result.profile = { relays: profile.published };
175
+ }
176
+
177
+ // 3. Configure wallet
178
+ if (opts.nwcUrl) {
179
+ console.log('⚔ Configuring Lightning wallet...');
180
+ const configFile = saveWalletConfig(dir, opts.nwcUrl);
181
+ result.wallet = { configFile };
182
+ }
183
+
184
+ // 4. ai.wot readiness
185
+ result.trust = {
186
+ protocol: 'ai.wot',
187
+ namespace: 'ai.wot',
188
+ ready: true,
189
+ scoreUrl: `https://wot.jeletor.cc/v1/score/${keys.pubkey}`,
190
+ };
191
+
192
+ // 5. DVM announcement
193
+ if (opts.dvm && !opts.skipPublish) {
194
+ console.log('šŸ¤– Publishing DVM announcement...');
195
+ const dvm = await publishDVMAnnouncement(keys, {
196
+ name: opts.name,
197
+ about: opts.about,
198
+ dvmKind: opts.dvmKind,
199
+ dvmPrice: opts.dvmPrice,
200
+ relays: opts.relays,
201
+ });
202
+ result.dvm = { kind: dvm.kind, price: dvm.price, relays: dvm.published };
203
+ }
204
+
205
+ // 6. Save manifest
206
+ saveBootstrapManifest(dir, result);
207
+
208
+ return result;
209
+ }
210
+
211
+ // ─── Status Check ───────────────────────────────────────────────
212
+
213
+ async function status(dir) {
214
+ dir = path.resolve(dir);
215
+ const result = { dir };
216
+
217
+ // Check keys
218
+ const keysFile = path.join(dir, 'nostr-keys.json');
219
+ if (fs.existsSync(keysFile)) {
220
+ const keys = JSON.parse(fs.readFileSync(keysFile, 'utf-8'));
221
+ result.identity = { npub: keys.npub, pubkeyHex: keys.publicKeyHex };
222
+ }
223
+
224
+ // Check wallet
225
+ const walletFile = path.join(dir, 'wallet-config.json');
226
+ if (fs.existsSync(walletFile)) {
227
+ result.wallet = { configured: true };
228
+ }
229
+
230
+ // Check manifest
231
+ const manifestFile = path.join(dir, 'bootstrap-manifest.json');
232
+ if (fs.existsSync(manifestFile)) {
233
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
234
+ result.profile = manifest.profile || null;
235
+ result.dvm = manifest.dvm || null;
236
+ }
237
+
238
+ return result;
239
+ }
240
+
241
+ // ─── Verify ─────────────────────────────────────────────────────
242
+
243
+ async function verify(dir) {
244
+ dir = path.resolve(dir);
245
+ const checks = [];
246
+
247
+ // 1. Keys exist and are valid
248
+ const keysFile = path.join(dir, 'nostr-keys.json');
249
+ if (fs.existsSync(keysFile)) {
250
+ try {
251
+ const keys = JSON.parse(fs.readFileSync(keysFile, 'utf-8'));
252
+ if (keys.publicKeyHex && keys.secretKeyHex && keys.publicKeyHex.length === 64) {
253
+ checks.push({ name: 'Identity', ok: true, message: `Keys valid (${keys.npub.substring(0, 20)}...)` });
254
+ } else {
255
+ checks.push({ name: 'Identity', ok: false, message: 'Keys file is malformed' });
256
+ }
257
+ } catch (e) {
258
+ checks.push({ name: 'Identity', ok: false, message: 'Keys file is not valid JSON' });
259
+ }
260
+ } else {
261
+ checks.push({ name: 'Identity', ok: false, message: 'No keys file found' });
262
+ }
263
+
264
+ // 2. Keys file permissions
265
+ if (fs.existsSync(keysFile)) {
266
+ const stat = fs.statSync(keysFile);
267
+ const mode = (stat.mode & 0o777).toString(8);
268
+ if (mode === '600') {
269
+ checks.push({ name: 'Key Security', ok: true, message: 'Keys file has correct permissions (600)' });
270
+ } else {
271
+ checks.push({ name: 'Key Security', ok: false, message: `Keys file permissions too open: ${mode} (should be 600)` });
272
+ }
273
+ }
274
+
275
+ // 3. Profile on relays
276
+ if (fs.existsSync(keysFile)) {
277
+ try {
278
+ const keys = JSON.parse(fs.readFileSync(keysFile, 'utf-8'));
279
+ const { Relay, useWebSocketImplementation } = require('nostr-tools/relay');
280
+ const WebSocket = require('ws');
281
+ useWebSocketImplementation(WebSocket);
282
+
283
+ let found = false;
284
+ const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
285
+ for (const url of relays) {
286
+ try {
287
+ const relay = await Relay.connect(url);
288
+ const events = [];
289
+ await new Promise((resolve) => {
290
+ relay.subscribe([{ kinds: [0], authors: [keys.publicKeyHex], limit: 1 }], {
291
+ onevent(e) { events.push(e); },
292
+ oneose() { resolve(); },
293
+ });
294
+ setTimeout(resolve, 5000);
295
+ });
296
+ relay.close();
297
+ if (events.length > 0) {
298
+ found = true;
299
+ const profile = JSON.parse(events[0].content);
300
+ checks.push({ name: 'Profile', ok: true, message: `Found on ${url}: "${profile.name}"` });
301
+ break;
302
+ }
303
+ } catch (e) {
304
+ // try next relay
305
+ }
306
+ }
307
+ if (!found) {
308
+ checks.push({ name: 'Profile', ok: false, message: 'Not found on any relay' });
309
+ }
310
+ } catch (e) {
311
+ checks.push({ name: 'Profile', ok: false, message: e.message });
312
+ }
313
+ }
314
+
315
+ // 4. Wallet config
316
+ const walletFile = path.join(dir, 'wallet-config.json');
317
+ if (fs.existsSync(walletFile)) {
318
+ try {
319
+ const wc = JSON.parse(fs.readFileSync(walletFile, 'utf-8'));
320
+ if (wc.nwcUrl && wc.nwcUrl.startsWith('nostr+walletconnect://')) {
321
+ checks.push({ name: 'Wallet', ok: true, message: 'NWC config present' });
322
+ } else {
323
+ checks.push({ name: 'Wallet', ok: false, message: 'Invalid NWC URL format' });
324
+ }
325
+ } catch (e) {
326
+ checks.push({ name: 'Wallet', ok: false, message: 'Wallet config is not valid JSON' });
327
+ }
328
+ } else {
329
+ checks.push({ name: 'Wallet', ok: false, message: 'No wallet config (optional)' });
330
+ }
331
+
332
+ // 5. Trust score reachable
333
+ if (fs.existsSync(keysFile)) {
334
+ try {
335
+ const keys = JSON.parse(fs.readFileSync(keysFile, 'utf-8'));
336
+ const resp = await fetch(`https://wot.jeletor.cc/v1/score/${keys.publicKeyHex}`);
337
+ if (resp.ok) {
338
+ const data = await resp.json();
339
+ checks.push({ name: 'Trust (ai.wot)', ok: true, message: `Score: ${data.score}/100 (${data.attestationCount} attestations)` });
340
+ } else {
341
+ checks.push({ name: 'Trust (ai.wot)', ok: false, message: `API returned ${resp.status}` });
342
+ }
343
+ } catch (e) {
344
+ checks.push({ name: 'Trust (ai.wot)', ok: false, message: 'Could not reach wot.jeletor.cc' });
345
+ }
346
+ }
347
+
348
+ const passed = checks.filter(c => c.ok).length;
349
+ return { checks, passed, total: checks.length };
350
+ }
351
+
352
+ module.exports = { bootstrap, status, verify, generateKeys, publishProfile, publishDVMAnnouncement };
package/test/run.cjs ADDED
@@ -0,0 +1,189 @@
1
+ // agent-bootstrap tests
2
+ const { generateKeys, bootstrap, status, verify } = require('../src/index.cjs');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ let passed = 0;
8
+ let failed = 0;
9
+
10
+ function test(name, fn) {
11
+ try {
12
+ fn();
13
+ console.log(` āœ… ${name}`);
14
+ passed++;
15
+ } catch (e) {
16
+ console.log(` āŒ ${name}: ${e.message}`);
17
+ failed++;
18
+ }
19
+ }
20
+
21
+ function assert(cond, msg) {
22
+ if (!cond) throw new Error(msg || 'Assertion failed');
23
+ }
24
+
25
+ async function asyncTest(name, fn) {
26
+ try {
27
+ await fn();
28
+ console.log(` āœ… ${name}`);
29
+ passed++;
30
+ } catch (e) {
31
+ console.log(` āŒ ${name}: ${e.message}`);
32
+ failed++;
33
+ }
34
+ }
35
+
36
+ // ─── Key Generation ─────────────────────────────────────────────
37
+
38
+ console.log('\nšŸ”‘ Key Generation');
39
+
40
+ test('generates valid keys', () => {
41
+ const keys = generateKeys();
42
+ assert(keys.pubkey && keys.pubkey.length === 64, 'pubkey should be 64 hex chars');
43
+ assert(keys.secretKeyHex && keys.secretKeyHex.length === 64, 'secretKeyHex should be 64 hex chars');
44
+ assert(keys.npub.startsWith('npub1'), 'npub should start with npub1');
45
+ assert(keys.nsec.startsWith('nsec1'), 'nsec should start with nsec1');
46
+ assert(keys.secretKey instanceof Uint8Array, 'secretKey should be Uint8Array');
47
+ assert(keys.secretKey.length === 32, 'secretKey should be 32 bytes');
48
+ });
49
+
50
+ test('generates unique keys each time', () => {
51
+ const k1 = generateKeys();
52
+ const k2 = generateKeys();
53
+ assert(k1.pubkey !== k2.pubkey, 'pubkeys should differ');
54
+ assert(k1.secretKeyHex !== k2.secretKeyHex, 'secret keys should differ');
55
+ });
56
+
57
+ // ─── Bootstrap (local, skip publish) ────────────────────────────
58
+
59
+ console.log('\nšŸš€ Bootstrap (local)');
60
+
61
+ const tmpDir = path.join(os.tmpdir(), 'agent-bootstrap-test-' + Date.now());
62
+
63
+ asyncTest('bootstrap creates identity files', async () => {
64
+ const result = await bootstrap({
65
+ name: 'Test Agent',
66
+ about: 'A test agent',
67
+ dir: tmpDir,
68
+ relays: [],
69
+ skipPublish: true,
70
+ });
71
+
72
+ assert(result.identity, 'should have identity');
73
+ assert(result.identity.npub.startsWith('npub1'), 'should have valid npub');
74
+ assert(result.identity.pubkeyHex.length === 64, 'should have valid pubkey');
75
+ assert(fs.existsSync(path.join(tmpDir, 'nostr-keys.json')), 'keys file should exist');
76
+ assert(fs.existsSync(path.join(tmpDir, 'bootstrap-manifest.json')), 'manifest should exist');
77
+ }).then(() => {
78
+
79
+ return asyncTest('keys file has correct permissions', async () => {
80
+ const keysFile = path.join(tmpDir, 'nostr-keys.json');
81
+ const stat = fs.statSync(keysFile);
82
+ const mode = (stat.mode & 0o777).toString(8);
83
+ assert(mode === '600', `permissions should be 600, got ${mode}`);
84
+ });
85
+
86
+ }).then(() => {
87
+
88
+ return asyncTest('keys file contains valid data', async () => {
89
+ const keysFile = path.join(tmpDir, 'nostr-keys.json');
90
+ const keys = JSON.parse(fs.readFileSync(keysFile, 'utf-8'));
91
+ assert(keys.publicKeyHex.length === 64, 'should have pubkey');
92
+ assert(keys.secretKeyHex.length === 64, 'should have secret key');
93
+ assert(keys.npub.startsWith('npub1'), 'should have npub');
94
+ assert(keys.nsec.startsWith('nsec1'), 'should have nsec');
95
+ assert(keys.createdAt, 'should have timestamp');
96
+ });
97
+
98
+ }).then(() => {
99
+
100
+ return asyncTest('manifest records bootstrap result', async () => {
101
+ const manifest = JSON.parse(fs.readFileSync(path.join(tmpDir, 'bootstrap-manifest.json'), 'utf-8'));
102
+ assert(manifest.identity, 'manifest should have identity');
103
+ assert(manifest.trust, 'manifest should have trust config');
104
+ assert(manifest.trust.protocol === 'ai.wot', 'trust protocol should be ai.wot');
105
+ assert(!manifest.profile, 'profile should not be set (skipPublish)');
106
+ assert(!manifest.dvm, 'dvm should not be set');
107
+ });
108
+
109
+ }).then(() => {
110
+
111
+ // ─── Bootstrap with wallet ────────────────────────────────────
112
+
113
+ console.log('\n⚔ Wallet Config');
114
+ const tmpDir2 = path.join(os.tmpdir(), 'agent-bootstrap-test2-' + Date.now());
115
+
116
+ return asyncTest('bootstrap with NWC saves wallet config', async () => {
117
+ const result = await bootstrap({
118
+ name: 'Wallet Agent',
119
+ dir: tmpDir2,
120
+ relays: [],
121
+ skipPublish: true,
122
+ nwcUrl: 'nostr+walletconnect://test-relay?secret=abc123',
123
+ });
124
+
125
+ assert(result.wallet, 'should have wallet');
126
+ const configFile = path.join(tmpDir2, 'wallet-config.json');
127
+ assert(fs.existsSync(configFile), 'wallet config should exist');
128
+ const wc = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
129
+ assert(wc.nwcUrl === 'nostr+walletconnect://test-relay?secret=abc123', 'should save NWC URL');
130
+
131
+ // Permissions
132
+ const stat = fs.statSync(configFile);
133
+ const mode = (stat.mode & 0o777).toString(8);
134
+ assert(mode === '600', `wallet config permissions should be 600, got ${mode}`);
135
+ });
136
+
137
+ }).then(() => {
138
+
139
+ // ─── Status ───────────────────────────────────────────────────
140
+
141
+ console.log('\nšŸ“Š Status');
142
+
143
+ return asyncTest('status reads existing bootstrap', async () => {
144
+ const s = await status(tmpDir);
145
+ assert(s.identity, 'should find identity');
146
+ assert(s.identity.npub.startsWith('npub1'), 'should have valid npub');
147
+ });
148
+
149
+ }).then(() => {
150
+
151
+ return asyncTest('status on empty dir returns no identity', async () => {
152
+ const emptyDir = path.join(os.tmpdir(), 'agent-bootstrap-empty-' + Date.now());
153
+ fs.mkdirSync(emptyDir, { recursive: true });
154
+ const s = await status(emptyDir);
155
+ assert(!s.identity, 'should not find identity');
156
+ });
157
+
158
+ }).then(() => {
159
+
160
+ // ─── Verify (local only) ─────────────────────────────────────
161
+
162
+ console.log('\nāœ“ Verify');
163
+
164
+ return asyncTest('verify checks identity and permissions', async () => {
165
+ const v = await verify(tmpDir);
166
+ assert(v.checks.length >= 2, 'should have at least 2 checks');
167
+ const identityCheck = v.checks.find(c => c.name === 'Identity');
168
+ assert(identityCheck && identityCheck.ok, 'identity check should pass');
169
+ const securityCheck = v.checks.find(c => c.name === 'Key Security');
170
+ assert(securityCheck && securityCheck.ok, 'security check should pass');
171
+ });
172
+
173
+ }).then(() => {
174
+
175
+ // ─── Cleanup & Summary ───────────────────────────────────────
176
+
177
+ console.log(`\n${'─'.repeat(40)}`);
178
+ console.log(` ${passed} passed, ${failed} failed, ${passed + failed} total`);
179
+ if (failed > 0) process.exit(1);
180
+
181
+ // Cleanup
182
+ try {
183
+ fs.rmSync(tmpDir, { recursive: true });
184
+ } catch (e) {}
185
+
186
+ }).catch(err => {
187
+ console.error('Test error:', err);
188
+ process.exit(1);
189
+ });