a2acalling 0.6.5 → 0.6.7

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
@@ -12,11 +12,41 @@
12
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
+ * a2a uninstall Stop server and remove local A2A config
15
16
  */
16
17
 
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const path = require('path');
21
+ const { spawn } = require('child_process');
17
22
  const { TokenStore } = require('../src/lib/tokens');
18
23
  const { A2AClient } = require('../src/lib/client');
19
24
 
25
+ const CONFIG_DIR = process.env.A2A_CONFIG_DIR || process.env.OPENCLAW_CONFIG_DIR || path.join(os.homedir(), '.config', 'openclaw');
26
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'a2a-config.json');
27
+ const ONBOARDING_EXEMPT = new Set([
28
+ 'quickstart',
29
+ 'help',
30
+ 'version',
31
+ 'update',
32
+ 'uninstall',
33
+ 'onboard',
34
+ 'gui',
35
+ 'dashboard',
36
+ 'server',
37
+ 'setup',
38
+ 'install'
39
+ ]);
40
+
41
+ function isOnboarded() {
42
+ try {
43
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
44
+ return config.onboarding?.version === 2 && config.onboarding?.step === 'complete';
45
+ } catch (err) {
46
+ return false;
47
+ }
48
+ }
49
+
20
50
  // Lazy load conversation store (requires better-sqlite3)
21
51
  let convStore = null;
