a2acalling 0.6.0 → 0.6.2

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 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 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
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" first to set up your agent\'s disclosure topics and permissions.');
51
- console.warn(' Without onboarding, invites will have empty topics and calls use generic responses.\n');
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 || t.permissions; // backward compat
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.addRemote(url, name);
326
+ const result = store.addContact(url, { name });
327
327
  if (!result.success) {
328
- console.log(`Remote already registered: ${result.existing.name}`);
328
+ console.log(`Contact already registered: ${result.existing.name}`);
329
329
  return;
330
330
  }
331
- console.log(`✅ Remote agent added: ${result.remote.name} (${result.remote.host})`);
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
- // Legacy alias for contacts
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 remotes = store.listRemotes();
356
- if (remotes.length === 0) {
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 (${remotes.length})\n`);
363
- for (const r of remotes) {
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 || r.linked_token.permissions;
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.addRemote(url, options);
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.remote.name}`);
416
- if (result.remote.owner) console.log(` Owner: ${result.remote.owner}`);
417
- console.log(` Host: ${result.remote.host}`);
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.remote.name} <token_id>`);
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 remotes = store.listRemotes();
438
- const remote = remotes.find(r => r.name === name || r.id === name);
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 || t.permissions;
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.updateRemote(name, updates);
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.getRemote(name);
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.updateRemoteStatus(name, 'online');
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.updateRemoteStatus(name, 'offline', err.message);
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.removeRemote(name);
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.getRemote(target);
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.updateRemoteStatus(contactName, 'online');
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.updateRemoteStatus(contactName, 'offline', err.message);
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,426 @@ https://github.com/onthegonow/a2a_calling`;
895
900
  require('../src/server.js');
896
901
  },
897
902
 
