@highway1/cli 0.1.43 → 0.1.44
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/bin/clawiverse.js +2 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +21 -9
- package/src/commands/card.ts +99 -0
- package/src/commands/discover.ts +168 -0
- package/src/commands/identity.ts +37 -0
- package/src/commands/init.ts +54 -0
- package/src/commands/join.ts +360 -0
- package/src/commands/send.ts +160 -0
- package/src/commands/status.ts +45 -0
- package/src/commands/trust.ts +215 -0
- package/src/config.ts +74 -0
- package/src/index.ts +49 -0
- package/src/ui.ts +38 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import {
|
|
3
|
+
createNode,
|
|
4
|
+
importKeyPair,
|
|
5
|
+
createAgentCard,
|
|
6
|
+
signAgentCard,
|
|
7
|
+
createDHTOperations,
|
|
8
|
+
createMessageRouter,
|
|
9
|
+
sign,
|
|
10
|
+
verify,
|
|
11
|
+
extractPublicKey,
|
|
12
|
+
} from '@highway1/core';
|
|
13
|
+
import { getIdentity, getAgentCard, getBootstrapPeers } from '../config.js';
|
|
14
|
+
import { success, error, spinner, printHeader, info } from '../ui.js';
|
|
15
|
+
|
|
16
|
+
export function registerJoinCommand(program: Command): void {
|
|
17
|
+
program
|
|
18
|
+
.command('join')
|
|
19
|
+
.description('Join the Clawiverse network')
|
|
20
|
+
.option('--bootstrap <peers...>', 'Bootstrap peer addresses')
|
|
21
|
+
.option('--relay', 'Run as a relay server and advertise relay capability')
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
try {
|
|
24
|
+
printHeader('Join Clawiverse Network');
|
|
25
|
+
|
|
26
|
+
const identity = getIdentity();
|
|
27
|
+
const card = getAgentCard();
|
|
28
|
+
|
|
29
|
+
if (!identity || !card) {
|
|
30
|
+
error('No identity found. Run "hw1 init" first.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const spin = spinner('Starting libp2p node...');
|
|
35
|
+
|
|
36
|
+
const keyPair = importKeyPair({
|
|
37
|
+
publicKey: identity.publicKey,
|
|
38
|
+
privateKey: identity.privateKey,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const bootstrapPeers = options.bootstrap || getBootstrapPeers();
|
|
42
|
+
const bootstrapPeerIds = bootstrapPeers
|
|
43
|
+
.map((addr: string) => addr.split('/p2p/')[1])
|
|
44
|
+
.filter((peerId: string | undefined): peerId is string => Boolean(peerId));
|
|
45
|
+
|
|
46
|
+
const node = await createNode({
|
|
47
|
+
keyPair,
|
|
48
|
+
bootstrapPeers,
|
|
49
|
+
enableDHT: true,
|
|
50
|
+
reserveRelaySlot: true,
|
|
51
|
+
enableRelay: options.relay ?? false,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await node.start();
|
|
55
|
+
|
|
56
|
+
spin.succeed('Node started successfully!');
|
|
57
|
+
|
|
58
|
+
info(`Peer ID: ${node.getPeerId()}`);
|
|
59
|
+
info(`DID: ${identity.did}`);
|
|
60
|
+
info(`Listening on: ${node.getMultiaddrs().join(', ')}`);
|
|
61
|
+
|
|
62
|
+
// Wait for bootstrap peer connection before publishing to DHT
|
|
63
|
+
const connectSpin = spinner('Connecting to bootstrap peers...');
|
|
64
|
+
|
|
65
|
+
// Phase 1: wait for peer:connect (up to 10s)
|
|
66
|
+
let connected = false;
|
|
67
|
+
await new Promise<void>((resolve) => {
|
|
68
|
+
const timeout = setTimeout(resolve, 10000);
|
|
69
|
+
node.libp2p.addEventListener('peer:connect', () => {
|
|
70
|
+
connected = true;
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
resolve();
|
|
73
|
+
}, { once: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// If discovery has not connected us yet, proactively dial bootstrap peers once.
|
|
77
|
+
if (!connected && bootstrapPeers.length > 0) {
|
|
78
|
+
info('No peer discovered yet, dialing bootstrap peers...');
|
|
79
|
+
for (const bootstrapAddr of bootstrapPeers) {
|
|
80
|
+
try {
|
|
81
|
+
await node.libp2p.dial(bootstrapAddr);
|
|
82
|
+
connected = true;
|
|
83
|
+
break;
|
|
84
|
+
} catch {
|
|
85
|
+
// try next bootstrap peer
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Phase 2: wait for relay reservation using event + polling (more reliable across libp2p versions)
|
|
91
|
+
const countRelayAddrs = () => node.getMultiaddrs().filter(a => a.includes('/p2p-circuit')).length;
|
|
92
|
+
const initialRelayCount = countRelayAddrs();
|
|
93
|
+
|
|
94
|
+
info(`Initial relay addresses: ${initialRelayCount}`);
|
|
95
|
+
info('Waiting for relay reservation...');
|
|
96
|
+
|
|
97
|
+
let reservationSucceeded = false;
|
|
98
|
+
if (initialRelayCount === 0) {
|
|
99
|
+
await new Promise<void>((resolve) => {
|
|
100
|
+
const RELAY_WAIT_MS = 30000;
|
|
101
|
+
const POLL_MS = 500;
|
|
102
|
+
let settled = false;
|
|
103
|
+
|
|
104
|
+
const finish = () => {
|
|
105
|
+
if (settled) return;
|
|
106
|
+
settled = true;
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
clearInterval(pollTimer);
|
|
109
|
+
(node.libp2p as any).removeEventListener('relay:reservation', onReservation);
|
|
110
|
+
(node.libp2p as any).removeEventListener('self:peer:update', onPeerUpdate);
|
|
111
|
+
resolve();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const onReservation = () => {
|
|
115
|
+
reservationSucceeded = true;
|
|
116
|
+
info('✓ Relay reservation successful!');
|
|
117
|
+
setTimeout(finish, 300);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const onPeerUpdate = () => {
|
|
121
|
+
if (countRelayAddrs() > 0) {
|
|
122
|
+
reservationSucceeded = true;
|
|
123
|
+
info('✓ Relay address detected from peer update');
|
|
124
|
+
finish();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const pollTimer = setInterval(() => {
|
|
129
|
+
if (countRelayAddrs() > 0) {
|
|
130
|
+
reservationSucceeded = true;
|
|
131
|
+
info('✓ Relay address detected');
|
|
132
|
+
finish();
|
|
133
|
+
}
|
|
134
|
+
}, POLL_MS);
|
|
135
|
+
|
|
136
|
+
const timeout = setTimeout(() => {
|
|
137
|
+
if (!reservationSucceeded) {
|
|
138
|
+
info(`Relay reservation timeout after ${RELAY_WAIT_MS / 1000}s.`);
|
|
139
|
+
info(`Connected peers: ${node.libp2p.getPeers().map(p => p.toString()).join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
finish();
|
|
142
|
+
}, RELAY_WAIT_MS);
|
|
143
|
+
|
|
144
|
+
// Event names vary by libp2p internals; keep as any to avoid typing mismatch.
|
|
145
|
+
(node.libp2p as any).addEventListener('relay:reservation', onReservation);
|
|
146
|
+
(node.libp2p as any).addEventListener('self:peer:update', onPeerUpdate);
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
reservationSucceeded = true;
|
|
150
|
+
info('✓ Relay address already present');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!reservationSucceeded) {
|
|
154
|
+
info('⚠ Relay reservation did not complete, continuing with fallback relay addresses');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
connectSpin.succeed('Connected to network!');
|
|
158
|
+
|
|
159
|
+
// Publish Agent Card with peerId and multiaddrs so others can dial us
|
|
160
|
+
const cardSpin = spinner('Publishing Agent Card to DHT...');
|
|
161
|
+
|
|
162
|
+
// Use relay addresses from libp2p (populated after reservation), fall back to manual construction
|
|
163
|
+
const directAddrs = node.getMultiaddrs().filter(a => !a.includes('/p2p-circuit'));
|
|
164
|
+
const relayAddrs = node.getMultiaddrs().filter(a => a.includes('/p2p-circuit'));
|
|
165
|
+
const fallbackRelayAddrs = relayAddrs.length === 0
|
|
166
|
+
? bootstrapPeers.map((r: string) => `${r}/p2p-circuit/p2p/${node.getPeerId()}`)
|
|
167
|
+
: [];
|
|
168
|
+
const allAddrs = [...directAddrs, ...relayAddrs, ...fallbackRelayAddrs];
|
|
169
|
+
|
|
170
|
+
const capabilities = (card.capabilities ?? []).map((capability: string) => ({
|
|
171
|
+
id: capability,
|
|
172
|
+
name: capability,
|
|
173
|
+
description: `Capability: ${capability}`,
|
|
174
|
+
}));
|
|
175
|
+
if (options.relay) {
|
|
176
|
+
capabilities.push({
|
|
177
|
+
id: 'relay',
|
|
178
|
+
name: 'relay',
|
|
179
|
+
description: 'Provides circuit relay service for NAT traversal',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const agentCard = createAgentCard(
|
|
184
|
+
identity.did,
|
|
185
|
+
card.name,
|
|
186
|
+
card.description,
|
|
187
|
+
capabilities,
|
|
188
|
+
allAddrs,
|
|
189
|
+
node.getPeerId()
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const signedCard = await signAgentCard(agentCard, (data) =>
|
|
193
|
+
sign(data, keyPair.privateKey)
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const dht = createDHTOperations(node.libp2p);
|
|
197
|
+
await dht.publishAgentCard(signedCard);
|
|
198
|
+
|
|
199
|
+
cardSpin.succeed('Agent Card published!');
|
|
200
|
+
|
|
201
|
+
// Keep bootstrap connectivity stable: proactively re-dial missing bootstrap peers.
|
|
202
|
+
const ensureBootstrapConnections = async () => {
|
|
203
|
+
const connections = node.libp2p.getConnections();
|
|
204
|
+
const connectedPeerIds = new Set(connections.map((conn) => conn.remotePeer.toString()));
|
|
205
|
+
|
|
206
|
+
for (const bootstrapAddr of bootstrapPeers) {
|
|
207
|
+
const targetPeerId = bootstrapAddr.split('/p2p/')[1];
|
|
208
|
+
if (!targetPeerId || connectedPeerIds.has(targetPeerId)) continue;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
await node.libp2p.dial(bootstrapAddr);
|
|
212
|
+
info(`Reconnected bootstrap peer: ${targetPeerId}`);
|
|
213
|
+
} catch {
|
|
214
|
+
// best effort; keep trying on next tick
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Keep connection alive by pinging peers periodically
|
|
220
|
+
const pingInterval = setInterval(async () => {
|
|
221
|
+
const peers = node.libp2p.getPeers();
|
|
222
|
+
if (peers.length === 0) {
|
|
223
|
+
// No peers connected, try to reconnect to bootstrap
|
|
224
|
+
for (const bootstrapAddr of bootstrapPeers) {
|
|
225
|
+
try {
|
|
226
|
+
await node.libp2p.dial(bootstrapAddr);
|
|
227
|
+
info('Reconnected to bootstrap peer');
|
|
228
|
+
break;
|
|
229
|
+
} catch {
|
|
230
|
+
// try next bootstrap peer
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
await ensureBootstrapConnections();
|
|
235
|
+
// Ping existing peers to keep connection alive
|
|
236
|
+
for (const peer of peers) {
|
|
237
|
+
try {
|
|
238
|
+
await (node.libp2p.services.ping as any).ping(peer);
|
|
239
|
+
} catch {
|
|
240
|
+
// ignore ping failures
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}, 15000); // ping every 15s (reduced from 30s for better stability)
|
|
245
|
+
|
|
246
|
+
// If a bootstrap peer disconnects, attempt immediate recovery.
|
|
247
|
+
const onPeerDisconnect = async (evt: any) => {
|
|
248
|
+
const disconnectedPeerId = evt?.detail?.toString?.() ?? '';
|
|
249
|
+
if (!bootstrapPeerIds.includes(disconnectedPeerId)) return;
|
|
250
|
+
|
|
251
|
+
info(`Bootstrap peer disconnected: ${disconnectedPeerId}, attempting reconnect...`);
|
|
252
|
+
for (const bootstrapAddr of bootstrapPeers) {
|
|
253
|
+
if (!bootstrapAddr.endsWith(`/p2p/${disconnectedPeerId}`)) continue;
|
|
254
|
+
try {
|
|
255
|
+
await node.libp2p.dial(bootstrapAddr);
|
|
256
|
+
info(`Recovered bootstrap connection: ${disconnectedPeerId}`);
|
|
257
|
+
return;
|
|
258
|
+
} catch {
|
|
259
|
+
// Continue trying other bootstrap peers if available
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
node.libp2p.addEventListener('peer:disconnect', onPeerDisconnect);
|
|
264
|
+
|
|
265
|
+
const verifyFn = async (signature: Uint8Array, data: Uint8Array): Promise<boolean> => {
|
|
266
|
+
try {
|
|
267
|
+
const decoded = JSON.parse(new TextDecoder().decode(data)) as { from?: string };
|
|
268
|
+
if (!decoded.from || typeof decoded.from !== 'string') return false;
|
|
269
|
+
const senderPublicKey = extractPublicKey(decoded.from);
|
|
270
|
+
return verify(signature, data, senderPublicKey);
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// Register message handlers for incoming messages
|
|
277
|
+
const router = createMessageRouter(
|
|
278
|
+
node.libp2p,
|
|
279
|
+
verifyFn,
|
|
280
|
+
dht
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Generic message handler that accepts any protocol
|
|
284
|
+
const messageHandler = async (envelope: any) => {
|
|
285
|
+
const payload = envelope.payload as Record<string, unknown>;
|
|
286
|
+
console.log();
|
|
287
|
+
success(`>>> Received message from ${envelope.from}`);
|
|
288
|
+
info(` Message ID: ${envelope.id}`);
|
|
289
|
+
info(` Protocol: ${envelope.protocol}`);
|
|
290
|
+
info(` Type: ${envelope.type}`);
|
|
291
|
+
info(` Payload: ${JSON.stringify(payload, null, 2)}`);
|
|
292
|
+
console.log();
|
|
293
|
+
|
|
294
|
+
// If this is a request, send back a simple acknowledgment response
|
|
295
|
+
if (envelope.type === 'request') {
|
|
296
|
+
info(' Sending acknowledgment response...');
|
|
297
|
+
|
|
298
|
+
const { createEnvelope, signEnvelope, sign } = await import('@highway1/core');
|
|
299
|
+
const identity = getIdentity();
|
|
300
|
+
const keyPair = importKeyPair({
|
|
301
|
+
publicKey: identity!.publicKey,
|
|
302
|
+
privateKey: identity!.privateKey,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const responseEnvelope = createEnvelope(
|
|
306
|
+
envelope.to, // We are the recipient, now we're the sender
|
|
307
|
+
envelope.from, // Original sender is now the recipient
|
|
308
|
+
'response',
|
|
309
|
+
envelope.protocol,
|
|
310
|
+
{
|
|
311
|
+
status: 'received',
|
|
312
|
+
message: 'Message received and processed',
|
|
313
|
+
originalPayload: payload,
|
|
314
|
+
timestamp: Date.now()
|
|
315
|
+
},
|
|
316
|
+
envelope.id // replyTo: original message ID
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const signedResponse = await signEnvelope(responseEnvelope, (data) =>
|
|
320
|
+
sign(data, keyPair.privateKey)
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return signedResponse;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return undefined;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Register handlers for common protocols
|
|
330
|
+
router.registerHandler('/clawiverse/msg/1.0.0', messageHandler);
|
|
331
|
+
router.registerHandler('/clawiverse/greet/1.0.0', messageHandler);
|
|
332
|
+
|
|
333
|
+
// Register catch-all handler for any other protocol
|
|
334
|
+
router.registerCatchAllHandler(messageHandler);
|
|
335
|
+
|
|
336
|
+
await router.start();
|
|
337
|
+
|
|
338
|
+
console.log();
|
|
339
|
+
success('Successfully joined the Clawiverse network!');
|
|
340
|
+
info('Listening for incoming messages...');
|
|
341
|
+
info('Press Ctrl+C to stop');
|
|
342
|
+
|
|
343
|
+
process.on('SIGINT', async () => {
|
|
344
|
+
console.log();
|
|
345
|
+
const stopSpin = spinner('Stopping node...');
|
|
346
|
+
clearInterval(pingInterval);
|
|
347
|
+
node.libp2p.removeEventListener('peer:disconnect', onPeerDisconnect);
|
|
348
|
+
await router.stop();
|
|
349
|
+
await node.stop();
|
|
350
|
+
stopSpin.succeed('Node stopped');
|
|
351
|
+
process.exit(0);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await new Promise(() => {});
|
|
355
|
+
} catch (err) {
|
|
356
|
+
error(`Failed to join network: ${(err as Error).message}`);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import {
|
|
3
|
+
createNode,
|
|
4
|
+
importKeyPair,
|
|
5
|
+
createEnvelope,
|
|
6
|
+
signEnvelope,
|
|
7
|
+
createMessageRouter,
|
|
8
|
+
createDHTOperations,
|
|
9
|
+
sign,
|
|
10
|
+
verify,
|
|
11
|
+
extractPublicKey,
|
|
12
|
+
} from '@highway1/core';
|
|
13
|
+
import { getIdentity, getBootstrapPeers } from '../config.js';
|
|
14
|
+
import { success, error, spinner, printHeader, info } from '../ui.js';
|
|
15
|
+
|
|
16
|
+
export function registerSendCommand(program: Command): void {
|
|
17
|
+
program
|
|
18
|
+
.command('send')
|
|
19
|
+
.description('Send a message to another agent')
|
|
20
|
+
.requiredOption('--to <did>', 'Recipient DID')
|
|
21
|
+
.requiredOption('--protocol <protocol>', 'Protocol identifier')
|
|
22
|
+
.requiredOption('--payload <json>', 'Message payload (JSON)')
|
|
23
|
+
.option('--type <type>', 'Message type (request|notification)', 'request')
|
|
24
|
+
.option('--bootstrap <peers...>', 'Bootstrap peer addresses')
|
|
25
|
+
.option('--peer <multiaddr>', 'Direct peer multiaddr (bypasses DHT lookup)')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
try {
|
|
28
|
+
printHeader('Send Message');
|
|
29
|
+
|
|
30
|
+
const identity = getIdentity();
|
|
31
|
+
|
|
32
|
+
if (!identity) {
|
|
33
|
+
error('No identity found. Run "hw1 init" first.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let payload;
|
|
38
|
+
try {
|
|
39
|
+
payload = JSON.parse(options.payload);
|
|
40
|
+
} catch {
|
|
41
|
+
error('Invalid JSON payload');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const spin = spinner('Starting node...');
|
|
46
|
+
|
|
47
|
+
const keyPair = importKeyPair({
|
|
48
|
+
publicKey: identity.publicKey,
|
|
49
|
+
privateKey: identity.privateKey,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const bootstrapPeers = options.bootstrap || getBootstrapPeers();
|
|
53
|
+
|
|
54
|
+
const node = await createNode({
|
|
55
|
+
keyPair,
|
|
56
|
+
bootstrapPeers,
|
|
57
|
+
enableDHT: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Register peer:identify listener BEFORE start() so we don't miss the event
|
|
61
|
+
const identifyDone = new Promise<void>((resolve) => {
|
|
62
|
+
const timeout = setTimeout(resolve, 12000);
|
|
63
|
+
node.libp2p.addEventListener('peer:identify', () => {
|
|
64
|
+
clearTimeout(timeout);
|
|
65
|
+
setTimeout(resolve, 500);
|
|
66
|
+
}, { once: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await node.start();
|
|
70
|
+
|
|
71
|
+
spin.text = 'Connecting to network...';
|
|
72
|
+
await identifyDone;
|
|
73
|
+
|
|
74
|
+
const dht = createDHTOperations(node.libp2p);
|
|
75
|
+
const verifyFn = async (signature: Uint8Array, data: Uint8Array): Promise<boolean> => {
|
|
76
|
+
try {
|
|
77
|
+
const decoded = JSON.parse(new TextDecoder().decode(data)) as { from?: string };
|
|
78
|
+
if (!decoded.from || typeof decoded.from !== 'string') return false;
|
|
79
|
+
const senderPublicKey = extractPublicKey(decoded.from);
|
|
80
|
+
return verify(signature, data, senderPublicKey);
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const router = createMessageRouter(
|
|
87
|
+
node.libp2p,
|
|
88
|
+
verifyFn,
|
|
89
|
+
dht,
|
|
90
|
+
bootstrapPeers
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
await router.start();
|
|
94
|
+
|
|
95
|
+
spin.text = 'Creating message...';
|
|
96
|
+
|
|
97
|
+
const envelope = createEnvelope(
|
|
98
|
+
identity.did,
|
|
99
|
+
options.to,
|
|
100
|
+
options.type,
|
|
101
|
+
options.protocol,
|
|
102
|
+
payload
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const signedEnvelope = await signEnvelope(envelope, (data) =>
|
|
106
|
+
sign(data, keyPair.privateKey)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
spin.text = 'Sending message...';
|
|
110
|
+
|
|
111
|
+
// Build peer hint from --peer option if provided
|
|
112
|
+
let peerHint = undefined;
|
|
113
|
+
if (options.peer) {
|
|
114
|
+
// Extract peerId from multiaddr (last component after /p2p/)
|
|
115
|
+
const parts = options.peer.split('/p2p/');
|
|
116
|
+
if (parts.length === 2) {
|
|
117
|
+
peerHint = {
|
|
118
|
+
peerId: parts[1],
|
|
119
|
+
multiaddrs: [options.peer],
|
|
120
|
+
};
|
|
121
|
+
info(`Direct peer: ${options.peer}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const response = await router.sendMessage(signedEnvelope, peerHint);
|
|
126
|
+
|
|
127
|
+
spin.succeed('Message sent successfully!');
|
|
128
|
+
|
|
129
|
+
console.log();
|
|
130
|
+
info(`Message ID: ${signedEnvelope.id}`);
|
|
131
|
+
info(`To: ${options.to}`);
|
|
132
|
+
info(`Protocol: ${options.protocol}`);
|
|
133
|
+
info(`Type: ${options.type}`);
|
|
134
|
+
info(`Payload: ${JSON.stringify(payload)}`);
|
|
135
|
+
|
|
136
|
+
// Display response if received
|
|
137
|
+
if (response) {
|
|
138
|
+
console.log();
|
|
139
|
+
success('>>> Received response from recipient');
|
|
140
|
+
info(`Response ID: ${response.id}`);
|
|
141
|
+
info(`Reply To: ${response.replyTo}`);
|
|
142
|
+
info(`Protocol: ${response.protocol}`);
|
|
143
|
+
info(`Payload: ${JSON.stringify(response.payload, null, 2)}`);
|
|
144
|
+
} else if (options.type === 'request') {
|
|
145
|
+
console.log();
|
|
146
|
+
info('No response received (recipient may not have returned a response)');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await router.stop();
|
|
150
|
+
await node.stop();
|
|
151
|
+
|
|
152
|
+
success('Done');
|
|
153
|
+
} catch (err) {
|
|
154
|
+
error(`Failed to send message: ${(err as Error).message}`);
|
|
155
|
+
if ((err as Error).cause) error(`Cause: ${((err as Error).cause as Error).message}`);
|
|
156
|
+
console.error(err);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getIdentity, getAgentCard, getBootstrapPeers } from '../config.js';
|
|
3
|
+
import { error, printHeader, printKeyValue, printSection } from '../ui.js';
|
|
4
|
+
|
|
5
|
+
export function registerStatusCommand(program: Command): void {
|
|
6
|
+
program
|
|
7
|
+
.command('status')
|
|
8
|
+
.description('Show current status')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
try {
|
|
11
|
+
printHeader('Clawiverse Status');
|
|
12
|
+
|
|
13
|
+
const identity = getIdentity();
|
|
14
|
+
const card = getAgentCard();
|
|
15
|
+
const bootstrapPeers = getBootstrapPeers();
|
|
16
|
+
|
|
17
|
+
if (!identity) {
|
|
18
|
+
error('No identity configured. Run "hw1 init" first.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
printSection('Identity');
|
|
23
|
+
printKeyValue('DID', identity.did);
|
|
24
|
+
printKeyValue('Public Key', identity.publicKey.substring(0, 16) + '...');
|
|
25
|
+
|
|
26
|
+
if (card) {
|
|
27
|
+
printSection('Agent Card');
|
|
28
|
+
printKeyValue('Name', card.name);
|
|
29
|
+
printKeyValue('Description', card.description);
|
|
30
|
+
printKeyValue('Capabilities', card.capabilities.join(', ') || 'None');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
printSection('Network');
|
|
34
|
+
printKeyValue(
|
|
35
|
+
'Bootstrap Peers',
|
|
36
|
+
bootstrapPeers.length > 0 ? bootstrapPeers.join(', ') : 'None configured'
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
console.log();
|
|
40
|
+
} catch (err) {
|
|
41
|
+
error(`Failed to show status: ${(err as Error).message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|