@highway1/cli 0.1.39 → 0.1.41
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 +17 -5
- 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 +272 -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
package/package.json
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@highway1/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI tool for
|
|
3
|
+
"version": "0.1.41",
|
|
4
|
+
"description": "CLI tool for Clawiverse network",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
+
"clawiverse": "./dist/index.js",
|
|
7
8
|
"hw1": "./dist/index.js"
|
|
8
9
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"dev": "tsup --watch",
|
|
13
|
+
"clean": "rm -rf dist"
|
|
14
|
+
},
|
|
11
15
|
"dependencies": {
|
|
12
|
-
"@highway1/core": "^0.1.
|
|
13
|
-
"commander": "^12.1.0",
|
|
16
|
+
"@highway1/core": "^0.1.30",
|
|
14
17
|
"chalk": "^5.3.0",
|
|
15
|
-
"ora": "^8.1.0",
|
|
16
|
-
"inquirer": "^13.3.0",
|
|
17
18
|
"cli-table3": "^0.6.5",
|
|
18
|
-
"
|
|
19
|
+
"commander": "^12.1.0",
|
|
20
|
+
"conf": "^13.0.1",
|
|
21
|
+
"inquirer": "^13.3.0",
|
|
22
|
+
"ora": "^8.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.10.2",
|
|
26
|
+
"tsup": "^8.3.5",
|
|
27
|
+
"typescript": "^5.7.2"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.0.0"
|
|
19
31
|
}
|
|
20
32
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getAgentCard, setAgentCard } from '../config.js';
|
|
3
|
+
import { success, error, printHeader } from '../ui.js';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
|
|
6
|
+
export function registerCardCommand(program: Command): void {
|
|
7
|
+
const card = program.command('card').description('Manage Agent Card');
|
|
8
|
+
|
|
9
|
+
card
|
|
10
|
+
.command('show')
|
|
11
|
+
.description('Show current Agent Card')
|
|
12
|
+
.action(async () => {
|
|
13
|
+
try {
|
|
14
|
+
printHeader('Agent Card');
|
|
15
|
+
|
|
16
|
+
const agentCard = getAgentCard();
|
|
17
|
+
|
|
18
|
+
if (!agentCard) {
|
|
19
|
+
error('No Agent Card found. Run "hw1 init" first.');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log(JSON.stringify(agentCard, null, 2));
|
|
24
|
+
} catch (err) {
|
|
25
|
+
error(`Failed to show card: ${(err as Error).message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
card
|
|
31
|
+
.command('edit')
|
|
32
|
+
.description('Edit Agent Card')
|
|
33
|
+
.option('--name <name>', 'Agent name')
|
|
34
|
+
.option('--description <description>', 'Agent description')
|
|
35
|
+
.option('--capabilities <capabilities>', 'Capabilities (comma-separated)')
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
try {
|
|
38
|
+
printHeader('Edit Agent Card');
|
|
39
|
+
|
|
40
|
+
const currentCard = getAgentCard();
|
|
41
|
+
|
|
42
|
+
if (!currentCard) {
|
|
43
|
+
error('No Agent Card found. Run "hw1 init" first.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let updatedCard;
|
|
48
|
+
|
|
49
|
+
// If any options provided, use non-interactive mode
|
|
50
|
+
if (options.name || options.description || options.capabilities) {
|
|
51
|
+
updatedCard = {
|
|
52
|
+
name: options.name || currentCard.name,
|
|
53
|
+
description: options.description || currentCard.description,
|
|
54
|
+
capabilities: options.capabilities
|
|
55
|
+
? options.capabilities.split(',').map((c: string) => c.trim()).filter((c: string) => c.length > 0)
|
|
56
|
+
: currentCard.capabilities,
|
|
57
|
+
};
|
|
58
|
+
} else {
|
|
59
|
+
// Interactive mode
|
|
60
|
+
const answers = await inquirer.prompt([
|
|
61
|
+
{
|
|
62
|
+
type: 'input',
|
|
63
|
+
name: 'name',
|
|
64
|
+
message: 'Agent name:',
|
|
65
|
+
default: currentCard.name,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'input',
|
|
69
|
+
name: 'description',
|
|
70
|
+
message: 'Agent description:',
|
|
71
|
+
default: currentCard.description,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'input',
|
|
75
|
+
name: 'capabilities',
|
|
76
|
+
message: 'Capabilities (comma-separated):',
|
|
77
|
+
default: currentCard.capabilities.join(', '),
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
updatedCard = {
|
|
82
|
+
name: answers.name,
|
|
83
|
+
description: answers.description,
|
|
84
|
+
capabilities: answers.capabilities
|
|
85
|
+
.split(',')
|
|
86
|
+
.map((c: string) => c.trim())
|
|
87
|
+
.filter((c: string) => c.length > 0),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setAgentCard(updatedCard);
|
|
92
|
+
|
|
93
|
+
success('Agent Card updated successfully!');
|
|
94
|
+
} catch (err) {
|
|
95
|
+
error(`Failed to edit card: ${(err as Error).message}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import {
|
|
3
|
+
createNode,
|
|
4
|
+
importKeyPair,
|
|
5
|
+
createDHTOperations,
|
|
6
|
+
} from '@highway1/core';
|
|
7
|
+
import { getIdentity, getBootstrapPeers } from '../config.js';
|
|
8
|
+
import { error, spinner, printHeader, info, success } from '../ui.js';
|
|
9
|
+
import Table from 'cli-table3';
|
|
10
|
+
|
|
11
|
+
export function registerDiscoverCommand(program: Command): void {
|
|
12
|
+
program
|
|
13
|
+
.command('discover')
|
|
14
|
+
.description('Discover agents on the network')
|
|
15
|
+
.option('--capability <capability>', 'Filter by capability')
|
|
16
|
+
.option('--did <did>', 'Query specific DID')
|
|
17
|
+
.option('--query <text>', 'Natural language query (e.g., "translate Japanese")')
|
|
18
|
+
.option('--min-trust <score>', 'Minimum trust score (0-1)')
|
|
19
|
+
.option('--language <lang>', 'Filter by language')
|
|
20
|
+
.option('--limit <number>', 'Maximum number of results', '10')
|
|
21
|
+
.option('--bootstrap <peers...>', 'Bootstrap peer addresses')
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
try {
|
|
24
|
+
printHeader('Discover Agents');
|
|
25
|
+
|
|
26
|
+
const identity = getIdentity();
|
|
27
|
+
|
|
28
|
+
if (!identity) {
|
|
29
|
+
error('No identity found. Run "hw1 init" first.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const spin = spinner('Starting node...');
|
|
34
|
+
|
|
35
|
+
const keyPair = importKeyPair({
|
|
36
|
+
publicKey: identity.publicKey,
|
|
37
|
+
privateKey: identity.privateKey,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const bootstrapPeers = options.bootstrap || getBootstrapPeers();
|
|
41
|
+
|
|
42
|
+
const node = await createNode({
|
|
43
|
+
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 });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
spin.text = 'Querying DHT...';
|
|
61
|
+
|
|
62
|
+
const dht = createDHTOperations(node.libp2p);
|
|
63
|
+
|
|
64
|
+
if (options.did) {
|
|
65
|
+
const card = await dht.queryAgentCard(options.did);
|
|
66
|
+
|
|
67
|
+
if (card) {
|
|
68
|
+
spin.succeed('Agent found!');
|
|
69
|
+
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()}`);
|
|
78
|
+
} else {
|
|
79
|
+
spin.fail('Agent not found');
|
|
80
|
+
}
|
|
81
|
+
} else if (options.capability || options.query) {
|
|
82
|
+
// Semantic search
|
|
83
|
+
const query: any = {
|
|
84
|
+
limit: parseInt(options.limit, 10),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (options.query) {
|
|
88
|
+
query.text = options.query;
|
|
89
|
+
} else if (options.capability) {
|
|
90
|
+
query.capability = options.capability;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (options.minTrust) {
|
|
94
|
+
query.filters = {
|
|
95
|
+
minTrustScore: parseFloat(options.minTrust),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (options.language) {
|
|
100
|
+
query.filters = query.filters || {};
|
|
101
|
+
query.filters.language = options.language;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const cards = await dht.searchSemantic(query);
|
|
105
|
+
|
|
106
|
+
spin.succeed(`Found ${cards.length} agents`);
|
|
107
|
+
|
|
108
|
+
if (cards.length > 0) {
|
|
109
|
+
const table = new Table({
|
|
110
|
+
head: ['DID', 'Name', 'Capabilities', 'Trust'],
|
|
111
|
+
colWidths: [40, 20, 35, 10],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
for (const card of cards) {
|
|
115
|
+
const capabilities = Array.isArray(card.capabilities)
|
|
116
|
+
? card.capabilities.map((cap: any) =>
|
|
117
|
+
typeof cap === 'string' ? cap : cap.name
|
|
118
|
+
).join(', ')
|
|
119
|
+
: '';
|
|
120
|
+
|
|
121
|
+
const trustScore = card.trust
|
|
122
|
+
? `${(card.trust.interactionScore * 100).toFixed(0)}%`
|
|
123
|
+
: 'N/A';
|
|
124
|
+
|
|
125
|
+
table.push([
|
|
126
|
+
card.did.substring(0, 35) + '...',
|
|
127
|
+
card.name,
|
|
128
|
+
capabilities.substring(0, 30) + (capabilities.length > 30 ? '...' : ''),
|
|
129
|
+
trustScore,
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log();
|
|
134
|
+
console.log(table.toString());
|
|
135
|
+
|
|
136
|
+
// Show detailed info for first result if query was used
|
|
137
|
+
if (options.query && cards.length > 0) {
|
|
138
|
+
const card = cards[0];
|
|
139
|
+
console.log();
|
|
140
|
+
info('Top match details:');
|
|
141
|
+
info(` DID: ${card.did}`);
|
|
142
|
+
info(` Name: ${card.name}`);
|
|
143
|
+
info(` Description: ${card.description}`);
|
|
144
|
+
|
|
145
|
+
if (Array.isArray(card.capabilities) && card.capabilities.length > 0) {
|
|
146
|
+
const cap = card.capabilities[0];
|
|
147
|
+
if (typeof cap === 'object' && cap !== null) {
|
|
148
|
+
info(` Capability: ${cap.name}`);
|
|
149
|
+
info(` ${cap.description}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
spin.fail('Please specify --did, --capability, or --query');
|
|
156
|
+
info('Usage: hw1 discover --did <did>');
|
|
157
|
+
info(' hw1 discover --capability <capability>');
|
|
158
|
+
info(' hw1 discover --query "translate Japanese"');
|
|
159
|
+
info(' hw1 discover --query "code review" --min-trust 0.8');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await node.stop();
|
|
163
|
+
} catch (err) {
|
|
164
|
+
error(`Failed to discover: ${(err as Error).message}`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getIdentity, getAgentCard } from '../config.js';
|
|
3
|
+
import { error, printHeader, printKeyValue } from '../ui.js';
|
|
4
|
+
|
|
5
|
+
export function registerIdentityCommand(program: Command): void {
|
|
6
|
+
program
|
|
7
|
+
.command('identity')
|
|
8
|
+
.description('Show current identity information')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
try {
|
|
11
|
+
printHeader('Clawiverse Identity');
|
|
12
|
+
|
|
13
|
+
const identity = getIdentity();
|
|
14
|
+
const card = getAgentCard();
|
|
15
|
+
|
|
16
|
+
if (!identity) {
|
|
17
|
+
error('No identity found. Run "hw1 init" first.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
printKeyValue('DID', identity.did);
|
|
22
|
+
printKeyValue('Public Key', identity.publicKey.substring(0, 16) + '...');
|
|
23
|
+
|
|
24
|
+
if (card) {
|
|
25
|
+
console.log();
|
|
26
|
+
printKeyValue('Name', card.name);
|
|
27
|
+
printKeyValue('Description', card.description);
|
|
28
|
+
printKeyValue('Capabilities', card.capabilities.join(', ') || 'None');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log();
|
|
32
|
+
} catch (err) {
|
|
33
|
+
error(`Failed to show identity: ${(err as Error).message}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { generateKeyPair, exportKeyPair, deriveDID } from '@highway1/core';
|
|
3
|
+
import { hasIdentity, setIdentity, setAgentCard } from '../config.js';
|
|
4
|
+
import { success, error, spinner, printHeader, printKeyValue } from '../ui.js';
|
|
5
|
+
|
|
6
|
+
export function registerInitCommand(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('init')
|
|
9
|
+
.description('Initialize a new Clawiverse identity')
|
|
10
|
+
.option('--name <name>', 'Agent name', 'My Agent')
|
|
11
|
+
.option('--description <description>', 'Agent description', 'A Clawiverse agent')
|
|
12
|
+
.option('--force', 'Overwrite existing identity')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
try {
|
|
15
|
+
printHeader('Initialize Clawiverse Identity');
|
|
16
|
+
|
|
17
|
+
if (hasIdentity() && !options.force) {
|
|
18
|
+
error('Identity already exists. Use --force to overwrite.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const spin = spinner('Generating key pair...');
|
|
23
|
+
|
|
24
|
+
const keyPair = await generateKeyPair();
|
|
25
|
+
const exported = exportKeyPair(keyPair);
|
|
26
|
+
const did = deriveDID(keyPair.publicKey);
|
|
27
|
+
|
|
28
|
+
setIdentity({
|
|
29
|
+
did,
|
|
30
|
+
publicKey: exported.publicKey,
|
|
31
|
+
privateKey: exported.privateKey,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
setAgentCard({
|
|
35
|
+
name: options.name,
|
|
36
|
+
description: options.description,
|
|
37
|
+
capabilities: [],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
spin.succeed('Identity created successfully!');
|
|
41
|
+
|
|
42
|
+
console.log();
|
|
43
|
+
printKeyValue('DID', did);
|
|
44
|
+
printKeyValue('Name', options.name);
|
|
45
|
+
printKeyValue('Description', options.description);
|
|
46
|
+
console.log();
|
|
47
|
+
|
|
48
|
+
success('Run "hw1 join" to connect to the network');
|
|
49
|
+
} catch (err) {
|
|
50
|
+
error(`Failed to initialize: ${(err as Error).message}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
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
|
+
|
|
43
|
+
const node = await createNode({
|
|
44
|
+
keyPair,
|
|
45
|
+
bootstrapPeers,
|
|
46
|
+
enableDHT: true,
|
|
47
|
+
reserveRelaySlot: true,
|
|
48
|
+
enableRelay: options.relay ?? false,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await node.start();
|
|
52
|
+
|
|
53
|
+
spin.succeed('Node started successfully!');
|
|
54
|
+
|
|
55
|
+
info(`Peer ID: ${node.getPeerId()}`);
|
|
56
|
+
info(`DID: ${identity.did}`);
|
|
57
|
+
info(`Listening on: ${node.getMultiaddrs().join(', ')}`);
|
|
58
|
+
|
|
59
|
+
// Wait for bootstrap peer connection before publishing to DHT
|
|
60
|
+
const connectSpin = spinner('Connecting to bootstrap peers...');
|
|
61
|
+
|
|
62
|
+
// Phase 1: wait for peer:connect (up to 10s)
|
|
63
|
+
await new Promise<void>((resolve) => {
|
|
64
|
+
const timeout = setTimeout(resolve, 10000);
|
|
65
|
+
node.libp2p.addEventListener('peer:connect', () => {
|
|
66
|
+
clearTimeout(timeout);
|
|
67
|
+
resolve();
|
|
68
|
+
}, { once: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Phase 2: wait for relay reservation (up to 15s after connection)
|
|
72
|
+
// Check if we already have relay addresses (might be auto-added by libp2p)
|
|
73
|
+
const countRelayAddrs = () => node.getMultiaddrs().filter(a => a.includes('/p2p-circuit')).length;
|
|
74
|
+
const initialRelayCount = countRelayAddrs();
|
|
75
|
+
|
|
76
|
+
info(`Initial relay addresses: ${initialRelayCount}`);
|
|
77
|
+
info('Waiting for relay reservation...');
|
|
78
|
+
|
|
79
|
+
let reservationSucceeded = false;
|
|
80
|
+
await new Promise<void>((resolve) => {
|
|
81
|
+
const timeout = setTimeout(() => {
|
|
82
|
+
if (!reservationSucceeded) {
|
|
83
|
+
info(`Relay reservation timeout after 15s.`);
|
|
84
|
+
info(`Connected peers: ${node.libp2p.getPeers().map(p => p.toString()).join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
resolve();
|
|
87
|
+
}, 15000);
|
|
88
|
+
|
|
89
|
+
// Listen for relay:reservation event (fires when reservation succeeds)
|
|
90
|
+
const onReservation = () => {
|
|
91
|
+
reservationSucceeded = true;
|
|
92
|
+
info(`✓ Relay reservation successful!`);
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
setTimeout(resolve, 500);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
(node.libp2p as any).addEventListener('relay:reservation', onReservation, { once: true });
|
|
98
|
+
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
(node.libp2p as any).removeEventListener('relay:reservation', onReservation);
|
|
101
|
+
}, 15000);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!reservationSucceeded) {
|
|
105
|
+
info('⚠ Relay reservation did not complete - using fallback relay addresses');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
connectSpin.succeed('Connected to network!');
|
|
109
|
+
|
|
110
|
+
// Publish Agent Card with peerId and multiaddrs so others can dial us
|
|
111
|
+
const cardSpin = spinner('Publishing Agent Card to DHT...');
|
|
112
|
+
|
|
113
|
+
// Use relay addresses from libp2p (populated after reservation), fall back to manual construction
|
|
114
|
+
const directAddrs = node.getMultiaddrs().filter(a => !a.includes('/p2p-circuit'));
|
|
115
|
+
const relayAddrs = node.getMultiaddrs().filter(a => a.includes('/p2p-circuit'));
|
|
116
|
+
const fallbackRelayAddrs = relayAddrs.length === 0
|
|
117
|
+
? bootstrapPeers.map((r: string) => `${r}/p2p-circuit/p2p/${node.getPeerId()}`)
|
|
118
|
+
: [];
|
|
119
|
+
const allAddrs = [...directAddrs, ...relayAddrs, ...fallbackRelayAddrs];
|
|
120
|
+
|
|
121
|
+
const capabilities = (card.capabilities ?? []).map((capability: string) => ({
|
|
122
|
+
id: capability,
|
|
123
|
+
name: capability,
|
|
124
|
+
description: `Capability: ${capability}`,
|
|
125
|
+
}));
|
|
126
|
+
if (options.relay) {
|
|
127
|
+
capabilities.push({
|
|
128
|
+
id: 'relay',
|
|
129
|
+
name: 'relay',
|
|
130
|
+
description: 'Provides circuit relay service for NAT traversal',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const agentCard = createAgentCard(
|
|
135
|
+
identity.did,
|
|
136
|
+
card.name,
|
|
137
|
+
card.description,
|
|
138
|
+
capabilities,
|
|
139
|
+
allAddrs,
|
|
140
|
+
node.getPeerId()
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const signedCard = await signAgentCard(agentCard, (data) =>
|
|
144
|
+
sign(data, keyPair.privateKey)
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const dht = createDHTOperations(node.libp2p);
|
|
148
|
+
await dht.publishAgentCard(signedCard);
|
|
149
|
+
|
|
150
|
+
cardSpin.succeed('Agent Card published!');
|
|
151
|
+
|
|
152
|
+
// Keep connection alive by pinging bootstrap peers periodically
|
|
153
|
+
const pingInterval = setInterval(async () => {
|
|
154
|
+
const peers = node.libp2p.getPeers();
|
|
155
|
+
if (peers.length === 0) {
|
|
156
|
+
// No peers connected, try to reconnect to bootstrap
|
|
157
|
+
for (const bootstrapAddr of bootstrapPeers) {
|
|
158
|
+
try {
|
|
159
|
+
await node.libp2p.dial(bootstrapAddr);
|
|
160
|
+
info('Reconnected to bootstrap peer');
|
|
161
|
+
break;
|
|
162
|
+
} catch {
|
|
163
|
+
// try next bootstrap peer
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// Ping existing peers to keep connection alive
|
|
168
|
+
for (const peer of peers) {
|
|
169
|
+
try {
|
|
170
|
+
await (node.libp2p.services.ping as any).ping(peer);
|
|
171
|
+
} catch {
|
|
172
|
+
// ignore ping failures
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}, 15000); // ping every 15s (reduced from 30s for better stability)
|
|
177
|
+
|
|
178
|
+
const verifyFn = async (signature: Uint8Array, data: Uint8Array): Promise<boolean> => {
|
|
179
|
+
try {
|
|
180
|
+
const decoded = JSON.parse(new TextDecoder().decode(data)) as { from?: string };
|
|
181
|
+
if (!decoded.from || typeof decoded.from !== 'string') return false;
|
|
182
|
+
const senderPublicKey = extractPublicKey(decoded.from);
|
|
183
|
+
return verify(signature, data, senderPublicKey);
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Register message handlers for incoming messages
|
|
190
|
+
const router = createMessageRouter(
|
|
191
|
+
node.libp2p,
|
|
192
|
+
verifyFn,
|
|
193
|
+
dht
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Generic message handler that accepts any protocol
|
|
197
|
+
const messageHandler = async (envelope: any) => {
|
|
198
|
+
const payload = envelope.payload as Record<string, unknown>;
|
|
199
|
+
console.log();
|
|
200
|
+
success(`>>> Received message from ${envelope.from}`);
|
|
201
|
+
info(` Message ID: ${envelope.id}`);
|
|
202
|
+
info(` Protocol: ${envelope.protocol}`);
|
|
203
|
+
info(` Type: ${envelope.type}`);
|
|
204
|
+
info(` Payload: ${JSON.stringify(payload, null, 2)}`);
|
|
205
|
+
console.log();
|
|
206
|
+
|
|
207
|
+
// If this is a request, send back a simple acknowledgment response
|
|
208
|
+
if (envelope.type === 'request') {
|
|
209
|
+
info(' Sending acknowledgment response...');
|
|
210
|
+
|
|
211
|
+
const { createEnvelope, signEnvelope, sign } = await import('@highway1/core');
|
|
212
|
+
const identity = getIdentity();
|
|
213
|
+
const keyPair = importKeyPair({
|
|
214
|
+
publicKey: identity!.publicKey,
|
|
215
|
+
privateKey: identity!.privateKey,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const responseEnvelope = createEnvelope(
|
|
219
|
+
envelope.to, // We are the recipient, now we're the sender
|
|
220
|
+
envelope.from, // Original sender is now the recipient
|
|
221
|
+
'response',
|
|
222
|
+
envelope.protocol,
|
|
223
|
+
{
|
|
224
|
+
status: 'received',
|
|
225
|
+
message: 'Message received and processed',
|
|
226
|
+
originalPayload: payload,
|
|
227
|
+
timestamp: Date.now()
|
|
228
|
+
},
|
|
229
|
+
envelope.id // replyTo: original message ID
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const signedResponse = await signEnvelope(responseEnvelope, (data) =>
|
|
233
|
+
sign(data, keyPair.privateKey)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return signedResponse;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return undefined;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Register handlers for common protocols
|
|
243
|
+
router.registerHandler('/clawiverse/msg/1.0.0', messageHandler);
|
|
244
|
+
router.registerHandler('/clawiverse/greet/1.0.0', messageHandler);
|
|
245
|
+
|
|
246
|
+
// Register catch-all handler for any other protocol
|
|
247
|
+
router.registerCatchAllHandler(messageHandler);
|
|
248
|
+
|
|
249
|
+
await router.start();
|
|
250
|
+
|
|
251
|
+
console.log();
|
|
252
|
+
success('Successfully joined the Clawiverse network!');
|
|
253
|
+
info('Listening for incoming messages...');
|
|
254
|
+
info('Press Ctrl+C to stop');
|
|
255
|
+
|
|
256
|
+
process.on('SIGINT', async () => {
|
|
257
|
+
console.log();
|
|
258
|
+
const stopSpin = spinner('Stopping node...');
|
|
259
|
+
clearInterval(pingInterval);
|
|
260
|
+
await router.stop();
|
|
261
|
+
await node.stop();
|
|
262
|
+
stopSpin.succeed('Node stopped');
|
|
263
|
+
process.exit(0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await new Promise(() => {});
|
|
267
|
+
} catch (err) {
|
|
268
|
+
error(`Failed to join network: ${(err as Error).message}`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|