@tjamescouch/agentchat 0.1.0
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/.claude/settings.local.json +12 -0
- package/.github/workflows/fly-deploy.yml +18 -0
- package/Dockerfile +12 -0
- package/README.md +296 -0
- package/ROADMAP.md +88 -0
- package/SPEC.md +279 -0
- package/bin/agentchat.js +702 -0
- package/fly.toml +21 -0
- package/lib/client.js +362 -0
- package/lib/deploy/akash.js +811 -0
- package/lib/deploy/config.js +128 -0
- package/lib/deploy/index.js +149 -0
- package/lib/identity.js +166 -0
- package/lib/protocol.js +236 -0
- package/lib/server.js +526 -0
- package/package.json +44 -0
- package/quick-test.sh +45 -0
- package/test/integration.test.js +536 -0
package/bin/agentchat.js
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentChat CLI
|
|
5
|
+
* Command-line interface for agent-to-agent communication
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { program } from 'commander';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { AgentChatClient, quickSend, listen } from '../lib/client.js';
|
|
12
|
+
import { startServer } from '../lib/server.js';
|
|
13
|
+
import { Identity, DEFAULT_IDENTITY_PATH } from '../lib/identity.js';
|
|
14
|
+
import {
|
|
15
|
+
deployToDocker,
|
|
16
|
+
generateDockerfile,
|
|
17
|
+
generateWallet,
|
|
18
|
+
checkBalance,
|
|
19
|
+
generateAkashSDL,
|
|
20
|
+
createDeployment,
|
|
21
|
+
listDeployments,
|
|
22
|
+
closeDeployment,
|
|
23
|
+
queryBids,
|
|
24
|
+
acceptBid,
|
|
25
|
+
getDeploymentStatus,
|
|
26
|
+
AkashWallet,
|
|
27
|
+
AKASH_WALLET_PATH
|
|
28
|
+
} from '../lib/deploy/index.js';
|
|
29
|
+
import { loadConfig, DEFAULT_CONFIG, generateExampleConfig } from '../lib/deploy/config.js';
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.name('agentchat')
|
|
33
|
+
.description('Real-time communication protocol for AI agents')
|
|
34
|
+
.version('0.1.0');
|
|
35
|
+
|
|
36
|
+
// Server command
|
|
37
|
+
program
|
|
38
|
+
.command('serve')
|
|
39
|
+
.description('Start an agentchat relay server')
|
|
40
|
+
.option('-p, --port <port>', 'Port to listen on', '6667')
|
|
41
|
+
.option('-H, --host <host>', 'Host to bind to', '0.0.0.0')
|
|
42
|
+
.option('-n, --name <name>', 'Server name', 'agentchat')
|
|
43
|
+
.option('--log-messages', 'Log all messages (for debugging)')
|
|
44
|
+
.option('--cert <file>', 'TLS certificate file (PEM format)')
|
|
45
|
+
.option('--key <file>', 'TLS private key file (PEM format)')
|
|
46
|
+
.action((options) => {
|
|
47
|
+
// Validate TLS options (both or neither)
|
|
48
|
+
if ((options.cert && !options.key) || (!options.cert && options.key)) {
|
|
49
|
+
console.error('Error: Both --cert and --key must be provided for TLS');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
startServer({
|
|
54
|
+
port: parseInt(options.port),
|
|
55
|
+
host: options.host,
|
|
56
|
+
name: options.name,
|
|
57
|
+
logMessages: options.logMessages,
|
|
58
|
+
cert: options.cert,
|
|
59
|
+
key: options.key
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Send command (fire-and-forget)
|
|
64
|
+
program
|
|
65
|
+
.command('send <server> <target> <message>')
|
|
66
|
+
.description('Send a message and disconnect (fire-and-forget)')
|
|
67
|
+
.option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
|
|
68
|
+
.option('-i, --identity <file>', 'Path to identity file')
|
|
69
|
+
.action(async (server, target, message, options) => {
|
|
70
|
+
try {
|
|
71
|
+
await quickSend(server, options.name, target, message, options.identity);
|
|
72
|
+
console.log('Message sent');
|
|
73
|
+
process.exit(0);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('Error:', err.message);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Listen command (stream messages to stdout)
|
|
81
|
+
program
|
|
82
|
+
.command('listen <server> [channels...]')
|
|
83
|
+
.description('Connect and stream messages as JSON lines')
|
|
84
|
+
.option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
|
|
85
|
+
.option('-i, --identity <file>', 'Path to identity file')
|
|
86
|
+
.option('-m, --max-messages <n>', 'Disconnect after receiving n messages (recommended for agents)')
|
|
87
|
+
.action(async (server, channels, options) => {
|
|
88
|
+
try {
|
|
89
|
+
// Default to #general if no channels specified
|
|
90
|
+
if (!channels || channels.length === 0) {
|
|
91
|
+
channels = ['#general'];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let messageCount = 0;
|
|
95
|
+
const maxMessages = options.maxMessages ? parseInt(options.maxMessages) : null;
|
|
96
|
+
|
|
97
|
+
const client = await listen(server, options.name, channels, (msg) => {
|
|
98
|
+
console.log(JSON.stringify(msg));
|
|
99
|
+
messageCount++;
|
|
100
|
+
|
|
101
|
+
if (maxMessages && messageCount >= maxMessages) {
|
|
102
|
+
console.error(`Received ${maxMessages} messages, disconnecting`);
|
|
103
|
+
client.disconnect();
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
}, options.identity);
|
|
107
|
+
|
|
108
|
+
console.error(`Connected as ${client.agentId}`);
|
|
109
|
+
console.error(`Joined: ${channels.join(', ')}`);
|
|
110
|
+
if (maxMessages) {
|
|
111
|
+
console.error(`Will disconnect after ${maxMessages} messages`);
|
|
112
|
+
} else {
|
|
113
|
+
console.error('Streaming messages to stdout (Ctrl+C to stop)');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
process.on('SIGINT', () => {
|
|
117
|
+
client.disconnect();
|
|
118
|
+
process.exit(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error('Error:', err.message);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Channels command (list available channels)
|
|
128
|
+
program
|
|
129
|
+
.command('channels <server>')
|
|
130
|
+
.description('List available channels on a server')
|
|
131
|
+
.option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
|
|
132
|
+
.option('-i, --identity <file>', 'Path to identity file')
|
|
133
|
+
.action(async (server, options) => {
|
|
134
|
+
try {
|
|
135
|
+
const client = new AgentChatClient({ server, name: options.name, identity: options.identity });
|
|
136
|
+
await client.connect();
|
|
137
|
+
|
|
138
|
+
const channels = await client.listChannels();
|
|
139
|
+
|
|
140
|
+
console.log('Available channels:');
|
|
141
|
+
for (const ch of channels) {
|
|
142
|
+
console.log(` ${ch.name} (${ch.agents} agents)`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
client.disconnect();
|
|
146
|
+
process.exit(0);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error('Error:', err.message);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Agents command (list agents in a channel)
|
|
154
|
+
program
|
|
155
|
+
.command('agents <server> <channel>')
|
|
156
|
+
.description('List agents in a channel')
|
|
157
|
+
.option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
|
|
158
|
+
.option('-i, --identity <file>', 'Path to identity file')
|
|
159
|
+
.action(async (server, channel, options) => {
|
|
160
|
+
try {
|
|
161
|
+
const client = new AgentChatClient({ server, name: options.name, identity: options.identity });
|
|
162
|
+
await client.connect();
|
|
163
|
+
|
|
164
|
+
const agents = await client.listAgents(channel);
|
|
165
|
+
|
|
166
|
+
console.log(`Agents in ${channel}:`);
|
|
167
|
+
for (const agent of agents) {
|
|
168
|
+
console.log(` ${agent}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
client.disconnect();
|
|
172
|
+
process.exit(0);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error('Error:', err.message);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Interactive connect command
|
|
180
|
+
program
|
|
181
|
+
.command('connect <server>')
|
|
182
|
+
.description('Interactive connection (for debugging)')
|
|
183
|
+
.option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
|
|
184
|
+
.option('-i, --identity <file>', 'Path to identity file')
|
|
185
|
+
.option('-j, --join <channels...>', 'Channels to join automatically')
|
|
186
|
+
.action(async (server, options) => {
|
|
187
|
+
try {
|
|
188
|
+
const client = new AgentChatClient({ server, name: options.name, identity: options.identity });
|
|
189
|
+
await client.connect();
|
|
190
|
+
|
|
191
|
+
console.log(`Connected as ${client.agentId}`);
|
|
192
|
+
|
|
193
|
+
// Auto-join channels
|
|
194
|
+
if (options.join) {
|
|
195
|
+
for (const ch of options.join) {
|
|
196
|
+
await client.join(ch);
|
|
197
|
+
console.log(`Joined ${ch}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Listen for messages
|
|
202
|
+
client.on('message', (msg) => {
|
|
203
|
+
console.log(`[${msg.to}] ${msg.from}: ${msg.content}`);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
client.on('agent_joined', (msg) => {
|
|
207
|
+
console.log(`* ${msg.agent} joined ${msg.channel}`);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
client.on('agent_left', (msg) => {
|
|
211
|
+
console.log(`* ${msg.agent} left ${msg.channel}`);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Read from stdin
|
|
215
|
+
console.log('Type messages as: #channel message or @agent message');
|
|
216
|
+
console.log('Commands: /join #channel, /leave #channel, /channels, /quit');
|
|
217
|
+
|
|
218
|
+
const readline = await import('readline');
|
|
219
|
+
const rl = readline.createInterface({
|
|
220
|
+
input: process.stdin,
|
|
221
|
+
output: process.stdout
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
rl.on('line', async (line) => {
|
|
225
|
+
line = line.trim();
|
|
226
|
+
if (!line) return;
|
|
227
|
+
|
|
228
|
+
// Commands
|
|
229
|
+
if (line.startsWith('/')) {
|
|
230
|
+
const [cmd, ...args] = line.slice(1).split(' ');
|
|
231
|
+
|
|
232
|
+
switch (cmd) {
|
|
233
|
+
case 'join':
|
|
234
|
+
if (args[0]) {
|
|
235
|
+
await client.join(args[0]);
|
|
236
|
+
console.log(`Joined ${args[0]}`);
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
case 'leave':
|
|
240
|
+
if (args[0]) {
|
|
241
|
+
await client.leave(args[0]);
|
|
242
|
+
console.log(`Left ${args[0]}`);
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
case 'channels':
|
|
246
|
+
const channels = await client.listChannels();
|
|
247
|
+
for (const ch of channels) {
|
|
248
|
+
console.log(` ${ch.name} (${ch.agents})`);
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
case 'quit':
|
|
252
|
+
case 'exit':
|
|
253
|
+
client.disconnect();
|
|
254
|
+
process.exit(0);
|
|
255
|
+
break;
|
|
256
|
+
default:
|
|
257
|
+
console.log('Unknown command');
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Messages: #channel msg or @agent msg
|
|
263
|
+
const match = line.match(/^([@#][^\s]+)\s+(.+)$/);
|
|
264
|
+
if (match) {
|
|
265
|
+
await client.send(match[1], match[2]);
|
|
266
|
+
} else {
|
|
267
|
+
console.log('Format: #channel message or @agent message');
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
rl.on('close', () => {
|
|
272
|
+
client.disconnect();
|
|
273
|
+
process.exit(0);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error('Error:', err.message);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Create channel command
|
|
283
|
+
program
|
|
284
|
+
.command('create <server> <channel>')
|
|
285
|
+
.description('Create a new channel')
|
|
286
|
+
.option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
|
|
287
|
+
.option('-i, --identity <file>', 'Path to identity file')
|
|
288
|
+
.option('-p, --private', 'Make channel invite-only')
|
|
289
|
+
.action(async (server, channel, options) => {
|
|
290
|
+
try {
|
|
291
|
+
const client = new AgentChatClient({ server, name: options.name, identity: options.identity });
|
|
292
|
+
await client.connect();
|
|
293
|
+
|
|
294
|
+
await client.createChannel(channel, options.private);
|
|
295
|
+
console.log(`Created ${channel}${options.private ? ' (invite-only)' : ''}`);
|
|
296
|
+
|
|
297
|
+
client.disconnect();
|
|
298
|
+
process.exit(0);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.error('Error:', err.message);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Invite command
|
|
306
|
+
program
|
|
307
|
+
.command('invite <server> <channel> <agent>')
|
|
308
|
+
.description('Invite an agent to a private channel')
|
|
309
|
+
.option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
|
|
310
|
+
.option('-i, --identity <file>', 'Path to identity file')
|
|
311
|
+
.action(async (server, channel, agent, options) => {
|
|
312
|
+
try {
|
|
313
|
+
const client = new AgentChatClient({ server, name: options.name, identity: options.identity });
|
|
314
|
+
await client.connect();
|
|
315
|
+
await client.join(channel);
|
|
316
|
+
|
|
317
|
+
await client.invite(channel, agent);
|
|
318
|
+
console.log(`Invited ${agent} to ${channel}`);
|
|
319
|
+
|
|
320
|
+
client.disconnect();
|
|
321
|
+
process.exit(0);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error('Error:', err.message);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Identity management command
|
|
329
|
+
program
|
|
330
|
+
.command('identity')
|
|
331
|
+
.description('Manage agent identity (Ed25519 keypair)')
|
|
332
|
+
.option('-g, --generate', 'Generate new keypair')
|
|
333
|
+
.option('-s, --show', 'Show current identity')
|
|
334
|
+
.option('-e, --export', 'Export public key for sharing (JSON to stdout)')
|
|
335
|
+
.option('-f, --file <path>', 'Identity file path', DEFAULT_IDENTITY_PATH)
|
|
336
|
+
.option('-n, --name <name>', 'Agent name (for --generate)', `agent-${process.pid}`)
|
|
337
|
+
.option('--force', 'Overwrite existing identity')
|
|
338
|
+
.action(async (options) => {
|
|
339
|
+
try {
|
|
340
|
+
if (options.generate) {
|
|
341
|
+
// Check if identity already exists
|
|
342
|
+
const exists = await Identity.exists(options.file);
|
|
343
|
+
if (exists && !options.force) {
|
|
344
|
+
console.error(`Identity already exists at ${options.file}`);
|
|
345
|
+
console.error('Use --force to overwrite');
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Generate new identity
|
|
350
|
+
const identity = Identity.generate(options.name);
|
|
351
|
+
await identity.save(options.file);
|
|
352
|
+
|
|
353
|
+
console.log('Generated new identity:');
|
|
354
|
+
console.log(` Name: ${identity.name}`);
|
|
355
|
+
console.log(` Fingerprint: ${identity.getFingerprint()}`);
|
|
356
|
+
console.log(` Agent ID: ${identity.getAgentId()}`);
|
|
357
|
+
console.log(` Saved to: ${options.file}`);
|
|
358
|
+
|
|
359
|
+
} else if (options.show) {
|
|
360
|
+
// Load and display identity
|
|
361
|
+
const identity = await Identity.load(options.file);
|
|
362
|
+
|
|
363
|
+
console.log('Current identity:');
|
|
364
|
+
console.log(` Name: ${identity.name}`);
|
|
365
|
+
console.log(` Fingerprint: ${identity.getFingerprint()}`);
|
|
366
|
+
console.log(` Agent ID: ${identity.getAgentId()}`);
|
|
367
|
+
console.log(` Created: ${identity.created}`);
|
|
368
|
+
console.log(` File: ${options.file}`);
|
|
369
|
+
|
|
370
|
+
} else if (options.export) {
|
|
371
|
+
// Export public key info
|
|
372
|
+
const identity = await Identity.load(options.file);
|
|
373
|
+
console.log(JSON.stringify(identity.export(), null, 2));
|
|
374
|
+
|
|
375
|
+
} else {
|
|
376
|
+
// Default: show if exists, otherwise show help
|
|
377
|
+
const exists = await Identity.exists(options.file);
|
|
378
|
+
if (exists) {
|
|
379
|
+
const identity = await Identity.load(options.file);
|
|
380
|
+
console.log('Current identity:');
|
|
381
|
+
console.log(` Name: ${identity.name}`);
|
|
382
|
+
console.log(` Fingerprint: ${identity.getFingerprint()}`);
|
|
383
|
+
console.log(` Agent ID: ${identity.getAgentId()}`);
|
|
384
|
+
console.log(` Created: ${identity.created}`);
|
|
385
|
+
} else {
|
|
386
|
+
console.log('No identity found.');
|
|
387
|
+
console.log(`Use --generate to create one at ${options.file}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
process.exit(0);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.error('Error:', err.message);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Deploy command
|
|
399
|
+
program
|
|
400
|
+
.command('deploy')
|
|
401
|
+
.description('Generate deployment files for agentchat server')
|
|
402
|
+
.option('--provider <provider>', 'Deployment target (docker, akash)', 'docker')
|
|
403
|
+
.option('--config <file>', 'Deploy configuration file (deploy.yaml)')
|
|
404
|
+
.option('--output <dir>', 'Output directory for generated files', '.')
|
|
405
|
+
.option('-p, --port <port>', 'Server port')
|
|
406
|
+
.option('-n, --name <name>', 'Server/container name')
|
|
407
|
+
.option('--volumes', 'Enable volume mounts for data persistence')
|
|
408
|
+
.option('--no-health-check', 'Disable health check configuration')
|
|
409
|
+
.option('--cert <file>', 'TLS certificate file path')
|
|
410
|
+
.option('--key <file>', 'TLS private key file path')
|
|
411
|
+
.option('--network <name>', 'Docker network name')
|
|
412
|
+
.option('--dockerfile', 'Also generate Dockerfile')
|
|
413
|
+
.option('--init-config', 'Generate example deploy.yaml config file')
|
|
414
|
+
// Akash-specific options
|
|
415
|
+
.option('--generate-wallet', 'Generate a new Akash wallet')
|
|
416
|
+
.option('--wallet <file>', 'Path to wallet file', AKASH_WALLET_PATH)
|
|
417
|
+
.option('--balance', 'Check wallet balance')
|
|
418
|
+
.option('--testnet', 'Use Akash testnet (default)')
|
|
419
|
+
.option('--mainnet', 'Use Akash mainnet (real funds!)')
|
|
420
|
+
.option('--create', 'Create deployment on Akash')
|
|
421
|
+
.option('--status', 'Show deployment status')
|
|
422
|
+
.option('--close <dseq>', 'Close a deployment by dseq')
|
|
423
|
+
.option('--generate-sdl', 'Generate SDL file without deploying')
|
|
424
|
+
.option('--force', 'Overwrite existing wallet')
|
|
425
|
+
.option('--bids <dseq>', 'Query bids for a deployment')
|
|
426
|
+
.option('--accept-bid <dseq>', 'Accept a bid (use with --provider-address)')
|
|
427
|
+
.option('--provider-address <address>', 'Provider address for --accept-bid')
|
|
428
|
+
.option('--dseq-status <dseq>', 'Get detailed status for a specific deployment')
|
|
429
|
+
.action(async (options) => {
|
|
430
|
+
try {
|
|
431
|
+
const isAkash = options.provider === 'akash';
|
|
432
|
+
const akashNetwork = options.mainnet ? 'mainnet' : 'testnet';
|
|
433
|
+
|
|
434
|
+
// Akash: Generate wallet
|
|
435
|
+
if (isAkash && options.generateWallet) {
|
|
436
|
+
try {
|
|
437
|
+
const wallet = await generateWallet(akashNetwork, options.wallet);
|
|
438
|
+
console.log('Generated new Akash wallet:');
|
|
439
|
+
console.log(` Network: ${wallet.network}`);
|
|
440
|
+
console.log(` Address: ${wallet.address}`);
|
|
441
|
+
console.log(` Saved to: ${options.wallet}`);
|
|
442
|
+
console.log('');
|
|
443
|
+
console.log('IMPORTANT: Back up your wallet file!');
|
|
444
|
+
console.log('The mnemonic inside is the only way to recover your funds.');
|
|
445
|
+
console.log('');
|
|
446
|
+
if (akashNetwork === 'testnet') {
|
|
447
|
+
console.log('To get testnet tokens, visit: https://faucet.sandbox-01.aksh.pw/');
|
|
448
|
+
} else {
|
|
449
|
+
console.log('To fund your wallet, send AKT to the address above.');
|
|
450
|
+
}
|
|
451
|
+
process.exit(0);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
if (err.message.includes('already exists') && !options.force) {
|
|
454
|
+
console.error(err.message);
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
throw err;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Akash: Check balance
|
|
462
|
+
if (isAkash && options.balance) {
|
|
463
|
+
const result = await checkBalance(options.wallet);
|
|
464
|
+
console.log('Wallet Balance:');
|
|
465
|
+
console.log(` Network: ${result.wallet.network}`);
|
|
466
|
+
console.log(` Address: ${result.wallet.address}`);
|
|
467
|
+
console.log(` Balance: ${result.balance.akt} AKT (${result.balance.uakt} uakt)`);
|
|
468
|
+
console.log(` Status: ${result.balance.sufficient ? 'Sufficient for deployment' : 'Insufficient - need at least 5 AKT'}`);
|
|
469
|
+
process.exit(0);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Akash: Generate SDL only
|
|
473
|
+
if (isAkash && options.generateSdl) {
|
|
474
|
+
const sdl = generateAkashSDL({
|
|
475
|
+
name: options.name,
|
|
476
|
+
port: options.port ? parseInt(options.port) : undefined
|
|
477
|
+
});
|
|
478
|
+
const outputDir = path.resolve(options.output);
|
|
479
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
480
|
+
const sdlPath = path.join(outputDir, 'deploy.yaml');
|
|
481
|
+
await fs.writeFile(sdlPath, sdl);
|
|
482
|
+
console.log(`Generated: ${sdlPath}`);
|
|
483
|
+
console.log('\nThis SDL can be used with the Akash CLI or Console.');
|
|
484
|
+
process.exit(0);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Akash: Create deployment
|
|
488
|
+
if (isAkash && options.create) {
|
|
489
|
+
console.log('Creating Akash deployment...');
|
|
490
|
+
try {
|
|
491
|
+
const result = await createDeployment({
|
|
492
|
+
walletPath: options.wallet,
|
|
493
|
+
name: options.name,
|
|
494
|
+
port: options.port ? parseInt(options.port) : undefined
|
|
495
|
+
});
|
|
496
|
+
console.log('Deployment created:');
|
|
497
|
+
console.log(` DSEQ: ${result.dseq}`);
|
|
498
|
+
console.log(` Status: ${result.status}`);
|
|
499
|
+
if (result.endpoint) {
|
|
500
|
+
console.log(` Endpoint: ${result.endpoint}`);
|
|
501
|
+
}
|
|
502
|
+
} catch (err) {
|
|
503
|
+
console.error('Deployment failed:', err.message);
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
process.exit(0);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Akash: Show status
|
|
510
|
+
if (isAkash && options.status) {
|
|
511
|
+
const deployments = await listDeployments(options.wallet);
|
|
512
|
+
if (deployments.length === 0) {
|
|
513
|
+
console.log('No active deployments.');
|
|
514
|
+
} else {
|
|
515
|
+
console.log('Active deployments:');
|
|
516
|
+
for (const d of deployments) {
|
|
517
|
+
console.log(` DSEQ ${d.dseq}: ${d.status} - ${d.endpoint || 'pending'}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
process.exit(0);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Akash: Close deployment
|
|
524
|
+
if (isAkash && options.close) {
|
|
525
|
+
console.log(`Closing deployment ${options.close}...`);
|
|
526
|
+
await closeDeployment(options.close, options.wallet);
|
|
527
|
+
console.log('Deployment closed.');
|
|
528
|
+
process.exit(0);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Akash: Query bids
|
|
532
|
+
if (isAkash && options.bids) {
|
|
533
|
+
console.log(`Querying bids for deployment ${options.bids}...`);
|
|
534
|
+
const bids = await queryBids(options.bids, options.wallet);
|
|
535
|
+
if (bids.length === 0) {
|
|
536
|
+
console.log('No bids received yet.');
|
|
537
|
+
} else {
|
|
538
|
+
console.log('Available bids:');
|
|
539
|
+
for (const b of bids) {
|
|
540
|
+
const bid = b.bid || {};
|
|
541
|
+
const price = bid.price?.amount || 'unknown';
|
|
542
|
+
const state = bid.state || 'unknown';
|
|
543
|
+
const provider = bid.bidId?.provider || 'unknown';
|
|
544
|
+
console.log(` Provider: ${provider}`);
|
|
545
|
+
console.log(` Price: ${price} uakt/block`);
|
|
546
|
+
console.log(` State: ${state}`);
|
|
547
|
+
console.log('');
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
process.exit(0);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Akash: Accept bid
|
|
554
|
+
if (isAkash && options.acceptBid) {
|
|
555
|
+
if (!options.providerAddress) {
|
|
556
|
+
console.error('Error: --provider-address is required with --accept-bid');
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
console.log(`Accepting bid from ${options.providerAddress}...`);
|
|
560
|
+
const lease = await acceptBid(options.acceptBid, options.providerAddress, options.wallet);
|
|
561
|
+
console.log('Lease created:');
|
|
562
|
+
console.log(` DSEQ: ${lease.dseq}`);
|
|
563
|
+
console.log(` Provider: ${lease.provider}`);
|
|
564
|
+
console.log(` TX: ${lease.txHash}`);
|
|
565
|
+
process.exit(0);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Akash: Get detailed deployment status
|
|
569
|
+
if (isAkash && options.dseqStatus) {
|
|
570
|
+
console.log(`Getting status for deployment ${options.dseqStatus}...`);
|
|
571
|
+
const status = await getDeploymentStatus(options.dseqStatus, options.wallet);
|
|
572
|
+
console.log('Deployment status:');
|
|
573
|
+
console.log(` DSEQ: ${status.dseq}`);
|
|
574
|
+
console.log(` Status: ${status.status}`);
|
|
575
|
+
console.log(` Created: ${status.createdAt}`);
|
|
576
|
+
if (status.provider) {
|
|
577
|
+
console.log(` Provider: ${status.provider}`);
|
|
578
|
+
}
|
|
579
|
+
if (status.bids) {
|
|
580
|
+
console.log(` Bids: ${status.bids.length}`);
|
|
581
|
+
for (const bid of status.bids) {
|
|
582
|
+
console.log(` - ${bid.provider}: ${bid.price} uakt (${bid.state})`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (status.leaseStatus) {
|
|
586
|
+
console.log(' Lease Status:', JSON.stringify(status.leaseStatus, null, 2));
|
|
587
|
+
}
|
|
588
|
+
if (status.leaseStatusError) {
|
|
589
|
+
console.log(` Lease Status Error: ${status.leaseStatusError}`);
|
|
590
|
+
}
|
|
591
|
+
process.exit(0);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Akash: Default action - show help
|
|
595
|
+
if (isAkash) {
|
|
596
|
+
console.log('Akash Deployment Options:');
|
|
597
|
+
console.log('');
|
|
598
|
+
console.log(' Setup:');
|
|
599
|
+
console.log(' --generate-wallet Generate a new wallet');
|
|
600
|
+
console.log(' --balance Check wallet balance');
|
|
601
|
+
console.log('');
|
|
602
|
+
console.log(' Deployment:');
|
|
603
|
+
console.log(' --generate-sdl Generate SDL file');
|
|
604
|
+
console.log(' --create Create deployment (auto-accepts best bid)');
|
|
605
|
+
console.log(' --status Show all deployments');
|
|
606
|
+
console.log(' --dseq-status <n> Get detailed status for deployment');
|
|
607
|
+
console.log(' --close <dseq> Close a deployment');
|
|
608
|
+
console.log('');
|
|
609
|
+
console.log(' Manual bid selection:');
|
|
610
|
+
console.log(' --bids <dseq> Query bids for a deployment');
|
|
611
|
+
console.log(' --accept-bid <dseq> --provider-address <addr>');
|
|
612
|
+
console.log(' Accept a specific bid');
|
|
613
|
+
console.log('');
|
|
614
|
+
console.log(' Options:');
|
|
615
|
+
console.log(' --testnet Use testnet (default)');
|
|
616
|
+
console.log(' --mainnet Use mainnet (real AKT)');
|
|
617
|
+
console.log(' --wallet <file> Custom wallet path');
|
|
618
|
+
console.log('');
|
|
619
|
+
console.log('Example workflow:');
|
|
620
|
+
console.log(' 1. agentchat deploy --provider akash --generate-wallet');
|
|
621
|
+
console.log(' 2. Fund wallet with AKT tokens');
|
|
622
|
+
console.log(' 3. agentchat deploy --provider akash --balance');
|
|
623
|
+
console.log(' 4. agentchat deploy --provider akash --create');
|
|
624
|
+
console.log('');
|
|
625
|
+
console.log('Manual workflow (select your own provider):');
|
|
626
|
+
console.log(' 1. agentchat deploy --provider akash --generate-sdl');
|
|
627
|
+
console.log(' 2. agentchat deploy --provider akash --create');
|
|
628
|
+
console.log(' 3. agentchat deploy --provider akash --bids <dseq>');
|
|
629
|
+
console.log(' 4. agentchat deploy --provider akash --accept-bid <dseq> --provider-address <addr>');
|
|
630
|
+
process.exit(0);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Generate example config
|
|
634
|
+
if (options.initConfig) {
|
|
635
|
+
const configPath = path.resolve(options.output, 'deploy.yaml');
|
|
636
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
637
|
+
await fs.writeFile(configPath, generateExampleConfig());
|
|
638
|
+
console.log(`Generated: ${configPath}`);
|
|
639
|
+
process.exit(0);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let config = { ...DEFAULT_CONFIG };
|
|
643
|
+
|
|
644
|
+
// Load config file if provided
|
|
645
|
+
if (options.config) {
|
|
646
|
+
const fileConfig = await loadConfig(options.config);
|
|
647
|
+
config = { ...config, ...fileConfig };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Override with CLI options
|
|
651
|
+
if (options.port) config.port = parseInt(options.port);
|
|
652
|
+
if (options.name) config.name = options.name;
|
|
653
|
+
if (options.volumes) config.volumes = true;
|
|
654
|
+
if (options.healthCheck === false) config.healthCheck = false;
|
|
655
|
+
if (options.network) config.network = options.network;
|
|
656
|
+
if (options.cert && options.key) {
|
|
657
|
+
config.tls = { cert: options.cert, key: options.key };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Validate TLS
|
|
661
|
+
if ((options.cert && !options.key) || (!options.cert && options.key)) {
|
|
662
|
+
console.error('Error: Both --cert and --key must be provided for TLS');
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Ensure output directory exists
|
|
667
|
+
const outputDir = path.resolve(options.output);
|
|
668
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
669
|
+
|
|
670
|
+
// Generate based on provider (Docker)
|
|
671
|
+
if (options.provider === 'docker' || config.provider === 'docker') {
|
|
672
|
+
// Generate docker-compose.yml
|
|
673
|
+
const compose = await deployToDocker(config);
|
|
674
|
+
const composePath = path.join(outputDir, 'docker-compose.yml');
|
|
675
|
+
await fs.writeFile(composePath, compose);
|
|
676
|
+
console.log(`Generated: ${composePath}`);
|
|
677
|
+
|
|
678
|
+
// Optionally generate Dockerfile
|
|
679
|
+
if (options.dockerfile) {
|
|
680
|
+
const dockerfile = await generateDockerfile(config);
|
|
681
|
+
const dockerfilePath = path.join(outputDir, 'Dockerfile.generated');
|
|
682
|
+
await fs.writeFile(dockerfilePath, dockerfile);
|
|
683
|
+
console.log(`Generated: ${dockerfilePath}`);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
console.log('\nTo deploy:');
|
|
687
|
+
console.log(` cd ${outputDir}`);
|
|
688
|
+
console.log(' docker-compose up -d');
|
|
689
|
+
|
|
690
|
+
} else {
|
|
691
|
+
console.error(`Unknown provider: ${options.provider}`);
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
process.exit(0);
|
|
696
|
+
} catch (err) {
|
|
697
|
+
console.error('Error:', err.message);
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
program.parse();
|