@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.
- package/dist/index.js +10802 -112048
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/commands/ask.ts +158 -0
- package/src/commands/card.ts +8 -3
- package/src/commands/discover.ts +52 -42
- package/src/commands/identity.ts +12 -3
- package/src/commands/inbox.ts +222 -0
- package/src/commands/join.ts +36 -228
- package/src/commands/peers.ts +85 -0
- package/src/commands/send.ts +56 -58
- package/src/commands/serve.ts +271 -0
- package/src/commands/status.ts +18 -3
- package/src/commands/stop.ts +49 -0
- package/src/commands/trust.ts +144 -5
- package/src/daemon/client.ts +31 -9
- package/src/daemon/server.ts +301 -88
- package/src/index.ts +10 -0
- package/LICENSE +0 -21
package/src/commands/join.ts
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import {
|
|
3
|
-
|
|
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
|
|
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('--
|
|
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('
|
|
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
|
|
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
|
-
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
|
376
|
-
clearInterval(pingInterval);
|
|
377
|
-
node.libp2p.removeEventListener('peer:disconnect', onPeerDisconnect);
|
|
185
|
+
const stopSpin = spinner('Stopping...');
|
|
378
186
|
await router.stop();
|
|
379
|
-
await
|
|
380
|
-
stopSpin.succeed('
|
|
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
|
+
}
|
package/src/commands/send.ts
CHANGED
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
createRelayClient,
|
|
4
|
+
createRelayIndexOperations,
|
|
4
5
|
importKeyPair,
|
|
5
6
|
createEnvelope,
|
|
6
7
|
signEnvelope,
|
|
7
8
|
createMessageRouter,
|
|
8
|
-
|
|
9
|
+
createAgentCard,
|
|
10
|
+
signAgentCard,
|
|
9
11
|
sign,
|
|
10
12
|
verify,
|
|
11
13
|
extractPublicKey,
|
|
12
14
|
} from '@highway1/core';
|
|
13
|
-
import { getIdentity,
|
|
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
|
-
.
|
|
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('--
|
|
29
|
-
.option('--
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
144
|
-
const spin = spinner('
|
|
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
|
|
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
|
|
183
|
+
const relayClient = createRelayClient({
|
|
184
|
+
relayUrls,
|
|
185
|
+
did: identity.did,
|
|
154
186
|
keyPair,
|
|
155
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
262
|
+
await relayClient.stop();
|
|
265
263
|
|
|
266
264
|
success('Done');
|
|
267
265
|
} catch (err) {
|