@highway1/cli 0.1.49 → 0.1.52

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.
@@ -1,26 +1,37 @@
1
1
  import { Command } from 'commander';
2
2
  import {
3
- createNode,
3
+ createRelayClient,
4
+ createRelayIndexOperations,
5
+ createMessageRouter,
4
6
  importKeyPair,
5
7
  createAgentCard,
6
8
  signAgentCard,
7
- createDHTOperations,
8
- createMessageRouter,
9
9
  sign,
10
10
  verify,
11
11
  extractPublicKey,
12
12
  } from '@highway1/core';
13
- import { getIdentity, getAgentCard, getBootstrapPeers } from '../config.js';
13
+ import { getIdentity, getAgentCard } from '../config.js';
14
14
  import { success, error, spinner, printHeader, info } from '../ui.js';
15
15
  import { writeFile, mkdir } from 'node:fs/promises';
16
16
  import { join, resolve } from 'node:path';
17
17
 
18
+ // Default relay URLs (CVP-0011)
19
+ const DEFAULT_RELAY_URLS = [
20
+ 'ws://relay.highway1.net:8080',
21
+ ];
22
+
23
+ function getRelayUrls(options: any): string[] {
24
+ if (options.relay) return [options.relay];
25
+ const envRelays = process.env.HW1_RELAY_URLS;
26
+ if (envRelays) return envRelays.split(',').map((u: string) => u.trim());
27
+ return DEFAULT_RELAY_URLS;
28
+ }
29
+
18
30
  export function registerJoinCommand(program: Command): void {
19
31
  program
20
32
  .command('join')
21
- .description('Join the Clawiverse network')
22
- .option('--bootstrap <peers...>', 'Bootstrap peer addresses')
23
- .option('--relay', 'Run as a relay server and advertise relay capability')
33
+ .description('Join the Clawiverse network via relay')
34
+ .option('--relay <url>', 'Relay WebSocket URL (e.g. ws://localhost:8080)')
24
35
  .option('--save-dir <path>', 'Directory to save received file attachments', './downloads')
25
36
  .action(async (options) => {
26
37
  try {
@@ -34,236 +45,47 @@ export function registerJoinCommand(program: Command): void {
34
45
  process.exit(1);
35
46
  }
36
47
 
37
- const spin = spinner('Starting libp2p node...');
48
+ const spin = spinner('Connecting to relay...');
38
49
 
39
50
  const keyPair = importKeyPair({
40
51
  publicKey: identity.publicKey,
41
52
  privateKey: identity.privateKey,
42
53
  });
43
54
 
44
- const bootstrapPeers = options.bootstrap || getBootstrapPeers();
45
- const bootstrapPeerIds = bootstrapPeers
46
- .map((addr: string) => addr.split('/p2p/')[1])
47
- .filter((peerId: string | undefined): peerId is string => Boolean(peerId));
48
-
49
- const node = await createNode({
50
- keyPair,
51
- bootstrapPeers,
52
- enableDHT: true,
53
- reserveRelaySlot: true,
54
- enableRelay: options.relay ?? false,
55
- });
56
-
57
- await node.start();
58
-
59
- spin.succeed('Node started successfully!');
60
-
61
- info(`Peer ID: ${node.getPeerId()}`);
62
- info(`DID: ${identity.did}`);
63
- info(`Listening on: ${node.getMultiaddrs().join(', ')}`);
64
-
65
- // Wait for bootstrap peer connection before publishing to DHT
66
- const connectSpin = spinner('Connecting to bootstrap peers...');
67
-
68
- // Phase 1: wait for peer:connect (up to 10s)
69
- let connected = false;
70
- await new Promise<void>((resolve) => {
71
- const timeout = setTimeout(resolve, 10000);
72
- node.libp2p.addEventListener('peer:connect', () => {
73
- connected = true;
74
- clearTimeout(timeout);
75
- resolve();
76
- }, { once: true });
77
- });
78
-
79
- // If discovery has not connected us yet, proactively dial bootstrap peers once.
80
- if (!connected && bootstrapPeers.length > 0) {
81
- info('No peer discovered yet, dialing bootstrap peers...');
82
- for (const bootstrapAddr of bootstrapPeers) {
83
- try {
84
- await node.libp2p.dial(bootstrapAddr);
85
- connected = true;
86
- break;
87
- } catch {
88
- // try next bootstrap peer
89
- }
90
- }
91
- }
92
-
93
- // Phase 2: wait for relay reservation using event + polling (more reliable across libp2p versions)
94
- const countRelayAddrs = () => node.getMultiaddrs().filter(a => a.includes('/p2p-circuit')).length;
95
- const initialRelayCount = countRelayAddrs();
96
-
97
- info(`Initial relay addresses: ${initialRelayCount}`);
98
- info('Waiting for relay reservation...');
99
-
100
- let reservationSucceeded = false;
101
- if (initialRelayCount === 0) {
102
- await new Promise<void>((resolve) => {
103
- const RELAY_WAIT_MS = 30000;
104
- const POLL_MS = 500;
105
- let settled = false;
106
-
107
- const finish = () => {
108
- if (settled) return;
109
- settled = true;
110
- clearTimeout(timeout);
111
- clearInterval(pollTimer);
112
- (node.libp2p as any).removeEventListener('relay:reservation', onReservation);
113
- (node.libp2p as any).removeEventListener('self:peer:update', onPeerUpdate);
114
- resolve();
115
- };
116
-
117
- const onReservation = () => {
118
- reservationSucceeded = true;
119
- info('✓ Relay reservation successful!');
120
- setTimeout(finish, 300);
121
- };
122
-
123
- const onPeerUpdate = () => {
124
- if (countRelayAddrs() > 0) {
125
- reservationSucceeded = true;
126
- info('✓ Relay address detected from peer update');
127
- finish();
128
- }
129
- };
130
-
131
- const pollTimer = setInterval(() => {
132
- if (countRelayAddrs() > 0) {
133
- reservationSucceeded = true;
134
- info('✓ Relay address detected');
135
- finish();
136
- }
137
- }, POLL_MS);
138
-
139
- const timeout = setTimeout(() => {
140
- if (!reservationSucceeded) {
141
- info(`Relay reservation timeout after ${RELAY_WAIT_MS / 1000}s.`);
142
- info(`Connected peers: ${node.libp2p.getPeers().map(p => p.toString()).join(', ')}`);
143
- }
144
- finish();
145
- }, RELAY_WAIT_MS);
146
-
147
- // Event names vary by libp2p internals; keep as any to avoid typing mismatch.
148
- (node.libp2p as any).addEventListener('relay:reservation', onReservation);
149
- (node.libp2p as any).addEventListener('self:peer:update', onPeerUpdate);
150
- });
151
- } else {
152
- reservationSucceeded = true;
153
- info('✓ Relay address already present');
154
- }
155
-
156
- if (!reservationSucceeded) {
157
- info('⚠ Relay reservation did not complete, continuing with fallback relay addresses');
158
- }
159
-
160
- connectSpin.succeed('Connected to network!');
161
-
162
- // Publish Agent Card with peerId and multiaddrs so others can dial us
163
- const cardSpin = spinner('Publishing Agent Card to DHT...');
164
-
165
- // Use relay addresses from libp2p (populated after reservation), fall back to manual construction
166
- const directAddrs = node.getMultiaddrs().filter(a => !a.includes('/p2p-circuit'));
167
- const relayAddrs = node.getMultiaddrs().filter(a => a.includes('/p2p-circuit'));
168
- const fallbackRelayAddrs = relayAddrs.length === 0
169
- ? bootstrapPeers.map((r: string) => `${r}/p2p-circuit/p2p/${node.getPeerId()}`)
170
- : [];
171
- const allAddrs = [...directAddrs, ...relayAddrs, ...fallbackRelayAddrs];
55
+ const relayUrls = getRelayUrls(options);
172
56
 
173
57
  const capabilities = (card.capabilities ?? []).map((capability: string) => ({
174
58
  id: capability,
175
59
  name: capability,
176
60
  description: `Capability: ${capability}`,
177
61
  }));
178
- if (options.relay) {
179
- capabilities.push({
180
- id: 'relay',
181
- name: 'relay',
182
- description: 'Provides circuit relay service for NAT traversal',
183
- });
184
- }
185
62
 
186
63
  const agentCard = createAgentCard(
187
64
  identity.did,
188
65
  card.name,
189
66
  card.description,
190
67
  capabilities,
191
- allAddrs,
192
- node.getPeerId()
68
+ [],
193
69
  );
194
70
 
195
71
  const signedCard = await signAgentCard(agentCard, (data) =>
196
72
  sign(data, keyPair.privateKey)
197
73
  );
198
74
 
199
- const dht = createDHTOperations(node.libp2p);
200
- await dht.publishAgentCard(signedCard);
201
-
202
- cardSpin.succeed('Agent Card published!');
203
-
204
- // Keep bootstrap connectivity stable: proactively re-dial missing bootstrap peers.
205
- const ensureBootstrapConnections = async () => {
206
- const connections = node.libp2p.getConnections();
207
- const connectedPeerIds = new Set(connections.map((conn) => conn.remotePeer.toString()));
208
-
209
- for (const bootstrapAddr of bootstrapPeers) {
210
- const targetPeerId = bootstrapAddr.split('/p2p/')[1];
211
- if (!targetPeerId || connectedPeerIds.has(targetPeerId)) continue;
212
-
213
- try {
214
- await node.libp2p.dial(bootstrapAddr);
215
- info(`Reconnected bootstrap peer: ${targetPeerId}`);
216
- } catch {
217
- // best effort; keep trying on next tick
218
- }
219
- }
220
- };
75
+ const relayClient = createRelayClient({
76
+ relayUrls,
77
+ did: identity.did,
78
+ keyPair,
79
+ card: signedCard,
80
+ });
221
81
 
222
- // Keep connection alive by pinging peers periodically
223
- const pingInterval = setInterval(async () => {
224
- const peers = node.libp2p.getPeers();
225
- if (peers.length === 0) {
226
- // No peers connected, try to reconnect to bootstrap
227
- for (const bootstrapAddr of bootstrapPeers) {
228
- try {
229
- await node.libp2p.dial(bootstrapAddr);
230
- info('Reconnected to bootstrap peer');
231
- break;
232
- } catch {
233
- // try next bootstrap peer
234
- }
235
- }
236
- } else {
237
- await ensureBootstrapConnections();
238
- // Ping existing peers to keep connection alive
239
- for (const peer of peers) {
240
- try {
241
- await (node.libp2p.services.ping as any).ping(peer);
242
- } catch {
243
- // ignore ping failures
244
- }
245
- }
246
- }
247
- }, 15000); // ping every 15s (reduced from 30s for better stability)
82
+ await relayClient.start();
248
83
 
249
- // If a bootstrap peer disconnects, attempt immediate recovery.
250
- const onPeerDisconnect = async (evt: any) => {
251
- const disconnectedPeerId = evt?.detail?.toString?.() ?? '';
252
- if (!bootstrapPeerIds.includes(disconnectedPeerId)) return;
84
+ spin.succeed('Connected to relay!');
253
85
 
254
- info(`Bootstrap peer disconnected: ${disconnectedPeerId}, attempting reconnect...`);
255
- for (const bootstrapAddr of bootstrapPeers) {
256
- if (!bootstrapAddr.endsWith(`/p2p/${disconnectedPeerId}`)) continue;
257
- try {
258
- await node.libp2p.dial(bootstrapAddr);
259
- info(`Recovered bootstrap connection: ${disconnectedPeerId}`);
260
- return;
261
- } catch {
262
- // Continue trying other bootstrap peers if available
263
- }
264
- }
265
- };
266
- node.libp2p.addEventListener('peer:disconnect', onPeerDisconnect);
86
+ info(`DID: ${identity.did}`);
87
+ info(`Connected relays: ${relayClient.getConnectedRelays().join(', ')}`);
88
+ info(`Network peers: ${relayClient.getPeerCount()}`);
267
89
 
268
90
  const verifyFn = async (signature: Uint8Array, data: Uint8Array): Promise<boolean> => {
269
91
  try {
@@ -276,16 +98,10 @@ export function registerJoinCommand(program: Command): void {
276
98
  }
277
99
  };
278
100
 
279
- // Register message handlers for incoming messages
280
- const router = createMessageRouter(
281
- node.libp2p,
282
- verifyFn,
283
- dht
284
- );
101
+ const router = createMessageRouter(relayClient, verifyFn);
285
102
 
286
103
  const saveDir = resolve(options.saveDir ?? './downloads');
287
104
 
288
- // Generic message handler that accepts any protocol
289
105
  const messageHandler = async (envelope: any) => {
290
106
  const payload = envelope.payload as Record<string, unknown>;
291
107
  console.log();
@@ -294,7 +110,6 @@ export function registerJoinCommand(program: Command): void {
294
110
  info(` Protocol: ${envelope.protocol}`);
295
111
  info(` Type: ${envelope.type}`);
296
112
 
297
- // Handle file attachment
298
113
  let savedPath: string | undefined;
299
114
  if (payload.attachment) {
300
115
  const att = payload.attachment as {
@@ -305,7 +120,6 @@ export function registerJoinCommand(program: Command): void {
305
120
  };
306
121
  try {
307
122
  await mkdir(saveDir, { recursive: true });
308
- // Avoid filename collisions by prefixing with message id fragment
309
123
  const safeName = att.filename.replace(/[^a-zA-Z0-9._-]/g, '_');
310
124
  savedPath = join(saveDir, `${envelope.id.slice(-8)}_${safeName}`);
311
125
  await writeFile(savedPath, Buffer.from(att.data, 'base64'));
@@ -321,7 +135,6 @@ export function registerJoinCommand(program: Command): void {
321
135
  }
322
136
  console.log();
323
137
 
324
- // If this is a request, send back a simple acknowledgment response
325
138
  if (envelope.type === 'request') {
326
139
  info(' Sending acknowledgment response...');
327
140
 
@@ -356,11 +169,8 @@ export function registerJoinCommand(program: Command): void {
356
169
  return undefined;
357
170
  };
358
171
 
359
- // Register handlers for common protocols
360
172
  router.registerHandler('/clawiverse/msg/1.0.0', messageHandler);
361
173
  router.registerHandler('/clawiverse/greet/1.0.0', messageHandler);
362
-
363
- // Register catch-all handler for any other protocol
364
174
  router.registerCatchAllHandler(messageHandler);
365
175
 
366
176
  await router.start();
@@ -372,12 +182,10 @@ export function registerJoinCommand(program: Command): void {
372
182
 
373
183
  process.on('SIGINT', async () => {
374
184
  console.log();
375
- const stopSpin = spinner('Stopping node...');
376
- clearInterval(pingInterval);
377
- node.libp2p.removeEventListener('peer:disconnect', onPeerDisconnect);
185
+ const stopSpin = spinner('Stopping...');
378
186
  await router.stop();
379
- await node.stop();
380
- stopSpin.succeed('Node stopped');
187
+ await relayClient.stop();
188
+ stopSpin.succeed('Disconnected');
381
189
  process.exit(0);
382
190
  });
383
191
 
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Peers Command - CVP-0010 §2.4
3
+ *
4
+ * hw1 peers - List online agents
5
+ * hw1 peers --capability translate - Filter by capability
6
+ * hw1 peers --format json
7
+ */
8
+
9
+ import { Command } from 'commander';
10
+ import { DaemonClient } from '../daemon/client.js';
11
+ import { createLogger } from '@highway1/core';
12
+
13
+ const logger = createLogger('cli:peers');
14
+
15
+ export function registerPeersCommand(program: Command): void {
16
+ program
17
+ .command('peers')
18
+ .description('List agents on the network')
19
+ .option('--capability <cap>', 'Filter by capability')
20
+ .option('--query <text>', 'Natural language search')
21
+ .option('--min-trust <score>', 'Minimum trust score (0-1)')
22
+ .option('--limit <n>', 'Max results', '20')
23
+ .option('--format <fmt>', 'Output format: text|json', 'text')
24
+ .action(async (options) => {
25
+ const client = new DaemonClient();
26
+ if (!(await client.isDaemonRunning())) {
27
+ console.error('Daemon not running. Start with: hw1 join');
28
+ process.exit(1);
29
+ }
30
+
31
+ try {
32
+ const searchQuery = options.query ?? options.capability ?? '';
33
+ const params: any = { query: searchQuery };
34
+ if (options.minTrust) {
35
+ params.filters = { minTrustScore: parseFloat(options.minTrust) };
36
+ }
37
+
38
+ const results = await client.send('discover', params);
39
+ const agents: any[] = results ?? [];
40
+
41
+ // Filter by capability if specified
42
+ const filtered = options.capability
43
+ ? agents.filter((a: any) => {
44
+ const caps: any[] = a.capabilities ?? [];
45
+ return caps.some((c: any) => {
46
+ const name = typeof c === 'string' ? c : c.name ?? c.id ?? '';
47
+ return name.toLowerCase().includes(options.capability.toLowerCase());
48
+ });
49
+ })
50
+ : agents;
51
+
52
+ const limited = filtered.slice(0, parseInt(options.limit, 10));
53
+
54
+ if (options.format === 'json') {
55
+ console.log(JSON.stringify(limited, null, 2));
56
+ return;
57
+ }
58
+
59
+ if (limited.length === 0) {
60
+ console.log('No agents found.');
61
+ return;
62
+ }
63
+
64
+ console.log(`\nPeers (${limited.length} found)\n`);
65
+ for (const agent of limited) {
66
+ const trust = agent.trust?.interactionScore ?? 0;
67
+ const trustStr = `${(trust * 100).toFixed(0)}%`;
68
+ const caps = (agent.capabilities ?? [])
69
+ .map((c: any) => typeof c === 'string' ? c : c.name ?? c.id ?? '')
70
+ .filter(Boolean)
71
+ .slice(0, 3)
72
+ .join(', ');
73
+ const shortDid = agent.did?.slice(0, 30) + '…';
74
+ console.log(` ${agent.name ?? shortDid} trust:${trustStr}`);
75
+ if (caps) console.log(` capabilities: ${caps}`);
76
+ console.log(` did: ${agent.did}`);
77
+ }
78
+ console.log();
79
+ } catch (err) {
80
+ logger.error('Peers failed', err);
81
+ console.error('Error:', (err as Error).message);
82
+ process.exit(1);
83
+ }
84
+ });
85
+ }
@@ -1,35 +1,50 @@
1
1
  import { Command } from 'commander';
2
2
  import {
3
- createNode,
3
+ createRelayClient,
4
+ createRelayIndexOperations,
4
5
  importKeyPair,
5
6
  createEnvelope,
6
7
  signEnvelope,
7
8
  createMessageRouter,
8
- createDHTOperations,
9
+ createAgentCard,
10
+ signAgentCard,
9
11
  sign,
10
12
  verify,
11
13
  extractPublicKey,
12
14
  } from '@highway1/core';
13
- import { getIdentity, getBootstrapPeers } from '../config.js';
15
+ import { getIdentity, getAgentCard } from '../config.js';
14
16
  import { success, error, spinner, printHeader, info } from '../ui.js';
15
17
  import { readFile } from 'node:fs/promises';
16
18
  import { basename } from 'node:path';
17
19
  import { DaemonClient } from '../daemon/client.js';
18
20
 
21
+ // Default relay URLs (CVP-0011)
22
+ const DEFAULT_RELAY_URLS = [
23
+ 'ws://relay.highway1.net:8080',
24
+ ];
25
+
26
+ function getRelayUrls(options: any): string[] {
27
+ if (options.relay) return [options.relay];
28
+ const envRelays = process.env.HW1_RELAY_URLS;
29
+ if (envRelays) return envRelays.split(',').map((u: string) => u.trim());
30
+ return DEFAULT_RELAY_URLS;
31
+ }
32
+
19
33
  export function registerSendCommand(program: Command): void {
20
34
  program
21
35
  .command('send')
22
36
  .description('Send a message to another agent')
23
- .requiredOption('--to <did>', 'Recipient DID')
24
- .requiredOption('--protocol <protocol>', 'Protocol identifier')
37
+ .requiredOption('--to <did-or-name>', 'Recipient DID or agent name')
38
+ .option('--protocol <protocol>', 'Protocol identifier', '/clawiverse/msg/1.0.0') // CVP-0010 §3.1: default protocol
39
+ .option('--message <text>', 'Message text (shorthand for --payload \'{"text":"..."}\')') // CVP-0010 §3.1: --message shorthand
25
40
  .option('--payload <json>', 'Message payload (JSON)')
26
41
  .option('--file <path>', 'Attach a file (image, binary, text) as payload attachment')
27
- .option('--type <type>', 'Message type (request|notification)', 'request')
28
- .option('--bootstrap <peers...>', 'Bootstrap peer addresses')
29
- .option('--peer <multiaddr>', 'Direct peer multiaddr (bypasses DHT lookup)')
42
+ .option('--type <type>', 'Message type (request|notification)', 'request') // CVP-0010 §3.1: default type
43
+ .option('--relay <url>', 'Relay WebSocket URL')
44
+ .option('--format <fmt>', 'Output format: text|json', 'text') // CVP-0010 §3.2: --format json
30
45
  .action(async (options) => {
31
46
  try {
32
- printHeader('Send Message');
47
+ if (options.format !== 'json') printHeader('Send Message');
33
48
 
34
49
  const identity = getIdentity();
35
50
 
@@ -38,14 +53,18 @@ export function registerSendCommand(program: Command): void {
38
53
  process.exit(1);
39
54
  }
40
55
 
41
- if (!options.payload && !options.file) {
42
- error('Either --payload <json> or --file <path> is required.');
56
+ // CVP-0010 §3.1: --message shorthand
57
+ if (!options.payload && !options.file && !options.message) {
58
+ error('Either --message <text>, --payload <json>, or --file <path> is required.');
43
59
  process.exit(1);
44
60
  }
45
61
 
46
62
  let payload: Record<string, unknown> = {};
47
63
 
48
- if (options.payload) {
64
+ // CVP-0010 §3.1: --message shorthand wraps text into {"text": "..."}
65
+ if (options.message) {
66
+ payload.text = options.message;
67
+ } else if (options.payload) {
49
68
  try {
50
69
  payload = JSON.parse(options.payload);
51
70
  } catch {
@@ -85,7 +104,6 @@ export function registerSendCommand(program: Command): void {
85
104
  protocol: options.protocol,
86
105
  payload,
87
106
  type: options.type,
88
- peer: options.peer,
89
107
  });
90
108
 
91
109
  spin.succeed('Message sent successfully!');
@@ -135,43 +153,43 @@ export function registerSendCommand(program: Command): void {
135
153
  }
136
154
  } else {
137
155
  console.log();
138
- info('⚠ Daemon not running, using ephemeral node (slower)');
156
+ info('⚠ Daemon not running, using ephemeral relay connection (slower)');
139
157
  info('Tip: Start daemon with "clawiverse daemon start" for faster messaging');
140
158
  console.log();
141
159
  }
142
160
 
143
- // Fallback: create ephemeral node (original behavior)
144
- const spin = spinner('Starting node...');
161
+ // Fallback: create ephemeral relay connection (CVP-0011)
162
+ const spin = spinner('Connecting to relay...');
145
163
 
146
164
  const keyPair = importKeyPair({
147
165
  publicKey: identity.publicKey,
148
166
  privateKey: identity.privateKey,
149
167
  });
150
168
 
151
- const bootstrapPeers = options.bootstrap || getBootstrapPeers();
169
+ const relayUrls = getRelayUrls(options);
170
+ const card = getAgentCard();
171
+ const capabilities = (card?.capabilities ?? []).map((c: string) => ({
172
+ id: c, name: c, description: `Capability: ${c}`,
173
+ }));
174
+ const agentCard = createAgentCard(
175
+ identity.did,
176
+ card?.name ?? 'Clawiverse Agent',
177
+ card?.description ?? '',
178
+ capabilities,
179
+ [],
180
+ );
181
+ const signedCard = await signAgentCard(agentCard, (data) => sign(data, keyPair.privateKey));
152
182
 
153
- const node = await createNode({
183
+ const relayClient = createRelayClient({
184
+ relayUrls,
185
+ did: identity.did,
154
186
  keyPair,
155
- bootstrapPeers,
156
- enableDHT: true,
157
- reserveRelaySlot: bootstrapPeers.length > 0,
158
- });
159
-
160
- // Register peer:identify listener BEFORE start() so we don't miss the event
161
- const identifyDone = new Promise<void>((resolve) => {
162
- const timeout = setTimeout(resolve, 2000);
163
- node.libp2p.addEventListener('peer:identify', () => {
164
- clearTimeout(timeout);
165
- resolve();
166
- }, { once: true });
187
+ card: signedCard,
167
188
  });
168
189
 
169
- await node.start();
190
+ await relayClient.start();
191
+ spin.text = 'Connected to relay';
170
192
 
171
- spin.text = 'Connecting to network...';
172
- await identifyDone;
173
-
174
- const dht = createDHTOperations(node.libp2p);
175
193
  const verifyFn = async (signature: Uint8Array, data: Uint8Array): Promise<boolean> => {
176
194
  try {
177
195
  const decoded = JSON.parse(new TextDecoder().decode(data)) as { from?: string };
@@ -183,13 +201,7 @@ export function registerSendCommand(program: Command): void {
183
201
  }
184
202
  };
185
203
 
186
- const router = createMessageRouter(
187
- node.libp2p,
188
- verifyFn,
189
- dht,
190
- bootstrapPeers
191
- );
192
-
204
+ const router = createMessageRouter(relayClient, verifyFn);
193
205
  await router.start();
194
206
 
195
207
  spin.text = 'Creating message...';
@@ -208,21 +220,7 @@ export function registerSendCommand(program: Command): void {
208
220
 
209
221
  spin.text = 'Sending message...';
210
222
 
211
- // Build peer hint from --peer option if provided
212
- let peerHint = undefined;
213
- if (options.peer) {
214
- // Extract peerId from multiaddr (last component after /p2p/)
215
- const parts = options.peer.split('/p2p/');
216
- if (parts.length >= 2) {
217
- peerHint = {
218
- peerId: parts[parts.length - 1],
219
- multiaddrs: [options.peer],
220
- };
221
- info(`Direct peer: ${options.peer}`);
222
- }
223
- }
224
-
225
- const response = await router.sendMessage(signedEnvelope, peerHint);
223
+ const response = await router.sendMessage(signedEnvelope);
226
224
 
227
225
  spin.succeed('Message sent successfully!');
228
226
 
@@ -261,7 +259,7 @@ export function registerSendCommand(program: Command): void {
261
259
  }
262
260
 
263
261
  await router.stop();
264
- await node.stop();
262
+ await relayClient.stop();
265
263
 
266
264
  success('Done');
267
265
  } catch (err) {