@tjamescouch/agentchat 0.21.1 → 0.22.1

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,1812 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * AgentChat CLI
5
+ * Command-line interface for agent-to-agent communication
6
+ */
7
+
8
+ import { program, Command } 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
+ AgentChatDaemon,
16
+ isDaemonRunning,
17
+ stopDaemon,
18
+ getDaemonStatus,
19
+ listDaemons,
20
+ stopAllDaemons,
21
+ getDaemonPaths,
22
+ DEFAULT_CHANNELS,
23
+ DEFAULT_INSTANCE
24
+ } from '../lib/daemon.js';
25
+ import {
26
+ deployToDocker,
27
+ generateDockerfile,
28
+ generateWallet,
29
+ checkBalance,
30
+ generateAkashSDL,
31
+ createDeployment,
32
+ listDeployments,
33
+ closeDeployment,
34
+ queryBids,
35
+ acceptBid,
36
+ getDeploymentStatus,
37
+ AkashWallet,
38
+ AKASH_WALLET_PATH
39
+ } from '../lib/deploy/index.js';
40
+ import { loadConfig, DEFAULT_CONFIG, generateExampleConfig } from '../lib/deploy/config.js';
41
+ import {
42
+ ReceiptStore,
43
+ DEFAULT_RECEIPTS_PATH,
44
+ readReceipts
45
+ } from '../lib/receipts.js';
46
+ import {
47
+ ReputationStore,
48
+ DEFAULT_RATINGS_PATH,
49
+ DEFAULT_RATING
50
+ } from '../lib/reputation.js';
51
+ import {
52
+ ServerDirectory,
53
+ DEFAULT_DIRECTORY_PATH
54
+ } from '../lib/server-directory.js';
55
+ import { enforceDirectorySafety, checkDirectorySafety } from '../lib/security.js';
56
+
57
+ // Type definitions for CLI options
58
+ interface ServeOptions {
59
+ port: string;
60
+ host: string;
61
+ name: string;
62
+ logMessages?: boolean;
63
+ cert?: string;
64
+ key?: string;
65
+ bufferSize: string;
66
+ }
67
+
68
+ interface SendOptions {
69
+ name: string;
70
+ identity?: string;
71
+ }
72
+
73
+ interface ListenOptions {
74
+ name: string;
75
+ identity?: string;
76
+ maxMessages?: string;
77
+ }
78
+
79
+ interface ChannelsOptions {
80
+ name: string;
81
+ identity?: string;
82
+ }
83
+
84
+ interface AgentsOptions {
85
+ name: string;
86
+ identity?: string;
87
+ }
88
+
89
+ interface CreateOptions {
90
+ name: string;
91
+ identity?: string;
92
+ private?: boolean;
93
+ }
94
+
95
+ interface InviteOptions {
96
+ name: string;
97
+ identity?: string;
98
+ }
99
+
100
+ interface ProposeOptions {
101
+ identity: string;
102
+ amount?: string;
103
+ currency?: string;
104
+ paymentCode?: string;
105
+ expires: string;
106
+ terms?: string;
107
+ eloStake?: string;
108
+ }
109
+
110
+ interface AcceptOptions {
111
+ identity: string;
112
+ paymentCode?: string;
113
+ eloStake?: string;
114
+ }
115
+
116
+ interface RejectOptions {
117
+ identity: string;
118
+ reason?: string;
119
+ }
120
+
121
+ interface CompleteOptions {
122
+ identity: string;
123
+ proof?: string;
124
+ }
125
+
126
+ interface DisputeOptions {
127
+ identity: string;
128
+ }
129
+
130
+ interface VerifyOptions {
131
+ identity: string;
132
+ }
133
+
134
+ interface IdentityOptions {
135
+ generate?: boolean;
136
+ show?: boolean;
137
+ export?: boolean;
138
+ rotate?: boolean;
139
+ verifyChain?: boolean;
140
+ revoke?: string | boolean;
141
+ verifyRevocation?: string;
142
+ file: string;
143
+ name: string;
144
+ force?: boolean;
145
+ }
146
+
147
+ interface DaemonOptions {
148
+ name: string;
149
+ identity: string;
150
+ channels: string[];
151
+ background?: boolean;
152
+ status?: boolean;
153
+ list?: boolean;
154
+ stop?: boolean;
155
+ stopAll?: boolean;
156
+ maxReconnectTime: string;
157
+ }
158
+
159
+ interface ReceiptsOptions {
160
+ format: string;
161
+ identity: string;
162
+ file: string;
163
+ }
164
+
165
+ interface RatingsOptions {
166
+ identity: string;
167
+ file: string;
168
+ export?: boolean;
169
+ recalculate?: boolean;
170
+ leaderboard?: string | boolean;
171
+ stats?: boolean;
172
+ }
173
+
174
+ interface SkillsOptions {
175
+ capability?: string;
176
+ rate?: number;
177
+ currency: string;
178
+ description?: string;
179
+ file?: string;
180
+ identity: string;
181
+ maxRate?: number;
182
+ limit?: number;
183
+ json?: boolean;
184
+ }
185
+
186
+ interface DiscoverOptions {
187
+ add?: string;
188
+ remove?: string;
189
+ name?: string;
190
+ description?: string;
191
+ region?: string;
192
+ online?: boolean;
193
+ json?: boolean;
194
+ check?: boolean;
195
+ directory: string;
196
+ }
197
+
198
+ interface DeployOptions {
199
+ provider: string;
200
+ config?: string;
201
+ output: string;
202
+ port?: string;
203
+ name?: string;
204
+ volumes?: boolean;
205
+ healthCheck?: boolean;
206
+ cert?: string;
207
+ key?: string;
208
+ network?: string;
209
+ dockerfile?: boolean;
210
+ initConfig?: boolean;
211
+ generateWallet?: boolean;
212
+ wallet: string;
213
+ balance?: boolean;
214
+ testnet?: boolean;
215
+ mainnet?: boolean;
216
+ create?: boolean;
217
+ status?: boolean;
218
+ close?: string;
219
+ generateSdl?: boolean;
220
+ force?: boolean;
221
+ bids?: string;
222
+ acceptBid?: string;
223
+ providerAddress?: string;
224
+ dseqStatus?: string;
225
+ }
226
+
227
+ program
228
+ .name('agentchat')
229
+ .description('Real-time communication protocol for AI agents')
230
+ .version('0.1.0');
231
+
232
+ // Server command
233
+ program
234
+ .command('serve')
235
+ .description('Start an agentchat relay server')
236
+ .option('-p, --port <port>', 'Port to listen on', '6667')
237
+ .option('-H, --host <host>', 'Host to bind to', '0.0.0.0')
238
+ .option('-n, --name <name>', 'Server name', 'agentchat')
239
+ .option('--log-messages', 'Log all messages (for debugging)')
240
+ .option('--cert <file>', 'TLS certificate file (PEM format)')
241
+ .option('--key <file>', 'TLS private key file (PEM format)')
242
+ .option('--buffer-size <n>', 'Message buffer size per channel for replay on join', '20')
243
+ .action((options: ServeOptions) => {
244
+ // Validate TLS options (both or neither)
245
+ if ((options.cert && !options.key) || (!options.cert && options.key)) {
246
+ console.error('Error: Both --cert and --key must be provided for TLS');
247
+ process.exit(1);
248
+ }
249
+
250
+ startServer({
251
+ port: parseInt(options.port),
252
+ host: options.host,
253
+ name: options.name,
254
+ logMessages: options.logMessages,
255
+ cert: options.cert,
256
+ key: options.key,
257
+ messageBufferSize: parseInt(options.bufferSize)
258
+ });
259
+ });
260
+
261
+ // Send command (fire-and-forget)
262
+ program
263
+ .command('send <server> <target> <message>')
264
+ .description('Send a message and disconnect (fire-and-forget)')
265
+ .option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
266
+ .option('-i, --identity <file>', 'Path to identity file')
267
+ .action(async (server: string, target: string, message: string, options: SendOptions) => {
268
+ try {
269
+ await quickSend(server, options.name, target, message, options.identity);
270
+ console.log('Message sent');
271
+ process.exit(0);
272
+ } catch (err) {
273
+ const error = err as Error & { code?: string };
274
+ if (error.code === 'ECONNREFUSED') {
275
+ console.error('Error: Connection refused. Is the server running?');
276
+ } else {
277
+ console.error('Error:', error.message || error.code || err);
278
+ }
279
+ process.exit(1);
280
+ }
281
+ });
282
+
283
+ // Listen command (stream messages to stdout)
284
+ program
285
+ .command('listen <server> [channels...]')
286
+ .description('Connect and stream messages as JSON lines')
287
+ .option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
288
+ .option('-i, --identity <file>', 'Path to identity file')
289
+ .option('-m, --max-messages <n>', 'Disconnect after receiving n messages (recommended for agents)')
290
+ .action(async (server: string, channels: string[], options: ListenOptions) => {
291
+ try {
292
+ // Default to #general if no channels specified
293
+ if (!channels || channels.length === 0) {
294
+ channels = ['#general'];
295
+ }
296
+
297
+ let messageCount = 0;
298
+ const maxMessages = options.maxMessages ? parseInt(options.maxMessages) : null;
299
+
300
+ const client = await listen(server, options.name, channels, (msg: unknown) => {
301
+ console.log(JSON.stringify(msg));
302
+ messageCount++;
303
+
304
+ if (maxMessages && messageCount >= maxMessages) {
305
+ console.error(`Received ${maxMessages} messages, disconnecting`);
306
+ client.disconnect();
307
+ process.exit(0);
308
+ }
309
+ }, options.identity);
310
+
311
+ console.error(`Connected as ${client.agentId}`);
312
+ console.error(`Joined: ${channels.join(', ')}`);
313
+ if (maxMessages) {
314
+ console.error(`Will disconnect after ${maxMessages} messages`);
315
+ } else {
316
+ console.error('Streaming messages to stdout (Ctrl+C to stop)');
317
+ }
318
+
319
+ process.on('SIGINT', () => {
320
+ client.disconnect();
321
+ process.exit(0);
322
+ });
323
+
324
+ } catch (err) {
325
+ const error = err as Error & { code?: string };
326
+ if (error.code === 'ECONNREFUSED') {
327
+ console.error('Error: Connection refused. Is the server running?');
328
+ console.error(` Try: agentchat serve --port 8080`);
329
+ } else {
330
+ console.error('Error:', error.message || error.code || err);
331
+ }
332
+ process.exit(1);
333
+ }
334
+ });
335
+
336
+ // Channels command (list available channels)
337
+ program
338
+ .command('channels <server>')
339
+ .description('List available channels on a server')
340
+ .option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
341
+ .option('-i, --identity <file>', 'Path to identity file')
342
+ .action(async (server: string, options: ChannelsOptions) => {
343
+ try {
344
+ const client = new AgentChatClient({ server, name: options.name, identity: options.identity });
345
+ await client.connect();
346
+
347
+ const channels = await client.listChannels();
348
+
349
+ console.log('Available channels:');
350
+ for (const ch of channels) {
351
+ console.log(` ${ch.name} (${ch.agents} agents)`);
352
+ }
353
+
354
+ client.disconnect();
355
+ process.exit(0);
356
+ } catch (err) {
357
+ const error = err as Error;
358
+ console.error('Error:', error.message);
359
+ process.exit(1);
360
+ }
361
+ });
362
+
363
+ // Agents command (list agents in a channel)
364
+ program
365
+ .command('agents <server> <channel>')
366
+ .description('List agents in a channel')
367
+ .option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
368
+ .option('-i, --identity <file>', 'Path to identity file')
369
+ .action(async (server: string, channel: string, options: AgentsOptions) => {
370
+ try {
371
+ const client = new AgentChatClient({ server, name: options.name, identity: options.identity });
372
+ await client.connect();
373
+
374
+ const agents = await client.listAgents(channel);
375
+
376
+ console.log(`Agents in ${channel}:`);
377
+ for (const agent of agents) {
378
+ const status = agent.status_text ? ` - ${agent.status_text}` : '';
379
+ console.log(` ${agent.id} (${agent.name}) [${agent.presence}]${status}`);
380
+ }
381
+
382
+ client.disconnect();
383
+ process.exit(0);
384
+ } catch (err) {
385
+ const error = err as Error;
386
+ console.error('Error:', error.message);
387
+ process.exit(1);
388
+ }
389
+ });
390
+
391
+ // Create channel command
392
+ program
393
+ .command('create <server> <channel>')
394
+ .description('Create a new channel')
395
+ .option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
396
+ .option('-i, --identity <file>', 'Path to identity file')
397
+ .option('-p, --private', 'Make channel invite-only')
398
+ .action(async (server: string, channel: string, options: CreateOptions) => {
399
+ try {
400
+ const client = new AgentChatClient({ server, name: options.name, identity: options.identity });
401
+ await client.connect();
402
+
403
+ await client.createChannel(channel, options.private);
404
+ console.log(`Created ${channel}${options.private ? ' (invite-only)' : ''}`);
405
+
406
+ client.disconnect();
407
+ process.exit(0);
408
+ } catch (err) {
409
+ const error = err as Error;
410
+ console.error('Error:', error.message);
411
+ process.exit(1);
412
+ }
413
+ });
414
+
415
+ // Invite command
416
+ program
417
+ .command('invite <server> <channel> <agent>')
418
+ .description('Invite an agent to a private channel')
419
+ .option('-n, --name <name>', 'Agent name', `agent-${process.pid}`)
420
+ .option('-i, --identity <file>', 'Path to identity file')
421
+ .action(async (server: string, channel: string, agent: string, options: InviteOptions) => {
422
+ try {
423
+ const client = new AgentChatClient({ server, name: options.name, identity: options.identity });
424
+ await client.connect();
425
+ await client.join(channel);
426
+
427
+ await client.invite(channel, agent);
428
+ console.log(`Invited ${agent} to ${channel}`);
429
+
430
+ client.disconnect();
431
+ process.exit(0);
432
+ } catch (err) {
433
+ const error = err as Error;
434
+ console.error('Error:', error.message);
435
+ process.exit(1);
436
+ }
437
+ });
438
+
439
+ // Propose command
440
+ program
441
+ .command('propose <server> <agent> <task>')
442
+ .description('Send a work proposal to another agent')
443
+ .option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
444
+ .option('-a, --amount <n>', 'Payment amount')
445
+ .option('-c, --currency <code>', 'Currency (SOL, USDC, AKT, etc)')
446
+ .option('-p, --payment-code <code>', 'Your payment code (BIP47, address)')
447
+ .option('-e, --expires <seconds>', 'Expiration time in seconds', '300')
448
+ .option('-t, --terms <terms>', 'Additional terms')
449
+ .option('-s, --elo-stake <n>', 'ELO points to stake on this proposal')
450
+ .action(async (server: string, agent: string, task: string, options: ProposeOptions) => {
451
+ try {
452
+ const client = new AgentChatClient({ server, identity: options.identity });
453
+ await client.connect();
454
+
455
+ const proposal = await client.propose(agent, {
456
+ task,
457
+ amount: options.amount ? parseFloat(options.amount) : undefined,
458
+ currency: options.currency,
459
+ payment_code: options.paymentCode,
460
+ terms: options.terms,
461
+ expires: parseInt(options.expires),
462
+ elo_stake: options.eloStake ? parseInt(options.eloStake) : undefined
463
+ });
464
+
465
+ console.log('Proposal sent:');
466
+ console.log(` ID: ${proposal.id}`);
467
+ console.log(` To: ${proposal.to}`);
468
+ console.log(` Task: ${proposal.task}`);
469
+ if (proposal.amount) console.log(` Amount: ${proposal.amount} ${proposal.currency || ''}`);
470
+ if (proposal.elo_stake) console.log(` ELO Stake: ${proposal.elo_stake}`);
471
+ if (proposal.expires) console.log(` Expires: ${new Date(proposal.expires).toISOString()}`);
472
+ console.log(`\nUse this ID to track responses.`);
473
+
474
+ client.disconnect();
475
+ process.exit(0);
476
+ } catch (err) {
477
+ const error = err as Error;
478
+ console.error('Error:', error.message);
479
+ process.exit(1);
480
+ }
481
+ });
482
+
483
+ // Accept proposal command
484
+ program
485
+ .command('accept <server> <proposal_id>')
486
+ .description('Accept a proposal')
487
+ .option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
488
+ .option('-p, --payment-code <code>', 'Your payment code for receiving payment')
489
+ .option('-s, --elo-stake <n>', 'ELO points to stake (as acceptor)')
490
+ .action(async (server: string, proposalId: string, options: AcceptOptions) => {
491
+ try {
492
+ const client = new AgentChatClient({ server, identity: options.identity });
493
+ await client.connect();
494
+
495
+ const eloStake = options.eloStake ? parseInt(options.eloStake) : undefined;
496
+ const response = await client.accept(proposalId, options.paymentCode, eloStake);
497
+
498
+ console.log('Proposal accepted:');
499
+ console.log(` Proposal ID: ${response.proposal_id}`);
500
+ console.log(` Status: ${response.status}`);
501
+ if (response.proposer_stake) console.log(` Proposer Stake: ${response.proposer_stake} ELO`);
502
+ if (response.acceptor_stake) console.log(` Your Stake: ${response.acceptor_stake} ELO`);
503
+
504
+ client.disconnect();
505
+ process.exit(0);
506
+ } catch (err) {
507
+ const error = err as Error;
508
+ console.error('Error:', error.message);
509
+ process.exit(1);
510
+ }
511
+ });
512
+
513
+ // Reject proposal command
514
+ program
515
+ .command('reject <server> <proposal_id>')
516
+ .description('Reject a proposal')
517
+ .option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
518
+ .option('-r, --reason <reason>', 'Reason for rejection')
519
+ .action(async (server: string, proposalId: string, options: RejectOptions) => {
520
+ try {
521
+ const client = new AgentChatClient({ server, identity: options.identity });
522
+ await client.connect();
523
+
524
+ const response = await client.reject(proposalId, options.reason);
525
+
526
+ console.log('Proposal rejected:');
527
+ console.log(` Proposal ID: ${response.proposal_id}`);
528
+ console.log(` Status: ${response.status}`);
529
+
530
+ client.disconnect();
531
+ process.exit(0);
532
+ } catch (err) {
533
+ const error = err as Error;
534
+ console.error('Error:', error.message);
535
+ process.exit(1);
536
+ }
537
+ });
538
+
539
+ // Complete proposal command
540
+ program
541
+ .command('complete <server> <proposal_id>')
542
+ .description('Mark a proposal as complete')
543
+ .option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
544
+ .option('-p, --proof <proof>', 'Proof of completion (tx hash, URL, etc)')
545
+ .action(async (server: string, proposalId: string, options: CompleteOptions) => {
546
+ try {
547
+ const client = new AgentChatClient({ server, identity: options.identity });
548
+ await client.connect();
549
+
550
+ const response = await client.complete(proposalId, options.proof);
551
+
552
+ console.log('Proposal completed:');
553
+ console.log(` Proposal ID: ${response.proposal_id}`);
554
+ console.log(` Status: ${response.status}`);
555
+
556
+ client.disconnect();
557
+ process.exit(0);
558
+ } catch (err) {
559
+ const error = err as Error;
560
+ console.error('Error:', error.message);
561
+ process.exit(1);
562
+ }
563
+ });
564
+
565
+ // Dispute proposal command
566
+ program
567
+ .command('dispute <server> <proposal_id> <reason>')
568
+ .description('Dispute a proposal')
569
+ .option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
570
+ .action(async (server: string, proposalId: string, reason: string, options: DisputeOptions) => {
571
+ try {
572
+ const client = new AgentChatClient({ server, identity: options.identity });
573
+ await client.connect();
574
+
575
+ const response = await client.dispute(proposalId, reason);
576
+
577
+ console.log('Proposal disputed:');
578
+ console.log(` Proposal ID: ${response.proposal_id}`);
579
+ console.log(` Status: ${response.status}`);
580
+ console.log(` Reason: ${reason}`);
581
+
582
+ client.disconnect();
583
+ process.exit(0);
584
+ } catch (err) {
585
+ const error = err as Error;
586
+ console.error('Error:', error.message);
587
+ process.exit(1);
588
+ }
589
+ });
590
+
591
+ // Verify agent identity command
592
+ program
593
+ .command('verify <server> <agent>')
594
+ .description('Verify another agent\'s identity via challenge-response')
595
+ .option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
596
+ .action(async (server: string, agent: string, options: VerifyOptions) => {
597
+ try {
598
+ const client = new AgentChatClient({ server, identity: options.identity });
599
+ await client.connect();
600
+
601
+ console.log(`Verifying identity of ${agent}...`);
602
+
603
+ const result = await client.verify(agent);
604
+
605
+ if (result.verified) {
606
+ console.log('Identity verified!');
607
+ console.log(` Agent: ${result.agent}`);
608
+ console.log(` Public Key:`);
609
+ console.log(result.pubkey.split('\n').map((line: string) => ` ${line}`).join('\n'));
610
+ } else {
611
+ console.log('Verification failed!');
612
+ console.log(` Target: ${result.target}`);
613
+ console.log(` Reason: ${result.reason}`);
614
+ }
615
+
616
+ client.disconnect();
617
+ process.exit(result.verified ? 0 : 1);
618
+ } catch (err) {
619
+ const error = err as Error;
620
+ console.error('Error:', error.message);
621
+ process.exit(1);
622
+ }
623
+ });
624
+
625
+ // Identity management command
626
+ program
627
+ .command('identity')
628
+ .description('Manage agent identity (Ed25519 keypair)')
629
+ .option('-g, --generate', 'Generate new keypair')
630
+ .option('-s, --show', 'Show current identity')
631
+ .option('-e, --export', 'Export public key for sharing (JSON to stdout)')
632
+ .option('-r, --rotate', 'Rotate to new keypair (signs new key with old key)')
633
+ .option('--verify-chain', 'Verify the rotation chain')
634
+ .option('--revoke [reason]', 'Generate signed revocation notice (outputs JSON)')
635
+ .option('--verify-revocation <file>', 'Verify a revocation notice file')
636
+ .option('-f, --file <path>', 'Identity file path', DEFAULT_IDENTITY_PATH)
637
+ .option('-n, --name <name>', 'Agent name (for --generate)', `agent-${process.pid}`)
638
+ .option('--force', 'Overwrite existing identity')
639
+ .action(async (options: IdentityOptions) => {
640
+ try {
641
+ if (options.generate) {
642
+ // Check if identity already exists
643
+ const exists = await Identity.exists(options.file);
644
+ if (exists && !options.force) {
645
+ console.error(`Identity already exists at ${options.file}`);
646
+ console.error('Use --force to overwrite');
647
+ process.exit(1);
648
+ }
649
+
650
+ // Generate new identity
651
+ const identity = Identity.generate(options.name);
652
+ await identity.save(options.file);
653
+
654
+ console.log('Generated new identity:');
655
+ console.log(` Name: ${identity.name}`);
656
+ console.log(` Fingerprint: ${identity.getFingerprint()}`);
657
+ console.log(` Agent ID: ${identity.getAgentId()}`);
658
+ console.log(` Saved to: ${options.file}`);
659
+
660
+ } else if (options.show) {
661
+ // Load and display identity
662
+ const identity = await Identity.load(options.file);
663
+
664
+ console.log('Current identity:');
665
+ console.log(` Name: ${identity.name}`);
666
+ console.log(` Fingerprint: ${identity.getFingerprint()}`);
667
+ console.log(` Agent ID: ${identity.getAgentId()}`);
668
+ console.log(` Created: ${identity.created}`);
669
+ console.log(` File: ${options.file}`);
670
+
671
+ } else if (options.export) {
672
+ // Export public key info
673
+ const identity = await Identity.load(options.file);
674
+ console.log(JSON.stringify(identity.export(), null, 2));
675
+
676
+ } else if (options.rotate) {
677
+ // Rotate to new keypair
678
+ const identity = await Identity.load(options.file);
679
+ const oldAgentId = identity.getAgentId();
680
+ const oldFingerprint = identity.getFingerprint();
681
+
682
+ console.log('Rotating identity...');
683
+ console.log(` Old Agent ID: ${oldAgentId}`);
684
+ console.log(` Old Fingerprint: ${oldFingerprint}`);
685
+
686
+ const record = identity.rotate();
687
+ await identity.save(options.file);
688
+
689
+ console.log('');
690
+ console.log('Rotation complete:');
691
+ console.log(` New Agent ID: ${identity.getAgentId()}`);
692
+ console.log(` New Fingerprint: ${identity.getFingerprint()}`);
693
+ console.log(` Total rotations: ${identity.rotations.length}`);
694
+ console.log('');
695
+ console.log('The new key has been signed by the old key for chain of custody.');
696
+ console.log('Share the rotation record to prove key continuity.');
697
+
698
+ } else if (options.verifyChain) {
699
+ // Verify rotation chain
700
+ const identity = await Identity.load(options.file);
701
+
702
+ if (identity.rotations.length === 0) {
703
+ console.log('No rotations to verify (original identity).');
704
+ console.log(` Agent ID: ${identity.getAgentId()}`);
705
+ process.exit(0);
706
+ }
707
+
708
+ console.log(`Verifying rotation chain (${identity.rotations.length} rotation(s))...`);
709
+ const result = identity.verifyRotationChain();
710
+
711
+ if (result.valid) {
712
+ console.log('Chain verified successfully!');
713
+ console.log(` Original Agent ID: ${identity.getOriginalAgentId()}`);
714
+ console.log(` Current Agent ID: ${identity.getAgentId()}`);
715
+ console.log(` Rotations: ${identity.rotations.length}`);
716
+ } else {
717
+ console.error('Chain verification FAILED:');
718
+ for (const error of result.errors) {
719
+ console.error(` - ${error}`);
720
+ }
721
+ process.exit(1);
722
+ }
723
+
724
+ } else if (options.revoke) {
725
+ // Generate revocation notice
726
+ const identity = await Identity.load(options.file);
727
+ const reason = typeof options.revoke === 'string' ? options.revoke : 'revoked';
728
+
729
+ console.error(`Generating revocation notice for identity...`);
730
+ console.error(` Agent ID: ${identity.getAgentId()}`);
731
+ console.error(` Reason: ${reason}`);
732
+ console.error('');
733
+ console.error('WARNING: Publishing this notice declares your key as untrusted.');
734
+ console.error('');
735
+
736
+ const notice = identity.revoke(reason);
737
+ console.log(JSON.stringify(notice, null, 2));
738
+
739
+ } else if (options.verifyRevocation) {
740
+ // Verify a revocation notice file
741
+ const noticeData = await fs.readFile(options.verifyRevocation, 'utf-8');
742
+ const notice = JSON.parse(noticeData);
743
+
744
+ console.log('Verifying revocation notice...');
745
+ const isValid = Identity.verifyRevocation(notice);
746
+
747
+ if (isValid) {
748
+ console.log('Revocation notice is VALID');
749
+ console.log(` Agent ID: ${notice.agent_id}`);
750
+ console.log(` Fingerprint: ${notice.fingerprint}`);
751
+ console.log(` Reason: ${notice.reason}`);
752
+ console.log(` Timestamp: ${notice.timestamp}`);
753
+ if (notice.original_agent_id) {
754
+ console.log(` Original Agent ID: ${notice.original_agent_id}`);
755
+ }
756
+ } else {
757
+ console.error('Revocation notice is INVALID');
758
+ process.exit(1);
759
+ }
760
+
761
+ } else {
762
+ // Default: show if exists, otherwise show help
763
+ const exists = await Identity.exists(options.file);
764
+ if (exists) {
765
+ const identity = await Identity.load(options.file);
766
+ console.log('Current identity:');
767
+ console.log(` Name: ${identity.name}`);
768
+ console.log(` Fingerprint: ${identity.getFingerprint()}`);
769
+ console.log(` Agent ID: ${identity.getAgentId()}`);
770
+ console.log(` Created: ${identity.created}`);
771
+ if (identity.rotations.length > 0) {
772
+ console.log(` Rotations: ${identity.rotations.length}`);
773
+ console.log(` Original Agent ID: ${identity.getOriginalAgentId()}`);
774
+ }
775
+ } else {
776
+ console.log('No identity found.');
777
+ console.log(`Use --generate to create one at ${options.file}`);
778
+ }
779
+ }
780
+
781
+ process.exit(0);
782
+ } catch (err) {
783
+ const error = err as Error;
784
+ console.error('Error:', error.message);
785
+ process.exit(1);
786
+ }
787
+ });
788
+
789
+ // Daemon command
790
+ program
791
+ .command('daemon [server]')
792
+ .description('Run persistent listener daemon with file-based inbox/outbox')
793
+ .option('-n, --name <name>', 'Daemon instance name (allows multiple daemons)', DEFAULT_INSTANCE)
794
+ .option('-i, --identity <file>', 'Path to identity file', DEFAULT_IDENTITY_PATH)
795
+ .option('-c, --channels <channels...>', 'Channels to join', DEFAULT_CHANNELS)
796
+ .option('-b, --background', 'Run in background (daemonize)')
797
+ .option('-s, --status', 'Show daemon status')
798
+ .option('-l, --list', 'List all daemon instances')
799
+ .option('--stop', 'Stop the daemon')
800
+ .option('--stop-all', 'Stop all running daemons')
801
+ .option('--max-reconnect-time <minutes>', 'Max time to attempt reconnection (default: 10 minutes)', '10')
802
+ .action(async (server: string | undefined, options: DaemonOptions) => {
803
+ try {
804
+ const instanceName = options.name;
805
+ const paths = getDaemonPaths(instanceName);
806
+
807
+ // Security check for operations that create files (not for status/list/stop)
808
+ const needsSafetyCheck = !options.list && !options.status && !options.stop && !options.stopAll;
809
+ if (needsSafetyCheck) {
810
+ enforceDirectorySafety(process.cwd(), { allowWarnings: true, silent: false });
811
+ }
812
+
813
+ // List all daemons
814
+ if (options.list) {
815
+ const instances = await listDaemons();
816
+ if (instances.length === 0) {
817
+ console.log('No daemon instances found');
818
+ } else {
819
+ console.log('Daemon instances:');
820
+ for (const inst of instances) {
821
+ const status = inst.running ? `running (PID: ${inst.pid})` : 'stopped';
822
+ console.log(` ${inst.name}: ${status}`);
823
+ }
824
+ }
825
+ process.exit(0);
826
+ }
827
+
828
+ // Stop all daemons
829
+ if (options.stopAll) {
830
+ const results = await stopAllDaemons();
831
+ if (results.length === 0) {
832
+ console.log('No running daemons to stop');
833
+ } else {
834
+ for (const r of results) {
835
+ console.log(`Stopped ${r.instance} (PID: ${r.pid})`);
836
+ }
837
+ }
838
+ process.exit(0);
839
+ }
840
+
841
+ // Status check
842
+ if (options.status) {
843
+ const status = await getDaemonStatus(instanceName);
844
+ if (!status.running) {
845
+ console.log(`Daemon '${instanceName}' is not running`);
846
+ } else {
847
+ console.log(`Daemon '${instanceName}' is running:`);
848
+ console.log(` PID: ${status.pid}`);
849
+ console.log(` Inbox: ${status.inboxPath} (${status.inboxLines} messages)`);
850
+ console.log(` Outbox: ${status.outboxPath}`);
851
+ console.log(` Log: ${status.logPath}`);
852
+ if (status.lastMessage) {
853
+ console.log(` Last message: ${JSON.stringify(status.lastMessage).substring(0, 80)}...`);
854
+ }
855
+ }
856
+ process.exit(0);
857
+ }
858
+
859
+ // Stop daemon
860
+ if (options.stop) {
861
+ const result = await stopDaemon(instanceName);
862
+ if (result.stopped) {
863
+ console.log(`Daemon '${instanceName}' stopped (PID: ${result.pid})`);
864
+ } else {
865
+ console.log(result.reason);
866
+ }
867
+ process.exit(0);
868
+ }
869
+
870
+ // Start daemon requires server
871
+ if (!server) {
872
+ console.error('Error: server URL required to start daemon');
873
+ console.error('Usage: agentchat daemon ws://localhost:6667 --name myagent');
874
+ process.exit(1);
875
+ }
876
+
877
+ // Check if already running
878
+ const status = await isDaemonRunning(instanceName);
879
+ if (status.running) {
880
+ console.error(`Daemon '${instanceName}' already running (PID: ${status.pid})`);
881
+ console.error('Use --stop to stop it first, or use a different --name');
882
+ process.exit(1);
883
+ }
884
+
885
+ // Background mode
886
+ if (options.background) {
887
+ const { spawn } = await import('child_process');
888
+
889
+ // Re-run ourselves without --background
890
+ const args = process.argv.slice(2).filter(a => a !== '-b' && a !== '--background');
891
+
892
+ const child = spawn(process.execPath, [process.argv[1], ...args], {
893
+ detached: true,
894
+ stdio: 'ignore'
895
+ });
896
+
897
+ child.unref();
898
+ console.log(`Daemon '${instanceName}' started in background (PID: ${child.pid})`);
899
+ console.log(` Inbox: ${paths.inbox}`);
900
+ console.log(` Outbox: ${paths.outbox}`);
901
+ console.log(` Log: ${paths.log}`);
902
+ console.log('');
903
+ console.log('To send messages, append to outbox:');
904
+ console.log(` echo '{"to":"#general","content":"Hello!"}' >> ${paths.outbox}`);
905
+ console.log('');
906
+ console.log('To read messages:');
907
+ console.log(` tail -f ${paths.inbox}`);
908
+ process.exit(0);
909
+ }
910
+
911
+ // Foreground mode
912
+ console.log('Starting daemon in foreground (Ctrl+C to stop)...');
913
+ console.log(` Instance: ${instanceName}`);
914
+ console.log(` Server: ${server}`);
915
+ console.log(` Identity: ${options.identity}`);
916
+
917
+ // Normalize channels: handle both comma-separated and space-separated formats
918
+ const normalizedChannels = options.channels
919
+ .flatMap(c => c.split(','))
920
+ .map(c => c.trim())
921
+ .filter(c => c.length > 0)
922
+ .map(c => c.startsWith('#') ? c : '#' + c);
923
+
924
+ console.log(` Channels: ${normalizedChannels.join(', ')}`);
925
+ console.log('');
926
+
927
+ const daemon = new AgentChatDaemon({
928
+ server,
929
+ name: instanceName,
930
+ identity: options.identity,
931
+ channels: normalizedChannels,
932
+ maxReconnectTime: parseInt(options.maxReconnectTime) * 60 * 1000 // Convert minutes to ms
933
+ });
934
+
935
+ await daemon.start();
936
+
937
+ // Keep process alive
938
+ process.stdin.resume();
939
+
940
+ } catch (err) {
941
+ const error = err as Error;
942
+ console.error('Error:', error.message);
943
+ process.exit(1);
944
+ }
945
+ });
946
+
947
+ // Receipts command
948
+ program
949
+ .command('receipts [action]')
950
+ .description('Manage completion receipts for portable reputation')
951
+ .option('-f, --format <format>', 'Export format (json, yaml)', 'json')
952
+ .option('-i, --identity <file>', 'Path to identity file', DEFAULT_IDENTITY_PATH)
953
+ .option('--file <path>', 'Receipts file path', DEFAULT_RECEIPTS_PATH)
954
+ .action(async (action: string | undefined, options: ReceiptsOptions) => {
955
+ try {
956
+ const store = new ReceiptStore(options.file);
957
+ const receipts = await store.getAll();
958
+
959
+ // Load identity to get agent ID for filtering
960
+ let agentId: string | null = null;
961
+ try {
962
+ const identity = await Identity.load(options.identity);
963
+ agentId = identity.getAgentId();
964
+ } catch {
965
+ // Identity not available, show all receipts
966
+ }
967
+
968
+ switch (action) {
969
+ case 'list':
970
+ if (receipts.length === 0) {
971
+ console.log('No receipts found.');
972
+ console.log(`\nReceipts are stored in: ${options.file}`);
973
+ console.log('Receipts are automatically saved when COMPLETE messages are received via daemon.');
974
+ } else {
975
+ console.log(`Found ${receipts.length} receipt(s):\n`);
976
+ for (const r of receipts) {
977
+ console.log(` Proposal: ${r.proposal_id || 'unknown'}`);
978
+ console.log(` Completed: ${r.completed_at ? new Date(r.completed_at).toISOString() : 'unknown'}`);
979
+ console.log(` By: ${r.completed_by || 'unknown'}`);
980
+ if (r.proof) console.log(` Proof: ${r.proof}`);
981
+ if (r.proposal?.task) console.log(` Task: ${r.proposal.task}`);
982
+ if (r.proposal?.amount) console.log(` Amount: ${r.proposal.amount} ${r.proposal.currency || ''}`);
983
+ console.log('');
984
+ }
985
+ }
986
+ break;
987
+
988
+ case 'export':
989
+ const output = await store.export(options.format, agentId);
990
+ console.log(output);
991
+ break;
992
+
993
+ case 'summary':
994
+ const stats = await store.getStats(agentId);
995
+ console.log('Receipt Summary:');
996
+ console.log(` Total receipts: ${stats.count}`);
997
+
998
+ if (stats.count > 0) {
999
+ if (stats.dateRange) {
1000
+ console.log(` Date range: ${stats.dateRange.oldest} to ${stats.dateRange.newest}`);
1001
+ }
1002
+
1003
+ if (stats.counterparties.length > 0) {
1004
+ console.log(` Counterparties (${stats.counterparties.length}):`);
1005
+ for (const cp of stats.counterparties) {
1006
+ console.log(` - ${cp}`);
1007
+ }
1008
+ }
1009
+
1010
+ const currencies = Object.entries(stats.currencies);
1011
+ if (currencies.length > 0) {
1012
+ console.log(' By currency:');
1013
+ for (const [currency, data] of currencies) {
1014
+ const currencyData = data as { count: number; totalAmount: number };
1015
+ if (currency !== 'unknown') {
1016
+ console.log(` ${currency}: ${currencyData.count} receipts, ${currencyData.totalAmount} total`);
1017
+ } else {
1018
+ console.log(` (no currency): ${currencyData.count} receipts`);
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+
1024
+ console.log(`\nReceipts file: ${options.file}`);
1025
+ if (agentId) {
1026
+ console.log(`Filtered for agent: @${agentId}`);
1027
+ }
1028
+ break;
1029
+
1030
+ default:
1031
+ // Default: show help
1032
+ console.log('Receipt Management Commands:');
1033
+ console.log('');
1034
+ console.log(' agentchat receipts list List all stored receipts');
1035
+ console.log(' agentchat receipts export Export receipts (--format json|yaml)');
1036
+ console.log(' agentchat receipts summary Show receipt statistics');
1037
+ console.log('');
1038
+ console.log('Options:');
1039
+ console.log(' --format <format> Export format: json (default) or yaml');
1040
+ console.log(' --identity <file> Identity file for filtering by agent');
1041
+ console.log(' --file <path> Custom receipts file path');
1042
+ console.log('');
1043
+ console.log(`Receipts are stored in: ${DEFAULT_RECEIPTS_PATH}`);
1044
+ console.log('');
1045
+ console.log('Receipts are automatically saved by the daemon when');
1046
+ console.log('COMPLETE messages are received for proposals you are party to.');
1047
+ }
1048
+
1049
+ process.exit(0);
1050
+ } catch (err) {
1051
+ const error = err as Error;
1052
+ console.error('Error:', error.message);
1053
+ process.exit(1);
1054
+ }
1055
+ });
1056
+
1057
+ // Ratings command
1058
+ program
1059
+ .command('ratings [agent]')
1060
+ .description('View and manage ELO-based reputation ratings')
1061
+ .option('-i, --identity <file>', 'Path to identity file', DEFAULT_IDENTITY_PATH)
1062
+ .option('--file <path>', 'Ratings file path', DEFAULT_RATINGS_PATH)
1063
+ .option('-e, --export', 'Export all ratings as JSON')
1064
+ .option('-r, --recalculate', 'Recalculate ratings from receipt history')
1065
+ .option('-l, --leaderboard [n]', 'Show top N agents by rating')
1066
+ .option('-s, --stats', 'Show rating system statistics')
1067
+ .action(async (agent: string | undefined, options: RatingsOptions) => {
1068
+ try {
1069
+ const store = new ReputationStore(options.file);
1070
+
1071
+ // Export all ratings
1072
+ if (options.export) {
1073
+ const ratings = await store.exportRatings();
1074
+ console.log(JSON.stringify(ratings, null, 2));
1075
+ process.exit(0);
1076
+ }
1077
+
1078
+ // Recalculate from receipts
1079
+ if (options.recalculate) {
1080
+ console.log('Recalculating ratings from receipt history...');
1081
+ const receipts = await readReceipts();
1082
+ const ratings = await store.recalculateFromReceipts(receipts);
1083
+ const count = Object.keys(ratings).length;
1084
+ console.log(`Processed ${receipts.length} receipts, updated ${count} agents.`);
1085
+
1086
+ const stats = await store.getStats();
1087
+ console.log(`\nRating Statistics:`);
1088
+ console.log(` Total agents: ${stats.totalAgents}`);
1089
+ console.log(` Average rating: ${stats.averageRating}`);
1090
+ console.log(` Highest: ${stats.highestRating}`);
1091
+ console.log(` Lowest: ${stats.lowestRating}`);
1092
+ process.exit(0);
1093
+ }
1094
+
1095
+ // Show leaderboard
1096
+ if (options.leaderboard) {
1097
+ const limit = typeof options.leaderboard === 'string'
1098
+ ? parseInt(options.leaderboard)
1099
+ : 10;
1100
+ const leaderboard = await store.getLeaderboard(limit);
1101
+
1102
+ if (leaderboard.length === 0) {
1103
+ console.log('No ratings recorded yet.');
1104
+ } else {
1105
+ console.log(`Top ${leaderboard.length} agents by rating:\n`);
1106
+ leaderboard.forEach((entry, i) => {
1107
+ console.log(` ${i + 1}. ${entry.agentId}`);
1108
+ console.log(` Rating: ${entry.rating} | Transactions: ${entry.transactions}`);
1109
+ });
1110
+ }
1111
+ process.exit(0);
1112
+ }
1113
+
1114
+ // Show stats
1115
+ if (options.stats) {
1116
+ const stats = await store.getStats();
1117
+ console.log('Rating System Statistics:');
1118
+ console.log(` Total agents: ${stats.totalAgents}`);
1119
+ console.log(` Total transactions: ${stats.totalTransactions}`);
1120
+ console.log(` Average rating: ${stats.averageRating}`);
1121
+ console.log(` Highest rating: ${stats.highestRating}`);
1122
+ console.log(` Lowest rating: ${stats.lowestRating}`);
1123
+ console.log(` Default rating: ${DEFAULT_RATING}`);
1124
+ console.log(`\nRatings file: ${options.file}`);
1125
+ process.exit(0);
1126
+ }
1127
+
1128
+ // Show specific agent's rating
1129
+ if (agent) {
1130
+ const rating = await store.getRating(agent);
1131
+ console.log(`Rating for ${rating.agentId}:`);
1132
+ console.log(` Rating: ${rating.rating}${rating.isNew ? ' (new agent)' : ''}`);
1133
+ console.log(` Transactions: ${rating.transactions}`);
1134
+ if (rating.updated) {
1135
+ console.log(` Last updated: ${rating.updated}`);
1136
+ }
1137
+
1138
+ // Show K-factor
1139
+ const kFactor = await store.getAgentKFactor(agent);
1140
+ console.log(` K-factor: ${kFactor}`);
1141
+ process.exit(0);
1142
+ }
1143
+
1144
+ // Default: show own rating (from identity)
1145
+ let agentId: string | null = null;
1146
+ try {
1147
+ const identity = await Identity.load(options.identity);
1148
+ agentId = `@${identity.getAgentId()}`;
1149
+ } catch {
1150
+ // No identity available
1151
+ }
1152
+
1153
+ if (agentId) {
1154
+ const rating = await store.getRating(agentId);
1155
+ console.log(`Your rating (${agentId}):`);
1156
+ console.log(` Rating: ${rating.rating}${rating.isNew ? ' (new agent)' : ''}`);
1157
+ console.log(` Transactions: ${rating.transactions}`);
1158
+ if (rating.updated) {
1159
+ console.log(` Last updated: ${rating.updated}`);
1160
+ }
1161
+ const kFactor = await store.getAgentKFactor(agentId);
1162
+ console.log(` K-factor: ${kFactor}`);
1163
+ } else {
1164
+ // Show help
1165
+ console.log('ELO-based Reputation Rating System');
1166
+ console.log('');
1167
+ console.log('Usage:');
1168
+ console.log(' agentchat ratings Show your rating (requires identity)');
1169
+ console.log(' agentchat ratings <agent-id> Show specific agent rating');
1170
+ console.log(' agentchat ratings --leaderboard [n] Show top N agents');
1171
+ console.log(' agentchat ratings --stats Show system statistics');
1172
+ console.log(' agentchat ratings --export Export all ratings as JSON');
1173
+ console.log(' agentchat ratings --recalculate Rebuild ratings from receipts');
1174
+ console.log('');
1175
+ console.log('How it works:');
1176
+ console.log(` - New agents start at ${DEFAULT_RATING}`);
1177
+ console.log(' - On COMPLETE: both parties gain rating');
1178
+ console.log(' - On DISPUTE: at-fault party loses rating');
1179
+ console.log(' - Completing with higher-rated agents = more gain');
1180
+ console.log(' - K-factor: 32 (new) -> 24 (intermediate) -> 16 (established)');
1181
+ console.log('');
1182
+ console.log(`Ratings file: ${options.file}`);
1183
+ }
1184
+
1185
+ process.exit(0);
1186
+ } catch (err) {
1187
+ const error = err as Error;
1188
+ console.error('Error:', error.message);
1189
+ process.exit(1);
1190
+ }
1191
+ });
1192
+
1193
+ // Skills command - skill discovery and announcement
1194
+ program
1195
+ .command('skills <action> [server]')
1196
+ .description('Manage skill discovery: announce, search, list')
1197
+ .option('-c, --capability <capability>', 'Skill capability for announce/search')
1198
+ .option('-r, --rate <rate>', 'Rate/price for the skill', parseFloat)
1199
+ .option('--currency <currency>', 'Currency for rate (e.g., SOL, TEST)', 'TEST')
1200
+ .option('--description <desc>', 'Description of skill')
1201
+ .option('-f, --file <file>', 'YAML file with skill definitions')
1202
+ .option('-i, --identity <file>', 'Path to identity file', DEFAULT_IDENTITY_PATH)
1203
+ .option('--max-rate <rate>', 'Maximum rate for search', parseFloat)
1204
+ .option('-l, --limit <n>', 'Limit search results', parseInt)
1205
+ .option('--json', 'Output as JSON')
1206
+ .action(async (action: string, server: string | undefined, options: SkillsOptions) => {
1207
+ try {
1208
+ if (action === 'announce') {
1209
+ if (!server) {
1210
+ console.error('Server URL required: agentchat skills announce <server>');
1211
+ process.exit(1);
1212
+ }
1213
+
1214
+ let skills: Array<{ capability: string; rate?: number; currency?: string; description?: string }> = [];
1215
+
1216
+ // Load from file if provided
1217
+ if (options.file) {
1218
+ const yaml = await import('js-yaml');
1219
+ const content = await fs.readFile(options.file, 'utf-8');
1220
+ const data = yaml.default.load(content) as { skills?: unknown[] } | unknown;
1221
+ skills = (data as { skills?: unknown[] }).skills as typeof skills || [data as typeof skills[0]];
1222
+ } else if (options.capability) {
1223
+ // Single skill from CLI args
1224
+ skills = [{
1225
+ capability: options.capability,
1226
+ rate: options.rate,
1227
+ currency: options.currency,
1228
+ description: options.description
1229
+ }];
1230
+ } else {
1231
+ console.error('Either --file or --capability required');
1232
+ process.exit(1);
1233
+ }
1234
+
1235
+ // Load identity and sign
1236
+ const identity = await Identity.load(options.identity);
1237
+ const skillsContent = JSON.stringify(skills);
1238
+ const sig = identity.sign(skillsContent);
1239
+
1240
+ // Connect and announce
1241
+ const client = new AgentChatClient({ server, identity: options.identity });
1242
+ await client.connect();
1243
+
1244
+ await client.sendRaw({
1245
+ type: 'REGISTER_SKILLS',
1246
+ skills,
1247
+ sig: sig.toString('base64')
1248
+ });
1249
+
1250
+ // Wait for response
1251
+ const response = await new Promise<{ type: string; skills_count?: number; agent_id?: string; message?: string }>((resolve, reject) => {
1252
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
1253
+ client.on('message', (msg: unknown) => {
1254
+ const message = msg as { type: string; skills_count?: number; agent_id?: string; message?: string };
1255
+ if (message.type === 'SKILLS_REGISTERED' || message.type === 'ERROR') {
1256
+ clearTimeout(timeout);
1257
+ resolve(message);
1258
+ }
1259
+ });
1260
+ });
1261
+
1262
+ client.disconnect();
1263
+
1264
+ if (response.type === 'ERROR') {
1265
+ console.error('Error:', response.message);
1266
+ process.exit(1);
1267
+ }
1268
+
1269
+ console.log(`Registered ${response.skills_count} skill(s) for ${response.agent_id}`);
1270
+
1271
+ } else if (action === 'search') {
1272
+ if (!server) {
1273
+ console.error('Server URL required: agentchat skills search <server>');
1274
+ process.exit(1);
1275
+ }
1276
+
1277
+ const query: { capability?: string; max_rate?: number; currency?: string; limit?: number } = {};
1278
+ if (options.capability) query.capability = options.capability;
1279
+ if (options.maxRate !== undefined) query.max_rate = options.maxRate;
1280
+ if (options.currency) query.currency = options.currency;
1281
+ if (options.limit) query.limit = options.limit;
1282
+
1283
+ // Connect and search
1284
+ const client = new AgentChatClient({ server });
1285
+ await client.connect();
1286
+
1287
+ const queryId = `q_${Date.now()}`;
1288
+ await client.sendRaw({
1289
+ type: 'SEARCH_SKILLS',
1290
+ query,
1291
+ query_id: queryId
1292
+ });
1293
+
1294
+ // Wait for response
1295
+ const response = await new Promise<{ type: string; results: Array<{ agent_id: string; capability: string; rate?: number; currency?: string; description?: string }>; total: number; message?: string }>((resolve, reject) => {
1296
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
1297
+ client.on('message', (msg: unknown) => {
1298
+ const message = msg as typeof response;
1299
+ if (message.type === 'SEARCH_RESULTS' || message.type === 'ERROR') {
1300
+ clearTimeout(timeout);
1301
+ resolve(message);
1302
+ }
1303
+ });
1304
+ });
1305
+
1306
+ client.disconnect();
1307
+
1308
+ if (response.type === 'ERROR') {
1309
+ console.error('Error:', response.message);
1310
+ process.exit(1);
1311
+ }
1312
+
1313
+ if (options.json) {
1314
+ console.log(JSON.stringify(response.results, null, 2));
1315
+ } else {
1316
+ console.log(`Found ${response.results.length} skill(s) (${response.total} total):\n`);
1317
+ for (const skill of response.results) {
1318
+ const rate = skill.rate !== undefined ? `${skill.rate} ${skill.currency || ''}` : 'negotiable';
1319
+ console.log(` ${skill.agent_id}`);
1320
+ console.log(` Capability: ${skill.capability}`);
1321
+ console.log(` Rate: ${rate}`);
1322
+ if (skill.description) console.log(` Description: ${skill.description}`);
1323
+ console.log('');
1324
+ }
1325
+ }
1326
+
1327
+ } else if (action === 'list') {
1328
+ // List own registered skills (if server supports it)
1329
+ console.error('List action not yet implemented');
1330
+ process.exit(1);
1331
+
1332
+ } else {
1333
+ console.error(`Unknown action: ${action}`);
1334
+ console.error('Valid actions: announce, search, list');
1335
+ process.exit(1);
1336
+ }
1337
+
1338
+ } catch (err) {
1339
+ const error = err as Error;
1340
+ console.error('Error:', error.message);
1341
+ process.exit(1);
1342
+ }
1343
+ });
1344
+
1345
+ // Discover command - find public AgentChat servers
1346
+ program
1347
+ .command('discover')
1348
+ .description('Discover available AgentChat servers')
1349
+ .option('--add <url>', 'Add a server to the directory')
1350
+ .option('--remove <url>', 'Remove a server from the directory')
1351
+ .option('--name <name>', 'Server name (for --add)')
1352
+ .option('--description <desc>', 'Server description (for --add)')
1353
+ .option('--region <region>', 'Server region (for --add)')
1354
+ .option('--online', 'Only show online servers')
1355
+ .option('--json', 'Output as JSON')
1356
+ .option('--no-check', 'List servers without health check')
1357
+ .option('--directory <path>', 'Custom directory file path', DEFAULT_DIRECTORY_PATH)
1358
+ .action(async (options: DiscoverOptions) => {
1359
+ try {
1360
+ const directory = new ServerDirectory({ directoryPath: options.directory });
1361
+ await directory.load();
1362
+
1363
+ // Add server
1364
+ if (options.add) {
1365
+ await directory.addServer({
1366
+ url: options.add,
1367
+ name: options.name || options.add,
1368
+ description: options.description || '',
1369
+ region: options.region || 'unknown'
1370
+ });
1371
+ console.log(`Added server: ${options.add}`);
1372
+ process.exit(0);
1373
+ }
1374
+
1375
+ // Remove server
1376
+ if (options.remove) {
1377
+ await directory.removeServer(options.remove);
1378
+ console.log(`Removed server: ${options.remove}`);
1379
+ process.exit(0);
1380
+ }
1381
+
1382
+ // List/discover servers
1383
+ let servers;
1384
+ if (options.check === false) {
1385
+ servers = directory.list().map(s => ({ ...s, status: 'unknown' as const }));
1386
+ } else {
1387
+ console.error('Checking server status...');
1388
+ servers = await directory.discover({ onlineOnly: options.online });
1389
+ }
1390
+
1391
+ if (options.json) {
1392
+ console.log(JSON.stringify(servers, null, 2));
1393
+ } else {
1394
+ if (servers.length === 0) {
1395
+ console.log('No servers found.');
1396
+ } else {
1397
+ console.log(`\nFound ${servers.length} server(s):\n`);
1398
+ for (const server of servers) {
1399
+ const statusIcon = server.status === 'online' ? '\u2713' :
1400
+ server.status === 'offline' ? '\u2717' : '?';
1401
+ console.log(` ${statusIcon} ${server.name}`);
1402
+ console.log(` URL: ${server.url}`);
1403
+ console.log(` Status: ${server.status}`);
1404
+ if (server.description) {
1405
+ console.log(` Description: ${server.description}`);
1406
+ }
1407
+ if (server.region) {
1408
+ console.log(` Region: ${server.region}`);
1409
+ }
1410
+ const serverWithHealth = server as { health?: { agents?: { connected?: number }; uptime_seconds?: number }; error?: string };
1411
+ if (serverWithHealth.health) {
1412
+ console.log(` Agents: ${serverWithHealth.health.agents?.connected || 0}`);
1413
+ console.log(` Uptime: ${serverWithHealth.health.uptime_seconds || 0}s`);
1414
+ }
1415
+ if (serverWithHealth.error) {
1416
+ console.log(` Error: ${serverWithHealth.error}`);
1417
+ }
1418
+ console.log('');
1419
+ }
1420
+ }
1421
+ console.log(`Directory: ${options.directory}`);
1422
+ }
1423
+
1424
+ process.exit(0);
1425
+ } catch (err) {
1426
+ const error = err as Error;
1427
+ console.error('Error:', error.message);
1428
+ process.exit(1);
1429
+ }
1430
+ });
1431
+
1432
+ // Deploy command
1433
+ program
1434
+ .command('deploy')
1435
+ .description('Generate deployment files for agentchat server')
1436
+ .option('--provider <provider>', 'Deployment target (docker, akash)', 'docker')
1437
+ .option('--config <file>', 'Deploy configuration file (deploy.yaml)')
1438
+ .option('--output <dir>', 'Output directory for generated files', '.')
1439
+ .option('-p, --port <port>', 'Server port')
1440
+ .option('-n, --name <name>', 'Server/container name')
1441
+ .option('--volumes', 'Enable volume mounts for data persistence')
1442
+ .option('--no-health-check', 'Disable health check configuration')
1443
+ .option('--cert <file>', 'TLS certificate file path')
1444
+ .option('--key <file>', 'TLS private key file path')
1445
+ .option('--network <name>', 'Docker network name')
1446
+ .option('--dockerfile', 'Also generate Dockerfile')
1447
+ .option('--init-config', 'Generate example deploy.yaml config file')
1448
+ // Akash-specific options
1449
+ .option('--generate-wallet', 'Generate a new Akash wallet')
1450
+ .option('--wallet <file>', 'Path to wallet file', AKASH_WALLET_PATH)
1451
+ .option('--balance', 'Check wallet balance')
1452
+ .option('--testnet', 'Use Akash testnet (default)')
1453
+ .option('--mainnet', 'Use Akash mainnet (real funds!)')
1454
+ .option('--create', 'Create deployment on Akash')
1455
+ .option('--status', 'Show deployment status')
1456
+ .option('--close <dseq>', 'Close a deployment by dseq')
1457
+ .option('--generate-sdl', 'Generate SDL file without deploying')
1458
+ .option('--force', 'Overwrite existing wallet')
1459
+ .option('--bids <dseq>', 'Query bids for a deployment')
1460
+ .option('--accept-bid <dseq>', 'Accept a bid (use with --provider-address)')
1461
+ .option('--provider-address <address>', 'Provider address for --accept-bid')
1462
+ .option('--dseq-status <dseq>', 'Get detailed status for a specific deployment')
1463
+ .action(async (options: DeployOptions) => {
1464
+ try {
1465
+ const isAkash = options.provider === 'akash';
1466
+ const akashNetwork = options.mainnet ? 'mainnet' : 'testnet';
1467
+
1468
+ // Akash: Generate wallet
1469
+ if (isAkash && options.generateWallet) {
1470
+ try {
1471
+ const wallet = await generateWallet(akashNetwork, options.wallet);
1472
+ console.log('Generated new Akash wallet:');
1473
+ console.log(` Network: ${wallet.network}`);
1474
+ console.log(` Address: ${wallet.address}`);
1475
+ console.log(` Saved to: ${options.wallet}`);
1476
+ console.log('');
1477
+ console.log('IMPORTANT: Back up your wallet file!');
1478
+ console.log('The mnemonic inside is the only way to recover your funds.');
1479
+ console.log('');
1480
+ if (akashNetwork === 'testnet') {
1481
+ console.log('To get testnet tokens, visit: https://faucet.sandbox-01.aksh.pw/');
1482
+ } else {
1483
+ console.log('To fund your wallet, send AKT to the address above.');
1484
+ }
1485
+ process.exit(0);
1486
+ } catch (err) {
1487
+ const error = err as Error;
1488
+ if (error.message.includes('already exists') && !options.force) {
1489
+ console.error(error.message);
1490
+ process.exit(1);
1491
+ }
1492
+ throw err;
1493
+ }
1494
+ }
1495
+
1496
+ // Akash: Check balance
1497
+ if (isAkash && options.balance) {
1498
+ const result = await checkBalance(options.wallet);
1499
+ console.log('Wallet Balance:');
1500
+ console.log(` Network: ${result.wallet.network}`);
1501
+ console.log(` Address: ${result.wallet.address}`);
1502
+ console.log(` Balance: ${result.balance.akt} AKT (${result.balance.uakt} uakt)`);
1503
+ console.log(` Status: ${result.balance.sufficient ? 'Sufficient for deployment' : 'Insufficient - need at least 5 AKT'}`);
1504
+ process.exit(0);
1505
+ }
1506
+
1507
+ // Akash: Generate SDL only
1508
+ if (isAkash && options.generateSdl) {
1509
+ const sdl = generateAkashSDL({
1510
+ name: options.name,
1511
+ port: options.port ? parseInt(options.port) : undefined
1512
+ });
1513
+ const outputDir = path.resolve(options.output);
1514
+ await fs.mkdir(outputDir, { recursive: true });
1515
+ const sdlPath = path.join(outputDir, 'deploy.yaml');
1516
+ await fs.writeFile(sdlPath, sdl);
1517
+ console.log(`Generated: ${sdlPath}`);
1518
+ console.log('\nThis SDL can be used with the Akash CLI or Console.');
1519
+ process.exit(0);
1520
+ }
1521
+
1522
+ // Akash: Create deployment
1523
+ if (isAkash && options.create) {
1524
+ console.log('Creating Akash deployment...');
1525
+ try {
1526
+ const result = await createDeployment({
1527
+ walletPath: options.wallet,
1528
+ name: options.name,
1529
+ port: options.port ? parseInt(options.port) : undefined
1530
+ });
1531
+ console.log('Deployment created:');
1532
+ console.log(` DSEQ: ${result.dseq}`);
1533
+ console.log(` Status: ${result.status}`);
1534
+ if (result.endpoint) {
1535
+ console.log(` Endpoint: ${result.endpoint}`);
1536
+ }
1537
+ } catch (err) {
1538
+ const error = err as Error;
1539
+ console.error('Deployment failed:', error.message);
1540
+ process.exit(1);
1541
+ }
1542
+ process.exit(0);
1543
+ }
1544
+
1545
+ // Akash: Show status
1546
+ if (isAkash && options.status) {
1547
+ const deployments = await listDeployments(options.wallet);
1548
+ if (deployments.length === 0) {
1549
+ console.log('No active deployments.');
1550
+ } else {
1551
+ console.log('Active deployments:');
1552
+ for (const d of deployments) {
1553
+ console.log(` DSEQ ${d.dseq}: ${d.status} - ${d.endpoint || 'pending'}`);
1554
+ }
1555
+ }
1556
+ process.exit(0);
1557
+ }
1558
+
1559
+ // Akash: Close deployment
1560
+ if (isAkash && options.close) {
1561
+ console.log(`Closing deployment ${options.close}...`);
1562
+ await closeDeployment(options.close, options.wallet);
1563
+ console.log('Deployment closed.');
1564
+ process.exit(0);
1565
+ }
1566
+
1567
+ // Akash: Query bids
1568
+ if (isAkash && options.bids) {
1569
+ console.log(`Querying bids for deployment ${options.bids}...`);
1570
+ const bids = await queryBids(options.bids, options.wallet);
1571
+ if (bids.length === 0) {
1572
+ console.log('No bids received yet.');
1573
+ } else {
1574
+ console.log('Available bids:');
1575
+ for (const b of bids) {
1576
+ const bid = b.bid || {};
1577
+ const price = bid.price?.amount || 'unknown';
1578
+ const state = bid.state || 'unknown';
1579
+ const provider = bid.bidId?.provider || 'unknown';
1580
+ console.log(` Provider: ${provider}`);
1581
+ console.log(` Price: ${price} uakt/block`);
1582
+ console.log(` State: ${state}`);
1583
+ console.log('');
1584
+ }
1585
+ }
1586
+ process.exit(0);
1587
+ }
1588
+
1589
+ // Akash: Accept bid
1590
+ if (isAkash && options.acceptBid) {
1591
+ if (!options.providerAddress) {
1592
+ console.error('Error: --provider-address is required with --accept-bid');
1593
+ process.exit(1);
1594
+ }
1595
+ console.log(`Accepting bid from ${options.providerAddress}...`);
1596
+ const lease = await acceptBid(options.acceptBid, options.providerAddress, options.wallet);
1597
+ console.log('Lease created:');
1598
+ console.log(` DSEQ: ${lease.dseq}`);
1599
+ console.log(` Provider: ${lease.provider}`);
1600
+ console.log(` TX: ${lease.txHash}`);
1601
+ process.exit(0);
1602
+ }
1603
+
1604
+ // Akash: Get detailed deployment status
1605
+ if (isAkash && options.dseqStatus) {
1606
+ console.log(`Getting status for deployment ${options.dseqStatus}...`);
1607
+ const status = await getDeploymentStatus(options.dseqStatus, options.wallet);
1608
+ console.log('Deployment status:');
1609
+ console.log(` DSEQ: ${status.dseq}`);
1610
+ console.log(` Status: ${status.status}`);
1611
+ console.log(` Created: ${status.createdAt}`);
1612
+ if (status.provider) {
1613
+ console.log(` Provider: ${status.provider}`);
1614
+ }
1615
+ if (status.bids) {
1616
+ console.log(` Bids: ${status.bids.length}`);
1617
+ for (const bid of status.bids) {
1618
+ console.log(` - ${bid.provider}: ${bid.price} uakt (${bid.state})`);
1619
+ }
1620
+ }
1621
+ if (status.leaseStatus) {
1622
+ console.log(' Lease Status:', JSON.stringify(status.leaseStatus, null, 2));
1623
+ }
1624
+ if (status.leaseStatusError) {
1625
+ console.log(` Lease Status Error: ${status.leaseStatusError}`);
1626
+ }
1627
+ process.exit(0);
1628
+ }
1629
+
1630
+ // Akash: Default action - show help
1631
+ if (isAkash) {
1632
+ console.log('Akash Deployment Options:');
1633
+ console.log('');
1634
+ console.log(' Setup:');
1635
+ console.log(' --generate-wallet Generate a new wallet');
1636
+ console.log(' --balance Check wallet balance');
1637
+ console.log('');
1638
+ console.log(' Deployment:');
1639
+ console.log(' --generate-sdl Generate SDL file');
1640
+ console.log(' --create Create deployment (auto-accepts best bid)');
1641
+ console.log(' --status Show all deployments');
1642
+ console.log(' --dseq-status <n> Get detailed status for deployment');
1643
+ console.log(' --close <dseq> Close a deployment');
1644
+ console.log('');
1645
+ console.log(' Manual bid selection:');
1646
+ console.log(' --bids <dseq> Query bids for a deployment');
1647
+ console.log(' --accept-bid <dseq> --provider-address <addr>');
1648
+ console.log(' Accept a specific bid');
1649
+ console.log('');
1650
+ console.log(' Options:');
1651
+ console.log(' --testnet Use testnet (default)');
1652
+ console.log(' --mainnet Use mainnet (real AKT)');
1653
+ console.log(' --wallet <file> Custom wallet path');
1654
+ console.log('');
1655
+ console.log('Example workflow:');
1656
+ console.log(' 1. agentchat deploy --provider akash --generate-wallet');
1657
+ console.log(' 2. Fund wallet with AKT tokens');
1658
+ console.log(' 3. agentchat deploy --provider akash --balance');
1659
+ console.log(' 4. agentchat deploy --provider akash --create');
1660
+ console.log('');
1661
+ console.log('Manual workflow (select your own provider):');
1662
+ console.log(' 1. agentchat deploy --provider akash --generate-sdl');
1663
+ console.log(' 2. agentchat deploy --provider akash --create');
1664
+ console.log(' 3. agentchat deploy --provider akash --bids <dseq>');
1665
+ console.log(' 4. agentchat deploy --provider akash --accept-bid <dseq> --provider-address <addr>');
1666
+ process.exit(0);
1667
+ }
1668
+
1669
+ // Generate example config
1670
+ if (options.initConfig) {
1671
+ const configPath = path.resolve(options.output, 'deploy.yaml');
1672
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
1673
+ await fs.writeFile(configPath, generateExampleConfig());
1674
+ console.log(`Generated: ${configPath}`);
1675
+ process.exit(0);
1676
+ }
1677
+
1678
+ let config: Record<string, unknown> = { ...DEFAULT_CONFIG };
1679
+
1680
+ // Load config file if provided
1681
+ if (options.config) {
1682
+ const fileConfig = await loadConfig(options.config);
1683
+ config = { ...config, ...fileConfig };
1684
+ }
1685
+
1686
+ // Override with CLI options
1687
+ if (options.port) config.port = parseInt(options.port);
1688
+ if (options.name) config.name = options.name;
1689
+ if (options.volumes) config.volumes = true;
1690
+ if (options.healthCheck === false) config.healthCheck = false;
1691
+ if (options.network) config.network = options.network;
1692
+ if (options.cert && options.key) {
1693
+ config.tls = { cert: options.cert, key: options.key };
1694
+ }
1695
+
1696
+ // Validate TLS
1697
+ if ((options.cert && !options.key) || (!options.cert && options.key)) {
1698
+ console.error('Error: Both --cert and --key must be provided for TLS');
1699
+ process.exit(1);
1700
+ }
1701
+
1702
+ // Ensure output directory exists
1703
+ const outputDir = path.resolve(options.output);
1704
+ await fs.mkdir(outputDir, { recursive: true });
1705
+
1706
+ // Generate based on provider (Docker)
1707
+ if (options.provider === 'docker' || config.provider === 'docker') {
1708
+ // Generate docker-compose.yml
1709
+ const compose = await deployToDocker(config);
1710
+ const composePath = path.join(outputDir, 'docker-compose.yml');
1711
+ await fs.writeFile(composePath, compose);
1712
+ console.log(`Generated: ${composePath}`);
1713
+
1714
+ // Optionally generate Dockerfile
1715
+ if (options.dockerfile) {
1716
+ const dockerfile = await generateDockerfile(config);
1717
+ const dockerfilePath = path.join(outputDir, 'Dockerfile.generated');
1718
+ await fs.writeFile(dockerfilePath, dockerfile);
1719
+ console.log(`Generated: ${dockerfilePath}`);
1720
+ }
1721
+
1722
+ console.log('\nTo deploy:');
1723
+ console.log(` cd ${outputDir}`);
1724
+ console.log(' docker-compose up -d');
1725
+
1726
+ } else {
1727
+ console.error(`Unknown provider: ${options.provider}`);
1728
+ process.exit(1);
1729
+ }
1730
+
1731
+ process.exit(0);
1732
+ } catch (err) {
1733
+ const error = err as Error;
1734
+ console.error('Error:', error.message);
1735
+ process.exit(1);
1736
+ }
1737
+ });
1738
+
1739
+ // Launcher mode: if no subcommand or just a name, setup MCP + launch Claude
1740
+ const subcommands = [
1741
+ 'serve', 'send', 'listen', 'channels', 'agents', 'create', 'invite',
1742
+ 'propose', 'accept', 'reject', 'complete', 'dispute', 'verify',
1743
+ 'identity', 'daemon', 'receipts', 'ratings', 'skills', 'discover', 'deploy',
1744
+ 'help', '--help', '-h', '--version', '-V'
1745
+ ];
1746
+
1747
+ const firstArg = process.argv[2];
1748
+
1749
+ if (!firstArg || !subcommands.includes(firstArg)) {
1750
+ // Launcher mode
1751
+
1752
+ // Security check: prevent running in root/system directories
1753
+ const safetyCheck = checkDirectorySafety(process.cwd());
1754
+ if (safetyCheck.level === 'error') {
1755
+ console.error(`\n\u274C ERROR: ${safetyCheck.error}`);
1756
+ process.exit(1);
1757
+ }
1758
+ if (safetyCheck.level === 'warning') {
1759
+ console.error(`\n\u26A0\uFE0F WARNING: ${safetyCheck.warning}`);
1760
+ }
1761
+
1762
+ import('child_process').then(({ execSync, spawn }) => {
1763
+ const name = firstArg; // May be undefined (anonymous) or a name
1764
+
1765
+ // 1. Check if MCP is configured
1766
+ let mcpConfigured = false;
1767
+ try {
1768
+ const mcpList = execSync('claude mcp list 2>&1', { encoding: 'utf-8' });
1769
+ mcpConfigured = mcpList.includes('agentchat');
1770
+ } catch {
1771
+ // claude command might not exist
1772
+ }
1773
+
1774
+ // 2. Setup MCP if needed
1775
+ if (!mcpConfigured) {
1776
+ console.log('Setting up AgentChat for Claude Code...');
1777
+ try {
1778
+ execSync('claude mcp add -s user agentchat -- npx -y @tjamescouch/agentchat-mcp', {
1779
+ stdio: 'inherit'
1780
+ });
1781
+ console.log('');
1782
+ console.log('AgentChat installed! Starting Claude Code...');
1783
+ console.log('');
1784
+ } catch {
1785
+ console.error('Failed to setup MCP. Is Claude Code installed?');
1786
+ console.error('Install from: https://claude.ai/download');
1787
+ process.exit(1);
1788
+ }
1789
+ }
1790
+
1791
+ // 3. Launch Claude with prompt
1792
+ const prompt = name
1793
+ ? `Connect to agentchat with name "${name}" and introduce yourself in #general. Read SKILL.md if you need help.`
1794
+ : `Connect to agentchat and introduce yourself in #general. Read SKILL.md if you need help.`;
1795
+
1796
+ const claude = spawn('claude', [prompt], {
1797
+ stdio: 'inherit'
1798
+ });
1799
+
1800
+ claude.on('error', (err: Error) => {
1801
+ console.error('Failed to start Claude Code:', err.message);
1802
+ process.exit(1);
1803
+ });
1804
+
1805
+ claude.on('close', (code: number | null) => {
1806
+ process.exit(code || 0);
1807
+ });
1808
+ });
1809
+ } else {
1810
+ // Normal CLI mode
1811
+ program.parse();
1812
+ }