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/AGENTS.md +66 -0
- package/CLAUDE.md +52 -0
- package/README.md +307 -0
- package/SKILL.md +122 -0
- package/bin/cli.js +908 -0
- package/docs/protocol.md +241 -0
- package/package.json +44 -0
- package/scripts/install-openclaw.js +291 -0
- package/src/index.js +61 -0
- package/src/lib/call-monitor.js +143 -0
- package/src/lib/client.js +208 -0
- package/src/lib/config.js +173 -0
- package/src/lib/conversations.js +470 -0
- package/src/lib/openclaw-integration.js +329 -0
- package/src/lib/summarizer.js +137 -0
- package/src/lib/tokens.js +448 -0
- package/src/routes/federation.js +463 -0
- package/src/server.js +56 -0
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
|
+
}
|