@highway1/cli 0.1.50 → 0.1.53

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@highway1/cli",
3
- "version": "0.1.50",
3
+ "version": "0.1.53",
4
4
  "description": "CLI tool for Clawiverse network",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "clean": "rm -rf dist"
14
14
  },
15
15
  "dependencies": {
16
- "@highway1/core": "^0.1.46",
16
+ "@highway1/core": "^0.1.53",
17
17
  "chalk": "^5.3.0",
18
18
  "cli-table3": "^0.6.5",
19
19
  "commander": "^12.1.0",
@@ -1,13 +1,25 @@
1
1
  import { Command } from 'commander';
2
2
  import {
3
- createNode,
3
+ createRelayClient,
4
+ createRelayIndexOperations,
4
5
  importKeyPair,
5
- createDHTOperations,
6
+ createAgentCard,
7
+ signAgentCard,
8
+ sign,
6
9
  } from '@highway1/core';
7
- import { getIdentity, getBootstrapPeers } from '../config.js';
10
+ import { getIdentity, getAgentCard } from '../config.js';
8
11
  import { error, spinner, printHeader, info, success } from '../ui.js';
9
12
  import Table from 'cli-table3';
10
13
 
14
+ const DEFAULT_RELAY_URLS = ['ws://relay.highway1.net:8080'];
15
+
16
+ function getRelayUrls(options: any): string[] {
17
+ if (options.relay) return [options.relay];
18
+ const envRelays = process.env.HW1_RELAY_URLS;
19
+ if (envRelays) return envRelays.split(',').map((u: string) => u.trim());
20
+ return DEFAULT_RELAY_URLS;
21
+ }
22
+
11
23
  export function registerDiscoverCommand(program: Command): void {
12
24
  program
13
25
  .command('discover')
@@ -18,7 +30,7 @@ export function registerDiscoverCommand(program: Command): void {
18
30
  .option('--min-trust <score>', 'Minimum trust score (0-1)')
19
31
  .option('--language <lang>', 'Filter by language')
20
32
  .option('--limit <number>', 'Maximum number of results', '10')
21
- .option('--bootstrap <peers...>', 'Bootstrap peer addresses')
33
+ .option('--relay <url>', 'Relay WebSocket URL')
22
34
  .action(async (options) => {
23
35
  try {
24
36
  printHeader('Discover Agents');
@@ -30,56 +42,58 @@ export function registerDiscoverCommand(program: Command): void {
30
42
  process.exit(1);
31
43
  }
32
44
 
33
- const spin = spinner('Starting node...');
45
+ const spin = spinner('Connecting to relay...');
34
46
 
35
47
  const keyPair = importKeyPair({
36
48
  publicKey: identity.publicKey,
37
49
  privateKey: identity.privateKey,
38
50
  });
39
51
 
40
- const bootstrapPeers = options.bootstrap || getBootstrapPeers();
41
-
42
- const node = await createNode({
52
+ const relayUrls = getRelayUrls(options);
53
+ const card = getAgentCard();
54
+ const capabilities = (card?.capabilities ?? []).map((c: string) => ({
55
+ id: c, name: c, description: `Capability: ${c}`,
56
+ }));
57
+ const agentCard = createAgentCard(
58
+ identity.did,
59
+ card?.name ?? 'Clawiverse Agent',
60
+ card?.description ?? '',
61
+ capabilities,
62
+ [],
63
+ );
64
+ const signedCard = await signAgentCard(agentCard, (data) => sign(data, keyPair.privateKey));
65
+
66
+ const relayClient = createRelayClient({
67
+ relayUrls,
68
+ did: identity.did,
43
69
  keyPair,
44
- bootstrapPeers,
45
- enableDHT: true,
46
- });
47
-
48
- await node.start();
49
-
50
- spin.text = 'Waiting for DHT peers...';
51
- // Wait until connected to at least one peer, then give DHT time to sync
52
- await new Promise<void>((resolve) => {
53
- const timeout = setTimeout(resolve, 15000);
54
- node.libp2p.addEventListener('peer:connect', () => {
55
- clearTimeout(timeout);
56
- setTimeout(resolve, 3000); // give DHT routing table time to populate
57
- }, { once: true });
70
+ card: signedCard,
58
71
  });
59
72
 
60
- spin.text = 'Querying DHT...';
73
+ await relayClient.start();
74
+ spin.text = 'Querying relay...';
61
75
 
62
- const dht = createDHTOperations(node.libp2p);
76
+ const relayIndex = createRelayIndexOperations(relayClient);
63
77
 
64
78
  if (options.did) {
65
- const card = await dht.queryAgentCard(options.did);
79
+ const agentCard = await relayIndex.queryAgentCard(options.did);
66
80
 
67
- if (card) {
81
+ if (agentCard) {
68
82
  spin.succeed('Agent found!');
69
83
  console.log();
70
- info(`DID: ${card.did}`);
71
- info(`Name: ${card.name}`);
72
- info(`Description: ${card.description}`);
73
- info(`Version: ${card.version}`);
74
- info(`Capabilities: ${card.capabilities.join(', ') || '(none)'}`);
75
- info(`Peer ID: ${card.peerId || '(unknown)'}`);
76
- info(`Endpoints: ${card.endpoints.length > 0 ? card.endpoints.join('\n ') : '(none)'}`);
77
- info(`Timestamp: ${new Date(card.timestamp).toISOString()}`);
84
+ info(`DID: ${agentCard.did}`);
85
+ info(`Name: ${agentCard.name}`);
86
+ info(`Description: ${agentCard.description}`);
87
+ info(`Version: ${agentCard.version}`);
88
+ const caps = agentCard.capabilities.map((c: any) =>
89
+ typeof c === 'string' ? c : c.name
90
+ ).join(', ');
91
+ info(`Capabilities: ${caps || '(none)'}`);
92
+ info(`Timestamp: ${new Date(agentCard.timestamp).toISOString()}`);
78
93
  } else {
79
94
  spin.fail('Agent not found');
80
95
  }
81
96
  } else if (options.capability || options.query) {
82
- // Semantic search
83
97
  const query: any = {
84
98
  limit: parseInt(options.limit, 10),
85
99
  };
@@ -91,9 +105,7 @@ export function registerDiscoverCommand(program: Command): void {
91
105
  }
92
106
 
93
107
  if (options.minTrust) {
94
- query.filters = {
95
- minTrustScore: parseFloat(options.minTrust),
96
- };
108
+ query.filters = { minTrustScore: parseFloat(options.minTrust) };
97
109
  }
98
110
 
99
111
  if (options.language) {
@@ -101,7 +113,7 @@ export function registerDiscoverCommand(program: Command): void {
101
113
  query.filters.language = options.language;
102
114
  }
103
115
 
104
- const cards = await dht.searchSemantic(query);
116
+ const cards = await relayIndex.searchSemantic(query);
105
117
 
106
118
  spin.succeed(`Found ${cards.length} agents`);
107
119
 
@@ -133,7 +145,6 @@ export function registerDiscoverCommand(program: Command): void {
133
145
  console.log();
134
146
  console.log(table.toString());
135
147
 
136
- // Show detailed info for first result if query was used
137
148
  if (options.query && cards.length > 0) {
138
149
  const card = cards[0];
139
150
  console.log();
@@ -156,10 +167,9 @@ export function registerDiscoverCommand(program: Command): void {
156
167
  info('Usage: hw1 discover --did <did>');
157
168
  info(' hw1 discover --capability <capability>');
158
169
  info(' hw1 discover --query "translate Japanese"');
159
- info(' hw1 discover --query "code review" --min-trust 0.8');
160
170
  }
161
171
 
162
- await node.stop();
172
+ await relayClient.stop();
163
173
  } catch (err) {
164
174
  error(`Failed to discover: ${(err as Error).message}`);
165
175
  process.exit(1);
@@ -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