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/README.md +13 -0
- package/SKILL.md +29 -1
- package/bin/cli.js +453 -521
- package/docs/plans/2026-02-14-agent-driven-disclosure-extraction.md +986 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +9 -0
- package/src/lib/disclosure.js +192 -15
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
return
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
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
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
|
|
978
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
//
|
|
1331
|
-
|
|
1114
|
+
// Save server config
|
|
1115
|
+
config.setAgent({ hostname: publicHost });
|
|
1332
1116
|
|
|
1333
|
-
if (
|
|
1334
|
-
console.log('
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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.
|
|
1394
|
-
|
|
1395
|
-
|
|
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('\
|
|
1532
|
-
console.error(`
|
|
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('\
|
|
1539
|
-
result.errors.forEach(err => console.error(`
|
|
1540
|
-
console.error(
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
-
|
|
1552
|
-
console.log(
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
-
|
|
1558
|
-
|
|
1559
|
-
console.log('
|
|
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
|
-
|
|
1564
|
-
|
|
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
|
-
|
|
1576
|
-
|
|
1577
|
-
console.log(
|
|
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
|
|
1640
|
-
--
|
|
1641
|
-
--
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
--
|
|
1645
|
-
--
|
|
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) {
|