@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.
- package/bin/agentchat.js +1 -1
- package/bin/agentchat.ts +1812 -0
- package/lib/allowlist.js +162 -0
- package/lib/client.js +2 -2
- package/lib/client.ts +877 -0
- package/lib/daemon.ts +662 -0
- package/lib/escrow-hooks.ts +391 -0
- package/lib/identity.ts +412 -0
- package/lib/jitter.ts +59 -0
- package/lib/proposals.ts +612 -0
- package/lib/protocol.js +40 -7
- package/lib/receipts.ts +359 -0
- package/lib/reputation.ts +790 -0
- package/lib/server/handlers/admin.js +94 -0
- package/lib/server/handlers/identity.js +16 -0
- package/lib/server/handlers/message.js +19 -6
- package/lib/server/handlers/presence.js +1 -0
- package/lib/server-directory.js +17 -8
- package/lib/server-directory.ts +232 -0
- package/lib/server.js +115 -10
- package/lib/server.ts +698 -0
- package/lib/supervisor/agent-health.sh +107 -0
- package/lib/supervisor/agent-monitor.sh +123 -0
- package/lib/supervisor/agentctl.sh +19 -3
- package/lib/supervisor/god-backup.sh +126 -0
- package/lib/supervisor/god-watchdog.sh +107 -0
- package/lib/supervisor/killswitch.sh +15 -8
- package/lib/types.ts +433 -0
- package/package.json +7 -3
package/bin/agentchat.ts
ADDED
|
@@ -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
|
+
}
|