22
52
  function getConvStore() {
@@ -40,20 +70,29 @@ function getConvStore() {
40
70
 
41
71
  const store = new TokenStore();
42
72
 
43
- // Check onboarding status — warns but does not block
44
- function checkOnboarding(commandName) {
45
- try {
46
- const { A2AConfig } = require('../src/lib/config');
47
- const config = new A2AConfig();
48
- if (!config.isOnboarded()) {
49
- console.warn('\n\u26a0\ufe0f A2A onboarding not complete.');
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
- return false;
73
+ function enforceOnboarding(command) {
74
+ if (ONBOARDING_EXEMPT.has(command)) {
75
+ return;
76
+ }
77
+
78
+ if (!isOnboarded()) {
79
+ // Check if we're mid-onboarding (server running, awaiting disclosure)
80
+ try {
81
+ const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
82
+ if (cfg.onboarding?.step === 'awaiting_disclosure') {
83
+ console.log('\nA2A setup in progress. Disclosure topics not yet submitted.\n');
84
+ console.log("Next: run `a2a onboard --submit '<json>'`\n");
85
+ process.exit(1);
86
+ }
87
+ } catch (e) {
88
+ if (e.code !== 'ENOENT' && e.name !== 'SyntaxError') {
89
+ console.error(`Warning: could not read config: ${e.message}`);
90
+ }
53
91
  }
54
- return true;
55
- } catch (e) {
56
- return true; // Don't block if config is broken
92
+
93
+ console.log('\nA2A not configured yet.\n');
94
+ console.log('Next: run `a2a quickstart`\n');
95
+ process.exit(1);
57
96
  }
58
97
  }
59
98
 
@@ -71,8 +110,6 @@ function formatTimeAgo(date) {
71
110
  }
72
111
 
73
112
  function openInBrowser(url) {
74
- const { spawn } = require('child_process');
75
-
76
113
  const platform = process.platform;
77
114
  let cmd = null;
78
115
  let args = [];
@@ -162,6 +199,17 @@ function parseArgs(argv) {
162
199
  return args;
163
200
  }
164
201
 
202
+ async function promptYesNo(question) {
203
+ return await new Promise(resolve => {
204
+ const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
205
+ rl.question(question, (answer) => {
206
+ rl.close();
207
+ const normalized = String(answer || '').trim().toLowerCase();
208
+ resolve(normalized === 'y' || normalized === 'yes');
209
+ });
210
+ });
211
+ }
212
+
165
213
  async function resolveInviteHostname() {
166
214
  const { resolveInviteHost } = require('../src/lib/invite-host');
167
215
 
@@ -184,7 +232,6 @@ async function resolveInviteHostname() {
184
232
  // Commands
185
233
  const commands = {
186
234
  create: async (args) => {
187
- checkOnboarding('create');
188
235
  // Parse max-calls: number, 'unlimited', or default (unlimited)
189
236
  let maxCalls = null; // Default: unlimited
190
237
  if (args.flags['max-calls']) {
@@ -753,7 +800,6 @@ https://github.com/onthegonow/a2a_calling`;
753
800
  },
754
801
 
755
802
  call: async (args) => {
756
- checkOnboarding('call');
757
803
  let target = args._[1];
758
804
  const message = args._.slice(2).join(' ') || args.flags.message || args.flags.m;
759
805
 
@@ -901,500 +947,204 @@ https://github.com/onthegonow/a2a_calling`;
901
947
  },
902
948
 
903
949
  quickstart: async (args) => {
904
- const http = require('http');
905
- const https = require('https');
906
950
  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');
951
+ const { tryBindPort, findAvailablePort, isPortListening } = require('../src/lib/port-scanner');
952
+ const { buildExtractionPrompt, MANIFEST_FILE } = require('../src/lib/disclosure');
913
953
  const { getExternalIp } = require('../src/lib/external-ip');
914
- const { CallbookStore } = require('../src/lib/callbook');
915
- const { isPortListening, tryBindPort } = require('../src/lib/port-scanner');
916
954
 
917
- const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
918
955
  const config = new A2AConfig();
919
956
 
920
957
  if (args.flags.force) {
921
958
  config.resetOnboarding();
922
959
  }
923
960
 
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
- })();
961
+ // Already onboarded skip unless --force
962
+ if (config.isOnboarded() && !args.flags.force) {
963
+ console.log('\nOnboarding already complete. Use --force to re-run.\n');
964
+ return;
965
+ }
929
966
 
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
967
+ // If server is already running and awaiting disclosure, skip to Step 2
968
+ let currentStep = 'not_started';
969
+ try {
970
+ const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
971
+ currentStep = cfg.onboarding?.step || 'not_started';
972
+ } catch (e) {
973
+ if (e.code !== 'ENOENT' && e.name !== 'SyntaxError') {
974
+ console.error(` Warning: could not read config: ${e.message}`);
936
975
  }
937
- return String(body || '').includes('"pong":true') || String(body || '').includes('"pong": true');
976
+ }
977
+ if (currentStep === 'awaiting_disclosure' && !args.flags.force) {
978
+ console.log('\nStep 1 already complete. Server is running.\n');
979
+ console.log('Step 2 of 4: Configure disclosure topics\n');
980
+ console.log(buildExtractionPrompt());
981
+ console.log('\n Read your workspace files, extract topics, and present to your owner for review.');
982
+ console.log(" Then submit with: a2a onboard --submit '<json>'\n");
983
+ return;
938
984
  }
939
985
 
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
- });
986
+ function parsePort(raw, fallback) {
987
+ const parsed = Number.parseInt(String(raw || '').trim(), 10);
988
+ if (Number.isFinite(parsed) && parsed > 0 && parsed <= 65535) {
989
+ return parsed;
990
+ }
991
+ return fallback;
975
992
  }
976
993
 
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
- // Try direct access first. In practice this is the most reliable signal:
988
- // it avoids flaky third-party proxies and catches obvious scheme/port mistakes.
989
- try {
990
- const direct = await fetchUrlText(targetUrl, 2500);
991
- return { ok: looksLikePong(direct.body), provider: 'direct', statusCode: direct.statusCode };
992
- } catch (err) {
993
- // Fall back to remote fetch providers below.
994
- }
995
-
996
- const providers = [
997
- {
998
- name: 'allorigins',
999
- buildUrl: () => {
1000
- const u = new URL('https://api.allorigins.win/raw');
1001
- u.searchParams.set('url', targetUrl);
1002
- return u.toString();
1003
- }
1004
- },
1005
- {
1006
- name: 'jina',
1007
- buildUrl: () => `https://r.jina.ai/${targetUrl}`
1008
- }
1009
- ];
994
+ // ── Step 1 of 4: Setting up A2A server ──────────────────
995
+ console.log('\nStep 1 of 4: Setting up A2A server\n');
1010
996
 
1011
- for (const provider of providers) {
1012
- try {
1013
- // eslint-disable-next-line no-await-in-loop
1014
- const res = await fetchUrlText(provider.buildUrl(), 8000);
1015
- if (looksLikePong(res.body)) {
1016
- return { ok: true, provider: provider.name, statusCode: res.statusCode };
1017
- }
1018
- } catch (err) {
1019
- // try next
997
+ const preferredPort = parsePort(args.flags.port || args.flags.p, null);
998
+
999
+ // If user specified a port, try that first
1000
+ let serverPort;
1001
+ let usingAlternatePort = false;
1002
+
1003
+ if (preferredPort) {
1004
+ console.log(` 1a. Checking preferred port ${preferredPort}...`);
1005
+ const preferredResult = await tryBindPort(preferredPort);
1006
+ if (preferredResult.ok) {
1007
+ console.log(` Port ${preferredPort} available.`);
1008
+ serverPort = preferredPort;
1009
+ usingAlternatePort = preferredPort !== 80;
1010
+ } else if (preferredResult.code === 'EACCES') {
1011
+ console.log(` Port ${preferredPort} requires elevated privileges.`);
1012
+ console.log(' Rerun with: sudo npm install -g a2acalling\n');
1013
+ process.exit(1);
1014
+ } else {
1015
+ console.log(` Port ${preferredPort} is in use. Scanning for alternatives...`);
1016
+ const candidates = [];
1017
+ for (let p = 3001; p < 3101; p++) candidates.push(p);
1018
+ serverPort = await findAvailablePort(candidates);
1019
+ if (!serverPort) {
1020
+ console.log(' Could not find a bindable port. Rerun with elevated privileges:');
1021
+ console.log(' sudo npm install -g a2acalling');
1022
+ process.exit(1);
1020
1023
  }
1024
+ console.log(` Port ${serverPort} available.`);
1025
+ usingAlternatePort = true;
1021
1026
  }
1022
- return { ok: false };
1023
- }
1024
-
1025
- function slugify(value) {
1026
- return String(value || '')
1027
- .toLowerCase()
1028
- .replace(/['"]/g, '')
1029
- .replace(/[^a-z0-9]+/g, '-')
1030
- .replace(/^-+|-+$/g, '')
1031
- .slice(0, 60);
1032
- }
1033
-
1034
- function uniqueNonEmpty(items, limit = 24) {
1035
- const out = [];
1036
- const seen = new Set();
1037
- for (const raw of items) {
1038
- const s = String(raw || '').trim();
1039
- if (!s) continue;
1040
- if (seen.has(s)) continue;
1041
- seen.add(s);
1042
- out.push(s);
1043
- if (out.length >= limit) break;
1044
- }
1045
- return out;
1046
- }
1047
-
1048
- function extractSectionBullets(markdown, headingRegex) {
1049
- const text = String(markdown || '');
1050
- const match = text.match(new RegExp(`##\\s*(?:${headingRegex})[^\\n]*\\n([\\s\\S]*?)(?=\\n##|$)`, 'i'));
1051
- if (!match) return [];
1052
- return match[1]
1053
- .split('\n')
1054
- .map(l => l.trim())
1055
- .filter(l => l.startsWith('-') || l.startsWith('*'))
1056
- .map(l => l.replace(/^[\\s\\-\\*]+/, '').trim())
1057
- .filter(Boolean);
1058
- }
1059
-
1060
- function tierFromManifest(manifest, tier, fallback = []) {
1061
- const t = (manifest && manifest.topics && manifest.topics[tier]) ? manifest.topics[tier] : null;
1062
- if (!t) return fallback;
1063
- const items = []
1064
- .concat(Array.isArray(t.lead_with) ? t.lead_with : [])
1065
- .concat(Array.isArray(t.discuss_freely) ? t.discuss_freely : [])
1066
- .concat(Array.isArray(t.deflect) ? t.deflect : []);
1067
- const topics = items.map(x => (x && x.topic) ? x.topic : '').filter(Boolean);
1068
- return topics.length ? topics : fallback;
1069
- }
1070
-
1071
- function buildTierRecommendations(contextFiles, manifest) {
1072
- const publicFallback = ['chat', 'openclaw', 'a2a'];
1073
- const friendsFallback = ['chat', 'search', 'openclaw', 'a2a'];
1074
- const familyFallback = ['chat', 'search', 'openclaw', 'a2a', 'tools', 'memory'];
1075
-
1076
- const rawPublic = tierFromManifest(manifest, 'public', publicFallback);
1077
- const rawFriends = tierFromManifest(manifest, 'friends', friendsFallback);
1078
- const rawFamily = tierFromManifest(manifest, 'family', familyFallback);
1079
-
1080
- const goalsFromUser = extractSectionBullets(contextFiles.user, 'Goals|Current|Seeking|Working On');
1081
- const baseGoals = goalsFromUser.length
1082
- ? goalsFromUser
1083
- : ['grow network', 'find collaborators', 'build in public'];
1084
-
1085
- const publicTopics = uniqueNonEmpty(rawPublic.map(slugify).filter(Boolean), 16);
1086
- const friendsTopics = uniqueNonEmpty(rawFriends.map(slugify).filter(Boolean), 20);
1087
- const familyTopics = uniqueNonEmpty(rawFamily.map(slugify).filter(Boolean), 24);
1088
-
1089
- const goals = uniqueNonEmpty(baseGoals.map(slugify).filter(Boolean), 12);
1090
-
1091
- return {
1092
- public: { topics: publicTopics, goals: goals.slice(0, 6) },
1093
- friends: { topics: uniqueNonEmpty([...publicTopics, ...friendsTopics], 24), goals: goals.slice(0, 8) },
1094
- family: { topics: uniqueNonEmpty([...publicTopics, ...friendsTopics, ...familyTopics], 30), goals }
1095
- };
1096
- }
1097
-
1098
- function printTierSummary(tiers) {
1099
- const format = (t) => {
1100
- const topics = (t.topics || []).join(' · ') || '(none)';
1101
- const goals = (t.goals || []).join(' · ') || '(none)';
1102
- return `Topics: ${topics}\nGoals: ${goals}`;
1103
- };
1104
- console.log('\nProposed permission tiers:\n');
1105
- console.log('PUBLIC');
1106
- console.log(format(tiers.public));
1107
- console.log('\nFRIENDS');
1108
- console.log(format(tiers.friends));
1109
- console.log('\nFAMILY');
1110
- console.log(format(tiers.family));
1111
- console.log('');
1112
- }
1113
-
1114
- // ── Step 1: Background bootstrap (config + manifest) ─────────
1115
- let contextFiles = {};
1116
- let manifest = {};
1117
- try {
1118
- contextFiles = disc.readContextFiles(workspaceDir);
1119
- const forceManifest = Boolean(args.flags.force || args.flags['regen-manifest'] || args.flags.regenManifest);
1120
- if (forceManifest) {
1121
- // Force-regen uses minimal starter; agent-driven extraction is the
1122
- // proper way to populate topics (via `a2a onboard --submit`).
1123
- const generated = disc.generateDefaultManifest();
1124
- disc.saveManifest(generated);
1125
- manifest = generated;
1126
- } else {
1127
- manifest = disc.loadManifest();
1128
- if (!manifest || Object.keys(manifest).length === 0) {
1129
- const generated = disc.generateDefaultManifest();
1130
- disc.saveManifest(generated);
1131
- manifest = generated;
1132
- }
1133
- }
1134
- } catch (err) {
1135
- // Non-fatal: onboarding can proceed even if manifest fails.
1136
- contextFiles = {};
1137
- manifest = {};
1138
- }
1139
-
1140
- console.log('\nA2A deterministic onboarding');
1141
- console.log('──────────────────────────');
1142
-
1143
- // ── Step 2: Owner dashboard access (local + optional remote) ─
1144
- config.setOnboarding({ step: 'access' });
1145
-
1146
- const hostnameFlagRaw = args.flags.hostname !== undefined ? String(args.flags.hostname) : '';
1147
- const normalizedHostname = normalizeHostInput(hostnameFlagRaw);
1148
-
1149
- // Invite host controls the a2a:// hostname we hand out (and remote dashboard pairing URL).
1150
- let inviteHost = '';
1151
- if (normalizedHostname) {
1152
- const parsed = splitHostPort(normalizedHostname);
1153
- const publicPortRaw = args.flags['public-port'] || args.flags.publicPort || process.env.A2A_PUBLIC_PORT || 443;
1154
- const publicPort = Number.parseInt(String(publicPortRaw), 10);
1155
- inviteHost = parsed.port
1156
- ? normalizedHostname
1157
- : `${parsed.hostname}:${(Number.isFinite(publicPort) && publicPort > 0 && publicPort <= 65535) ? publicPort : 443}`;
1158
- config.setAgent({ hostname: inviteHost });
1159
1027
  } else {
1160
- const existing = normalizeHostInput((config.getAgent() || {}).hostname || '');
1161
- inviteHost = existing || `localhost:${backendPort}`;
1162
- if (!existing) {
1163
- config.setAgent({ hostname: inviteHost });
1028
+ // Default: check port 80 first, then scan
1029
+ console.log(' 1a. Checking port 80...');
1030
+ const port80Result = await tryBindPort(80);
1031
+
1032
+ if (port80Result.ok) {
1033
+ console.log(' Port 80 available.');
1034
+ serverPort = 80;
1035
+ } else if (port80Result.code === 'EACCES') {
1036
+ console.log(' Port 80 is available but requires elevated privileges.');
1037
+ console.log(' A2A needs to bind to a port to function. Rerun with:');
1038
+ console.log(' sudo npm install -g a2acalling\n');
1039
+ console.log(' Onboarding cannot continue without a bound port.');
1040
+ process.exit(1);
1041
+ } else {
1042
+ console.log(' Port 80 is in use by another process.');
1043
+ console.log(' 1b. Scanning for available port...');
1044
+
1045
+ const candidates = [];
1046
+ for (let p = 3001; p < 3101; p++) candidates.push(p);
1047
+ serverPort = await findAvailablePort(candidates);
1048
+
1049
+ if (!serverPort) {
1050
+ console.log(' Could not find a bindable port. Rerun with elevated privileges:');
1051
+ console.log(' sudo npm install -g a2acalling');
1052
+ process.exit(1);
1053
+ }
1054
+ console.log(` Port ${serverPort} available.`);
1055
+ usingAlternatePort = true;
1164
1056
  }
1165
1057
  }
1166
1058
 
1167
- const inviteParsed = splitHostPort(inviteHost);
1168
- const invitePort = inviteParsed.port;
1169
- const schemeOverride = String(process.env.A2A_PUBLIC_SCHEME || '').trim();
1170
- const inviteScheme = schemeOverride || ((!invitePort || invitePort === 443) ? 'https' : 'http');
1171
- const expectedPingUrl = `${inviteScheme}://${inviteHost}/api/a2a/ping`;
1172
- const inviteLooksLocal = isLocalOrUnroutableHost(inviteParsed.hostname);
1059
+ // Start server
1060
+ console.log(` Starting A2A server on port ${serverPort}...`);
1173
1061
 
1174
- console.log('\n2️⃣ Owner dashboard access');
1175
- console.log(`Local dashboard: http://127.0.0.1:${backendPort}/dashboard/`);
1176
- console.log(`Invite host: ${inviteHost}`);
1062
+ async function startServer(port) {
1063
+ const listening = await isPortListening(port, '127.0.0.1', { timeoutMs: 250 });
1064
+ if (listening.listening) return { started: false, existing: true };
1065
+ const serverScript = path.join(__dirname, '../src/server.js');
1066
+ const child = spawn(process.execPath, [serverScript], {
1067
+ env: { ...process.env, PORT: String(port) },
1068
+ detached: true,
1069
+ stdio: 'ignore'
1070
+ });
1071
+ child.unref();
1072
+ await new Promise(r => setTimeout(r, 300));
1073
+ return { started: true, pid: child.pid };
1074
+ }
1177
1075
 
1178
- if (inviteLooksLocal) {
1179
- console.log('Remote dashboard: not configured (invite host looks local/unroutable)');
1180
- console.log(' To enable remote access, rerun with: --hostname YOUR_DOMAIN:443');
1181
- } else {
1182
- const callbookStore = new CallbookStore();
1183
- if (!callbookStore.isAvailable()) {
1184
- console.log('Remote dashboard: Callbook Remote not available (storage unavailable)');
1185
- console.log(` Hint: ${callbookStore.getDbError ? callbookStore.getDbError() : 'storage_unavailable'}`);
1186
- } else {
1187
- const label = String(args.flags['device-label'] || args.flags.deviceLabel || 'Callbook Remote').trim().slice(0, 120);
1188
- const ttlHoursRaw = args.flags['callbook-ttl-hours'] || args.flags.callbookTtlHours || 24;
1189
- const ttlHours = Math.max(1, Math.min(168, Number.parseInt(String(ttlHoursRaw), 10) || 24));
1190
- const created = callbookStore.createProvisionCode({ label, ttlMs: ttlHours * 60 * 60 * 1000 });
1191
- if (created && created.success) {
1192
- const installUrl = `${inviteScheme}://${inviteHost}/callbook/install#code=${created.code}`;
1193
- console.log(`Remote dashboard: ${installUrl} (one-time, ${ttlHours}h)`);
1194
- } else {
1195
- console.log('Remote dashboard: failed to create install link');
1196
- console.log(` Hint: ${created && created.message ? created.message : (created && created.error ? created.error : 'unknown_error')}`);
1197
- }
1076
+ async function waitForServer(port) {
1077
+ for (let i = 0; i < 18; i++) {
1078
+ const listening = await isPortListening(port, '127.0.0.1', { timeoutMs: 250 });
1079
+ if (listening.listening) return true;
1080
+ await new Promise(r => setTimeout(r, 250));
1198
1081
  }
1082
+ return false;
1199
1083
  }
1200
1084
 
1201
- // ── Step 3: Permission tiers (topics + goals) ───────────────
1202
- const onboardingAfterAccess = config.getOnboarding();
1203
- if (!onboardingAfterAccess.tiers_confirmed) {
1204
- const recommendations = buildTierRecommendations(contextFiles, manifest);
1205
-
1206
- const parseFreeTextList = (raw) => {
1207
- if (raw === undefined || raw === null || raw === true) return [];
1208
- const text = String(raw || '').trim();
1209
- if (!text) return [];
1210
- return text
1211
- .split(/[\n,]+/g)
1212
- .map(s => s.trim())
1213
- .filter(Boolean);
1214
- };
1215
-
1216
- const promptLine = async (question) => {
1217
- const readline = require('readline');
1218
- return await new Promise(resolve => {
1219
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1220
- rl.question(question, (answer) => {
1221
- rl.close();
1222
- resolve(String(answer || '').trim());
1223
- });
1224
- });
1225
- };
1226
-
1227
- // Optional owner override: Friends tier topics/interests (most important tier).
1228
- const interactive = Boolean(
1229
- args.flags.interactive ||
1230
- args.flags['ask-friends-topics'] ||
1231
- args.flags.askFriendsTopics
1232
- );
1233
- let friendsTopicsOverride = parseFreeTextList(args.flags['friends-topics'] || args.flags.friendsTopics);
1234
- const noWorkspaceContext = !contextFiles.user && !contextFiles.heartbeat && !contextFiles.soul &&
1235
- !contextFiles.memory && !contextFiles.claude;
1236
- const shouldPromptFriendsTopics = (interactive || noWorkspaceContext) &&
1237
- friendsTopicsOverride.length === 0 &&
1238
- process.stdin.isTTY &&
1239
- process.stdout.isTTY;
1240
- if (shouldPromptFriendsTopics) {
1241
- const suggested = (recommendations.friends.topics || []).slice(0, 12).join(', ');
1242
- const answer = await promptLine(`Friends-tier topics/interests (comma-separated).\nSuggested: ${suggested}\n> `);
1243
- friendsTopicsOverride = parseFreeTextList(answer);
1244
- }
1245
-
1246
- if (friendsTopicsOverride.length > 0) {
1247
- const normalized = uniqueNonEmpty(friendsTopicsOverride.map(slugify).filter(Boolean), 24);
1248
- recommendations.friends.topics = uniqueNonEmpty(
1249
- [...(recommendations.public.topics || []), ...normalized],
1250
- 24
1251
- );
1252
- recommendations.family.topics = uniqueNonEmpty(
1253
- [...(recommendations.friends.topics || []), ...(recommendations.family.topics || [])],
1254
- 30
1255
- );
1256
- }
1257
-
1258
- try {
1259
- config.setTier('public', recommendations.public);
1260
- config.setTier('friends', recommendations.friends);
1261
- config.setTier('family', recommendations.family);
1262
- } catch (err) {
1263
- console.error('\n❌ Tier configuration validation failed.');
1264
- console.error(` ${err.message}`);
1265
- if (err.hint) {
1266
- console.error(` Hint: ${err.hint}`);
1267
- }
1268
- console.error('');
1269
- process.exit(1);
1270
- }
1271
-
1272
- printTierSummary(recommendations);
1273
-
1274
- config.setOnboarding({
1275
- step: 'tiers',
1276
- tiers_confirmed: true
1277
- });
1278
- }
1279
-
1280
- // ── Step 4: Port scan + reverse proxy guidance (if needed) ──
1281
- console.log('\n4️⃣ Port scan + reverse proxy');
1282
- console.log(`Invite host: ${inviteHost}`);
1283
- console.log(`Expected ping URL: ${expectedPingUrl}\n`);
1284
-
1285
- const expectsReverseProxy = Boolean(
1286
- (invitePort === 80 && backendPort !== 80) ||
1287
- ((!invitePort || invitePort === 443) && backendPort !== 443)
1288
- );
1289
-
1290
- if (expectsReverseProxy) {
1291
- const port80Listening = await isPortListening(80, '127.0.0.1', { timeoutMs: 500 });
1292
- const port80Bind = await tryBindPort(80, '0.0.0.0');
1293
- const port80Ping = port80Listening.listening ? await probeLocalPing(80) : { ok: false };
1294
-
1295
- console.log('Port 80:');
1296
- if (port80Ping.ok) {
1297
- console.log(' ✅ serves /api/a2a/ping (A2A detected on :80)');
1298
- } else if (port80Listening.listening) {
1299
- console.log(` ⚠️ has a listener (${port80Listening.code || 'in_use'})`);
1300
- } else if (!port80Bind.ok && port80Bind.code === 'EACCES') {
1301
- console.log(' ⚠️ appears free but is not bindable by this user (EACCES)');
1302
- } else if (port80Bind.ok) {
1303
- console.log(' ✅ free and bindable by this user');
1304
- } else {
1305
- console.log(` ⚠️ not bindable (${port80Bind.code || 'unknown'})`);
1306
- }
1085
+ const serverResult = await startServer(serverPort);
1086
+ if (serverResult.existing) {
1087
+ console.log(' Existing server detected on this port.');
1088
+ }
1089
+ const serverUp = await waitForServer(serverPort);
1090
+ if (!serverUp) {
1091
+ console.log(' Server failed to start. Check logs and retry:');
1092
+ console.log(` PORT=${serverPort} node ${path.join(__dirname, '../src/server.js')}`);
1093
+ process.exit(1);
1094
+ }
1095
+ console.log(' Server running.\n');
1307
1096
 
1308
- console.log('\nReverse proxy required (example routes):');
1309
- console.log(` /api/a2a/* -> http://127.0.0.1:${backendPort}`);
1310
- console.log(` /dashboard/* -> http://127.0.0.1:${backendPort}`);
1311
- console.log(` /callbook/* -> http://127.0.0.1:${backendPort}`);
1312
- console.log('');
1313
- console.log('If you have configured your reverse proxy and want to continue, run:');
1314
- console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-ingress`);
1315
- console.log('');
1316
- if (!args.flags['confirm-ingress']) {
1317
- return;
1318
- }
1319
- } else {
1320
- console.log('✅ No reverse proxy required based on invite host/port.');
1097
+ // Store server PID for cleanup
1098
+ if (serverResult.pid) {
1099
+ config.setOnboarding({ server_pid: serverResult.pid, server_port: serverPort });
1321
1100
  }
1322
1101
 
1323
- if (!config.getOnboarding().ingress_confirmed) {
1324
- config.setOnboarding({
1325
- step: 'ingress',
1326
- ingress_confirmed: true
1327
- });
1102
+ // Detect external IP
1103
+ const ipResult = await getExternalIp();
1104
+ if (!ipResult.ip) {
1105
+ console.log(' Warning: Could not detect external IP address.');
1106
+ console.log(' Set your hostname via environment variable and re-run:');
1107
+ console.log(` A2A_HOSTNAME=YOUR_IP${serverPort !== 80 ? ':' + serverPort : ''} a2a quickstart --force\n`);
1328
1108
  }
1109
+ const externalIp = ipResult.ip || null;
1110
+ const publicHost = externalIp
1111
+ ? (serverPort === 80 ? externalIp : `${externalIp}:${serverPort}`)
1112
+ : `localhost:${serverPort}`;
1329
1113
 
1330
- // ── Step 5: External IP + reachability check ───────────────
1331
- console.log('\n5️⃣ External IP + reachability check');
1114
+ // Save server config
1115
+ config.setAgent({ hostname: publicHost });
1332
1116
 
1333
- if (inviteLooksLocal) {
1334
- console.log('Skipping external IP probe: invite host looks local/unroutable.');
1335
- } else {
1336
- const external = await getExternalIp({ forceRefresh: true });
1337
- if (external && external.ip) {
1338
- console.log(`External IP (${external.source || 'resolver'}): ${external.ip}`);
1339
- } else {
1340
- console.log(`External IP lookup failed: ${external && external.error ? external.error : 'unknown_error'}`);
1341
- }
1117
+ if (usingAlternatePort) {
1118
+ console.log(' External access required.');
1119
+ console.log(' Something is already bound to port 80 on this machine.');
1120
+ console.log(' Two options to make your A2A server reachable:\n');
1121
+ console.log(' Option A (recommended): Set up a reverse proxy (HTTP or HTTPS).');
1122
+ console.log(` Configure your web server to forward /api/a2a/* to localhost:${serverPort}.`);
1123
+ console.log(' If you serve HTTPS on port 443, proxy from there instead.');
1124
+ console.log(' A reverse proxy avoids firewall changes entirely.\n');
1125
+ console.log(` Option B: Open port ${serverPort} in your firewall.`);
1126
+ console.log(' This requires the owner to manually allow inbound traffic on');
1127
+ console.log(` port ${serverPort} (e.g. ufw allow ${serverPort}, or cloud provider security group).`);
1128
+ console.log(' Most users prefer not to modify firewall settings.\n');
1342
1129
  }
1343
1130
 
1344
- const localListener = await isPortListening(backendPort, '127.0.0.1', { timeoutMs: 500 });
1345
- if (!localListener.listening) {
1346
- console.log('\n⚠️ A2A server is not reachable locally yet.');
1347
- console.log('Start it, then rerun quickstart:');
1348
- console.log(` A2A_HOSTNAME="${inviteHost}" a2a server --port ${backendPort}`);
1349
- console.log('');
1350
- return;
1351
- }
1352
- const localPing = await probeLocalPing(backendPort, inviteLooksLocal ? 250 : 1000);
1353
- if (!localPing.ok) {
1354
- if (inviteLooksLocal) {
1355
- console.log(`\n⚠️ Port ${backendPort} is listening but /api/a2a/ping did not respond within a short timeout.`);
1356
- console.log('Continuing onboarding anyway (invite host is local/unroutable).');
1357
- } else {
1358
- console.log('\n⚠️ A2A server is not responding locally yet.');
1359
- console.log('Start it, then rerun quickstart:');
1360
- console.log(` A2A_HOSTNAME="${inviteHost}" a2a server --port ${backendPort}`);
1361
- console.log('');
1362
- return;
1363
- }
1364
- }
1365
-
1366
- if (inviteLooksLocal) {
1367
- console.log('Skipping external reachability check: invite host looks local/unroutable.');
1368
- } else {
1369
- const extPing = await externalPingCheck(expectedPingUrl);
1370
- if (extPing.ok) {
1371
- console.log(`✅ External ping OK (${extPing.provider})`);
1372
- } else if (args.flags['skip-verify']) {
1373
- console.log('⚠️ External ping FAILED (skipped via --skip-verify).');
1374
- } else if (args.flags['confirm-ingress']) {
1375
- console.log('⚠️ External ping FAILED (continuing due to --confirm-ingress).');
1376
- } else {
1377
- console.log('⚠️ External ping FAILED (server may not be publicly reachable yet).');
1378
- console.log('Fix ingress (DNS/reverse proxy/firewall), then rerun. If you want to proceed anyway:');
1379
- console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-ingress`);
1380
- console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --skip-verify`);
1381
- console.log('');
1382
- return;
1383
- }
1384
- }
1385
-
1386
- if (!config.getOnboarding().verify_confirmed) {
1387
- config.setOnboarding({
1388
- step: 'verify',
1389
- verify_confirmed: true
1390
- });
1131
+ if (externalIp) {
1132
+ const verifyUrl = `http://${publicHost}/api/a2a/ping`;
1133
+ console.log(' Verify externally:');
1134
+ console.log(` curl -s ${verifyUrl}`);
1135
+ console.log(' Or ask your owner to check: https://canyouseeme.org/\n');
1391
1136
  }
1392
1137
 
1393
- config.completeOnboarding();
1394
- console.log('✅ Onboarding complete.');
1395
- console.log('Next: a2a gui or a2a create or a2a server');
1138
+ config.setOnboarding({ step: 'awaiting_disclosure' });
1139
+
1140
+ // ── Step 2 of 4: Configure disclosure topics ────────────
1141
+ console.log('Step 2 of 4: Configure disclosure topics\n');
1142
+ console.log(buildExtractionPrompt());
1143
+ console.log('\n Read your workspace files, extract topics, and present to your owner for review.');
1144
+ console.log(" Then submit with: a2a onboard --submit '<json>'\n");
1396
1145
  },
1397
1146
 
1147
+
1398
1148
  install: () => {
1399
1149
  require('../scripts/install-openclaw.js');
1400
1150
  },
@@ -1403,6 +1153,136 @@ https://github.com/onthegonow/a2a_calling`;
1403
1153
  require('../scripts/install-openclaw.js');
1404
1154
  },
1405
1155
 
1156
+ uninstall: async (args) => {
1157
+ const fs = require('fs');
1158
+ const path = require('path');
1159
+ const { spawnSync } = require('child_process');
1160
+
1161
+ const keepConfig = Boolean(args.flags['keep-config'] || args.flags.keepConfig);
1162
+ const force = Boolean(args.flags.force || args.flags.f);
1163
+
1164
+ const configDir = process.env.A2A_CONFIG_DIR ||
1165
+ process.env.OPENCLAW_CONFIG_DIR ||
1166
+ path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
1167
+
1168
+ const configFile = path.join(configDir, 'a2a-config.json');
1169
+ const disclosureFile = path.join(configDir, 'a2a-disclosure.json');
1170
+ const tokensFile = path.join(configDir, 'a2a-tokens.json');
1171
+ const dbFile = path.join(configDir, 'a2a-conversations.db');
1172
+ const logsDbFile = path.join(configDir, 'a2a-logs.db');
1173
+ const callbookDbFile = path.join(configDir, 'a2a-callbook.db');
1174
+
1175
+ console.log(`\n🗑️ A2A Uninstall`);
1176
+ console.log('─────────────────\n');
1177
+
1178
+ if (!keepConfig && !force) {
1179
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1180
+ console.error('Refusing to prompt without a TTY. Re-run with --force to confirm uninstall.');
1181
+ process.exit(1);
1182
+ }
1183
+
1184
+ const existing = [configFile, disclosureFile, tokensFile, dbFile, logsDbFile, callbookDbFile].filter(f => fs.existsSync(f));
1185
+ const list = existing.length ? existing.map(f => ` - ${f}`).join('\n') : ' (no local config/database files found)';
1186
+ const ok = await promptYesNo(
1187
+ `This will stop the pm2 process "a2a" and delete:\n${list}\nProceed? (y/N) `
1188
+ );
1189
+ if (!ok) {
1190
+ console.log('\nCancelled.\n');
1191
+ return;
1192
+ }
1193
+ }
1194
+
1195
+ function pm2Exists() {
1196
+ const res = spawnSync('pm2', ['--version'], { stdio: 'ignore', timeout: 4000 });
1197
+ if (res.error && res.error.code === 'ENOENT') return false;
1198
+ return res.status === 0;
1199
+ }
1200
+
1201
+ function pm2HasProcess(name) {
1202
+ const res = spawnSync('pm2', ['describe', name], { encoding: 'utf8', timeout: 6000 });
1203
+ if (res.error && res.error.code === 'ENOENT') return false;
1204
+ return res.status === 0;
1205
+ }
1206
+
1207
+ function pm2StopAndDelete(name) {
1208
+ if (!pm2Exists()) return { ok: true, skipped: true };
1209
+ if (!pm2HasProcess(name)) return { ok: true, skipped: true };
1210
+
1211
+ const stop = spawnSync('pm2', ['stop', name], { encoding: 'utf8', timeout: 8000 });
1212
+ if (stop.status !== 0) {
1213
+ const msg = (stop.stderr || stop.stdout || '').trim();
1214
+ return { ok: false, error: msg || 'pm2 stop failed' };
1215
+ }
1216
+
1217
+ const del = spawnSync('pm2', ['delete', name], { encoding: 'utf8', timeout: 8000 });
1218
+ if (del.status !== 0) {
1219
+ const msg = (del.stderr || del.stdout || '').trim();
1220
+ return { ok: false, error: msg || 'pm2 delete failed' };
1221
+ }
1222
+
1223
+ return { ok: true };
1224
+ }
1225
+
1226
+ function rmFileSafe(filePath) {
1227
+ try {
1228
+ fs.rmSync(filePath, { force: true });
1229
+ return { ok: true };
1230
+ } catch (err) {
1231
+ return { ok: false, error: err.message };
1232
+ }
1233
+ }
1234
+
1235
+ process.stdout.write('Stopping server... ');
1236
+ const stopped = pm2StopAndDelete('a2a');
1237
+ if (!stopped.ok) {
1238
+ console.log('❌');
1239
+ console.error(` ${stopped.error}`);
1240
+ process.exit(1);
1241
+ }
1242
+ console.log('✅');
1243
+
1244
+ let configOk = true;
1245
+ let dbOk = true;
1246
+
1247
+ if (!keepConfig) {
1248
+ process.stdout.write('Removing config... ');
1249
+ const c1 = rmFileSafe(configFile);
1250
+ const c2 = rmFileSafe(disclosureFile);
1251
+ const c3 = rmFileSafe(tokensFile);
1252
+ configOk = Boolean(c1.ok && c2.ok && c3.ok);
1253
+ console.log(configOk ? '✅' : '❌');
1254
+ if (!configOk) {
1255
+ if (!c1.ok) console.error(` ${configFile}: ${c1.error}`);
1256
+ if (!c2.ok) console.error(` ${disclosureFile}: ${c2.error}`);
1257
+ if (!c3.ok) console.error(` ${tokensFile}: ${c3.error}`);
1258
+ }
1259
+
1260
+ process.stdout.write('Removing database... ');
1261
+ const d1 = rmFileSafe(dbFile);
1262
+ const d2 = rmFileSafe(logsDbFile);
1263
+ const d3 = rmFileSafe(callbookDbFile);
1264
+ dbOk = Boolean(d1.ok && d2.ok && d3.ok);
1265
+ console.log(dbOk ? '✅' : '❌');
1266
+ if (!dbOk) {
1267
+ if (!d1.ok) console.error(` ${dbFile}: ${d1.error}`);
1268
+ if (!d2.ok) console.error(` ${logsDbFile}: ${d2.error}`);
1269
+ if (!d3.ok) console.error(` ${callbookDbFile}: ${d3.error}`);
1270
+ }
1271
+
1272
+ if (!configOk || !dbOk) {
1273
+ process.exit(1);
1274
+ }
1275
+ } else {
1276
+ console.log('Removing config... ⏭️');
1277
+ console.log('Removing database... ⏭️');
1278
+ }
1279
+
1280
+ console.log('\nTo complete removal:');
1281
+ console.log(' npm uninstall -g a2acalling\n');
1282
+ console.log(`Config preserved: ${keepConfig ? 'yes' : 'no'}`);
1283
+ console.log(`Location: ${configDir}`);
1284
+ },
1285
+
1406
1286
  update: async (args) => {
1407
1287
  const { execSync } = require('child_process');
1408
1288
  const path = require('path');
@@ -1510,11 +1390,9 @@ https://github.com/onthegonow/a2a_calling`;
1510
1390
  }
1511
1391
  },
1512
1392
 
1513
- onboard: (args) => {
1393
+ onboard: async (args) => {
1514
1394
  const { A2AConfig } = require('../src/lib/config');
1515
1395
  const {
1516
- readContextFiles,
1517
- buildExtractionPrompt,
1518
1396
  validateDisclosureSubmission,
1519
1397
  saveManifest,
1520
1398
  MANIFEST_FILE
@@ -1528,54 +1406,109 @@ https://github.com/onthegonow/a2a_calling`;
1528
1406
  try {
1529
1407
  parsed = JSON.parse(String(submitRaw));
1530
1408
  } catch (e) {
1531
- console.error('\n\u274c Invalid JSON in --submit flag.');
1532
- console.error(` Parse error: ${e.message}\n`);
1409
+ console.error('\nInvalid JSON in --submit flag.');
1410
+ console.error(` Parse error: ${e.message}\n`);
1533
1411
  process.exit(1);
1534
1412
  }
1535
1413
 
1536
1414
  const result = validateDisclosureSubmission(parsed);
1537
1415
  if (!result.valid) {
1538
- console.error('\n\u274c Disclosure submission validation failed:\n');
1539
- result.errors.forEach(err => console.error(` \u2022 ${err}`));
1540
- console.error(`\nFix the errors above and resubmit with: a2a onboard --submit '<json>'\n`);
1416
+ console.error('\nDisclosure submission validation failed:\n');
1417
+ result.errors.forEach(err => console.error(` - ${err}`));
1418
+ console.error("\nFix the errors above and resubmit with: a2a onboard --submit '<json>'\n");
1541
1419
  process.exit(1);
1542
1420
  }
1543
1421
 
1544
1422
  saveManifest(result.manifest);
1423
+ console.log('\nStep 3 of 4: Disclosure manifest saved.');
1424
+ console.log(` Manifest: ${MANIFEST_FILE}`);
1425
+
1426
+ // Sync tier config from manifest
1427
+ const manifest = result.manifest;
1428
+ function flattenTopics(sections) {
1429
+ const out = [];
1430
+ for (const section of sections) {
1431
+ for (const item of section) {
1432
+ const t = String(item && item.topic || '').trim();
1433
+ if (t && !out.includes(t)) out.push(t);
1434
+ }
1435
+ }
1436
+ return out;
1437
+ }
1438
+
1439
+ try {
1440
+ config.setTier('public', {
1441
+ topics: flattenTopics([manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect]),
1442
+ disclosure: 'public'
1443
+ });
1444
+ config.setTier('friends', {
1445
+ topics: flattenTopics([
1446
+ manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect,
1447
+ manifest.topics.friends.lead_with, manifest.topics.friends.discuss_freely, manifest.topics.friends.deflect
1448
+ ]),
1449
+ disclosure: 'minimal'
1450
+ });
1451
+ config.setTier('family', {
1452
+ topics: flattenTopics([
1453
+ manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect,
1454
+ manifest.topics.friends.lead_with, manifest.topics.friends.discuss_freely, manifest.topics.friends.deflect,
1455
+ manifest.topics.family.lead_with, manifest.topics.family.discuss_freely, manifest.topics.family.deflect
1456
+ ]),
1457
+ disclosure: 'minimal'
1458
+ });
1459
+ } catch (err) {
1460
+ console.error(` Warning: could not sync tier config: ${err.message}`);
1461
+ }
1545
1462
 
1546
- const agentName = args.flags.name || config.getAgent().name || process.env.A2A_AGENT_NAME || '';
1547
- const hostname = args.flags.hostname || config.getAgent().hostname || process.env.A2A_HOSTNAME || '';
1548
- if (agentName) config.setAgent({ name: agentName });
1549
- if (hostname) config.setAgent({ hostname });
1463
+ // If already onboarded, this is a topic update — no invite generation needed
1464
+ if (config.isOnboarded()) {
1465
+ console.log('\nDisclosure topics updated. Your agent will use these on the next inbound call.\n');
1466
+ return;
1467
+ }
1550
1468
 
1551
- console.log('\n\u2705 Disclosure manifest saved.');
1552
- console.log(` Manifest: ${MANIFEST_FILE}`);
1553
- console.log(' Next: a2a quickstart\n');
1554
- return;
1555
- }
1469
+ // ── Step 4 of 4: Generate first invite and complete ─────
1470
+ console.log('\nStep 4 of 4: Generating your first invite...\n');
1471
+
1472
+ const agentName = args.flags.name || config.getAgent().name || process.env.A2A_AGENT_NAME || 'my-agent';
1473
+ const hostname = config.getAgent().hostname || process.env.A2A_HOSTNAME || 'localhost';
1474
+ if (args.flags.name) config.setAgent({ name: agentName });
1475
+
1476
+ const publicTopics = flattenTopics([
1477
+ manifest.topics.public.lead_with,
1478
+ manifest.topics.public.discuss_freely
1479
+ ]);
1480
+
1481
+ const { token } = store.create({
1482
+ name: agentName,
1483
+ owner: agentName,
1484
+ permissions: 'public',
1485
+ disclosure: 'minimal',
1486
+ expires: 'never',
1487
+ maxCalls: null,
1488
+ allowedTopics: publicTopics,
1489
+ allowedGoals: ['grow-network', 'find-collaborators', 'build-in-public'],
1490
+ notify: 'all'
1491
+ });
1556
1492
 
1557
- // ── Prompt mode: print extraction instructions for agent ──
1558
- if (config.isOnboarded() && !args.flags.force) {
1559
- console.log('\u2705 Onboarding already complete. Use --force to regenerate.');
1493
+ const inviteUrl = `a2a://${hostname}/${token}`;
1494
+ console.log(` Invite URL: ${inviteUrl}`);
1495
+ console.log(' Share this invite to let other agents call you.\n');
1496
+
1497
+ config.completeOnboarding();
1498
+ console.log('Onboarding complete.\n');
1499
+ console.log(` Config: ${CONFIG_PATH}`);
1500
+ console.log(` Disclosure: ${MANIFEST_FILE}`);
1501
+ console.log(` Invite: ${inviteUrl}\n`);
1560
1502
  return;
1561
1503
  }
1562
1504
 
1563
- const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
1564
- const contextFiles = readContextFiles(workspaceDir);
1565
-
1566
- const availableFiles = {
1567
- 'USER.md': Boolean(contextFiles.user),
1568
- 'SOUL.md': Boolean(contextFiles.soul),
1569
- 'HEARTBEAT.md': Boolean(contextFiles.heartbeat),
1570
- 'SKILL.md': Boolean(contextFiles.skill),
1571
- 'CLAUDE.md': Boolean(contextFiles.claude),
1572
- 'memory/*.md': Boolean(contextFiles.memory)
1573
- };
1505
+ // ── No --submit: same as quickstart ───────────────────────
1506
+ return commands.quickstart(args);
1507
+ },
1574
1508
 
1575
- console.log(buildExtractionPrompt(availableFiles));
1576
- console.log('\n---');
1577
- console.log('After the owner confirms, submit with:');
1578
- console.log(" a2a onboard --submit '<json>'\n");
1509
+ version: () => {
1510
+ const pkg = require('../package.json');
1511
+ console.log(pkg.version);
1579
1512
  },
1580
1513
 
1581
1514
  help: () => {
@@ -1636,27 +1569,24 @@ Server:
1636
1569
  server Start the A2A server
1637
1570
  --port, -p Port to listen on (default: 3001)
1638
1571
 
1639
- quickstart Onboarding (access tiers ingress → verify)
1640
- --hostname Public hostname for remote access (e.g. myserver.com:443)
1641
- --public-port Port to assume when --hostname omits a port (default: 443)
1642
- --port A2A server port to run locally (default: 3001)
1643
- --friends-topics Override Friends tier topics/interests (comma or newline-separated)
1644
- --interactive Prompt for Friends tier topics if needed
1645
- --confirm-ingress Confirm reverse proxy/ingress is configured and continue
1646
- --skip-verify Skip external reachability check (not recommended)
1647
- --force Reset onboarding + regenerate disclosure manifest
1648
- --regen-manifest Regenerate disclosure manifest (no onboarding reset)
1649
-
1650
- onboard Generate disclosure manifest from workspace context
1572
+ quickstart Set up A2A server and start onboarding
1573
+ --port, -p Preferred server port (default: 80, fallback: 3001+)
1574
+ --force Reset onboarding and re-run from scratch
1575
+
1576
+ onboard Submit disclosure topics or resume quickstart
1577
+ --submit '<json>' Submit disclosure JSON (Step 3 of onboarding)
1578
+ --name Agent name for invite generation
1651
1579
  --force Re-run even if already onboarded
1652
- --name Agent name
1653
- --hostname Agent hostname
1654
1580
 
1655
1581
  update Update A2A to latest version (npm or git pull)
1656
1582
  --check, -c Check for updates without installing
1657
1583
 
1658
1584
  install Install A2A for OpenClaw
1659
1585
  setup Auto setup (gateway-aware dashboard install)
1586
+ uninstall Stop server and remove local config/DB
1587
+ --keep-config Preserve config/DB (for reinstall)
1588
+ --force Skip confirmation prompt
1589
+ version Show installed package version
1660
1590
 
1661
1591
  Examples:
1662
1592
  a2a create --name "bappybot" --owner "Benjamin Pollack" --expires 7d
@@ -1680,6 +1610,8 @@ if (!commands[command]) {
1680
1610
  process.exit(1);
1681
1611
  }
1682
1612
 
1613
+ enforceOnboarding(command);
1614
+
1683
1615
  // Handle async commands
1684
1616
  const result = commands[command](args);
1685
1617
  if (result instanceof Promise) {