a2acalling 0.6.1 → 0.6.3
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/README.md +33 -9
- package/SKILL.md +69 -5
- package/bin/cli.js +539 -162
- package/docs/protocol.md +24 -14
- package/package.json +1 -1
- package/scripts/install-openclaw.js +64 -64
- package/src/dashboard/public/app.js +765 -28
- package/src/dashboard/public/index.html +57 -13
- package/src/dashboard/public/style.css +16 -0
- package/src/lib/callbook.js +358 -0
- package/src/lib/client.js +1 -2
- package/src/lib/config.js +214 -16
- package/src/lib/conversations.js +74 -0
- package/src/lib/disclosure.js +3 -43
- package/src/lib/external-ip.js +18 -7
- package/src/lib/invite-host.js +24 -21
- package/src/lib/logger.js +26 -14
- package/src/lib/tokens.js +314 -113
- package/src/routes/a2a.js +11 -2
- package/src/routes/callbook.js +142 -0
- package/src/routes/dashboard.js +605 -37
- package/src/server.js +6 -0
package/bin/cli.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* a2a create [options] Create an A2A token
|
|
7
7
|
* a2a list List active tokens
|
|
8
8
|
* a2a revoke <id> Revoke a token
|
|
9
|
-
* a2a add <url> [name] Add a
|
|
10
|
-
* a2a remotes List
|
|
11
|
-
* a2a call <url> <msg> Call a
|
|
12
|
-
* a2a ping <url> Ping
|
|
9
|
+
* a2a add <url> [name] Add a contact (alias of "contacts add")
|
|
10
|
+
* a2a remotes List contacts (alias of "contacts")
|
|
11
|
+
* a2a call <url> <msg> Call a contact (or invite URL)
|
|
12
|
+
* a2a ping <url> Ping an invite URL
|
|
13
13
|
* a2a gui Open the local dashboard GUI in a browser
|
|
14
14
|
* a2a setup Auto setup (gateway-aware dashboard install)
|
|
15
15
|
*/
|
|
@@ -47,8 +47,8 @@ function checkOnboarding(commandName) {
|
|
|
47
47
|
const config = new A2AConfig();
|
|
48
48
|
if (!config.isOnboarded()) {
|
|
49
49
|
console.warn('\n\u26a0\ufe0f A2A onboarding not complete.');
|
|
50
|
-
console.warn(' Run "a2a quickstart"
|
|
51
|
-
console.warn(' Without onboarding, invites
|
|
50
|
+
console.warn(' Run "a2a quickstart" to complete deterministic onboarding.');
|
|
51
|
+
console.warn(' Without onboarding, invites may use default topics/goals and remote dashboard access may not be configured.\n');
|
|
52
52
|
return false;
|
|
53
53
|
}
|
|
54
54
|
return true;
|
|
@@ -286,7 +286,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
286
286
|
for (const t of tokens) {
|
|
287
287
|
const expired = t.expires_at && new Date(t.expires_at) < new Date();
|
|
288
288
|
const status = expired ? '⚠️ EXPIRED' : '✅ Active';
|
|
289
|
-
const tier = t.tier ||
|
|
289
|
+
const tier = t.tier || 'public';
|
|
290
290
|
const topics = t.allowed_topics || ['chat'];
|
|
291
291
|
console.log(`${status} ${t.id}`);
|
|
292
292
|
console.log(` Name: ${t.name}`);
|
|
@@ -323,12 +323,12 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
try {
|
|
326
|
-
const result = store.
|
|
326
|
+
const result = store.addContact(url, { name });
|
|
327
327
|
if (!result.success) {
|
|
328
|
-
console.log(`
|
|
328
|
+
console.log(`Contact already registered: ${result.existing.name}`);
|
|
329
329
|
return;
|
|
330
330
|
}
|
|
331
|
-
console.log(`✅
|
|
331
|
+
console.log(`✅ Contact added: ${result.contact.name} (${result.contact.host})`);
|
|
332
332
|
} catch (err) {
|
|
333
333
|
console.error(err.message);
|
|
334
334
|
process.exit(1);
|
|
@@ -336,7 +336,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
336
336
|
},
|
|
337
337
|
|
|
338
338
|
remotes: () => {
|
|
339
|
-
//
|
|
339
|
+
// Alias for contacts
|
|
340
340
|
commands.contacts({ _: ['contacts'], flags: {} });
|
|
341
341
|
},
|
|
342
342
|
|
|
@@ -352,22 +352,22 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
352
352
|
if (subcommand === 'rm' || subcommand === 'remove') return commands['contacts:rm'](args);
|
|
353
353
|
|
|
354
354
|
// Default: list contacts
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
355
|
+
const contacts = store.listContacts();
|
|
356
|
+
if (contacts.length === 0) {
|
|
357
357
|
console.log('📇 No contacts yet.\n');
|
|
358
358
|
console.log('Add one with: a2a contacts add <invite_url>');
|
|
359
359
|
return;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
console.log(`📇 Agent Contacts (${
|
|
363
|
-
for (const r of
|
|
362
|
+
console.log(`📇 Agent Contacts (${contacts.length})\n`);
|
|
363
|
+
for (const r of contacts) {
|
|
364
364
|
const statusIcon = r.status === 'online' ? '🟢' : r.status === 'offline' ? '🔴' : '⚪';
|
|
365
365
|
const ownerText = r.owner ? ` — ${r.owner}` : '';
|
|
366
366
|
|
|
367
367
|
// Permission badge from linked token (what YOU gave THEM)
|
|
368
368
|
let permBadge = '';
|
|
369
369
|
if (r.linked_token) {
|
|
370
|
-
const tier = r.linked_token.tier ||
|
|
370
|
+
const tier = r.linked_token.tier || 'public';
|
|
371
371
|
permBadge = tier === 'family' ? ' ⚡' : tier === 'friends' ? ' 🔧' : ' 🌐';
|
|
372
372
|
}
|
|
373
373
|
|
|
@@ -392,6 +392,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
392
392
|
console.error('Options:');
|
|
393
393
|
console.error(' --name, -n Agent name');
|
|
394
394
|
console.error(' --owner, -o Owner name');
|
|
395
|
+
console.error(' --server-name Server label (optional)');
|
|
395
396
|
console.error(' --notes Notes about this contact');
|
|
396
397
|
console.error(' --tags Comma-separated tags');
|
|
397
398
|
console.error(' --link Link to token ID you gave them');
|
|
@@ -401,24 +402,26 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
401
402
|
const options = {
|
|
402
403
|
name: args.flags.name || args.flags.n,
|
|
403
404
|
owner: args.flags.owner || args.flags.o,
|
|
405
|
+
server_name: args.flags['server-name'] || args.flags.server_name || args.flags.serverName || null,
|
|
404
406
|
notes: args.flags.notes,
|
|
405
407
|
tags: args.flags.tags ? args.flags.tags.split(',').map(t => t.trim()) : [],
|
|
406
408
|
linkedTokenId: args.flags.link || null
|
|
407
409
|
};
|
|
408
410
|
|
|
409
411
|
try {
|
|
410
|
-
const result = store.
|
|
412
|
+
const result = store.addContact(url, options);
|
|
411
413
|
if (!result.success) {
|
|
412
414
|
console.log(`Contact already exists: ${result.existing.name}`);
|
|
413
415
|
return;
|
|
414
416
|
}
|
|
415
|
-
console.log(`✅ Contact added: ${result.
|
|
416
|
-
if (result.
|
|
417
|
-
console.log(`
|
|
417
|
+
console.log(`✅ Contact added: ${result.contact.name}`);
|
|
418
|
+
if (result.contact.owner) console.log(` Owner: ${result.contact.owner}`);
|
|
419
|
+
if (result.contact.server_name) console.log(` Server: ${result.contact.server_name}`);
|
|
420
|
+
console.log(` Host: ${result.contact.host}`);
|
|
418
421
|
if (options.linkedTokenId) {
|
|
419
422
|
console.log(` Linked to token: ${options.linkedTokenId}`);
|
|
420
423
|
} else {
|
|
421
|
-
console.log(`\n💡 Link a token: a2a contacts link ${result.
|
|
424
|
+
console.log(`\n💡 Link a token: a2a contacts link ${result.contact.name} <token_id>`);
|
|
422
425
|
}
|
|
423
426
|
} catch (err) {
|
|
424
427
|
console.error(err.message);
|
|
@@ -434,8 +437,8 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
434
437
|
}
|
|
435
438
|
|
|
436
439
|
// Get contact with linked token info
|
|
437
|
-
const
|
|
438
|
-
const remote =
|
|
440
|
+
const contacts = store.listContacts();
|
|
441
|
+
const remote = contacts.find(r => r.name === name || r.id === name);
|
|
439
442
|
if (!remote) {
|
|
440
443
|
console.error(`Contact not found: ${name}`);
|
|
441
444
|
process.exit(1);
|
|
@@ -453,7 +456,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
453
456
|
// Show linked token (permissions you gave them)
|
|
454
457
|
if (remote.linked_token) {
|
|
455
458
|
const t = remote.linked_token;
|
|
456
|
-
const tier = t.tier ||
|
|
459
|
+
const tier = t.tier || 'public';
|
|
457
460
|
const topics = t.allowed_topics || ['chat'];
|
|
458
461
|
const tierIcon = tier === 'family' ? '⚡' : tier === 'friends' ? '🔧' : '🌐';
|
|
459
462
|
console.log(`🔐 Your token to them: ${t.id}`);
|
|
@@ -497,6 +500,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
497
500
|
console.error('Options:');
|
|
498
501
|
console.error(' --name New name');
|
|
499
502
|
console.error(' --owner Owner name');
|
|
503
|
+
console.error(' --server-name Server label');
|
|
500
504
|
console.error(' --notes Notes');
|
|
501
505
|
console.error(' --tags Comma-separated tags');
|
|
502
506
|
process.exit(1);
|
|
@@ -505,6 +509,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
505
509
|
const updates = {};
|
|
506
510
|
if (args.flags.name) updates.name = args.flags.name;
|
|
507
511
|
if (args.flags.owner) updates.owner = args.flags.owner;
|
|
512
|
+
if (args.flags['server-name'] || args.flags.server_name || args.flags.serverName) updates.server_name = args.flags['server-name'] || args.flags.server_name || args.flags.serverName;
|
|
508
513
|
if (args.flags.notes) updates.notes = args.flags.notes;
|
|
509
514
|
if (args.flags.tags) updates.tags = args.flags.tags.split(',').map(t => t.trim());
|
|
510
515
|
|
|
@@ -513,13 +518,13 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
513
518
|
process.exit(1);
|
|
514
519
|
}
|
|
515
520
|
|
|
516
|
-
const result = store.
|
|
521
|
+
const result = store.updateContact(name, updates);
|
|
517
522
|
if (!result.success) {
|
|
518
523
|
console.error(`Contact not found: ${name}`);
|
|
519
524
|
process.exit(1);
|
|
520
525
|
}
|
|
521
526
|
|
|
522
|
-
console.log(`✅ Contact updated: ${result.remote.name}`);
|
|
527
|
+
console.log(`✅ Contact updated: ${(result.contact || result.remote).name}`);
|
|
523
528
|
},
|
|
524
529
|
|
|
525
530
|
'contacts:link': (args) => {
|
|
@@ -548,7 +553,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
548
553
|
result.token.tier === 'friends' ? '🔧 friends' : '🌐 public';
|
|
549
554
|
|
|
550
555
|
console.log(`✅ Linked token to contact`);
|
|
551
|
-
console.log(` Contact: ${result.remote.name}`);
|
|
556
|
+
console.log(` Contact: ${result.contact?.name || result.remote.name}`);
|
|
552
557
|
console.log(` Token: ${result.token.id} (${result.token.name})`);
|
|
553
558
|
console.log(` Permissions: ${permLabel}`);
|
|
554
559
|
},
|
|
@@ -560,7 +565,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
560
565
|
process.exit(1);
|
|
561
566
|
}
|
|
562
567
|
|
|
563
|
-
const remote = store.
|
|
568
|
+
const remote = store.getContact(name);
|
|
564
569
|
if (!remote) {
|
|
565
570
|
console.error(`Contact not found: ${name}`);
|
|
566
571
|
process.exit(1);
|
|
@@ -573,12 +578,12 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
573
578
|
|
|
574
579
|
try {
|
|
575
580
|
const result = await client.ping(url);
|
|
576
|
-
store.
|
|
581
|
+
store.updateContactStatus(name, 'online');
|
|
577
582
|
console.log(`🟢 ${remote.name} is online`);
|
|
578
583
|
console.log(` Agent: ${result.name}`);
|
|
579
584
|
console.log(` Version: ${result.version}`);
|
|
580
585
|
} catch (err) {
|
|
581
|
-
store.
|
|
586
|
+
store.updateContactStatus(name, 'offline', err.message);
|
|
582
587
|
console.log(`🔴 ${remote.name} is offline`);
|
|
583
588
|
console.log(` Error: ${err.message}`);
|
|
584
589
|
}
|
|
@@ -591,13 +596,13 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
591
596
|
process.exit(1);
|
|
592
597
|
}
|
|
593
598
|
|
|
594
|
-
const result = store.
|
|
599
|
+
const result = store.removeContact(name);
|
|
595
600
|
if (!result.success) {
|
|
596
601
|
console.error(`Contact not found: ${name}`);
|
|
597
602
|
process.exit(1);
|
|
598
603
|
}
|
|
599
604
|
|
|
600
|
-
console.log(`✅ Contact removed: ${result.remote.name}`);
|
|
605
|
+
console.log(`✅ Contact removed: ${(result.contact || result.remote).name}`);
|
|
601
606
|
},
|
|
602
607
|
|
|
603
608
|
// ========== CONVERSATIONS ==========
|
|
@@ -761,7 +766,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
761
766
|
let url = target;
|
|
762
767
|
let contactName = null;
|
|
763
768
|
if (!target.startsWith('a2a://')) {
|
|
764
|
-
const remote = store.
|
|
769
|
+
const remote = store.getContact(target);
|
|
765
770
|
if (remote) {
|
|
766
771
|
url = `a2a://${remote.host}/${remote.token}`;
|
|
767
772
|
contactName = remote.name;
|
|
@@ -778,7 +783,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
778
783
|
|
|
779
784
|
// Update contact status on success
|
|
780
785
|
if (contactName) {
|
|
781
|
-
store.
|
|
786
|
+
store.updateContactStatus(contactName, 'online');
|
|
782
787
|
}
|
|
783
788
|
|
|
784
789
|
console.log(`\n✅ Response:\n`);
|
|
@@ -789,7 +794,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
789
794
|
} catch (err) {
|
|
790
795
|
// Update contact status on failure
|
|
791
796
|
if (contactName) {
|
|
792
|
-
store.
|
|
797
|
+
store.updateContactStatus(contactName, 'offline', err.message);
|
|
793
798
|
}
|
|
794
799
|
console.error(`❌ Call failed: ${err.message}`);
|
|
795
800
|
process.exit(1);
|
|
@@ -895,119 +900,485 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
895
900
|
require('../src/server.js');
|
|
896
901
|
},
|
|
897
902
|
|
|
898
|
-
quickstart: (args) => {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if (contextFiles.skills) sources.push('installed skills');
|
|
918
|
-
if (contextFiles.claude) sources.push('CLAUDE.md');
|
|
919
|
-
console.log(` Sources: ${sources.length > 0 ? sources.join(', ') : '(none found - using defaults)'}`);
|
|
920
|
-
|
|
921
|
-
conf.completeOnboarding();
|
|
922
|
-
console.log(' \u2705 Disclosure manifest generated and onboarding marked complete.\n');
|
|
923
|
-
}
|
|
924
|
-
} catch (e) {
|
|
925
|
-
// Non-fatal, continue with quickstart
|
|
903
|
+
quickstart: async (args) => {
|
|
904
|
+
const http = require('http');
|
|
905
|
+
const https = require('https');
|
|
906
|
+
const { A2AConfig } = require('../src/lib/config');
|
|
907
|
+
const disc = require('../src/lib/disclosure');
|
|
908
|
+
const {
|
|
909
|
+
normalizeHostInput,
|
|
910
|
+
splitHostPort,
|
|
911
|
+
isLocalOrUnroutableHost
|
|
912
|
+
} = require('../src/lib/invite-host');
|
|
913
|
+
const { getExternalIp } = require('../src/lib/external-ip');
|
|
914
|
+
const { CallbookStore } = require('../src/lib/callbook');
|
|
915
|
+
const { isPortListening, tryBindPort } = require('../src/lib/port-scanner');
|
|
916
|
+
|
|
917
|
+
const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
|
|
918
|
+
const config = new A2AConfig();
|
|
919
|
+
|
|
920
|
+
if (args.flags.force) {
|
|
921
|
+
config.resetOnboarding();
|
|
926
922
|
}
|
|
927
923
|
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
924
|
+
const backendPort = (() => {
|
|
925
|
+
const raw = args.flags.port || process.env.A2A_PORT || process.env.PORT || 3001;
|
|
926
|
+
const n = Number.parseInt(String(raw), 10);
|
|
927
|
+
return (Number.isFinite(n) && n > 0 && n <= 65535) ? n : 3001;
|
|
928
|
+
})();
|
|
933
929
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
930
|
+
function looksLikePong(body) {
|
|
931
|
+
try {
|
|
932
|
+
const parsed = JSON.parse(String(body || ''));
|
|
933
|
+
if (parsed && typeof parsed === 'object' && parsed.pong === true) return true;
|
|
934
|
+
} catch (err) {
|
|
935
|
+
// ignore
|
|
936
|
+
}
|
|
937
|
+
return String(body || '').includes('"pong":true') || String(body || '').includes('"pong": true');
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function fetchUrlText(url, timeoutMs = 5000) {
|
|
941
|
+
return new Promise((resolve, reject) => {
|
|
942
|
+
let parsed;
|
|
943
|
+
try {
|
|
944
|
+
parsed = new URL(url);
|
|
945
|
+
} catch (err) {
|
|
946
|
+
reject(new Error('invalid_url'));
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const client = parsed.protocol === 'https:' ? https : http;
|
|
950
|
+
const req = client.request({
|
|
951
|
+
protocol: parsed.protocol,
|
|
952
|
+
hostname: parsed.hostname,
|
|
953
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
954
|
+
method: 'GET',
|
|
955
|
+
path: parsed.pathname + parsed.search,
|
|
956
|
+
headers: {
|
|
957
|
+
'User-Agent': `a2acalling/${process.env.npm_package_version || 'dev'} (quickstart)`
|
|
958
|
+
},
|
|
959
|
+
timeout: timeoutMs
|
|
960
|
+
}, (res) => {
|
|
961
|
+
let data = '';
|
|
962
|
+
res.setEncoding('utf8');
|
|
963
|
+
res.on('data', (chunk) => {
|
|
964
|
+
data += chunk;
|
|
965
|
+
if (data.length > 1024 * 256) {
|
|
966
|
+
req.destroy(new Error('response_too_large'));
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: data }));
|
|
970
|
+
});
|
|
971
|
+
req.on('error', reject);
|
|
972
|
+
req.on('timeout', () => req.destroy(new Error('timeout')));
|
|
973
|
+
req.end();
|
|
946
974
|
});
|
|
947
|
-
|
|
948
|
-
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
949
|
-
req.end();
|
|
950
|
-
});
|
|
975
|
+
}
|
|
951
976
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
977
|
+
async function probeLocalPing(port, timeoutMs = 1000) {
|
|
978
|
+
try {
|
|
979
|
+
const res = await fetchUrlText(`http://127.0.0.1:${port}/api/a2a/ping`, timeoutMs);
|
|
980
|
+
return { ok: looksLikePong(res.body), statusCode: res.statusCode, body: res.body };
|
|
981
|
+
} catch (err) {
|
|
982
|
+
return { ok: false, error: err && err.message ? err.message : 'request_failed' };
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function externalPingCheck(targetUrl) {
|
|
987
|
+
const providers = [
|
|
988
|
+
{
|
|
989
|
+
name: 'allorigins',
|
|
990
|
+
buildUrl: () => {
|
|
991
|
+
const u = new URL('https://api.allorigins.win/raw');
|
|
992
|
+
u.searchParams.set('url', targetUrl);
|
|
993
|
+
return u.toString();
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
name: 'jina',
|
|
998
|
+
buildUrl: () => `https://r.jina.ai/${targetUrl}`
|
|
999
|
+
}
|
|
1000
|
+
];
|
|
1001
|
+
|
|
1002
|
+
for (const provider of providers) {
|
|
1003
|
+
try {
|
|
1004
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1005
|
+
const res = await fetchUrlText(provider.buildUrl(), 8000);
|
|
1006
|
+
if (looksLikePong(res.body)) {
|
|
1007
|
+
return { ok: true, provider: provider.name, statusCode: res.statusCode };
|
|
1008
|
+
}
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
// try next
|
|
1011
|
+
}
|
|
963
1012
|
}
|
|
1013
|
+
return { ok: false };
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function slugify(value) {
|
|
1017
|
+
return String(value || '')
|
|
1018
|
+
.toLowerCase()
|
|
1019
|
+
.replace(/['"]/g, '')
|
|
1020
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1021
|
+
.replace(/^-+|-+$/g, '')
|
|
1022
|
+
.slice(0, 60);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function uniqueNonEmpty(items, limit = 24) {
|
|
1026
|
+
const out = [];
|
|
1027
|
+
const seen = new Set();
|
|
1028
|
+
for (const raw of items) {
|
|
1029
|
+
const s = String(raw || '').trim();
|
|
1030
|
+
if (!s) continue;
|
|
1031
|
+
if (seen.has(s)) continue;
|
|
1032
|
+
seen.add(s);
|
|
1033
|
+
out.push(s);
|
|
1034
|
+
if (out.length >= limit) break;
|
|
1035
|
+
}
|
|
1036
|
+
return out;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function extractSectionBullets(markdown, headingRegex) {
|
|
1040
|
+
const text = String(markdown || '');
|
|
1041
|
+
const match = text.match(new RegExp(`##\\s*(?:${headingRegex})[^\\n]*\\n([\\s\\S]*?)(?=\\n##|$)`, 'i'));
|
|
1042
|
+
if (!match) return [];
|
|
1043
|
+
return match[1]
|
|
1044
|
+
.split('\n')
|
|
1045
|
+
.map(l => l.trim())
|
|
1046
|
+
.filter(l => l.startsWith('-') || l.startsWith('*'))
|
|
1047
|
+
.map(l => l.replace(/^[\\s\\-\\*]+/, '').trim())
|
|
1048
|
+
.filter(Boolean);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function tierFromManifest(manifest, tier, fallback = []) {
|
|
1052
|
+
const t = (manifest && manifest.topics && manifest.topics[tier]) ? manifest.topics[tier] : null;
|
|
1053
|
+
if (!t) return fallback;
|
|
1054
|
+
const items = []
|
|
1055
|
+
.concat(Array.isArray(t.lead_with) ? t.lead_with : [])
|
|
1056
|
+
.concat(Array.isArray(t.discuss_freely) ? t.discuss_freely : [])
|
|
1057
|
+
.concat(Array.isArray(t.deflect) ? t.deflect : []);
|
|
1058
|
+
const topics = items.map(x => (x && x.topic) ? x.topic : '').filter(Boolean);
|
|
1059
|
+
return topics.length ? topics : fallback;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function buildTierRecommendations(contextFiles, manifest) {
|
|
1063
|
+
const publicFallback = ['chat', 'openclaw', 'a2a'];
|
|
1064
|
+
const friendsFallback = ['chat', 'search', 'openclaw', 'a2a'];
|
|
1065
|
+
const familyFallback = ['chat', 'search', 'openclaw', 'a2a', 'tools', 'memory'];
|
|
1066
|
+
|
|
1067
|
+
const rawPublic = tierFromManifest(manifest, 'public', publicFallback);
|
|
1068
|
+
const rawFriends = tierFromManifest(manifest, 'friends', friendsFallback);
|
|
1069
|
+
const rawFamily = tierFromManifest(manifest, 'family', familyFallback);
|
|
1070
|
+
|
|
1071
|
+
const goalsFromUser = extractSectionBullets(contextFiles.user, 'Goals|Current|Seeking|Working On');
|
|
1072
|
+
const baseGoals = goalsFromUser.length
|
|
1073
|
+
? goalsFromUser
|
|
1074
|
+
: ['grow network', 'find collaborators', 'build in public'];
|
|
1075
|
+
|
|
1076
|
+
const publicTopics = uniqueNonEmpty(rawPublic.map(slugify).filter(Boolean), 16);
|
|
1077
|
+
const friendsTopics = uniqueNonEmpty(rawFriends.map(slugify).filter(Boolean), 20);
|
|
1078
|
+
const familyTopics = uniqueNonEmpty(rawFamily.map(slugify).filter(Boolean), 24);
|
|
1079
|
+
|
|
1080
|
+
const goals = uniqueNonEmpty(baseGoals.map(slugify).filter(Boolean), 12);
|
|
1081
|
+
|
|
1082
|
+
return {
|
|
1083
|
+
public: { topics: publicTopics, goals: goals.slice(0, 6) },
|
|
1084
|
+
friends: { topics: uniqueNonEmpty([...publicTopics, ...friendsTopics], 24), goals: goals.slice(0, 8) },
|
|
1085
|
+
family: { topics: uniqueNonEmpty([...publicTopics, ...friendsTopics, ...familyTopics], 30), goals }
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function printTierSummary(tiers) {
|
|
1090
|
+
const format = (t) => {
|
|
1091
|
+
const topics = (t.topics || []).join(' · ') || '(none)';
|
|
1092
|
+
const goals = (t.goals || []).join(' · ') || '(none)';
|
|
1093
|
+
return `Topics: ${topics}\nGoals: ${goals}`;
|
|
1094
|
+
};
|
|
1095
|
+
console.log('\nProposed permission tiers:\n');
|
|
1096
|
+
console.log('PUBLIC');
|
|
1097
|
+
console.log(format(tiers.public));
|
|
1098
|
+
console.log('\nFRIENDS');
|
|
1099
|
+
console.log(format(tiers.friends));
|
|
1100
|
+
console.log('\nFAMILY');
|
|
1101
|
+
console.log(format(tiers.family));
|
|
1102
|
+
console.log('');
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// ── Step 1: Background bootstrap (config + manifest) ─────────
|
|
1106
|
+
let contextFiles = {};
|
|
1107
|
+
let manifest = {};
|
|
1108
|
+
try {
|
|
1109
|
+
contextFiles = disc.readContextFiles(workspaceDir);
|
|
1110
|
+
const forceManifest = Boolean(args.flags.force || args.flags['regen-manifest'] || args.flags.regenManifest);
|
|
1111
|
+
if (forceManifest) {
|
|
1112
|
+
const generated = disc.generateDefaultManifest(contextFiles);
|
|
1113
|
+
disc.saveManifest(generated);
|
|
1114
|
+
manifest = generated;
|
|
1115
|
+
} else {
|
|
1116
|
+
manifest = disc.loadManifest();
|
|
1117
|
+
if (!manifest || Object.keys(manifest).length === 0) {
|
|
1118
|
+
const generated = disc.generateDefaultManifest(contextFiles);
|
|
1119
|
+
disc.saveManifest(generated);
|
|
1120
|
+
manifest = generated;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
// Non-fatal: onboarding can proceed even if manifest fails.
|
|
1125
|
+
contextFiles = {};
|
|
1126
|
+
manifest = {};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
console.log('\nA2A deterministic onboarding');
|
|
1130
|
+
console.log('──────────────────────────');
|
|
1131
|
+
|
|
1132
|
+
// ── Step 2: Owner dashboard access (local + optional remote) ─
|
|
1133
|
+
config.setOnboarding({ step: 'access' });
|
|
1134
|
+
|
|
1135
|
+
const hostnameFlagRaw = args.flags.hostname !== undefined ? String(args.flags.hostname) : '';
|
|
1136
|
+
const normalizedHostname = normalizeHostInput(hostnameFlagRaw);
|
|
1137
|
+
|
|
1138
|
+
// Invite host controls the a2a:// hostname we hand out (and remote dashboard pairing URL).
|
|
1139
|
+
let inviteHost = '';
|
|
1140
|
+
if (normalizedHostname) {
|
|
1141
|
+
const parsed = splitHostPort(normalizedHostname);
|
|
1142
|
+
const publicPortRaw = args.flags['public-port'] || args.flags.publicPort || process.env.A2A_PUBLIC_PORT || 443;
|
|
1143
|
+
const publicPort = Number.parseInt(String(publicPortRaw), 10);
|
|
1144
|
+
inviteHost = parsed.port
|
|
1145
|
+
? normalizedHostname
|
|
1146
|
+
: `${parsed.hostname}:${(Number.isFinite(publicPort) && publicPort > 0 && publicPort <= 65535) ? publicPort : 443}`;
|
|
1147
|
+
config.setAgent({ hostname: inviteHost });
|
|
1148
|
+
} else {
|
|
1149
|
+
const existing = normalizeHostInput((config.getAgent() || {}).hostname || '');
|
|
1150
|
+
inviteHost = existing || `localhost:${backendPort}`;
|
|
1151
|
+
if (!existing) {
|
|
1152
|
+
config.setAgent({ hostname: inviteHost });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
964
1155
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
permissions: 'public',
|
|
972
|
-
maxCalls: 100
|
|
973
|
-
});
|
|
1156
|
+
const inviteParsed = splitHostPort(inviteHost);
|
|
1157
|
+
const invitePort = inviteParsed.port;
|
|
1158
|
+
const schemeOverride = String(process.env.A2A_PUBLIC_SCHEME || '').trim();
|
|
1159
|
+
const inviteScheme = schemeOverride || ((!invitePort || invitePort === 443) ? 'https' : 'http');
|
|
1160
|
+
const expectedPingUrl = `${inviteScheme}://${inviteHost}/api/a2a/ping`;
|
|
1161
|
+
const inviteLooksLocal = isLocalOrUnroutableHost(inviteParsed.hostname);
|
|
974
1162
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
});
|
|
1163
|
+
console.log('\n2️⃣ Owner dashboard access');
|
|
1164
|
+
console.log(`Local dashboard: http://127.0.0.1:${backendPort}/dashboard/`);
|
|
1165
|
+
console.log(`Invite host: ${inviteHost}`);
|
|
979
1166
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1167
|
+
if (inviteLooksLocal) {
|
|
1168
|
+
console.log('Remote dashboard: not configured (invite host looks local/unroutable)');
|
|
1169
|
+
console.log(' To enable remote access, rerun with: --hostname YOUR_DOMAIN:443');
|
|
1170
|
+
} else {
|
|
1171
|
+
const callbookStore = new CallbookStore();
|
|
1172
|
+
if (!callbookStore.isAvailable()) {
|
|
1173
|
+
console.log('Remote dashboard: Callbook Remote not available (storage unavailable)');
|
|
1174
|
+
console.log(` Hint: ${callbookStore.getDbError ? callbookStore.getDbError() : 'storage_unavailable'}`);
|
|
1175
|
+
} else {
|
|
1176
|
+
const label = String(args.flags['device-label'] || args.flags.deviceLabel || 'Callbook Remote').trim().slice(0, 120);
|
|
1177
|
+
const ttlHoursRaw = args.flags['callbook-ttl-hours'] || args.flags.callbookTtlHours || 24;
|
|
1178
|
+
const ttlHours = Math.max(1, Math.min(168, Number.parseInt(String(ttlHoursRaw), 10) || 24));
|
|
1179
|
+
const created = callbookStore.createProvisionCode({ label, ttlMs: ttlHours * 60 * 60 * 1000 });
|
|
1180
|
+
if (created && created.success) {
|
|
1181
|
+
const installUrl = `${inviteScheme}://${inviteHost}/callbook/install#code=${created.code}`;
|
|
1182
|
+
console.log(`Remote dashboard: ${installUrl} (one-time, ${ttlHours}h)`);
|
|
1183
|
+
} else {
|
|
1184
|
+
console.log('Remote dashboard: failed to create install link');
|
|
1185
|
+
console.log(` Hint: ${created && created.message ? created.message : (created && created.error ? created.error : 'unknown_error')}`);
|
|
983
1186
|
}
|
|
984
|
-
console.warn('');
|
|
985
1187
|
}
|
|
1188
|
+
}
|
|
986
1189
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1190
|
+
// ── Step 3: Permission tiers (topics + goals) ───────────────
|
|
1191
|
+
const onboardingAfterAccess = config.getOnboarding();
|
|
1192
|
+
if (!onboardingAfterAccess.tiers_confirmed) {
|
|
1193
|
+
const recommendations = buildTierRecommendations(contextFiles, manifest);
|
|
1194
|
+
|
|
1195
|
+
const parseFreeTextList = (raw) => {
|
|
1196
|
+
if (raw === undefined || raw === null || raw === true) return [];
|
|
1197
|
+
const text = String(raw || '').trim();
|
|
1198
|
+
if (!text) return [];
|
|
1199
|
+
return text
|
|
1200
|
+
.split(/[\n,]+/g)
|
|
1201
|
+
.map(s => s.trim())
|
|
1202
|
+
.filter(Boolean);
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const promptLine = async (question) => {
|
|
1206
|
+
const readline = require('readline');
|
|
1207
|
+
return await new Promise(resolve => {
|
|
1208
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1209
|
+
rl.question(question, (answer) => {
|
|
1210
|
+
rl.close();
|
|
1211
|
+
resolve(String(answer || '').trim());
|
|
1212
|
+
});
|
|
1213
|
+
});
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
// Optional owner override: Friends tier topics/interests (most important tier).
|
|
1217
|
+
const interactive = Boolean(
|
|
1218
|
+
args.flags.interactive ||
|
|
1219
|
+
args.flags['ask-friends-topics'] ||
|
|
1220
|
+
args.flags.askFriendsTopics
|
|
1221
|
+
);
|
|
1222
|
+
let friendsTopicsOverride = parseFreeTextList(args.flags['friends-topics'] || args.flags.friendsTopics);
|
|
1223
|
+
const noWorkspaceContext = !contextFiles.user && !contextFiles.heartbeat && !contextFiles.soul &&
|
|
1224
|
+
!contextFiles.memory && !contextFiles.claude;
|
|
1225
|
+
const shouldPromptFriendsTopics = (interactive || noWorkspaceContext) &&
|
|
1226
|
+
friendsTopicsOverride.length === 0 &&
|
|
1227
|
+
process.stdin.isTTY &&
|
|
1228
|
+
process.stdout.isTTY;
|
|
1229
|
+
if (shouldPromptFriendsTopics) {
|
|
1230
|
+
const suggested = (recommendations.friends.topics || []).slice(0, 12).join(', ');
|
|
1231
|
+
const answer = await promptLine(`Friends-tier topics/interests (comma-separated).\nSuggested: ${suggested}\n> `);
|
|
1232
|
+
friendsTopicsOverride = parseFreeTextList(answer);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (friendsTopicsOverride.length > 0) {
|
|
1236
|
+
const normalized = uniqueNonEmpty(friendsTopicsOverride.map(slugify).filter(Boolean), 24);
|
|
1237
|
+
recommendations.friends.topics = uniqueNonEmpty(
|
|
1238
|
+
[...(recommendations.public.topics || []), ...normalized],
|
|
1239
|
+
24
|
|
1240
|
+
);
|
|
1241
|
+
recommendations.family.topics = uniqueNonEmpty(
|
|
1242
|
+
[...(recommendations.friends.topics || []), ...(recommendations.family.topics || [])],
|
|
1243
|
+
30
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
try {
|
|
1248
|
+
config.setTier('public', recommendations.public);
|
|
1249
|
+
config.setTier('friends', recommendations.friends);
|
|
1250
|
+
config.setTier('family', recommendations.family);
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
console.error('\n❌ Tier configuration validation failed.');
|
|
1253
|
+
console.error(` ${err.message}`);
|
|
1254
|
+
if (err.hint) {
|
|
1255
|
+
console.error(` Hint: ${err.hint}`);
|
|
1256
|
+
}
|
|
1257
|
+
console.error('');
|
|
1258
|
+
process.exit(1);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
printTierSummary(recommendations);
|
|
1262
|
+
|
|
1263
|
+
config.setOnboarding({
|
|
1264
|
+
step: 'tiers',
|
|
1265
|
+
tiers_confirmed: true
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// ── Step 4: Port scan + reverse proxy guidance (if needed) ──
|
|
1270
|
+
console.log('\n4️⃣ Port scan + reverse proxy');
|
|
1271
|
+
console.log(`Invite host: ${inviteHost}`);
|
|
1272
|
+
console.log(`Expected ping URL: ${expectedPingUrl}\n`);
|
|
1273
|
+
|
|
1274
|
+
const expectsReverseProxy = Boolean(
|
|
1275
|
+
(invitePort === 80 && backendPort !== 80) ||
|
|
1276
|
+
((!invitePort || invitePort === 443) && backendPort !== 443)
|
|
1277
|
+
);
|
|
1278
|
+
|
|
1279
|
+
if (expectsReverseProxy) {
|
|
1280
|
+
const port80Listening = await isPortListening(80, '127.0.0.1', { timeoutMs: 500 });
|
|
1281
|
+
const port80Bind = await tryBindPort(80, '0.0.0.0');
|
|
1282
|
+
const port80Ping = port80Listening.listening ? await probeLocalPing(80) : { ok: false };
|
|
1283
|
+
|
|
1284
|
+
console.log('Port 80:');
|
|
1285
|
+
if (port80Ping.ok) {
|
|
1286
|
+
console.log(' ✅ serves /api/a2a/ping (A2A detected on :80)');
|
|
1287
|
+
} else if (port80Listening.listening) {
|
|
1288
|
+
console.log(` ⚠️ has a listener (${port80Listening.code || 'in_use'})`);
|
|
1289
|
+
} else if (!port80Bind.ok && port80Bind.code === 'EACCES') {
|
|
1290
|
+
console.log(' ⚠️ appears free but is not bindable by this user (EACCES)');
|
|
1291
|
+
} else if (port80Bind.ok) {
|
|
1292
|
+
console.log(' ✅ free and bindable by this user');
|
|
1293
|
+
} else {
|
|
1294
|
+
console.log(` ⚠️ not bindable (${port80Bind.code || 'unknown'})`);
|
|
1295
|
+
}
|
|
995
1296
|
|
|
996
|
-
|
|
1297
|
+
console.log('\nReverse proxy required (example routes):');
|
|
1298
|
+
console.log(` /api/a2a/* -> http://127.0.0.1:${backendPort}`);
|
|
1299
|
+
console.log(` /dashboard/* -> http://127.0.0.1:${backendPort}`);
|
|
1300
|
+
console.log(` /callbook/* -> http://127.0.0.1:${backendPort}`);
|
|
1301
|
+
console.log('');
|
|
1302
|
+
console.log('If you have configured your reverse proxy and want to continue, run:');
|
|
1303
|
+
console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-ingress`);
|
|
1304
|
+
console.log('');
|
|
1305
|
+
if (!args.flags['confirm-ingress']) {
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
} else {
|
|
1309
|
+
console.log('✅ No reverse proxy required based on invite host/port.');
|
|
1310
|
+
}
|
|
997
1311
|
|
|
998
|
-
|
|
1312
|
+
if (!config.getOnboarding().ingress_confirmed) {
|
|
1313
|
+
config.setOnboarding({
|
|
1314
|
+
step: 'ingress',
|
|
1315
|
+
ingress_confirmed: true
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
999
1318
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1319
|
+
// ── Step 5: External IP + reachability check ───────────────
|
|
1320
|
+
console.log('\n5️⃣ External IP + reachability check');
|
|
1002
1321
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1322
|
+
if (inviteLooksLocal) {
|
|
1323
|
+
console.log('Skipping external IP probe: invite host looks local/unroutable.');
|
|
1324
|
+
} else {
|
|
1325
|
+
const external = await getExternalIp({ forceRefresh: true });
|
|
1326
|
+
if (external && external.ip) {
|
|
1327
|
+
console.log(`External IP (${external.source || 'resolver'}): ${external.ip}`);
|
|
1328
|
+
} else {
|
|
1329
|
+
console.log(`External IP lookup failed: ${external && external.error ? external.error : 'unknown_error'}`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const localListener = await isPortListening(backendPort, '127.0.0.1', { timeoutMs: 500 });
|
|
1334
|
+
if (!localListener.listening) {
|
|
1335
|
+
console.log('\n⚠️ A2A server is not reachable locally yet.');
|
|
1336
|
+
console.log('Start it, then rerun quickstart:');
|
|
1337
|
+
console.log(` A2A_HOSTNAME="${inviteHost}" a2a server --port ${backendPort}`);
|
|
1338
|
+
console.log('');
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
const localPing = await probeLocalPing(backendPort, inviteLooksLocal ? 250 : 1000);
|
|
1342
|
+
if (!localPing.ok) {
|
|
1343
|
+
if (inviteLooksLocal) {
|
|
1344
|
+
console.log(`\n⚠️ Port ${backendPort} is listening but /api/a2a/ping did not respond within a short timeout.`);
|
|
1345
|
+
console.log('Continuing onboarding anyway (invite host is local/unroutable).');
|
|
1346
|
+
} else {
|
|
1347
|
+
console.log('\n⚠️ A2A server is not responding locally yet.');
|
|
1348
|
+
console.log('Start it, then rerun quickstart:');
|
|
1349
|
+
console.log(` A2A_HOSTNAME="${inviteHost}" a2a server --port ${backendPort}`);
|
|
1350
|
+
console.log('');
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (inviteLooksLocal) {
|
|
1356
|
+
console.log('Skipping external reachability check: invite host looks local/unroutable.');
|
|
1357
|
+
} else {
|
|
1358
|
+
const extPing = await externalPingCheck(expectedPingUrl);
|
|
1359
|
+
if (extPing.ok) {
|
|
1360
|
+
console.log(`✅ External ping OK (${extPing.provider})`);
|
|
1361
|
+
} else if (!args.flags['skip-verify']) {
|
|
1362
|
+
console.log('⚠️ External ping FAILED (server may not be publicly reachable yet).');
|
|
1363
|
+
console.log('Fix ingress (DNS/reverse proxy/firewall), then rerun with:');
|
|
1364
|
+
console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-ingress`);
|
|
1365
|
+
console.log('');
|
|
1366
|
+
return;
|
|
1367
|
+
} else {
|
|
1368
|
+
console.log('⚠️ External ping FAILED (skipped via --skip-verify).');
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (!config.getOnboarding().verify_confirmed) {
|
|
1373
|
+
config.setOnboarding({
|
|
1374
|
+
step: 'verify',
|
|
1375
|
+
verify_confirmed: true
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
config.completeOnboarding();
|
|
1380
|
+
console.log('✅ Onboarding complete.');
|
|
1381
|
+
console.log('Next: a2a gui or a2a create or a2a server');
|
|
1011
1382
|
},
|
|
1012
1383
|
|
|
1013
1384
|
install: () => {
|
|
@@ -1125,15 +1496,15 @@ https://github.com/onthegonow/a2a_calling
|
|
|
1125
1496
|
}
|
|
1126
1497
|
},
|
|
1127
1498
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1499
|
+
onboard: (args) => {
|
|
1500
|
+
const { A2AConfig } = require('../src/lib/config');
|
|
1501
|
+
const { readContextFiles, generateDefaultManifest, saveManifest, MANIFEST_FILE } = require('../src/lib/disclosure');
|
|
1502
|
+
const config = new A2AConfig();
|
|
1503
|
+
|
|
1504
|
+
if (config.isOnboarded() && !args.flags.force) {
|
|
1505
|
+
console.log('\u2705 Onboarding already complete. Use --force to regenerate the disclosure manifest.');
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1137
1508
|
|
|
1138
1509
|
const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
|
|
1139
1510
|
console.log('\n\ud83d\ude80 A2A Onboarding\n' + '\u2550'.repeat(50) + '\n');
|
|
@@ -1141,15 +1512,14 @@ https://github.com/onthegonow/a2a_calling
|
|
|
1141
1512
|
|
|
1142
1513
|
const contextFiles = readContextFiles(workspaceDir);
|
|
1143
1514
|
// Print what was found
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
};
|
|
1515
|
+
const sources = {
|
|
1516
|
+
'USER.md': contextFiles.user,
|
|
1517
|
+
'HEARTBEAT.md': contextFiles.heartbeat,
|
|
1518
|
+
'SOUL.md': contextFiles.soul,
|
|
1519
|
+
'SKILL.md': contextFiles.skill,
|
|
1520
|
+
'CLAUDE.md': contextFiles.claude,
|
|
1521
|
+
'memory/*.md': contextFiles.memory
|
|
1522
|
+
};
|
|
1153
1523
|
for (const [name, content] of Object.entries(sources)) {
|
|
1154
1524
|
console.log(` ${content ? '\u2705' : '\u274c'} ${name}`);
|
|
1155
1525
|
}
|
|
@@ -1159,15 +1529,13 @@ https://github.com/onthegonow/a2a_calling
|
|
|
1159
1529
|
|
|
1160
1530
|
const agentName = args.flags.name || config.getAgent().name || process.env.A2A_AGENT_NAME || '';
|
|
1161
1531
|
const hostname = args.flags.hostname || config.getAgent().hostname || process.env.A2A_HOSTNAME || '';
|
|
1162
|
-
|
|
1163
|
-
|
|
1532
|
+
if (agentName) config.setAgent({ name: agentName });
|
|
1533
|
+
if (hostname) config.setAgent({ hostname });
|
|
1164
1534
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
console.log(` Next: a2a quickstart or a2a server\n`);
|
|
1170
|
-
},
|
|
1535
|
+
console.log(`\n\u2705 Disclosure manifest generated.`);
|
|
1536
|
+
console.log(` Manifest: ${MANIFEST_FILE}`);
|
|
1537
|
+
console.log(' Next: a2a quickstart\n');
|
|
1538
|
+
},
|
|
1171
1539
|
|
|
1172
1540
|
help: () => {
|
|
1173
1541
|
console.log(`A2A Calling - Agent-to-Agent Communication
|
|
@@ -1194,11 +1562,13 @@ Contacts:
|
|
|
1194
1562
|
contacts add <url> Add a contact
|
|
1195
1563
|
--name, -n Agent name
|
|
1196
1564
|
--owner, -o Owner name
|
|
1565
|
+
--server-name Server label (optional)
|
|
1197
1566
|
--notes Notes about this contact
|
|
1198
1567
|
--tags Comma-separated tags
|
|
1199
1568
|
--link Link to token ID you gave them
|
|
1200
1569
|
contacts show <n> Show contact details + linked token
|
|
1201
1570
|
contacts edit <n> Edit contact metadata
|
|
1571
|
+
--server-name Server label (optional)
|
|
1202
1572
|
contacts link <n> <tok> Link a token to a contact
|
|
1203
1573
|
contacts ping <n> Ping contact, update status
|
|
1204
1574
|
contacts rm <n> Remove contact
|
|
@@ -1215,7 +1585,7 @@ Conversations:
|
|
|
1215
1585
|
conversations end <id> End and summarize conversation
|
|
1216
1586
|
|
|
1217
1587
|
Calling:
|
|
1218
|
-
call <contact|url> <msg> Call a
|
|
1588
|
+
call <contact|url> <msg> Call a contact (or invite URL)
|
|
1219
1589
|
ping <url> Check if agent is reachable
|
|
1220
1590
|
status <url> Get A2A status
|
|
1221
1591
|
gui Open the local dashboard GUI in a browser
|
|
@@ -1225,9 +1595,16 @@ Server:
|
|
|
1225
1595
|
server Start the A2A server
|
|
1226
1596
|
--port, -p Port to listen on (default: 3001)
|
|
1227
1597
|
|
|
1228
|
-
quickstart
|
|
1229
|
-
--
|
|
1230
|
-
--
|
|
1598
|
+
quickstart Onboarding (access → tiers → ingress → verify)
|
|
1599
|
+
--hostname Public hostname for remote access (e.g. myserver.com:443)
|
|
1600
|
+
--public-port Port to assume when --hostname omits a port (default: 443)
|
|
1601
|
+
--port A2A server port to run locally (default: 3001)
|
|
1602
|
+
--friends-topics Override Friends tier topics/interests (comma or newline-separated)
|
|
1603
|
+
--interactive Prompt for Friends tier topics if needed
|
|
1604
|
+
--confirm-ingress Confirm reverse proxy/ingress is configured and continue
|
|
1605
|
+
--skip-verify Skip external reachability check (not recommended)
|
|
1606
|
+
--force Reset onboarding + regenerate disclosure manifest
|
|
1607
|
+
--regen-manifest Regenerate disclosure manifest (no onboarding reset)
|
|
1231
1608
|
|
|
1232
1609
|
onboard Generate disclosure manifest from workspace context
|
|
1233
1610
|
--force Re-run even if already onboarded
|