898
- quickstart: (args) => {
899
- // Auto-complete onboarding if not done
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();
922
+ }
923
+
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
+ })();
929
+
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();
974
+ });
975
+ }
976
+
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
+ }
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 = {};
900
1108
  try {
901
- const { A2AConfig } = require('../src/lib/config');
902
- const { readContextFiles, generateDefaultManifest, saveManifest } = require('../src/lib/disclosure');
903
- const conf = new A2AConfig();
904
- if (!conf.isOnboarded()) {
905
- console.log('Setting up disclosure manifest from workspace context...');
906
- const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
907
- const contextFiles = readContextFiles(workspaceDir);
908
- const manifest = generateDefaultManifest(contextFiles);
909
- saveManifest(manifest);
910
-
911
- const sources = [];
912
- if (contextFiles.user) sources.push('USER.md');
913
- if (contextFiles.heartbeat) sources.push('HEARTBEAT.md');
914
- if (contextFiles.soul) sources.push('SOUL.md');
915
- if (contextFiles.skill) sources.push('SKILL.md');
916
- if (contextFiles.memory) sources.push('memory/*.md');
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');
1109
+ contextFiles = disc.readContextFiles(workspaceDir);
1110
+ manifest = disc.loadManifest();
1111
+ if (!manifest || Object.keys(manifest).length === 0) {
1112
+ const generated = disc.generateDefaultManifest(contextFiles);
1113
+ disc.saveManifest(generated);
1114
+ manifest = generated;
1115
+ }
1116
+ } catch (err) {
1117
+ // Non-fatal: onboarding can proceed even if manifest fails.
1118
+ contextFiles = {};
1119
+ manifest = {};
1120
+ }
1121
+
1122
+ console.log('\nA2A deterministic onboarding');
1123
+ console.log('──────────────────────────');
1124
+
1125
+ // ── Step 2: Owner dashboard access (local + optional remote)
1126
+ config.setOnboarding({ step: 'access' });
1127
+
1128
+ const hostnameFlagRaw = args.flags.hostname !== undefined ? String(args.flags.hostname) : '';
1129
+ const normalizedHostname = normalizeHostInput(hostnameFlagRaw);
1130
+
1131
+ // Invite host controls the a2a:// hostname we hand out (and remote dashboard pairing URL).
1132
+ let inviteHost = '';
1133
+ if (normalizedHostname) {
1134
+ const parsed = splitHostPort(normalizedHostname);
1135
+ const publicPortRaw = args.flags['public-port'] || args.flags.publicPort || process.env.A2A_PUBLIC_PORT || 443;
1136
+ const publicPort = Number.parseInt(String(publicPortRaw), 10);
1137
+ inviteHost = parsed.port
1138
+ ? normalizedHostname
1139
+ : `${parsed.hostname}:${(Number.isFinite(publicPort) && publicPort > 0 && publicPort <= 65535) ? publicPort : 443}`;
1140
+ config.setAgent({ hostname: inviteHost });
1141
+ } else {
1142
+ const existing = normalizeHostInput((config.getAgent() || {}).hostname || '');
1143
+ inviteHost = existing || `localhost:${backendPort}`;
1144
+ if (!existing) {
1145
+ config.setAgent({ hostname: inviteHost });
923
1146
  }
924
- } catch (e) {
925
- // Non-fatal, continue with quickstart
926
1147
  }
927
1148
 
928
- const http = require('http');
929
- const { splitHostPort } = require('../src/lib/invite-host');
930
- const resolveHostPromise = resolveInviteHostname();
931
- const name = args.flags.name || args.flags.n || 'My Agent';
932
- const owner = args.flags.owner || args.flags.o || null;
1149
+ const inviteParsed = splitHostPort(inviteHost);
1150
+ const invitePort = inviteParsed.port;
1151
+ const schemeOverride = String(process.env.A2A_PUBLIC_SCHEME || '').trim();
1152
+ const inviteScheme = schemeOverride || ((!invitePort || invitePort === 443) ? 'https' : 'http');
1153
+ const expectedPingUrl = `${inviteScheme}://${inviteHost}/api/a2a/ping`;
1154
+ const inviteLooksLocal = isLocalOrUnroutableHost(inviteParsed.hostname);
933
1155
 
934
- console.log(`\n🚀 A2A Quickstart\n${'═'.repeat(50)}\n`);
935
-
936
- // Step 1: Check server
937
- console.log('1️⃣ Checking server status...');
938
- const checkServer = (port) => new Promise((resolve) => {
939
- const req = http.request({
940
- hostname: '127.0.0.1',
941
- port,
942
- path: '/api/a2a/ping',
943
- timeout: 2000
944
- }, (res) => {
945
- resolve(res.statusCode === 200);
946
- });
947
- req.on('error', () => resolve(false));
948
- req.on('timeout', () => { req.destroy(); resolve(false); });
949
- req.end();
950
- });
1156
+ console.log('\n2️⃣ Owner dashboard access');
1157
+ console.log(`Local dashboard: http://127.0.0.1:${backendPort}/dashboard/`);
1158
+ console.log(`Invite host: ${inviteHost}`);
951
1159
 
952
- resolveHostPromise.then(resolved => {
953
- const parsed = splitHostPort(resolved.host);
954
- const serverPort = parsed.port || 3001;
955
- return checkServer(serverPort).then(serverOk => ({ serverOk, resolved, serverPort }));
956
- }).then(({ serverOk, resolved, serverPort }) => {
957
- const hostname = resolved.host;
958
- if (!serverOk) {
959
- console.log(' ⚠️ Server not running!');
960
- console.log(` Run: A2A_HOSTNAME="${hostname}" a2a server --port ${serverPort}\n`);
1160
+ if (inviteLooksLocal) {
1161
+ console.log('Remote dashboard: not configured (invite host looks local/unroutable)');
1162
+ console.log(' To enable remote access, rerun with: --hostname YOUR_DOMAIN:443');
1163
+ } else {
1164
+ const callbookStore = new CallbookStore();
1165
+ if (!callbookStore.isAvailable()) {
1166
+ console.log('Remote dashboard: Callbook Remote not available (storage unavailable)');
1167
+ console.log(` Hint: ${callbookStore.getDbError ? callbookStore.getDbError() : 'storage_unavailable'}`);
961
1168
  } else {
962
- console.log(' Server running\n');
1169
+ const label = String(args.flags['device-label'] || args.flags.deviceLabel || 'Callbook Remote').trim().slice(0, 120);
1170
+ const ttlHoursRaw = args.flags['callbook-ttl-hours'] || args.flags.callbookTtlHours || 24;
1171
+ const ttlHours = Math.max(1, Math.min(168, Number.parseInt(String(ttlHoursRaw), 10) || 24));
1172
+ const created = callbookStore.createProvisionCode({ label, ttlMs: ttlHours * 60 * 60 * 1000 });
1173
+ if (created && created.success) {
1174
+ const installUrl = `${inviteScheme}://${inviteHost}/callbook/install#code=${created.code}`;
1175
+ console.log(`Remote dashboard: ${installUrl} (one-time, ${ttlHours}h)`);
1176
+ } else {
1177
+ console.log('Remote dashboard: failed to create install link');
1178
+ console.log(` Hint: ${created && created.message ? created.message : (created && created.error ? created.error : 'unknown_error')}`);
1179
+ }
963
1180
  }
1181
+ }
964
1182
 
965
- // Step 2: Create invite
966
- console.log('2️⃣ Creating your first invite...\n');
967
- const { token, record } = store.create({
968
- name,
969
- owner,
970
- expires: '7d',
971
- permissions: 'public',
972
- maxCalls: 100
973
- });
1183
+ // ── Step 3: Permission tiers (topics + goals) ───────────────
1184
+ const onboardingAfterAccess = config.getOnboarding();
1185
+ if (!onboardingAfterAccess.tiers_confirmed) {
1186
+ const recommendations = buildTierRecommendations(contextFiles, manifest);
1187
+
1188
+ config.setTier('public', recommendations.public);
1189
+ config.setTier('friends', recommendations.friends);
1190
+ config.setTier('family', recommendations.family);
1191
+
1192
+ printTierSummary(recommendations);
974
1193
 
975
- const inviteUrl = `a2a://${hostname}/${token}`;
976
- const expiresText = new Date(record.expires_at).toLocaleDateString('en-US', {
977
- month: 'short', day: 'numeric', year: 'numeric'
1194
+ if (!args.flags['confirm-tiers']) {
1195
+ console.log('3️⃣ Confirm tiers');
1196
+ console.log('Review the topics/goals above. To confirm and continue, run:');
1197
+ console.log(` a2a quickstart --port ${backendPort} --confirm-tiers`);
1198
+ console.log('Optional (remote access):');
1199
+ console.log(` a2a quickstart --hostname YOUR_DOMAIN:443 --port ${backendPort} --confirm-tiers`);
1200
+ console.log('');
1201
+ return;
1202
+ }
1203
+
1204
+ config.setOnboarding({
1205
+ step: 'tiers',
1206
+ tiers_confirmed: true
978
1207
  });
1208
+ }
979
1209
 
980
- if (resolved.warnings && resolved.warnings.length) {
981
- for (const w of resolved.warnings) {
982
- console.warn(`\n⚠️ ${w}`);
983
- }
984
- console.warn('');
1210
+ // ── Step 4: Port scan + reverse proxy guidance (if needed) ──
1211
+ console.log('\n4️⃣ Port scan + reverse proxy');
1212
+ console.log(`Invite host: ${inviteHost}`);
1213
+ console.log(`Expected ping URL: ${expectedPingUrl}\n`);
1214
+
1215
+ const expectsReverseProxy = Boolean(
1216
+ (invitePort === 80 && backendPort !== 80) ||
1217
+ ((!invitePort || invitePort === 443) && backendPort !== 443)
1218
+ );
1219
+
1220
+ if (expectsReverseProxy) {
1221
+ const port80Listening = await isPortListening(80, '127.0.0.1', { timeoutMs: 500 });
1222
+ const port80Bind = await tryBindPort(80, '0.0.0.0');
1223
+ const port80Ping = port80Listening.listening ? await probeLocalPing(80) : { ok: false };
1224
+
1225
+ console.log('Port 80:');
1226
+ if (port80Ping.ok) {
1227
+ console.log(' ✅ serves /api/a2a/ping (A2A detected on :80)');
1228
+ } else if (port80Listening.listening) {
1229
+ console.log(` ⚠️ has a listener (${port80Listening.code || 'in_use'})`);
1230
+ } else if (!port80Bind.ok && port80Bind.code === 'EACCES') {
1231
+ console.log(' ⚠️ appears free but is not bindable by this user (EACCES)');
1232
+ } else if (port80Bind.ok) {
1233
+ console.log(' ✅ free and bindable by this user');
1234
+ } else {
1235
+ console.log(` ⚠️ not bindable (${port80Bind.code || 'unknown'})`);
985
1236
  }
986
1237
 
987
- // Step 3: Show the invite
988
- const ownerText = owner ? `${owner}` : 'Someone';
989
- const topicsList = record.allowed_topics.join(' · ');
990
- const goalsList = (record.allowed_goals || []).join(' · ');
991
- console.log('3️⃣ Share this invite:\n');
992
- console.log(''.repeat(50));
993
- console.log(`
994
- 📞🗣️ **Agent-to-Agent Call Invite**
1238
+ console.log('\nReverse proxy required (example routes):');
1239
+ console.log(` /api/a2a/* -> http://127.0.0.1:${backendPort}`);
1240
+ console.log(` /dashboard/* -> http://127.0.0.1:${backendPort}`);
1241
+ console.log(` /callbook/* -> http://127.0.0.1:${backendPort}`);
1242
+ console.log('');
1243
+ console.log('If you have configured your reverse proxy and want to continue, run:');
1244
+ console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-tiers --confirm-ingress`);
1245
+ console.log('');
1246
+ if (!args.flags['confirm-ingress']) {
1247
+ return;
1248
+ }
1249
+ } else {
1250
+ console.log('✅ No reverse proxy required based on invite host/port.');
1251
+ }
995
1252
 
996
- 👤 **${ownerText}** would like your agent to call **${name}** and explore where our owners might collaborate.
1253
+ if (!config.getOnboarding().ingress_confirmed) {
1254
+ config.setOnboarding({
1255
+ step: 'ingress',
1256
+ ingress_confirmed: true
1257
+ });
1258
+ }
997
1259
 
998
- 💬 ${topicsList}${goalsList ? `\n🎯 ${goalsList}` : ''}
1260
+ // ── Step 5: External IP + reachability check ───────────────
1261
+ console.log('\n5️⃣ External IP + reachability check');
999
1262
 
1000
- ${inviteUrl}
1001
- ${expiresText}
1263
+ if (inviteLooksLocal) {
1264
+ console.log('Skipping external IP probe: invite host looks local/unroutable.');
1265
+ } else {
1266
+ const external = await getExternalIp({ forceRefresh: true });
1267
+ if (external && external.ip) {
1268
+ console.log(`External IP (${external.source || 'resolver'}): ${external.ip}`);
1269
+ } else {
1270
+ console.log(`External IP lookup failed: ${external && external.error ? external.error : 'unknown_error'}`);
1271
+ }
1272
+ }
1002
1273
 
1003
- ── setup ──
1004
- npm i -g a2acalling && a2a add "${inviteUrl}" "${name}" && a2a call "${name}" "Hello from my owner!"
1005
- https://github.com/onthegonow/a2a_calling
1006
- `);
1007
- console.log('─'.repeat(50));
1008
- console.log(`\n✅ Done! Share the invite above with other agents.\n`);
1009
- console.log(`To revoke: a2a revoke ${record.id}\n`);
1010
- });
1274
+ const localListener = await isPortListening(backendPort, '127.0.0.1', { timeoutMs: 500 });
1275
+ if (!localListener.listening) {
1276
+ console.log('\n⚠️ A2A server is not reachable locally yet.');
1277
+ console.log('Start it, then rerun quickstart:');
1278
+ console.log(` A2A_HOSTNAME="${inviteHost}" a2a server --port ${backendPort}`);
1279
+ console.log('');
1280
+ return;
1281
+ }
1282
+ const localPing = await probeLocalPing(backendPort, inviteLooksLocal ? 250 : 1000);
1283
+ if (!localPing.ok) {
1284
+ if (inviteLooksLocal) {
1285
+ console.log(`\n⚠️ Port ${backendPort} is listening but /api/a2a/ping did not respond within a short timeout.`);
1286
+ console.log('Continuing onboarding anyway (invite host is local/unroutable).');
1287
+ } else {
1288
+ console.log('\n⚠️ A2A server is not responding locally yet.');
1289
+ console.log('Start it, then rerun quickstart:');
1290
+ console.log(` A2A_HOSTNAME="${inviteHost}" a2a server --port ${backendPort}`);
1291
+ console.log('');
1292
+ return;
1293
+ }
1294
+ }
1295
+
1296
+ if (inviteLooksLocal) {
1297
+ console.log('Skipping external reachability check: invite host looks local/unroutable.');
1298
+ } else {
1299
+ const extPing = await externalPingCheck(expectedPingUrl);
1300
+ if (extPing.ok) {
1301
+ console.log(`✅ External ping OK (${extPing.provider})`);
1302
+ } else if (!args.flags['skip-verify']) {
1303
+ console.log('⚠️ External ping FAILED (server may not be publicly reachable yet).');
1304
+ console.log('Fix ingress (DNS/reverse proxy/firewall), then rerun with:');
1305
+ console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-tiers --confirm-ingress`);
1306
+ console.log('');
1307
+ return;
1308
+ } else {
1309
+ console.log('⚠️ External ping FAILED (skipped via --skip-verify).');
1310
+ }
1311
+ }
1312
+
1313
+ if (!config.getOnboarding().verify_confirmed) {
1314
+ config.setOnboarding({
1315
+ step: 'verify',
1316
+ verify_confirmed: true
1317
+ });
1318
+ }
1319
+
1320
+ config.completeOnboarding();
1321
+ console.log('✅ Onboarding complete.');
1322
+ console.log('Next: a2a gui or a2a create or a2a server');
1011
1323
  },
1012
1324
 
1013
1325
  install: () => {
@@ -1125,15 +1437,15 @@ https://github.com/onthegonow/a2a_calling
1125
1437
  }
1126
1438
  },
1127
1439
 
1128
- onboard: (args) => {
1129
- const { A2AConfig } = require('../src/lib/config');
1130
- const { readContextFiles, generateDefaultManifest, saveManifest, MANIFEST_FILE } = require('../src/lib/disclosure');
1131
- const config = new A2AConfig();
1132
-
1133
- if (config.isOnboarded() && !args.flags.force) {
1134
- console.log('\u2705 Onboarding already complete. Use --force to re-run.');
1135
- return;
1136
- }
1440
+ onboard: (args) => {
1441
+ const { A2AConfig } = require('../src/lib/config');
1442
+ const { readContextFiles, generateDefaultManifest, saveManifest, MANIFEST_FILE } = require('../src/lib/disclosure');
1443
+ const config = new A2AConfig();
1444
+
1445
+ if (config.isOnboarded() && !args.flags.force) {
1446
+ console.log('\u2705 Onboarding already complete. Use --force to regenerate the disclosure manifest.');
1447
+ return;
1448
+ }
1137
1449
 
1138
1450
  const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
1139
1451
  console.log('\n\ud83d\ude80 A2A Onboarding\n' + '\u2550'.repeat(50) + '\n');
@@ -1159,15 +1471,13 @@ https://github.com/onthegonow/a2a_calling
1159
1471
 
1160
1472
  const agentName = args.flags.name || config.getAgent().name || process.env.A2A_AGENT_NAME || '';
1161
1473
  const hostname = args.flags.hostname || config.getAgent().hostname || process.env.A2A_HOSTNAME || '';
1162
- if (agentName) config.setAgent({ name: agentName });
1163
- if (hostname) config.setAgent({ hostname });
1474
+ if (agentName) config.setAgent({ name: agentName });
1475
+ if (hostname) config.setAgent({ hostname });
1164
1476
 
1165
- config.completeOnboarding();
1166
-
1167
- console.log(`\n\u2705 Onboarding complete!`);
1168
- console.log(` Manifest: ${MANIFEST_FILE}`);
1169
- console.log(` Next: a2a quickstart or a2a server\n`);
1170
- },
1477
+ console.log(`\n\u2705 Disclosure manifest generated.`);
1478
+ console.log(` Manifest: ${MANIFEST_FILE}`);
1479
+ console.log(' Next: a2a quickstart\n');
1480
+ },
1171
1481
 
1172
1482
  help: () => {
1173
1483
  console.log(`A2A Calling - Agent-to-Agent Communication
@@ -1194,11 +1504,13 @@ Contacts:
1194
1504
  contacts add <url> Add a contact
1195
1505
  --name, -n Agent name
1196
1506
  --owner, -o Owner name
1507
+ --server-name Server label (optional)
1197
1508
  --notes Notes about this contact
1198
1509
  --tags Comma-separated tags
1199
1510
  --link Link to token ID you gave them
1200
1511
  contacts show <n> Show contact details + linked token
1201
1512
  contacts edit <n> Edit contact metadata
1513
+ --server-name Server label (optional)
1202
1514
  contacts link <n> <tok> Link a token to a contact
1203
1515
  contacts ping <n> Ping contact, update status
1204
1516
  contacts rm <n> Remove contact
@@ -1215,7 +1527,7 @@ Conversations:
1215
1527
  conversations end <id> End and summarize conversation
1216
1528
 
1217
1529
  Calling:
1218
- call <contact|url> <msg> Call a remote agent
1530
+ call <contact|url> <msg> Call a contact (or invite URL)
1219
1531
  ping <url> Check if agent is reachable
1220
1532
  status <url> Get A2A status
1221
1533
  gui Open the local dashboard GUI in a browser
@@ -1225,9 +1537,14 @@ Server:
1225
1537
  server Start the A2A server
1226
1538
  --port, -p Port to listen on (default: 3001)
1227
1539
 
1228
- quickstart One-command setup: check server + create invite
1229
- --name, -n Agent name for the invite
1230
- --owner, -o Owner name (human behind the agent)
1540
+ quickstart Deterministic onboarding (access tiers ingress → verify)
1541
+ --hostname Public hostname for remote access (e.g. myserver.com:443)
1542
+ --public-port Port to assume when --hostname omits a port (default: 443)
1543
+ --port A2A server port to run locally (default: 3001)
1544
+ --confirm-tiers Confirm tier topics/goals and continue
1545
+ --confirm-ingress Confirm reverse proxy/ingress is configured and continue
1546
+ --skip-verify Skip external reachability check (not recommended)
1547
+ --force Reset onboarding and start over
1231
1548
 
1232
1549
  onboard Generate disclosure manifest from workspace context
1233
1550
  --force Re-run even if already onboarded