a2acalling 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js ADDED
@@ -0,0 +1,908 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * A2A Calling CLI
4
+ *
5
+ * Usage:
6
+ * a2a create [options] Create a federation token
7
+ * a2a list List active tokens
8
+ * a2a revoke <id> Revoke a token
9
+ * a2a add <url> [name] Add a remote agent
10
+ * a2a remotes List remote agents
11
+ * a2a call <url> <msg> Call a remote agent
12
+ * a2a ping <url> Ping a remote agent
13
+ */
14
+
15
+ const { TokenStore } = require('../src/lib/tokens');
16
+ const { A2AClient } = require('../src/lib/client');
17
+
18
+ // Lazy load conversation store (requires better-sqlite3)
19
+ let convStore = null;
20
+ function getConvStore() {
21
+ if (convStore === false) return null; // Already tried and failed
22
+ if (!convStore) {
23
+ try {
24
+ const { ConversationStore } = require('../src/lib/conversations');
25
+ convStore = new ConversationStore();
26
+ if (!convStore.isAvailable()) {
27
+ console.error(`[a2a] ${convStore.getError()}`);
28
+ convStore = false;
29
+ return null;
30
+ }
31
+ } catch (err) {
32
+ convStore = false;
33
+ return null;
34
+ }
35
+ }
36
+ return convStore;
37
+ }
38
+
39
+ const store = new TokenStore();
40
+
41
+ // Format relative time
42
+ function formatTimeAgo(date) {
43
+ const seconds = Math.floor((new Date() - date) / 1000);
44
+ if (seconds < 60) return 'just now';
45
+ const minutes = Math.floor(seconds / 60);
46
+ if (minutes < 60) return `${minutes}m ago`;
47
+ const hours = Math.floor(minutes / 60);
48
+ if (hours < 24) return `${hours}h ago`;
49
+ const days = Math.floor(hours / 24);
50
+ if (days < 7) return `${days}d ago`;
51
+ return date.toLocaleDateString();
52
+ }
53
+
54
+ // Parse arguments
55
+ function parseArgs(argv) {
56
+ const args = { _: [], flags: {} };
57
+ let i = 2;
58
+ while (i < argv.length) {
59
+ if (argv[i].startsWith('--')) {
60
+ const key = argv[i].slice(2);
61
+ const val = argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[++i] : true;
62
+ args.flags[key] = val;
63
+ } else if (argv[i].startsWith('-') && argv[i].length === 2) {
64
+ const key = argv[i].slice(1);
65
+ const val = argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[++i] : true;
66
+ args.flags[key] = val;
67
+ } else {
68
+ args._.push(argv[i]);
69
+ }
70
+ i++;
71
+ }
72
+ return args;
73
+ }
74
+
75
+ // Get hostname for invite URLs
76
+ function getHostname() {
77
+ return process.env.A2A_HOSTNAME || process.env.OPENCLAW_HOSTNAME || process.env.HOSTNAME || 'localhost';
78
+ }
79
+
80
+ // Commands
81
+ const commands = {
82
+ create: (args) => {
83
+ // Parse max-calls: number, 'unlimited', or default (unlimited)
84
+ let maxCalls = null; // Default: unlimited
85
+ if (args.flags['max-calls']) {
86
+ if (args.flags['max-calls'] === 'unlimited') {
87
+ maxCalls = null;
88
+ } else {
89
+ maxCalls = parseInt(args.flags['max-calls']) || null;
90
+ }
91
+ }
92
+
93
+ // Parse custom topics if provided
94
+ const customTopics = args.flags.topics ?
95
+ args.flags.topics.split(',').map(t => t.trim()) : null;
96
+
97
+ const { token, record } = store.create({
98
+ name: args.flags.name || args.flags.n || 'unnamed',
99
+ owner: args.flags.owner || args.flags.o || null,
100
+ expires: args.flags.expires || args.flags.e || 'never',
101
+ permissions: args.flags.permissions || args.flags.p || 'chat-only',
102
+ disclosure: args.flags.disclosure || args.flags.d || 'minimal',
103
+ notify: args.flags.notify || 'all',
104
+ maxCalls,
105
+ allowedTopics: customTopics
106
+ });
107
+
108
+ const hostname = getHostname();
109
+ const inviteUrl = `a2a://${hostname}/${token}`;
110
+
111
+ const expiresText = record.expires_at
112
+ ? new Date(record.expires_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
113
+ : 'never';
114
+
115
+ // Auto-link to contact if specified
116
+ const linkContact = args.flags.link || args.flags.l;
117
+ if (linkContact) {
118
+ const linkResult = store.linkTokenToContact(linkContact, record.id);
119
+ if (linkResult.success) {
120
+ console.log(`✅ Token created & linked to ${linkContact}\n`);
121
+ } else {
122
+ console.log(`✅ Token created (link failed: ${linkResult.error})\n`);
123
+ }
124
+ } else {
125
+ console.log(`✅ Federation token created\n`);
126
+ }
127
+
128
+ console.log(`Name: ${record.name}`);
129
+ if (record.owner) console.log(`Owner: ${record.owner}`);
130
+ console.log(`Expires: ${record.expires_at || 'never'}`);
131
+ console.log(`Tier: ${record.tier}`);
132
+ console.log(`Topics: ${record.allowed_topics.join(', ')}`);
133
+ console.log(`Disclosure: ${record.disclosure}`);
134
+ console.log(`Notify: ${record.notify}`);
135
+ console.log(`Max calls: ${record.max_calls || 'unlimited'}`);
136
+ if (linkContact) console.log(`Linked to: ${linkContact}`);
137
+ console.log(`\nTo revoke: a2a revoke ${record.id}`);
138
+ console.log(`\n${'─'.repeat(50)}`);
139
+ console.log(`📋 SHAREABLE INVITE (copy everything below):`);
140
+ console.log(`${'─'.repeat(50)}\n`);
141
+
142
+ // Generate shareable invite block
143
+ // Human-friendly above the fold, agent instructions below
144
+ const ownerText = record.owner || 'Someone';
145
+ const agentName = record.name;
146
+ const topicsList = record.allowed_topics.join(', ');
147
+
148
+ const invite = `🤝 **${ownerText}** is inviting you to connect agents!
149
+
150
+ Your agent can reach ${agentName} for: ${topicsList}
151
+
152
+ ${inviteUrl}
153
+
154
+ ${expiresText === 'never' ? '' : `⏰ Expires: ${expiresText}\n`}
155
+ ━━━ For your agent ━━━
156
+
157
+ npm install -g a2acalling
158
+ a2a add "${inviteUrl}" "${agentName}"
159
+ a2a call "${agentName}" "Hello!"
160
+
161
+ 📚 https://github.com/onthegonow/A2A_for_OpenClaw`;
162
+
163
+ console.log(invite);
164
+ console.log(`\n${'─'.repeat(50)}`);
165
+ },
166
+
167
+ list: () => {
168
+ const tokens = store.list();
169
+ if (tokens.length === 0) {
170
+ console.log('No active federation tokens.');
171
+ return;
172
+ }
173
+
174
+ console.log('Active federation tokens:\n');
175
+ for (const t of tokens) {
176
+ const expired = t.expires_at && new Date(t.expires_at) < new Date();
177
+ const status = expired ? '⚠️ EXPIRED' : '✅ Active';
178
+ const tier = t.tier || t.permissions; // backward compat
179
+ const topics = t.allowed_topics || ['chat'];
180
+ console.log(`${status} ${t.id}`);
181
+ console.log(` Name: ${t.name}`);
182
+ console.log(` Tier: ${tier} → ${topics.join(', ')}`);
183
+ console.log(` Expires: ${t.expires_at || 'never'}`);
184
+ console.log(` Calls: ${t.calls_made}${t.max_calls ? '/' + t.max_calls : ''}`);
185
+ console.log();
186
+ }
187
+ },
188
+
189
+ revoke: (args) => {
190
+ const id = args._[1];
191
+ if (!id) {
192
+ console.error('Usage: a2a revoke <token_id>');
193
+ process.exit(1);
194
+ }
195
+
196
+ const result = store.revoke(id);
197
+ if (!result.success) {
198
+ console.error(`Token not found: ${id}`);
199
+ process.exit(1);
200
+ }
201
+
202
+ console.log(`✅ Token revoked: ${result.record.name} (${result.record.id})`);
203
+ },
204
+
205
+ add: (args) => {
206
+ const url = args._[1];
207
+ const name = args._[2] || args.flags.name;
208
+
209
+ if (!url) {
210
+ console.error('Usage: a2a add <invite_url> [name]');
211
+ process.exit(1);
212
+ }
213
+
214
+ try {
215
+ const result = store.addRemote(url, name);
216
+ if (!result.success) {
217
+ console.log(`Remote already registered: ${result.existing.name}`);
218
+ return;
219
+ }
220
+ console.log(`✅ Remote agent added: ${result.remote.name} (${result.remote.host})`);
221
+ } catch (err) {
222
+ console.error(err.message);
223
+ process.exit(1);
224
+ }
225
+ },
226
+
227
+ remotes: () => {
228
+ // Legacy alias for contacts
229
+ commands.contacts({ _: ['contacts'], flags: {} });
230
+ },
231
+
232
+ contacts: (args) => {
233
+ const subcommand = args._[1];
234
+
235
+ // Sub-commands
236
+ if (subcommand === 'add') return commands['contacts:add'](args);
237
+ if (subcommand === 'show') return commands['contacts:show'](args);
238
+ if (subcommand === 'edit') return commands['contacts:edit'](args);
239
+ if (subcommand === 'ping') return commands['contacts:ping'](args);
240
+ if (subcommand === 'link') return commands['contacts:link'](args);
241
+ if (subcommand === 'rm' || subcommand === 'remove') return commands['contacts:rm'](args);
242
+
243
+ // Default: list contacts
244
+ const remotes = store.listRemotes();
245
+ if (remotes.length === 0) {
246
+ console.log('📇 No contacts yet.\n');
247
+ console.log('Add one with: a2a contacts add <invite_url>');
248
+ return;
249
+ }
250
+
251
+ console.log(`📇 Agent Contacts (${remotes.length})\n`);
252
+ for (const r of remotes) {
253
+ const statusIcon = r.status === 'online' ? '🟢' : r.status === 'offline' ? '🔴' : '⚪';
254
+ const ownerText = r.owner ? ` — ${r.owner}` : '';
255
+
256
+ // Permission badge from linked token (what YOU gave THEM)
257
+ let permBadge = '';
258
+ if (r.linked_token) {
259
+ const tier = r.linked_token.tier || r.linked_token.permissions;
260
+ permBadge = tier === 'tools-write' ? ' ⚡' : tier === 'tools-read' ? ' 🔧' : ' 🌐';
261
+ }
262
+
263
+ console.log(`${statusIcon} ${r.name}${ownerText}${permBadge}`);
264
+ if (r.tags && r.tags.length > 0) {
265
+ console.log(` 🏷️ ${r.tags.join(', ')}`);
266
+ }
267
+ if (r.last_seen) {
268
+ const ago = formatTimeAgo(new Date(r.last_seen));
269
+ console.log(` 📍 Last seen: ${ago}`);
270
+ }
271
+ console.log();
272
+ }
273
+
274
+ console.log('Legend: 🌐 chat-only 🔧 tools-read ⚡ tools-write');
275
+ },
276
+
277
+ 'contacts:add': (args) => {
278
+ const url = args._[2];
279
+ if (!url) {
280
+ console.error('Usage: a2a contacts add <invite_url> [options]');
281
+ console.error('Options:');
282
+ console.error(' --name, -n Agent name');
283
+ console.error(' --owner, -o Owner name');
284
+ console.error(' --notes Notes about this contact');
285
+ console.error(' --tags Comma-separated tags');
286
+ console.error(' --link Link to token ID you gave them');
287
+ process.exit(1);
288
+ }
289
+
290
+ const options = {
291
+ name: args.flags.name || args.flags.n,
292
+ owner: args.flags.owner || args.flags.o,
293
+ notes: args.flags.notes,
294
+ tags: args.flags.tags ? args.flags.tags.split(',').map(t => t.trim()) : [],
295
+ linkedTokenId: args.flags.link || null
296
+ };
297
+
298
+ try {
299
+ const result = store.addRemote(url, options);
300
+ if (!result.success) {
301
+ console.log(`Contact already exists: ${result.existing.name}`);
302
+ return;
303
+ }
304
+ console.log(`✅ Contact added: ${result.remote.name}`);
305
+ if (result.remote.owner) console.log(` Owner: ${result.remote.owner}`);
306
+ console.log(` Host: ${result.remote.host}`);
307
+ if (options.linkedTokenId) {
308
+ console.log(` Linked to token: ${options.linkedTokenId}`);
309
+ } else {
310
+ console.log(`\n💡 Link a token: a2a contacts link ${result.remote.name} <token_id>`);
311
+ }
312
+ } catch (err) {
313
+ console.error(err.message);
314
+ process.exit(1);
315
+ }
316
+ },
317
+
318
+ 'contacts:show': (args) => {
319
+ const name = args._[2];
320
+ if (!name) {
321
+ console.error('Usage: a2a contacts show <name>');
322
+ process.exit(1);
323
+ }
324
+
325
+ // Get contact with linked token info
326
+ const remotes = store.listRemotes();
327
+ const remote = remotes.find(r => r.name === name || r.id === name);
328
+ if (!remote) {
329
+ console.error(`Contact not found: ${name}`);
330
+ process.exit(1);
331
+ }
332
+
333
+ const statusIcon = remote.status === 'online' ? '🟢' : remote.status === 'offline' ? '🔴' : '⚪';
334
+
335
+ console.log(`\n${'═'.repeat(50)}`);
336
+ console.log(`${statusIcon} ${remote.name}`);
337
+ console.log(`${'═'.repeat(50)}\n`);
338
+
339
+ if (remote.owner) console.log(`👤 Owner: ${remote.owner}`);
340
+ console.log(`🌐 Host: ${remote.host}`);
341
+
342
+ // Show linked token (permissions you gave them)
343
+ if (remote.linked_token) {
344
+ const t = remote.linked_token;
345
+ const tier = t.tier || t.permissions;
346
+ const topics = t.allowed_topics || ['chat'];
347
+ const tierIcon = tier === 'tools-write' ? '⚡' : tier === 'tools-read' ? '🔧' : '🌐';
348
+ console.log(`🔐 Your token to them: ${t.id}`);
349
+ console.log(` Tier: ${tierIcon} ${tier}`);
350
+ console.log(` Topics: ${topics.join(', ')}`);
351
+ console.log(` Calls: ${t.calls_made}${t.max_calls ? '/' + t.max_calls : ''}`);
352
+ if (t.revoked) console.log(` ⚠️ REVOKED`);
353
+ } else {
354
+ console.log(`🔐 No linked token (you haven't given them access yet)`);
355
+ }
356
+
357
+ if (remote.tags && remote.tags.length > 0) {
358
+ console.log(`🏷️ Tags: ${remote.tags.join(', ')}`);
359
+ }
360
+ if (remote.notes) {
361
+ console.log(`📝 Notes: ${remote.notes}`);
362
+ }
363
+
364
+ console.log(`\n📅 Added: ${new Date(remote.added_at).toLocaleDateString()}`);
365
+ if (remote.last_seen) {
366
+ console.log(`📍 Last seen: ${formatTimeAgo(new Date(remote.last_seen))}`);
367
+ }
368
+ if (remote.last_check) {
369
+ console.log(`🔄 Last check: ${formatTimeAgo(new Date(remote.last_check))}`);
370
+ }
371
+
372
+ console.log(`\n${'─'.repeat(50)}`);
373
+ console.log(`Quick actions:`);
374
+ console.log(` a2a contacts ping ${name}`);
375
+ console.log(` a2a call ${name} "Hello!"`);
376
+ if (!remote.linked_token) {
377
+ console.log(` a2a contacts link ${name} <token_id>`);
378
+ }
379
+ console.log(`${'─'.repeat(50)}\n`);
380
+ },
381
+
382
+ 'contacts:edit': (args) => {
383
+ const name = args._[2];
384
+ if (!name) {
385
+ console.error('Usage: a2a contacts edit <name> [options]');
386
+ console.error('Options:');
387
+ console.error(' --name New name');
388
+ console.error(' --owner Owner name');
389
+ console.error(' --notes Notes');
390
+ console.error(' --tags Comma-separated tags');
391
+ process.exit(1);
392
+ }
393
+
394
+ const updates = {};
395
+ if (args.flags.name) updates.name = args.flags.name;
396
+ if (args.flags.owner) updates.owner = args.flags.owner;
397
+ if (args.flags.notes) updates.notes = args.flags.notes;
398
+ if (args.flags.tags) updates.tags = args.flags.tags.split(',').map(t => t.trim());
399
+
400
+ if (Object.keys(updates).length === 0) {
401
+ console.error('No updates specified. Use --name, --owner, --notes, or --tags');
402
+ process.exit(1);
403
+ }
404
+
405
+ const result = store.updateRemote(name, updates);
406
+ if (!result.success) {
407
+ console.error(`Contact not found: ${name}`);
408
+ process.exit(1);
409
+ }
410
+
411
+ console.log(`✅ Contact updated: ${result.remote.name}`);
412
+ },
413
+
414
+ 'contacts:link': (args) => {
415
+ const contactName = args._[2];
416
+ const tokenId = args._[3];
417
+
418
+ if (!contactName || !tokenId) {
419
+ console.error('Usage: a2a contacts link <contact_name> <token_id>');
420
+ console.error('\nLinks a token you created to a contact, showing what access they have.');
421
+ console.error('\nExample:');
422
+ console.error(' a2a contacts link Alice tok_abc123');
423
+ process.exit(1);
424
+ }
425
+
426
+ const result = store.linkTokenToContact(contactName, tokenId);
427
+ if (!result.success) {
428
+ if (result.error === 'contact_not_found') {
429
+ console.error(`Contact not found: ${contactName}`);
430
+ } else if (result.error === 'token_not_found') {
431
+ console.error(`Token not found: ${tokenId}`);
432
+ }
433
+ process.exit(1);
434
+ }
435
+
436
+ const permLabel = result.token.permissions === 'tools-write' ? '⚡ tools-write' :
437
+ result.token.permissions === 'tools-read' ? '🔧 tools-read' : '🌐 chat-only';
438
+
439
+ console.log(`✅ Linked token to contact`);
440
+ console.log(` Contact: ${result.remote.name}`);
441
+ console.log(` Token: ${result.token.id} (${result.token.name})`);
442
+ console.log(` Permissions: ${permLabel}`);
443
+ },
444
+
445
+ 'contacts:ping': async (args) => {
446
+ const name = args._[2];
447
+ if (!name) {
448
+ console.error('Usage: a2a contacts ping <name>');
449
+ process.exit(1);
450
+ }
451
+
452
+ const remote = store.getRemote(name);
453
+ if (!remote) {
454
+ console.error(`Contact not found: ${name}`);
455
+ process.exit(1);
456
+ }
457
+
458
+ const client = new A2AClient({});
459
+ const url = `a2a://${remote.host}/${remote.token}`;
460
+
461
+ console.log(`🔍 Pinging ${remote.name}...`);
462
+
463
+ try {
464
+ const result = await client.ping(url);
465
+ store.updateRemoteStatus(name, 'online');
466
+ console.log(`🟢 ${remote.name} is online`);
467
+ console.log(` Agent: ${result.name}`);
468
+ console.log(` Version: ${result.version}`);
469
+ } catch (err) {
470
+ store.updateRemoteStatus(name, 'offline', err.message);
471
+ console.log(`🔴 ${remote.name} is offline`);
472
+ console.log(` Error: ${err.message}`);
473
+ }
474
+ },
475
+
476
+ 'contacts:rm': (args) => {
477
+ const name = args._[2];
478
+ if (!name) {
479
+ console.error('Usage: a2a contacts rm <name>');
480
+ process.exit(1);
481
+ }
482
+
483
+ const result = store.removeRemote(name);
484
+ if (!result.success) {
485
+ console.error(`Contact not found: ${name}`);
486
+ process.exit(1);
487
+ }
488
+
489
+ console.log(`✅ Contact removed: ${result.remote.name}`);
490
+ },
491
+
492
+ // ========== CONVERSATIONS ==========
493
+
494
+ conversations: (args) => {
495
+ const subcommand = args._[1];
496
+
497
+ if (subcommand === 'show') return commands['conversations:show'](args);
498
+ if (subcommand === 'end') return commands['conversations:end'](args);
499
+
500
+ // Default: list conversations
501
+ const cs = getConvStore();
502
+ if (!cs) {
503
+ console.log('💬 Conversation storage not available.');
504
+ console.log('Install: npm install better-sqlite3');
505
+ return;
506
+ }
507
+
508
+ const { contact, status, limit = 20 } = args.flags;
509
+ const conversations = cs.listConversations({
510
+ contactId: contact,
511
+ status,
512
+ limit: parseInt(limit),
513
+ includeMessages: true,
514
+ messageLimit: 1
515
+ });
516
+
517
+ if (conversations.length === 0) {
518
+ console.log('💬 No conversations yet.');
519
+ return;
520
+ }
521
+
522
+ console.log(`💬 Conversations (${conversations.length})\n`);
523
+ for (const conv of conversations) {
524
+ const statusIcon = conv.status === 'concluded' ? '✅' : conv.status === 'timeout' ? '⏱️' : '💬';
525
+ const timeAgo = formatTimeAgo(new Date(conv.last_message_at));
526
+ const preview = conv.messages?.[0]?.content?.slice(0, 50) || '';
527
+
528
+ console.log(`${statusIcon} ${conv.id}`);
529
+ console.log(` Contact: ${conv.contact_name || conv.contact_id || 'unknown'}`);
530
+ console.log(` Messages: ${conv.message_count} | ${timeAgo}`);
531
+ if (conv.summary) {
532
+ console.log(` Summary: ${conv.summary.slice(0, 80)}...`);
533
+ } else if (preview) {
534
+ console.log(` Preview: "${preview}..."`);
535
+ }
536
+ if (conv.owner_relevance) {
537
+ console.log(` Relevance: ${conv.owner_relevance}`);
538
+ }
539
+ console.log();
540
+ }
541
+ },
542
+
543
+ 'conversations:show': (args) => {
544
+ const convId = args._[2];
545
+ if (!convId) {
546
+ console.error('Usage: a2a conversations show <conversation_id>');
547
+ process.exit(1);
548
+ }
549
+
550
+ const cs = getConvStore();
551
+ if (!cs) {
552
+ console.error('Conversation storage not available. Install: npm install better-sqlite3');
553
+ process.exit(1);
554
+ }
555
+
556
+ const context = cs.getConversationContext(convId, args.flags.messages || 20);
557
+ if (!context) {
558
+ console.error(`Conversation not found: ${convId}`);
559
+ process.exit(1);
560
+ }
561
+
562
+ console.log(`\n${'═'.repeat(60)}`);
563
+ console.log(`💬 ${context.id}`);
564
+ console.log(`${'═'.repeat(60)}\n`);
565
+
566
+ console.log(`👤 Contact: ${context.contact || 'unknown'}`);
567
+ console.log(`📊 Status: ${context.status}`);
568
+ console.log(`📝 Messages: ${context.messageCount}`);
569
+ console.log(`📅 Started: ${new Date(context.startedAt).toLocaleString()}`);
570
+ if (context.endedAt) {
571
+ console.log(`🏁 Ended: ${new Date(context.endedAt).toLocaleString()}`);
572
+ }
573
+
574
+ if (context.summary) {
575
+ console.log(`\n${'─'.repeat(60)}`);
576
+ console.log(`📋 Summary:\n${context.summary}`);
577
+ }
578
+
579
+ if (context.ownerContext) {
580
+ console.log(`\n${'─'.repeat(60)}`);
581
+ console.log(`🔒 Owner Context (private):`);
582
+ console.log(` Relevance: ${context.ownerContext.relevance || 'unknown'}`);
583
+ if (context.ownerContext.summary) {
584
+ console.log(` Summary: ${context.ownerContext.summary}`);
585
+ }
586
+ if (context.ownerContext.goalsTouched?.length) {
587
+ console.log(` Goals: ${context.ownerContext.goalsTouched.join(', ')}`);
588
+ }
589
+ if (context.ownerContext.actionItems?.length) {
590
+ console.log(` Actions: ${context.ownerContext.actionItems.join(', ')}`);
591
+ }
592
+ if (context.ownerContext.followUp) {
593
+ console.log(` Follow-up: ${context.ownerContext.followUp}`);
594
+ }
595
+ if (context.ownerContext.notes) {
596
+ console.log(` Notes: ${context.ownerContext.notes}`);
597
+ }
598
+ }
599
+
600
+ console.log(`\n${'─'.repeat(60)}`);
601
+ console.log(`Recent messages:`);
602
+ console.log(`${'─'.repeat(60)}`);
603
+ for (const msg of context.recentMessages) {
604
+ const role = msg.direction === 'inbound' ? '← In' : '→ Out';
605
+ const time = new Date(msg.timestamp).toLocaleTimeString();
606
+ console.log(`\n[${time}] ${role}:`);
607
+ console.log(msg.content);
608
+ }
609
+ console.log(`\n${'═'.repeat(60)}\n`);
610
+ },
611
+
612
+ 'conversations:end': async (args) => {
613
+ const convId = args._[2];
614
+ if (!convId) {
615
+ console.error('Usage: a2a conversations end <conversation_id>');
616
+ process.exit(1);
617
+ }
618
+
619
+ const cs = getConvStore();
620
+ if (!cs) {
621
+ console.error('Conversation storage not available');
622
+ process.exit(1);
623
+ }
624
+
625
+ // For now, conclude without LLM summarizer
626
+ const result = await cs.concludeConversation(convId, {});
627
+
628
+ if (!result.success) {
629
+ console.error(`Failed to end conversation: ${result.error}`);
630
+ process.exit(1);
631
+ }
632
+
633
+ console.log(`✅ Conversation concluded: ${convId}`);
634
+ if (result.summary) {
635
+ console.log(`📋 Summary: ${result.summary}`);
636
+ }
637
+ },
638
+
639
+ call: async (args) => {
640
+ let target = args._[1];
641
+ const message = args._.slice(2).join(' ') || args.flags.message || args.flags.m;
642
+
643
+ if (!target || !message) {
644
+ console.error('Usage: a2a call <contact_or_url> <message>');
645
+ process.exit(1);
646
+ }
647
+
648
+ // Check if target is a contact name (not a URL)
649
+ let url = target;
650
+ let contactName = null;
651
+ if (!target.startsWith('a2a://')) {
652
+ const remote = store.getRemote(target);
653
+ if (remote) {
654
+ url = `a2a://${remote.host}/${remote.token}`;
655
+ contactName = remote.name;
656
+ }
657
+ }
658
+
659
+ const client = new A2AClient({
660
+ caller: { name: args.flags.name || 'CLI User' }
661
+ });
662
+
663
+ try {
664
+ console.log(`📞 Calling ${contactName || url}...`);
665
+ const response = await client.call(url, message);
666
+
667
+ // Update contact status on success
668
+ if (contactName) {
669
+ store.updateRemoteStatus(contactName, 'online');
670
+ }
671
+
672
+ console.log(`\n✅ Response:\n`);
673
+ console.log(response.response);
674
+ if (response.conversation_id) {
675
+ console.log(`\n📝 Conversation ID: ${response.conversation_id}`);
676
+ }
677
+ } catch (err) {
678
+ // Update contact status on failure
679
+ if (contactName) {
680
+ store.updateRemoteStatus(contactName, 'offline', err.message);
681
+ }
682
+ console.error(`❌ Call failed: ${err.message}`);
683
+ process.exit(1);
684
+ }
685
+ },
686
+
687
+ ping: async (args) => {
688
+ const url = args._[1];
689
+ if (!url) {
690
+ console.error('Usage: a2a ping <invite_url>');
691
+ process.exit(1);
692
+ }
693
+
694
+ const client = new A2AClient();
695
+ const result = await client.ping(url);
696
+
697
+ if (result.pong) {
698
+ console.log(`✅ Agent reachable at ${url}`);
699
+ if (result.timestamp) {
700
+ console.log(` Timestamp: ${result.timestamp}`);
701
+ }
702
+ } else {
703
+ console.log(`❌ Agent not reachable at ${url}`);
704
+ process.exit(1);
705
+ }
706
+ },
707
+
708
+ status: async (args) => {
709
+ const url = args._[1];
710
+ if (!url) {
711
+ console.error('Usage: a2a status <invite_url>');
712
+ process.exit(1);
713
+ }
714
+
715
+ const client = new A2AClient();
716
+ try {
717
+ const status = await client.status(url);
718
+ console.log(`Federation status for ${url}:\n`);
719
+ console.log(JSON.stringify(status, null, 2));
720
+ } catch (err) {
721
+ console.error(`❌ Failed to get status: ${err.message}`);
722
+ process.exit(1);
723
+ }
724
+ },
725
+
726
+ server: (args) => {
727
+ const port = args.flags.port || args.flags.p || process.env.PORT || 3001;
728
+ process.env.PORT = port;
729
+ console.log(`Starting A2A federation server on port ${port}...`);
730
+ require('../src/server.js');
731
+ },
732
+
733
+ quickstart: (args) => {
734
+ const hostname = process.env.A2A_HOSTNAME || process.env.HOSTNAME || 'localhost:3001';
735
+ const name = args.flags.name || args.flags.n || 'My Agent';
736
+ const owner = args.flags.owner || args.flags.o || null;
737
+
738
+ console.log(`\n🚀 A2A Quickstart\n${'═'.repeat(50)}\n`);
739
+
740
+ // Step 1: Check server
741
+ console.log('1️⃣ Checking server status...');
742
+ const http = require('http');
743
+ const serverHost = hostname.split(':')[0];
744
+ const serverPort = hostname.split(':')[1] || 3001;
745
+
746
+ const checkServer = () => new Promise((resolve) => {
747
+ const req = http.request({
748
+ hostname: serverHost === 'localhost' ? '127.0.0.1' : serverHost,
749
+ port: serverPort,
750
+ path: '/api/federation/ping',
751
+ timeout: 2000
752
+ }, (res) => {
753
+ resolve(res.statusCode === 200);
754
+ });
755
+ req.on('error', () => resolve(false));
756
+ req.on('timeout', () => { req.destroy(); resolve(false); });
757
+ req.end();
758
+ });
759
+
760
+ checkServer().then(serverOk => {
761
+ if (!serverOk) {
762
+ console.log(' ⚠️ Server not running!');
763
+ console.log(` Run: A2A_HOSTNAME="${hostname}" a2a server\n`);
764
+ } else {
765
+ console.log(' ✅ Server running\n');
766
+ }
767
+
768
+ // Step 2: Create invite
769
+ console.log('2️⃣ Creating your first invite...\n');
770
+ const { token, record } = store.create({
771
+ name,
772
+ owner,
773
+ expires: '7d',
774
+ permissions: 'chat-only',
775
+ maxCalls: 100
776
+ });
777
+
778
+ const inviteUrl = `a2a://${hostname}/${token}`;
779
+ const expiresText = new Date(record.expires_at).toLocaleDateString('en-US', {
780
+ month: 'short', day: 'numeric', year: 'numeric'
781
+ });
782
+
783
+ // Step 3: Show the invite
784
+ const ownerText = owner ? `${owner}'s agent ${name}` : name;
785
+ console.log('3️⃣ Share this invite:\n');
786
+ console.log('─'.repeat(50));
787
+ console.log(`
788
+ 🤝 Agent-to-Agent Invite
789
+
790
+ ${ownerText} is inviting your agent to connect!
791
+
792
+ 📡 Connection URL:
793
+ ${inviteUrl}
794
+
795
+ ⏰ Expires: ${expiresText}
796
+ 🔐 Permissions: chat-only
797
+ 📊 Limits: 100 calls, 10/min rate limit
798
+
799
+ ━━━ Quick Setup ━━━
800
+
801
+ 1. Install: npm install -g a2acalling
802
+
803
+ 2. Connect: a2a add "${inviteUrl}" "${name}"
804
+
805
+ 3. Call: a2a call "${inviteUrl}" "Hello!"
806
+
807
+ 📚 Docs: https://github.com/onthegonow/A2A_for_OpenClaw
808
+ `);
809
+ console.log('─'.repeat(50));
810
+ console.log(`\n✅ Done! Share the invite above with other agents.\n`);
811
+ console.log(`To revoke: a2a revoke ${record.id}\n`);
812
+ });
813
+ },
814
+
815
+ install: () => {
816
+ require('../scripts/install-openclaw.js');
817
+ },
818
+
819
+ help: () => {
820
+ console.log(`A2A Calling - Agent-to-Agent Communication
821
+
822
+ Usage: a2a <command> [options]
823
+
824
+ Commands:
825
+ create Create a federation token
826
+ --name, -n Token/agent name
827
+ --owner, -o Owner name (human behind the agent)
828
+ --expires, -e Expiration (1h, 1d, 7d, 30d, never)
829
+ --permissions, -p Tier (chat-only, tools-read, tools-write)
830
+ --topics Custom topics (comma-separated, overrides tier defaults)
831
+ --disclosure, -d Disclosure level (public, minimal, none)
832
+ --notify Owner notification (all, summary, none)
833
+ --max-calls Maximum invocations (default: 100)
834
+ --link, -l Auto-link to contact name
835
+
836
+ list List active tokens
837
+ revoke <id> Revoke a token
838
+
839
+ Contacts:
840
+ contacts List all contacts (shows permission badges)
841
+ contacts add <url> Add a contact
842
+ --name, -n Agent name
843
+ --owner, -o Owner name
844
+ --notes Notes about this contact
845
+ --tags Comma-separated tags
846
+ --link Link to token ID you gave them
847
+ contacts show <n> Show contact details + linked token
848
+ contacts edit <n> Edit contact metadata
849
+ contacts link <n> <tok> Link a token to a contact
850
+ contacts ping <n> Ping contact, update status
851
+ contacts rm <n> Remove contact
852
+
853
+ Permission badges: 🌐 chat-only 🔧 tools-read ⚡ tools-write
854
+
855
+ Conversations:
856
+ conversations List all conversations
857
+ --contact Filter by contact
858
+ --status Filter by status (active, concluded, timeout)
859
+ --limit Max results (default: 20)
860
+ conversations show <id> Show conversation with messages
861
+ --messages Number of recent messages (default: 20)
862
+ conversations end <id> End and summarize conversation
863
+
864
+ Calling:
865
+ call <contact|url> <msg> Call a remote agent
866
+ ping <url> Check if agent is reachable
867
+ status <url> Get federation status
868
+
869
+ Server:
870
+ server Start the federation server
871
+ --port, -p Port to listen on (default: 3001)
872
+
873
+ quickstart One-command setup: check server + create invite
874
+ --name, -n Agent name for the invite
875
+ --owner, -o Owner name (human behind the agent)
876
+
877
+ install Install A2A for OpenClaw
878
+
879
+ Examples:
880
+ a2a create --name "bappybot" --owner "Benjamin Pollack" --expires 7d
881
+ a2a create --name "custom" --topics "chat,calendar.read,email.read"
882
+ a2a contacts add a2a://host/fed_xxx --name "Alice" --owner "Alice Chen"
883
+ a2a contacts link Alice tok_abc123
884
+ a2a call Alice "Hello!"
885
+ a2a conversations show conv_abc123
886
+ a2a server --port 3001
887
+ `);
888
+ }
889
+ };
890
+
891
+ // Main
892
+ const args = parseArgs(process.argv);
893
+ const command = args._[0] || 'help';
894
+
895
+ if (!commands[command]) {
896
+ console.error(`Unknown command: ${command}`);
897
+ console.log('Run "a2a help" for usage.');
898
+ process.exit(1);
899
+ }
900
+
901
+ // Handle async commands
902
+ const result = commands[command](args);
903
+ if (result instanceof Promise) {
904
+ result.catch(err => {
905
+ console.error(err.message);
906
+ process.exit(1);
907
+ });
908
+ }