@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.
@@ -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();