a2acalling 0.6.4 → 0.6.6
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 +890 -474
- package/docs/plans/2026-02-14-agent-driven-disclosure-extraction.md +986 -0
- package/package.json +2 -1
- package/scripts/install-openclaw.js +2 -3
- package/scripts/postinstall.js +25 -0
- package/src/lib/disclosure.js +362 -155
package/bin/cli.js
CHANGED
|
@@ -12,11 +12,42 @@
|
|
|
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 readline = require('readline');
|
|
22
|
+
const { spawn } = require('child_process');
|
|
17
23
|
const { TokenStore } = require('../src/lib/tokens');
|
|
18
24
|
const { A2AClient } = require('../src/lib/client');
|
|
19
25
|
|
|
26
|
+
const CONFIG_DIR = process.env.A2A_CONFIG_DIR || process.env.OPENCLAW_CONFIG_DIR || path.join(os.homedir(), '.config', 'openclaw');
|
|
27
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'a2a-config.json');
|
|
28
|
+
const ONBOARDING_EXEMPT = new Set([
|
|
29
|
+
'quickstart',
|
|
30
|
+
'help',
|
|
31
|
+
'version',
|
|
32
|
+
'update',
|
|
33
|
+
'uninstall',
|
|
34
|
+
'onboard',
|
|
35
|
+
'gui',
|
|
36
|
+
'dashboard',
|
|
37
|
+
'server',
|
|
38
|
+
'setup',
|
|
39
|
+
'install'
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
function isOnboarded() {
|
|
43
|
+
try {
|
|
44
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
45
|
+
return config.onboarding?.version === 2 && config.onboarding?.step === 'complete';
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
20
51
|
// Lazy load conversation store (requires better-sqlite3)
|
|
21
52
|
let convStore = null;
|
|
22
53
|
function getConvStore() {
|
|
@@ -40,20 +71,20 @@ function getConvStore() {
|
|
|
40
71
|
|
|
41
72
|
const store = new TokenStore();
|
|
42
73
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
function enforceOnboarding(command) {
|
|
75
|
+
if (ONBOARDING_EXEMPT.has(command)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!isOnboarded()) {
|
|
80
|
+
console.log('\n⚠️ A2A not configured yet.');
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log('Run this first:');
|
|
83
|
+
console.log(' a2a quickstart --hostname YOUR_DOMAIN:PORT');
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log('Example:');
|
|
86
|
+
console.log(' a2a quickstart --hostname myserver.com:3001');
|
|
87
|
+
process.exit(1);
|
|
57
88
|
}
|
|
58
89
|
}
|
|
59
90
|
|
|
@@ -71,8 +102,6 @@ function formatTimeAgo(date) {
|
|
|
71
102
|
}
|
|
72
103
|
|
|
73
104
|
function openInBrowser(url) {
|
|
74
|
-
const { spawn } = require('child_process');
|
|
75
|
-
|
|
76
105
|
const platform = process.platform;
|
|
77
106
|
let cmd = null;
|
|
78
107
|
let args = [];
|
|
@@ -162,6 +191,17 @@ function parseArgs(argv) {
|
|
|
162
191
|
return args;
|
|
163
192
|
}
|
|
164
193
|
|
|
194
|
+
async function promptYesNo(question) {
|
|
195
|
+
return await new Promise(resolve => {
|
|
196
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
197
|
+
rl.question(question, (answer) => {
|
|
198
|
+
rl.close();
|
|
199
|
+
const normalized = String(answer || '').trim().toLowerCase();
|
|
200
|
+
resolve(normalized === 'y' || normalized === 'yes');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
165
205
|
async function resolveInviteHostname() {
|
|
166
206
|
const { resolveInviteHost } = require('../src/lib/invite-host');
|
|
167
207
|
|
|
@@ -184,7 +224,6 @@ async function resolveInviteHostname() {
|
|
|
184
224
|
// Commands
|
|
185
225
|
const commands = {
|
|
186
226
|
create: async (args) => {
|
|
187
|
-
checkOnboarding('create');
|
|
188
227
|
// Parse max-calls: number, 'unlimited', or default (unlimited)
|
|
189
228
|
let maxCalls = null; // Default: unlimited
|
|
190
229
|
if (args.flags['max-calls']) {
|
|
@@ -753,7 +792,6 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
753
792
|
},
|
|
754
793
|
|
|
755
794
|
call: async (args) => {
|
|
756
|
-
checkOnboarding('call');
|
|
757
795
|
let target = args._[1];
|
|
758
796
|
const message = args._.slice(2).join(' ') || args.flags.message || args.flags.m;
|
|
759
797
|
|
|
@@ -902,495 +940,705 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
902
940
|
|
|
903
941
|
quickstart: async (args) => {
|
|
904
942
|
const http = require('http');
|
|
905
|
-
const https = require('https');
|
|
906
943
|
const { A2AConfig } = require('../src/lib/config');
|
|
907
|
-
const
|
|
944
|
+
const { isPortListening } = require('../src/lib/port-scanner');
|
|
945
|
+
const {
|
|
946
|
+
readContextFiles,
|
|
947
|
+
generateDefaultManifest,
|
|
948
|
+
saveManifest
|
|
949
|
+
} = require('../src/lib/disclosure');
|
|
908
950
|
const {
|
|
909
951
|
normalizeHostInput,
|
|
910
952
|
splitHostPort,
|
|
911
953
|
isLocalOrUnroutableHost
|
|
912
954
|
} = require('../src/lib/invite-host');
|
|
913
|
-
const { getExternalIp } = require('../src/lib/external-ip');
|
|
914
|
-
const { CallbookStore } = require('../src/lib/callbook');
|
|
915
|
-
const { isPortListening, tryBindPort } = require('../src/lib/port-scanner');
|
|
916
955
|
|
|
917
|
-
const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
|
|
918
956
|
const config = new A2AConfig();
|
|
957
|
+
const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
|
|
919
958
|
|
|
920
959
|
if (args.flags.force) {
|
|
921
960
|
config.resetOnboarding();
|
|
922
961
|
}
|
|
923
962
|
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
})();
|
|
929
|
-
|
|
930
|
-
function looksLikePong(body) {
|
|
931
|
-
try {
|
|
932
|
-
const parsed = JSON.parse(String(body || ''));
|
|
933
|
-
if (parsed && typeof parsed === 'object' && parsed.pong === true) return true;
|
|
934
|
-
} catch (err) {
|
|
935
|
-
// ignore
|
|
963
|
+
function parsePort(raw, fallback) {
|
|
964
|
+
const parsed = Number.parseInt(String(raw || '').trim(), 10);
|
|
965
|
+
if (Number.isFinite(parsed) && parsed > 0 && parsed <= 65535) {
|
|
966
|
+
return parsed;
|
|
936
967
|
}
|
|
937
|
-
return
|
|
968
|
+
return fallback;
|
|
938
969
|
}
|
|
939
970
|
|
|
940
|
-
function
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
parsed = new URL(url);
|
|
945
|
-
} catch (err) {
|
|
946
|
-
reject(new Error('invalid_url'));
|
|
947
|
-
return;
|
|
971
|
+
function uniqueNonEmpty(values, limit = 80) {
|
|
972
|
+
const normalizeValue = (value) => {
|
|
973
|
+
if (typeof value === 'string') {
|
|
974
|
+
return String(value || '').trim();
|
|
948
975
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
protocol: parsed.protocol,
|
|
952
|
-
hostname: parsed.hostname,
|
|
953
|
-
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
954
|
-
method: 'GET',
|
|
955
|
-
path: parsed.pathname + parsed.search,
|
|
956
|
-
headers: {
|
|
957
|
-
'User-Agent': `a2acalling/${process.env.npm_package_version || 'dev'} (quickstart)`
|
|
958
|
-
},
|
|
959
|
-
timeout: timeoutMs
|
|
960
|
-
}, (res) => {
|
|
961
|
-
let data = '';
|
|
962
|
-
res.setEncoding('utf8');
|
|
963
|
-
res.on('data', (chunk) => {
|
|
964
|
-
data += chunk;
|
|
965
|
-
if (data.length > 1024 * 256) {
|
|
966
|
-
req.destroy(new Error('response_too_large'));
|
|
967
|
-
}
|
|
968
|
-
});
|
|
969
|
-
res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: data }));
|
|
970
|
-
});
|
|
971
|
-
req.on('error', reject);
|
|
972
|
-
req.on('timeout', () => req.destroy(new Error('timeout')));
|
|
973
|
-
req.end();
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
async function probeLocalPing(port, timeoutMs = 1000) {
|
|
978
|
-
try {
|
|
979
|
-
const res = await fetchUrlText(`http://127.0.0.1:${port}/api/a2a/ping`, timeoutMs);
|
|
980
|
-
return { ok: looksLikePong(res.body), statusCode: res.statusCode, body: res.body };
|
|
981
|
-
} catch (err) {
|
|
982
|
-
return { ok: false, error: err && err.message ? err.message : 'request_failed' };
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
async function externalPingCheck(targetUrl) {
|
|
987
|
-
// 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}`
|
|
976
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && 'topic' in value) {
|
|
977
|
+
return String(value.topic || '').trim();
|
|
1008
978
|
}
|
|
1009
|
-
|
|
979
|
+
return '';
|
|
980
|
+
};
|
|
1010
981
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
982
|
+
const out = [];
|
|
983
|
+
const seen = new Set();
|
|
984
|
+
for (const value of values) {
|
|
985
|
+
const text = normalizeValue(value);
|
|
986
|
+
if (!text) continue;
|
|
987
|
+
const key = text.toLowerCase();
|
|
988
|
+
if (seen.has(key)) continue;
|
|
989
|
+
seen.add(key);
|
|
990
|
+
out.push(text);
|
|
991
|
+
if (out.length >= limit) break;
|
|
1021
992
|
}
|
|
1022
|
-
return
|
|
993
|
+
return out;
|
|
1023
994
|
}
|
|
1024
995
|
|
|
1025
|
-
function
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
996
|
+
function normalizeTopicRecord(raw) {
|
|
997
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
998
|
+
return {
|
|
999
|
+
topic: String(raw.topic || '').trim(),
|
|
1000
|
+
detail: String(raw.detail || '').trim()
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
topic: String(raw || '').trim(),
|
|
1005
|
+
detail: ''
|
|
1006
|
+
};
|
|
1032
1007
|
}
|
|
1033
1008
|
|
|
1034
|
-
function
|
|
1009
|
+
function uniqueTopicRecords(values, limit = 80) {
|
|
1035
1010
|
const out = [];
|
|
1036
1011
|
const seen = new Set();
|
|
1037
|
-
for (const
|
|
1038
|
-
const
|
|
1039
|
-
if (!
|
|
1040
|
-
|
|
1041
|
-
seen.
|
|
1042
|
-
|
|
1012
|
+
for (const value of values) {
|
|
1013
|
+
const item = normalizeTopicRecord(value);
|
|
1014
|
+
if (!item.topic) continue;
|
|
1015
|
+
const key = item.topic.toLowerCase();
|
|
1016
|
+
if (seen.has(key)) continue;
|
|
1017
|
+
seen.add(key);
|
|
1018
|
+
out.push(item);
|
|
1043
1019
|
if (out.length >= limit) break;
|
|
1044
1020
|
}
|
|
1045
1021
|
return out;
|
|
1046
1022
|
}
|
|
1047
1023
|
|
|
1048
|
-
function
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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);
|
|
1024
|
+
function sanitizeSectionItems(values, limit = 80) {
|
|
1025
|
+
return uniqueTopicRecords(values, limit).map(item => ({
|
|
1026
|
+
topic: item.topic,
|
|
1027
|
+
detail: item.detail || ''
|
|
1028
|
+
}));
|
|
1058
1029
|
}
|
|
1059
1030
|
|
|
1060
|
-
function
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1031
|
+
function cloneDraft(draft = {}) {
|
|
1032
|
+
return JSON.parse(JSON.stringify(draft));
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function makeDraft(manifest) {
|
|
1036
|
+
const src = (manifest && manifest.topics) ? manifest.topics : {};
|
|
1037
|
+
return {
|
|
1038
|
+
public: {
|
|
1039
|
+
lead_with: sanitizeSectionItems((src.public && src.public.lead_with) || [], 60),
|
|
1040
|
+
discuss_freely: sanitizeSectionItems((src.public && src.public.discuss_freely) || [], 60),
|
|
1041
|
+
deflect: sanitizeSectionItems((src.public && src.public.deflect) || [], 60)
|
|
1042
|
+
},
|
|
1043
|
+
friends: {
|
|
1044
|
+
lead_with: sanitizeSectionItems((src.friends && src.friends.lead_with) || [], 60),
|
|
1045
|
+
discuss_freely: sanitizeSectionItems((src.friends && src.friends.discuss_freely) || [], 60),
|
|
1046
|
+
deflect: sanitizeSectionItems((src.friends && src.friends.deflect) || [], 60)
|
|
1047
|
+
},
|
|
1048
|
+
family: {
|
|
1049
|
+
lead_with: sanitizeSectionItems((src.family && src.family.lead_with) || [], 60),
|
|
1050
|
+
discuss_freely: sanitizeSectionItems((src.family && src.family.discuss_freely) || [], 60),
|
|
1051
|
+
deflect: sanitizeSectionItems((src.family && src.family.deflect) || [], 60)
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1069
1054
|
}
|
|
1070
1055
|
|
|
1071
|
-
function
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1056
|
+
function summarizeLine(content, maxLen = 60) {
|
|
1057
|
+
const text = String(content || '').split('\n').map((line) => line.trim()).find((line) => {
|
|
1058
|
+
return line && !line.startsWith('#') && !line.startsWith('---') && line.length <= 220;
|
|
1059
|
+
});
|
|
1060
|
+
if (!text) {
|
|
1061
|
+
return 'found';
|
|
1062
|
+
}
|
|
1063
|
+
return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function countMemoryDocs(root) {
|
|
1067
|
+
try {
|
|
1068
|
+
const dir = path.join(root, 'memory');
|
|
1069
|
+
if (!fs.existsSync(dir)) return 0;
|
|
1070
|
+
return fs.readdirSync(dir).filter(name => name.endsWith('.md')).length;
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
return 0;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
1075
|
|
|
1076
|
-
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1076
|
+
function renderWorkspaceScan(contextFiles) {
|
|
1077
|
+
const memoryCount = countMemoryDocs(workspaceDir);
|
|
1078
|
+
console.log('\n🔍 Scanning workspace for context...\n');
|
|
1079
|
+
console.log('Found:');
|
|
1080
|
+
const rows = [
|
|
1081
|
+
{ label: 'USER.md', found: Boolean(contextFiles.user), note: summarizeLine(contextFiles.user, 72) },
|
|
1082
|
+
{ label: 'SOUL.md', found: Boolean(contextFiles.soul), note: summarizeLine(contextFiles.soul, 72) },
|
|
1083
|
+
{ label: 'HEARTBEAT.md', found: Boolean(contextFiles.heartbeat), note: 'contains agent tasks, not disclosure topics' },
|
|
1084
|
+
{ label: 'SKILL.md', found: Boolean(contextFiles.skill), note: null },
|
|
1085
|
+
{ label: 'memory/*.md', found: memoryCount > 0, note: `${memoryCount} file${memoryCount === 1 ? '' : 's'}` }
|
|
1086
|
+
];
|
|
1079
1087
|
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
?
|
|
1083
|
-
|
|
1088
|
+
for (const row of rows) {
|
|
1089
|
+
const check = row.found ? '✅' : '❌';
|
|
1090
|
+
const note = row.found && row.note ? ` — ${row.note}` : '';
|
|
1091
|
+
const skip = row.label === 'HEARTBEAT.md' && row.found ? ' (skipped)' : '';
|
|
1092
|
+
console.log(` ${check} ${row.label}${skip}${note}`);
|
|
1093
|
+
}
|
|
1094
|
+
console.log('');
|
|
1095
|
+
}
|
|
1084
1096
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1097
|
+
function sectionLabel(sectionName) {
|
|
1098
|
+
if (sectionName === 'lead_with') return 'Lead with';
|
|
1099
|
+
if (sectionName === 'discuss_freely') return 'Discuss freely';
|
|
1100
|
+
return 'Deflect';
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function flattenDraft(draft) {
|
|
1104
|
+
const flat = [];
|
|
1105
|
+
let index = 1;
|
|
1106
|
+
['public', 'friends', 'family'].forEach((tier) => {
|
|
1107
|
+
['lead_with', 'discuss_freely', 'deflect'].forEach((section) => {
|
|
1108
|
+
(draft[tier][section] || []).forEach((item, itemIndex) => {
|
|
1109
|
+
flat.push({
|
|
1110
|
+
index,
|
|
1111
|
+
tier,
|
|
1112
|
+
section,
|
|
1113
|
+
item,
|
|
1114
|
+
itemIndex,
|
|
1115
|
+
list: draft[tier][section]
|
|
1116
|
+
});
|
|
1117
|
+
index += 1;
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
});
|
|
1121
|
+
return flat;
|
|
1122
|
+
}
|
|
1088
1123
|
|
|
1089
|
-
|
|
1124
|
+
function renderDraft(draft, neverDisclose) {
|
|
1125
|
+
console.log('\n📋 Proposed Permission Tiers');
|
|
1126
|
+
console.log('═'.repeat(60));
|
|
1090
1127
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1128
|
+
let index = 1;
|
|
1129
|
+
const titleByTier = {
|
|
1130
|
+
public: 'PUBLIC (anyone can see):',
|
|
1131
|
+
friends: 'FRIENDS (trusted contacts):',
|
|
1132
|
+
family: 'FAMILY (inner circle):'
|
|
1095
1133
|
};
|
|
1134
|
+
|
|
1135
|
+
['public', 'friends', 'family'].forEach((tier) => {
|
|
1136
|
+
console.log(`\n${titleByTier[tier]}`);
|
|
1137
|
+
['lead_with', 'discuss_freely', 'deflect'].forEach((section) => {
|
|
1138
|
+
console.log(` ${sectionLabel(section)}:`);
|
|
1139
|
+
const list = draft[tier][section] || [];
|
|
1140
|
+
if (list.length === 0) {
|
|
1141
|
+
console.log(' (none)');
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
list.forEach((item) => {
|
|
1145
|
+
const detail = item.detail ? ` — ${item.detail}` : '';
|
|
1146
|
+
console.log(` ${index}. ${item.topic}${detail}`);
|
|
1147
|
+
index += 1;
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
console.log('\nNEVER DISCLOSE:');
|
|
1153
|
+
const staticNever = (neverDisclose || ['API keys', 'Other users\' data', 'Financial figures']);
|
|
1154
|
+
staticNever.forEach((item) => console.log(` • ${item}`));
|
|
1155
|
+
console.log('═'.repeat(60));
|
|
1156
|
+
return flattenDraft(draft);
|
|
1096
1157
|
}
|
|
1097
1158
|
|
|
1098
|
-
function
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1159
|
+
function parseSections(target) {
|
|
1160
|
+
if (!target) return null;
|
|
1161
|
+
const [tierRaw, sectionRaw] = String(target).toLowerCase().split('.');
|
|
1162
|
+
if (!tierRaw || !sectionRaw) return null;
|
|
1163
|
+
if (!['public', 'friends', 'family'].includes(tierRaw)) return null;
|
|
1164
|
+
|
|
1165
|
+
const section = {
|
|
1166
|
+
lead: 'lead_with',
|
|
1167
|
+
lead_with: 'lead_with',
|
|
1168
|
+
discuss: 'discuss_freely',
|
|
1169
|
+
discuss_freely: 'discuss_freely',
|
|
1170
|
+
deflect: 'deflect'
|
|
1171
|
+
}[sectionRaw];
|
|
1172
|
+
|
|
1173
|
+
if (!section) return null;
|
|
1174
|
+
return { tier: tierRaw, section };
|
|
1112
1175
|
}
|
|
1113
1176
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const generated = disc.generateDefaultManifest(contextFiles);
|
|
1128
|
-
disc.saveManifest(generated);
|
|
1129
|
-
manifest = generated;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
} catch (err) {
|
|
1133
|
-
// Non-fatal: onboarding can proceed even if manifest fails.
|
|
1134
|
-
contextFiles = {};
|
|
1135
|
-
manifest = {};
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
console.log('\nA2A deterministic onboarding');
|
|
1139
|
-
console.log('──────────────────────────');
|
|
1140
|
-
|
|
1141
|
-
// ── Step 2: Owner dashboard access (local + optional remote) ─
|
|
1142
|
-
config.setOnboarding({ step: 'access' });
|
|
1143
|
-
|
|
1144
|
-
const hostnameFlagRaw = args.flags.hostname !== undefined ? String(args.flags.hostname) : '';
|
|
1145
|
-
const normalizedHostname = normalizeHostInput(hostnameFlagRaw);
|
|
1146
|
-
|
|
1147
|
-
// Invite host controls the a2a:// hostname we hand out (and remote dashboard pairing URL).
|
|
1148
|
-
let inviteHost = '';
|
|
1149
|
-
if (normalizedHostname) {
|
|
1150
|
-
const parsed = splitHostPort(normalizedHostname);
|
|
1151
|
-
const publicPortRaw = args.flags['public-port'] || args.flags.publicPort || process.env.A2A_PUBLIC_PORT || 443;
|
|
1152
|
-
const publicPort = Number.parseInt(String(publicPortRaw), 10);
|
|
1153
|
-
inviteHost = parsed.port
|
|
1154
|
-
? normalizedHostname
|
|
1155
|
-
: `${parsed.hostname}:${(Number.isFinite(publicPort) && publicPort > 0 && publicPort <= 65535) ? publicPort : 443}`;
|
|
1156
|
-
config.setAgent({ hostname: inviteHost });
|
|
1157
|
-
} else {
|
|
1158
|
-
const existing = normalizeHostInput((config.getAgent() || {}).hostname || '');
|
|
1159
|
-
inviteHost = existing || `localhost:${backendPort}`;
|
|
1160
|
-
if (!existing) {
|
|
1161
|
-
config.setAgent({ hostname: inviteHost });
|
|
1162
|
-
}
|
|
1177
|
+
function splitCommand(input) {
|
|
1178
|
+
const raw = String(input || '').trim();
|
|
1179
|
+
if (!raw) return [];
|
|
1180
|
+
const match = raw.match(/"([^"]*)"|'([^']*)'|`([^`]*)`|\S+/g);
|
|
1181
|
+
if (!match) return [];
|
|
1182
|
+
return match.map((token) => {
|
|
1183
|
+
if ((token.startsWith('"') && token.endsWith('"')) ||
|
|
1184
|
+
(token.startsWith("'") && token.endsWith("'")) ||
|
|
1185
|
+
(token.startsWith('`') && token.endsWith('`'))) {
|
|
1186
|
+
return token.slice(1, -1);
|
|
1187
|
+
}
|
|
1188
|
+
return token;
|
|
1189
|
+
});
|
|
1163
1190
|
}
|
|
1164
1191
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const expectedPingUrl = `${inviteScheme}://${inviteHost}/api/a2a/ping`;
|
|
1170
|
-
const inviteLooksLocal = isLocalOrUnroutableHost(inviteParsed.hostname);
|
|
1192
|
+
function findByIndex(draft, index) {
|
|
1193
|
+
const target = flattenDraft(draft).find(item => item.index === index);
|
|
1194
|
+
return target || null;
|
|
1195
|
+
}
|
|
1171
1196
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1197
|
+
function readNameFromUserContext(content) {
|
|
1198
|
+
const lines = String(content || '').split('\n');
|
|
1199
|
+
for (const line of lines) {
|
|
1200
|
+
const trimmed = String(line || '').trim();
|
|
1201
|
+
if (!trimmed) continue;
|
|
1175
1202
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1203
|
+
const nameMatch = trimmed.match(/^\*{0,2}Name:\*{0,2}\s*(.+)$/i);
|
|
1204
|
+
if (nameMatch && nameMatch[1]) {
|
|
1205
|
+
return String(nameMatch[1]).trim();
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (/^(owner|ownername):/i.test(trimmed)) {
|
|
1209
|
+
const ownerMatch = trimmed.replace(/^[^:]+:\s*/, '');
|
|
1210
|
+
if (ownerMatch) return ownerMatch.trim();
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (trimmed.startsWith('-') || trimmed.startsWith('*') || trimmed.startsWith('#')) {
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (/^[A-Za-z][\w\-,.\s]{2,}$/i.test(trimmed)) {
|
|
1218
|
+
const candidate = trimmed.split('|')[0].split('\t')[0].trim();
|
|
1219
|
+
if (candidate && candidate.length <= 80) {
|
|
1220
|
+
return candidate;
|
|
1221
|
+
}
|
|
1195
1222
|
}
|
|
1196
1223
|
}
|
|
1224
|
+
return '';
|
|
1197
1225
|
}
|
|
1198
1226
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const parseFreeTextList = (raw) => {
|
|
1205
|
-
if (raw === undefined || raw === null || raw === true) return [];
|
|
1206
|
-
const text = String(raw || '').trim();
|
|
1207
|
-
if (!text) return [];
|
|
1208
|
-
return text
|
|
1209
|
-
.split(/[\n,]+/g)
|
|
1210
|
-
.map(s => s.trim())
|
|
1211
|
-
.filter(Boolean);
|
|
1212
|
-
};
|
|
1213
|
-
|
|
1214
|
-
const promptLine = async (question) => {
|
|
1215
|
-
const readline = require('readline');
|
|
1216
|
-
return await new Promise(resolve => {
|
|
1217
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1218
|
-
rl.question(question, (answer) => {
|
|
1219
|
-
rl.close();
|
|
1220
|
-
resolve(String(answer || '').trim());
|
|
1221
|
-
});
|
|
1222
|
-
});
|
|
1223
|
-
};
|
|
1224
|
-
|
|
1225
|
-
// Optional owner override: Friends tier topics/interests (most important tier).
|
|
1226
|
-
const interactive = Boolean(
|
|
1227
|
-
args.flags.interactive ||
|
|
1228
|
-
args.flags['ask-friends-topics'] ||
|
|
1229
|
-
args.flags.askFriendsTopics
|
|
1230
|
-
);
|
|
1231
|
-
let friendsTopicsOverride = parseFreeTextList(args.flags['friends-topics'] || args.flags.friendsTopics);
|
|
1232
|
-
const noWorkspaceContext = !contextFiles.user && !contextFiles.heartbeat && !contextFiles.soul &&
|
|
1233
|
-
!contextFiles.memory && !contextFiles.claude;
|
|
1234
|
-
const shouldPromptFriendsTopics = (interactive || noWorkspaceContext) &&
|
|
1235
|
-
friendsTopicsOverride.length === 0 &&
|
|
1236
|
-
process.stdin.isTTY &&
|
|
1237
|
-
process.stdout.isTTY;
|
|
1238
|
-
if (shouldPromptFriendsTopics) {
|
|
1239
|
-
const suggested = (recommendations.friends.topics || []).slice(0, 12).join(', ');
|
|
1240
|
-
const answer = await promptLine(`Friends-tier topics/interests (comma-separated).\nSuggested: ${suggested}\n> `);
|
|
1241
|
-
friendsTopicsOverride = parseFreeTextList(answer);
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
if (friendsTopicsOverride.length > 0) {
|
|
1245
|
-
const normalized = uniqueNonEmpty(friendsTopicsOverride.map(slugify).filter(Boolean), 24);
|
|
1246
|
-
recommendations.friends.topics = uniqueNonEmpty(
|
|
1247
|
-
[...(recommendations.public.topics || []), ...normalized],
|
|
1248
|
-
24
|
|
1249
|
-
);
|
|
1250
|
-
recommendations.family.topics = uniqueNonEmpty(
|
|
1251
|
-
[...(recommendations.friends.topics || []), ...(recommendations.family.topics || [])],
|
|
1252
|
-
30
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
try {
|
|
1257
|
-
config.setTier('public', recommendations.public);
|
|
1258
|
-
config.setTier('friends', recommendations.friends);
|
|
1259
|
-
config.setTier('family', recommendations.family);
|
|
1260
|
-
} catch (err) {
|
|
1261
|
-
console.error('\n❌ Tier configuration validation failed.');
|
|
1262
|
-
console.error(` ${err.message}`);
|
|
1263
|
-
if (err.hint) {
|
|
1264
|
-
console.error(` Hint: ${err.hint}`);
|
|
1265
|
-
}
|
|
1266
|
-
console.error('');
|
|
1267
|
-
process.exit(1);
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
printTierSummary(recommendations);
|
|
1271
|
-
|
|
1272
|
-
config.setOnboarding({
|
|
1273
|
-
step: 'tiers',
|
|
1274
|
-
tiers_confirmed: true
|
|
1275
|
-
});
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
// ── Step 4: Port scan + reverse proxy guidance (if needed) ──
|
|
1279
|
-
console.log('\n4️⃣ Port scan + reverse proxy');
|
|
1280
|
-
console.log(`Invite host: ${inviteHost}`);
|
|
1281
|
-
console.log(`Expected ping URL: ${expectedPingUrl}\n`);
|
|
1282
|
-
|
|
1283
|
-
const expectsReverseProxy = Boolean(
|
|
1284
|
-
(invitePort === 80 && backendPort !== 80) ||
|
|
1285
|
-
((!invitePort || invitePort === 443) && backendPort !== 443)
|
|
1286
|
-
);
|
|
1227
|
+
function flattenTopicStrings(section) {
|
|
1228
|
+
return uniqueNonEmpty((section || []).map(item => String(item && item.topic || '').trim()), 200)
|
|
1229
|
+
.filter(Boolean);
|
|
1230
|
+
}
|
|
1287
1231
|
|
|
1288
|
-
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
if (port80Ping.ok) {
|
|
1295
|
-
console.log(' ✅ serves /api/a2a/ping (A2A detected on :80)');
|
|
1296
|
-
} else if (port80Listening.listening) {
|
|
1297
|
-
console.log(` ⚠️ has a listener (${port80Listening.code || 'in_use'})`);
|
|
1298
|
-
} else if (!port80Bind.ok && port80Bind.code === 'EACCES') {
|
|
1299
|
-
console.log(' ⚠️ appears free but is not bindable by this user (EACCES)');
|
|
1300
|
-
} else if (port80Bind.ok) {
|
|
1301
|
-
console.log(' ✅ free and bindable by this user');
|
|
1302
|
-
} else {
|
|
1303
|
-
console.log(` ⚠️ not bindable (${port80Bind.code || 'unknown'})`);
|
|
1232
|
+
async function editLoop(draft, neverDisclose, reloadManifest) {
|
|
1233
|
+
const shouldPrompt = process.stdin.isTTY && process.stdout.isTTY;
|
|
1234
|
+
if (!shouldPrompt) {
|
|
1235
|
+
console.log('\n⏩ Non-interactive shell detected. Proceeding with proposed topics.');
|
|
1236
|
+
renderDraft(draft, neverDisclose);
|
|
1237
|
+
return draft;
|
|
1304
1238
|
}
|
|
1305
1239
|
|
|
1306
|
-
console.log('\
|
|
1307
|
-
console.log(
|
|
1308
|
-
console.log(
|
|
1309
|
-
console.log(
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1240
|
+
console.log('\nEdit commands:');
|
|
1241
|
+
console.log(' move N to TIER.SECTION — Move topic #N to a section');
|
|
1242
|
+
console.log(' remove N — Remove topic #N');
|
|
1243
|
+
console.log(' add TIER.SECTION "Topic" "Detail" — Add a topic');
|
|
1244
|
+
console.log(' edit N topic "new" — Edit topic #N label');
|
|
1245
|
+
console.log(' edit N detail "new" — Edit topic #N detail');
|
|
1246
|
+
console.log(' reset — Rescan workspace and regenerate');
|
|
1247
|
+
console.log(' done — Save and continue\n');
|
|
1248
|
+
|
|
1249
|
+
let done = false;
|
|
1250
|
+
renderDraft(draft, neverDisclose);
|
|
1251
|
+
|
|
1252
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1253
|
+
return await new Promise((resolve) => {
|
|
1254
|
+
const finish = () => {
|
|
1255
|
+
if (!done) {
|
|
1256
|
+
done = true;
|
|
1257
|
+
resolve(draft);
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
const prompt = () => {
|
|
1262
|
+
rl.question('Your choice: ', (answer) => {
|
|
1263
|
+
const parts = splitCommand(answer);
|
|
1264
|
+
const command = String(parts[0] || '').toLowerCase();
|
|
1265
|
+
if (!parts.length) {
|
|
1266
|
+
renderDraft(draft, neverDisclose);
|
|
1267
|
+
return prompt();
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (command === 'done') {
|
|
1271
|
+
rl.close();
|
|
1272
|
+
return finish();
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (command === 'reset') {
|
|
1276
|
+
draft = cloneDraft(reloadManifest());
|
|
1277
|
+
renderDraft(draft, neverDisclose);
|
|
1278
|
+
return prompt();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (command === 'remove') {
|
|
1282
|
+
const target = findByIndex(draft, Number.parseInt(parts[1], 10));
|
|
1283
|
+
if (!target) {
|
|
1284
|
+
console.log(`Could not find topic #${parts[1]}.`);
|
|
1285
|
+
} else {
|
|
1286
|
+
target.list.splice(target.itemIndex, 1);
|
|
1287
|
+
console.log(`Removed topic #${parts[1]}.`);
|
|
1288
|
+
}
|
|
1289
|
+
renderDraft(draft, neverDisclose);
|
|
1290
|
+
return prompt();
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (command === 'move') {
|
|
1294
|
+
const target = findByIndex(draft, Number.parseInt(parts[1], 10));
|
|
1295
|
+
const destination = parseSections(parts[2] === 'to' ? parts[3] : parts[2]);
|
|
1296
|
+
if (!target) {
|
|
1297
|
+
console.log(`Could not find topic #${parts[1]}.`);
|
|
1298
|
+
} else if (!destination) {
|
|
1299
|
+
console.log('Invalid target. Use format: move N to friends.lead');
|
|
1300
|
+
} else {
|
|
1301
|
+
target.list.splice(target.itemIndex, 1);
|
|
1302
|
+
draft[destination.tier][destination.section].push(target.item);
|
|
1303
|
+
console.log(`Moved topic #${parts[1]} to ${destination.tier}.${destination.section}`);
|
|
1304
|
+
}
|
|
1305
|
+
renderDraft(draft, neverDisclose);
|
|
1306
|
+
return prompt();
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (command === 'add') {
|
|
1310
|
+
const destination = parseSections(parts[1]);
|
|
1311
|
+
const topic = parts[2];
|
|
1312
|
+
const detail = parts[3] || '';
|
|
1313
|
+
if (!destination || !topic) {
|
|
1314
|
+
console.log('Add format: add TIER.SECTION "Topic" "Detail"');
|
|
1315
|
+
} else {
|
|
1316
|
+
draft[destination.tier][destination.section].push({ topic, detail });
|
|
1317
|
+
console.log(`Added topic to ${destination.tier}.${destination.section}.`);
|
|
1318
|
+
}
|
|
1319
|
+
renderDraft(draft, neverDisclose);
|
|
1320
|
+
return prompt();
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (command === 'edit') {
|
|
1324
|
+
const target = findByIndex(draft, Number.parseInt(parts[1], 10));
|
|
1325
|
+
const field = String(parts[2] || '').toLowerCase();
|
|
1326
|
+
const value = parts[3] || '';
|
|
1327
|
+
if (!target || !field || !['topic', 'detail'].includes(field)) {
|
|
1328
|
+
console.log('Edit format: edit N topic "new" | edit N detail "new"');
|
|
1329
|
+
} else {
|
|
1330
|
+
target.item[field] = value;
|
|
1331
|
+
console.log(`Updated topic #${parts[1]} ${field}.`);
|
|
1332
|
+
}
|
|
1333
|
+
renderDraft(draft, neverDisclose);
|
|
1334
|
+
return prompt();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
console.log('Unknown command.');
|
|
1338
|
+
renderDraft(draft, neverDisclose);
|
|
1339
|
+
return prompt();
|
|
1340
|
+
});
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
rl.on('close', finish);
|
|
1344
|
+
prompt();
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
async function probePing(port) {
|
|
1349
|
+
return await new Promise((resolve) => {
|
|
1350
|
+
const req = http.request({
|
|
1351
|
+
hostname: '127.0.0.1',
|
|
1352
|
+
port,
|
|
1353
|
+
path: '/api/a2a/ping',
|
|
1354
|
+
method: 'GET',
|
|
1355
|
+
timeout: 1200
|
|
1356
|
+
}, (res) => {
|
|
1357
|
+
let body = '';
|
|
1358
|
+
res.setEncoding('utf8');
|
|
1359
|
+
res.on('data', chunk => { body += String(chunk || ''); });
|
|
1360
|
+
res.on('end', () => {
|
|
1361
|
+
const ok = body.includes('"pong":true') || body.includes('"pong": true');
|
|
1362
|
+
resolve({ ok, statusCode: res.statusCode || 0, body });
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1365
|
+
req.on('error', () => resolve({ ok: false }));
|
|
1366
|
+
req.on('timeout', () => {
|
|
1367
|
+
req.destroy(new Error('timeout'));
|
|
1368
|
+
resolve({ ok: false });
|
|
1369
|
+
});
|
|
1370
|
+
req.end();
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
async function waitForLocalServer(port) {
|
|
1375
|
+
for (let i = 0; i < 18; i++) {
|
|
1376
|
+
const listening = await isPortListening(port, '127.0.0.1', { timeoutMs: 250 });
|
|
1377
|
+
if (!listening.listening) {
|
|
1378
|
+
await new Promise(r => setTimeout(r, 250));
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const probe = await probePing(port);
|
|
1383
|
+
if (probe.ok) {
|
|
1384
|
+
return true;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return false;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
async function startServer(port) {
|
|
1391
|
+
const listening = await isPortListening(port, '127.0.0.1', { timeoutMs: 250 });
|
|
1392
|
+
if (listening.listening) {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const serverScript = path.join(__dirname, '../src/server.js');
|
|
1397
|
+
const child = spawn(process.execPath, [serverScript], {
|
|
1398
|
+
env: {
|
|
1399
|
+
...process.env,
|
|
1400
|
+
PORT: String(port),
|
|
1401
|
+
A2A_WORKSPACE: workspaceDir
|
|
1402
|
+
},
|
|
1403
|
+
detached: true,
|
|
1404
|
+
stdio: 'ignore'
|
|
1405
|
+
});
|
|
1406
|
+
child.unref();
|
|
1407
|
+
await new Promise(r => setTimeout(r, 300));
|
|
1408
|
+
return true;
|
|
1319
1409
|
}
|
|
1320
1410
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1411
|
+
function looksLikePong(body) {
|
|
1412
|
+
try {
|
|
1413
|
+
const parsed = JSON.parse(String(body || ''));
|
|
1414
|
+
if (parsed && parsed.pong === true) return true;
|
|
1415
|
+
} catch (e) {}
|
|
1416
|
+
return String(body || '').includes('"pong":true') || String(body || '').includes('"pong": true');
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Step 1: discover context
|
|
1420
|
+
const contextFiles = (() => {
|
|
1421
|
+
try {
|
|
1422
|
+
return readContextFiles(workspaceDir);
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
return {};
|
|
1425
|
+
}
|
|
1426
|
+
})();
|
|
1427
|
+
|
|
1428
|
+
renderWorkspaceScan(contextFiles);
|
|
1429
|
+
|
|
1430
|
+
const backendPort = parsePort(args.flags.port || args.flags.p || process.env.A2A_PORT || process.env.PORT, 3001);
|
|
1431
|
+
const hostFlag = normalizeHostInput(
|
|
1432
|
+
args.flags.hostname !== undefined
|
|
1433
|
+
? String(args.flags.hostname)
|
|
1434
|
+
: (config.getAgent().hostname || `localhost:${backendPort}`)
|
|
1435
|
+
);
|
|
1436
|
+
const parsedHost = splitHostPort(hostFlag || `localhost:${backendPort}`);
|
|
1437
|
+
const inviteHost = parsedHost.port
|
|
1438
|
+
? `${parsedHost.hostname}:${parsedHost.port}`
|
|
1439
|
+
: `${parsedHost.hostname || 'localhost'}:${backendPort}`;
|
|
1440
|
+
|
|
1441
|
+
// Step 2: seed draft from workspace context
|
|
1442
|
+
let manifest = generateDefaultManifest(contextFiles);
|
|
1443
|
+
let draft = makeDraft(manifest);
|
|
1444
|
+
const neverDisclose = uniqueNonEmpty(manifest.never_disclose || [
|
|
1445
|
+
'API keys',
|
|
1446
|
+
'Other users\' data',
|
|
1447
|
+
'Financial figures'
|
|
1448
|
+
], 30);
|
|
1449
|
+
|
|
1450
|
+
draft = await editLoop(draft, neverDisclose, () => {
|
|
1451
|
+
try {
|
|
1452
|
+
const refreshedContext = readContextFiles(workspaceDir);
|
|
1453
|
+
const freshManifest = generateDefaultManifest(refreshedContext);
|
|
1454
|
+
manifest = freshManifest;
|
|
1455
|
+
return makeDraft(freshManifest);
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
return draft;
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
const finalManifest = {
|
|
1462
|
+
version: 1,
|
|
1463
|
+
generated_at: manifest.generated_at || new Date().toISOString(),
|
|
1464
|
+
updated_at: new Date().toISOString(),
|
|
1465
|
+
topics: {
|
|
1466
|
+
public: {
|
|
1467
|
+
lead_with: sanitizeSectionItems(draft.public.lead_with, 80),
|
|
1468
|
+
discuss_freely: sanitizeSectionItems(draft.public.discuss_freely, 80),
|
|
1469
|
+
deflect: sanitizeSectionItems(draft.public.deflect, 80)
|
|
1470
|
+
},
|
|
1471
|
+
friends: {
|
|
1472
|
+
lead_with: sanitizeSectionItems(draft.friends.lead_with, 80),
|
|
1473
|
+
discuss_freely: sanitizeSectionItems(draft.friends.discuss_freely, 80),
|
|
1474
|
+
deflect: sanitizeSectionItems(draft.friends.deflect, 80)
|
|
1475
|
+
},
|
|
1476
|
+
family: {
|
|
1477
|
+
lead_with: sanitizeSectionItems(draft.family.lead_with, 80),
|
|
1478
|
+
discuss_freely: sanitizeSectionItems(draft.family.discuss_freely, 80),
|
|
1479
|
+
deflect: sanitizeSectionItems(draft.family.deflect, 80)
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1482
|
+
never_disclose: neverDisclose,
|
|
1483
|
+
personality_notes: manifest.personality_notes || ''
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
// Keep config in sync with the edited disclosure.
|
|
1487
|
+
try {
|
|
1488
|
+
config.setTier('public', {
|
|
1489
|
+
topics: flattenTopicStrings([...finalManifest.topics.public.lead_with, ...finalManifest.topics.public.discuss_freely, ...finalManifest.topics.public.deflect]),
|
|
1490
|
+
disclosure: 'public'
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
config.setTier('friends', {
|
|
1494
|
+
topics: flattenTopicStrings([
|
|
1495
|
+
...finalManifest.topics.public.lead_with,
|
|
1496
|
+
...finalManifest.topics.public.discuss_freely,
|
|
1497
|
+
...finalManifest.topics.public.deflect,
|
|
1498
|
+
...finalManifest.topics.friends.lead_with,
|
|
1499
|
+
...finalManifest.topics.friends.discuss_freely,
|
|
1500
|
+
...finalManifest.topics.friends.deflect
|
|
1501
|
+
]),
|
|
1502
|
+
disclosure: 'minimal'
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
config.setTier('family', {
|
|
1506
|
+
topics: flattenTopicStrings([
|
|
1507
|
+
...finalManifest.topics.public.lead_with,
|
|
1508
|
+
...finalManifest.topics.public.discuss_freely,
|
|
1509
|
+
...finalManifest.topics.public.deflect,
|
|
1510
|
+
...finalManifest.topics.friends.lead_with,
|
|
1511
|
+
...finalManifest.topics.friends.discuss_freely,
|
|
1512
|
+
...finalManifest.topics.friends.deflect,
|
|
1513
|
+
...finalManifest.topics.family.lead_with,
|
|
1514
|
+
...finalManifest.topics.family.discuss_freely,
|
|
1515
|
+
...finalManifest.topics.family.deflect
|
|
1516
|
+
]),
|
|
1517
|
+
disclosure: 'minimal'
|
|
1325
1518
|
});
|
|
1519
|
+
|
|
1520
|
+
saveManifest(finalManifest);
|
|
1521
|
+
config.setOnboarding({ step: 'tiers', tiers_confirmed: true });
|
|
1522
|
+
} catch (err) {
|
|
1523
|
+
console.error('\n❌ Failed to save tier updates.');
|
|
1524
|
+
console.error(` ${err.message}`);
|
|
1525
|
+
throw err;
|
|
1326
1526
|
}
|
|
1327
1527
|
|
|
1328
|
-
|
|
1329
|
-
console.log(
|
|
1528
|
+
console.log('\n🚀 Starting A2A server...');
|
|
1529
|
+
console.log(`Port: ${backendPort}`);
|
|
1530
|
+
console.log(`Hostname: ${inviteHost}`);
|
|
1330
1531
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1532
|
+
const started = await startServer(backendPort);
|
|
1533
|
+
const localRunning = await waitForLocalServer(backendPort);
|
|
1534
|
+
if (!localRunning) {
|
|
1535
|
+
console.log('⚠️ Local server not reachable. Start it manually and retry if needed:');
|
|
1536
|
+
console.log(` A2A_HOSTNAME="${inviteHost}" a2a server --port ${backendPort}`);
|
|
1333
1537
|
} else {
|
|
1334
|
-
|
|
1335
|
-
if (
|
|
1336
|
-
console.log(
|
|
1337
|
-
} else {
|
|
1338
|
-
console.log(`External IP lookup failed: ${external && external.error ? external.error : 'unknown_error'}`);
|
|
1538
|
+
console.log('✅ Server running!');
|
|
1539
|
+
if (started) {
|
|
1540
|
+
console.log('🟢 Local server started automatically.');
|
|
1339
1541
|
}
|
|
1340
1542
|
}
|
|
1341
1543
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
console.log('Skipping external reachability check: invite host looks local/unroutable.');
|
|
1366
|
-
} else {
|
|
1367
|
-
const extPing = await externalPingCheck(expectedPingUrl);
|
|
1368
|
-
if (extPing.ok) {
|
|
1369
|
-
console.log(`✅ External ping OK (${extPing.provider})`);
|
|
1370
|
-
} else if (args.flags['skip-verify']) {
|
|
1371
|
-
console.log('⚠️ External ping FAILED (skipped via --skip-verify).');
|
|
1372
|
-
} else if (args.flags['confirm-ingress']) {
|
|
1373
|
-
console.log('⚠️ External ping FAILED (continuing due to --confirm-ingress).');
|
|
1374
|
-
} else {
|
|
1375
|
-
console.log('⚠️ External ping FAILED (server may not be publicly reachable yet).');
|
|
1376
|
-
console.log('Fix ingress (DNS/reverse proxy/firewall), then rerun. If you want to proceed anyway:');
|
|
1377
|
-
console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-ingress`);
|
|
1378
|
-
console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --skip-verify`);
|
|
1379
|
-
console.log('');
|
|
1380
|
-
return;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
if (!config.getOnboarding().verify_confirmed) {
|
|
1385
|
-
config.setOnboarding({
|
|
1386
|
-
step: 'verify',
|
|
1387
|
-
verify_confirmed: true
|
|
1544
|
+
const dashboard = `http://127.0.0.1:${backendPort}/dashboard/`;
|
|
1545
|
+
|
|
1546
|
+
const hostSplit = splitHostPort(inviteHost);
|
|
1547
|
+
const isPrivateHost = isLocalOrUnroutableHost(hostSplit.hostname);
|
|
1548
|
+
const expectedPingUrl = `${isPrivateHost ? 'http' : (hostSplit.port === 443 ? 'https' : 'http')}://${inviteHost}/api/a2a/ping`;
|
|
1549
|
+
|
|
1550
|
+
if (isPrivateHost) {
|
|
1551
|
+
console.log('✅ External ping OK (local testing host)');
|
|
1552
|
+
} else {
|
|
1553
|
+
const external = await new Promise(resolve => {
|
|
1554
|
+
const req = http.get(expectedPingUrl, (res) => {
|
|
1555
|
+
let body = '';
|
|
1556
|
+
res.setEncoding('utf8');
|
|
1557
|
+
res.on('data', chunk => { body += chunk; });
|
|
1558
|
+
res.on('end', () => {
|
|
1559
|
+
resolve({ ok: looksLikePong(body), statusCode: res.statusCode || 0, body });
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
req.on('error', () => resolve({ ok: false }));
|
|
1563
|
+
req.setTimeout(1500, () => {
|
|
1564
|
+
req.destroy(new Error('timeout'));
|
|
1565
|
+
resolve({ ok: false });
|
|
1566
|
+
});
|
|
1388
1567
|
});
|
|
1568
|
+
|
|
1569
|
+
if (!external.ok && !args.flags['confirm-ingress'] && !args.flags['skip-verify']) {
|
|
1570
|
+
console.log('⚠️ External ping FAILED. Fix host/reachability and rerun quickstart, or use --skip-verify.');
|
|
1571
|
+
console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --skip-verify`);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
if (!external.ok) {
|
|
1576
|
+
console.log('⚠️ External ping FAILED (continuing).');
|
|
1577
|
+
} else {
|
|
1578
|
+
console.log(`✅ External ping OK (${expectedPingUrl})`);
|
|
1579
|
+
}
|
|
1389
1580
|
}
|
|
1390
1581
|
|
|
1582
|
+
console.log(`Dashboard: ${dashboard}`);
|
|
1583
|
+
|
|
1584
|
+
// Step 5: generate first invite
|
|
1585
|
+
const publicTopicsForInvite = flattenTopicStrings([
|
|
1586
|
+
...draft.public.lead_with,
|
|
1587
|
+
...draft.public.discuss_freely
|
|
1588
|
+
]);
|
|
1589
|
+
const goalItems = ['grow-network', 'find-collaborators', 'build-in-public'];
|
|
1590
|
+
|
|
1591
|
+
const ownerName = args.flags.owner || config.getAgent().name || readNameFromUserContext(contextFiles.user) || 'Someone';
|
|
1592
|
+
const peerName = args.flags.name || 'my-agent';
|
|
1593
|
+
|
|
1594
|
+
config.setAgent({ name: ownerName, hostname: inviteHost });
|
|
1595
|
+
|
|
1596
|
+
const { token, record } = store.create({
|
|
1597
|
+
name: peerName,
|
|
1598
|
+
owner: ownerName,
|
|
1599
|
+
permissions: 'public',
|
|
1600
|
+
disclosure: 'minimal',
|
|
1601
|
+
expires: 'never',
|
|
1602
|
+
maxCalls: null,
|
|
1603
|
+
allowedTopics: publicTopicsForInvite,
|
|
1604
|
+
allowedGoals: goalItems,
|
|
1605
|
+
notify: 'all'
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
const inviteUrl = `a2a://${inviteHost}/${token}`;
|
|
1609
|
+
const topicLine = publicTopicsForInvite.length > 0 ? publicTopicsForInvite.slice(0, 6).join(' · ') : 'chat';
|
|
1610
|
+
const goalLine = goalItems.join(' · ');
|
|
1611
|
+
|
|
1612
|
+
console.log('\n📞 Your first invite (public tier):\n');
|
|
1613
|
+
console.log('─'.repeat(60));
|
|
1614
|
+
const inviteText = `📞🗣️ **Agent-to-Agent Call Invite**
|
|
1615
|
+
|
|
1616
|
+
👤 **${ownerName}** would like your agent to call **${peerName}**
|
|
1617
|
+
|
|
1618
|
+
💬 ${topicLine}
|
|
1619
|
+
🎯 ${goalLine}
|
|
1620
|
+
|
|
1621
|
+
${inviteUrl}
|
|
1622
|
+
|
|
1623
|
+
── setup ──
|
|
1624
|
+
npm i -g a2acalling && a2a add "${inviteUrl}" "${peerName}" && a2a call "${peerName}" "Hello!"
|
|
1625
|
+
https://github.com/onthegonow/a2a_calling`;
|
|
1626
|
+
console.log(inviteText);
|
|
1627
|
+
console.log('─'.repeat(60));
|
|
1628
|
+
console.log('Share this invite to let other agents call you!\n');
|
|
1629
|
+
|
|
1391
1630
|
config.completeOnboarding();
|
|
1392
|
-
|
|
1393
|
-
console.log('
|
|
1631
|
+
|
|
1632
|
+
console.log('✅ A2A setup complete!\n');
|
|
1633
|
+
console.log('Your agent is now:');
|
|
1634
|
+
console.log(` • Listening on ${inviteHost}`);
|
|
1635
|
+
console.log(' • Ready to receive calls');
|
|
1636
|
+
console.log(` • Configured with ${Object.keys(finalManifest.topics).length} permission tiers`);
|
|
1637
|
+
console.log('\nNext steps:');
|
|
1638
|
+
console.log(' a2a invite friends — Create a friends-tier invite');
|
|
1639
|
+
console.log(' a2a contacts — View your contacts');
|
|
1640
|
+
console.log(' a2a gui — Open the dashboard\n');
|
|
1641
|
+
console.log('Happy calling! 🤝');
|
|
1394
1642
|
},
|
|
1395
1643
|
|
|
1396
1644
|
install: () => {
|
|
@@ -1401,6 +1649,136 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1401
1649
|
require('../scripts/install-openclaw.js');
|
|
1402
1650
|
},
|
|
1403
1651
|
|
|
1652
|
+
uninstall: async (args) => {
|
|
1653
|
+
const fs = require('fs');
|
|
1654
|
+
const path = require('path');
|
|
1655
|
+
const { spawnSync } = require('child_process');
|
|
1656
|
+
|
|
1657
|
+
const keepConfig = Boolean(args.flags['keep-config'] || args.flags.keepConfig);
|
|
1658
|
+
const force = Boolean(args.flags.force || args.flags.f);
|
|
1659
|
+
|
|
1660
|
+
const configDir = process.env.A2A_CONFIG_DIR ||
|
|
1661
|
+
process.env.OPENCLAW_CONFIG_DIR ||
|
|
1662
|
+
path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
|
|
1663
|
+
|
|
1664
|
+
const configFile = path.join(configDir, 'a2a-config.json');
|
|
1665
|
+
const disclosureFile = path.join(configDir, 'a2a-disclosure.json');
|
|
1666
|
+
const tokensFile = path.join(configDir, 'a2a-tokens.json');
|
|
1667
|
+
const dbFile = path.join(configDir, 'a2a-conversations.db');
|
|
1668
|
+
const logsDbFile = path.join(configDir, 'a2a-logs.db');
|
|
1669
|
+
const callbookDbFile = path.join(configDir, 'a2a-callbook.db');
|
|
1670
|
+
|
|
1671
|
+
console.log(`\n🗑️ A2A Uninstall`);
|
|
1672
|
+
console.log('─────────────────\n');
|
|
1673
|
+
|
|
1674
|
+
if (!keepConfig && !force) {
|
|
1675
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1676
|
+
console.error('Refusing to prompt without a TTY. Re-run with --force to confirm uninstall.');
|
|
1677
|
+
process.exit(1);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const existing = [configFile, disclosureFile, tokensFile, dbFile, logsDbFile, callbookDbFile].filter(f => fs.existsSync(f));
|
|
1681
|
+
const list = existing.length ? existing.map(f => ` - ${f}`).join('\n') : ' (no local config/database files found)';
|
|
1682
|
+
const ok = await promptYesNo(
|
|
1683
|
+
`This will stop the pm2 process "a2a" and delete:\n${list}\nProceed? (y/N) `
|
|
1684
|
+
);
|
|
1685
|
+
if (!ok) {
|
|
1686
|
+
console.log('\nCancelled.\n');
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function pm2Exists() {
|
|
1692
|
+
const res = spawnSync('pm2', ['--version'], { stdio: 'ignore', timeout: 4000 });
|
|
1693
|
+
if (res.error && res.error.code === 'ENOENT') return false;
|
|
1694
|
+
return res.status === 0;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function pm2HasProcess(name) {
|
|
1698
|
+
const res = spawnSync('pm2', ['describe', name], { encoding: 'utf8', timeout: 6000 });
|
|
1699
|
+
if (res.error && res.error.code === 'ENOENT') return false;
|
|
1700
|
+
return res.status === 0;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function pm2StopAndDelete(name) {
|
|
1704
|
+
if (!pm2Exists()) return { ok: true, skipped: true };
|
|
1705
|
+
if (!pm2HasProcess(name)) return { ok: true, skipped: true };
|
|
1706
|
+
|
|
1707
|
+
const stop = spawnSync('pm2', ['stop', name], { encoding: 'utf8', timeout: 8000 });
|
|
1708
|
+
if (stop.status !== 0) {
|
|
1709
|
+
const msg = (stop.stderr || stop.stdout || '').trim();
|
|
1710
|
+
return { ok: false, error: msg || 'pm2 stop failed' };
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const del = spawnSync('pm2', ['delete', name], { encoding: 'utf8', timeout: 8000 });
|
|
1714
|
+
if (del.status !== 0) {
|
|
1715
|
+
const msg = (del.stderr || del.stdout || '').trim();
|
|
1716
|
+
return { ok: false, error: msg || 'pm2 delete failed' };
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
return { ok: true };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function rmFileSafe(filePath) {
|
|
1723
|
+
try {
|
|
1724
|
+
fs.rmSync(filePath, { force: true });
|
|
1725
|
+
return { ok: true };
|
|
1726
|
+
} catch (err) {
|
|
1727
|
+
return { ok: false, error: err.message };
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
process.stdout.write('Stopping server... ');
|
|
1732
|
+
const stopped = pm2StopAndDelete('a2a');
|
|
1733
|
+
if (!stopped.ok) {
|
|
1734
|
+
console.log('❌');
|
|
1735
|
+
console.error(` ${stopped.error}`);
|
|
1736
|
+
process.exit(1);
|
|
1737
|
+
}
|
|
1738
|
+
console.log('✅');
|
|
1739
|
+
|
|
1740
|
+
let configOk = true;
|
|
1741
|
+
let dbOk = true;
|
|
1742
|
+
|
|
1743
|
+
if (!keepConfig) {
|
|
1744
|
+
process.stdout.write('Removing config... ');
|
|
1745
|
+
const c1 = rmFileSafe(configFile);
|
|
1746
|
+
const c2 = rmFileSafe(disclosureFile);
|
|
1747
|
+
const c3 = rmFileSafe(tokensFile);
|
|
1748
|
+
configOk = Boolean(c1.ok && c2.ok && c3.ok);
|
|
1749
|
+
console.log(configOk ? '✅' : '❌');
|
|
1750
|
+
if (!configOk) {
|
|
1751
|
+
if (!c1.ok) console.error(` ${configFile}: ${c1.error}`);
|
|
1752
|
+
if (!c2.ok) console.error(` ${disclosureFile}: ${c2.error}`);
|
|
1753
|
+
if (!c3.ok) console.error(` ${tokensFile}: ${c3.error}`);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
process.stdout.write('Removing database... ');
|
|
1757
|
+
const d1 = rmFileSafe(dbFile);
|
|
1758
|
+
const d2 = rmFileSafe(logsDbFile);
|
|
1759
|
+
const d3 = rmFileSafe(callbookDbFile);
|
|
1760
|
+
dbOk = Boolean(d1.ok && d2.ok && d3.ok);
|
|
1761
|
+
console.log(dbOk ? '✅' : '❌');
|
|
1762
|
+
if (!dbOk) {
|
|
1763
|
+
if (!d1.ok) console.error(` ${dbFile}: ${d1.error}`);
|
|
1764
|
+
if (!d2.ok) console.error(` ${logsDbFile}: ${d2.error}`);
|
|
1765
|
+
if (!d3.ok) console.error(` ${callbookDbFile}: ${d3.error}`);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
if (!configOk || !dbOk) {
|
|
1769
|
+
process.exit(1);
|
|
1770
|
+
}
|
|
1771
|
+
} else {
|
|
1772
|
+
console.log('Removing config... ⏭️');
|
|
1773
|
+
console.log('Removing database... ⏭️');
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
console.log('\nTo complete removal:');
|
|
1777
|
+
console.log(' npm uninstall -g a2acalling\n');
|
|
1778
|
+
console.log(`Config preserved: ${keepConfig ? 'yes' : 'no'}`);
|
|
1779
|
+
console.log(`Location: ${configDir}`);
|
|
1780
|
+
},
|
|
1781
|
+
|
|
1404
1782
|
update: async (args) => {
|
|
1405
1783
|
const { execSync } = require('child_process');
|
|
1406
1784
|
const path = require('path');
|
|
@@ -1508,46 +1886,78 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1508
1886
|
}
|
|
1509
1887
|
},
|
|
1510
1888
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1889
|
+
onboard: (args) => {
|
|
1890
|
+
const { A2AConfig } = require('../src/lib/config');
|
|
1891
|
+
const {
|
|
1892
|
+
readContextFiles,
|
|
1893
|
+
buildExtractionPrompt,
|
|
1894
|
+
validateDisclosureSubmission,
|
|
1895
|
+
saveManifest,
|
|
1896
|
+
MANIFEST_FILE
|
|
1897
|
+
} = require('../src/lib/disclosure');
|
|
1898
|
+
const config = new A2AConfig();
|
|
1899
|
+
|
|
1900
|
+
// ── Submit mode: agent sends structured JSON ──────────────
|
|
1901
|
+
const submitRaw = args.flags.submit;
|
|
1902
|
+
if (submitRaw) {
|
|
1903
|
+
let parsed;
|
|
1904
|
+
try {
|
|
1905
|
+
parsed = JSON.parse(String(submitRaw));
|
|
1906
|
+
} catch (e) {
|
|
1907
|
+
console.error('\n\u274c Invalid JSON in --submit flag.');
|
|
1908
|
+
console.error(` Parse error: ${e.message}\n`);
|
|
1909
|
+
process.exit(1);
|
|
1910
|
+
}
|
|
1520
1911
|
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1912
|
+
const result = validateDisclosureSubmission(parsed);
|
|
1913
|
+
if (!result.valid) {
|
|
1914
|
+
console.error('\n\u274c Disclosure submission validation failed:\n');
|
|
1915
|
+
result.errors.forEach(err => console.error(` \u2022 ${err}`));
|
|
1916
|
+
console.error(`\nFix the errors above and resubmit with: a2a onboard --submit '<json>'\n`);
|
|
1917
|
+
process.exit(1);
|
|
1918
|
+
}
|
|
1524
1919
|
|
|
1920
|
+
saveManifest(result.manifest);
|
|
1921
|
+
|
|
1922
|
+
const agentName = args.flags.name || config.getAgent().name || process.env.A2A_AGENT_NAME || '';
|
|
1923
|
+
const hostname = args.flags.hostname || config.getAgent().hostname || process.env.A2A_HOSTNAME || '';
|
|
1924
|
+
if (agentName) config.setAgent({ name: agentName });
|
|
1925
|
+
if (hostname) config.setAgent({ hostname });
|
|
1926
|
+
|
|
1927
|
+
console.log('\n\u2705 Disclosure manifest saved.');
|
|
1928
|
+
console.log(` Manifest: ${MANIFEST_FILE}`);
|
|
1929
|
+
console.log(' Next: a2a quickstart\n');
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// ── Prompt mode: print extraction instructions for agent ──
|
|
1934
|
+
if (config.isOnboarded() && !args.flags.force) {
|
|
1935
|
+
console.log('\u2705 Onboarding already complete. Use --force to regenerate.');
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
|
|
1525
1940
|
const contextFiles = readContextFiles(workspaceDir);
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
console.log(`\n\u2705 Disclosure manifest generated.`);
|
|
1548
|
-
console.log(` Manifest: ${MANIFEST_FILE}`);
|
|
1549
|
-
console.log(' Next: a2a quickstart\n');
|
|
1550
|
-
},
|
|
1941
|
+
|
|
1942
|
+
const availableFiles = {
|
|
1943
|
+
'USER.md': Boolean(contextFiles.user),
|
|
1944
|
+
'SOUL.md': Boolean(contextFiles.soul),
|
|
1945
|
+
'HEARTBEAT.md': Boolean(contextFiles.heartbeat),
|
|
1946
|
+
'SKILL.md': Boolean(contextFiles.skill),
|
|
1947
|
+
'CLAUDE.md': Boolean(contextFiles.claude),
|
|
1948
|
+
'memory/*.md': Boolean(contextFiles.memory)
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1951
|
+
console.log(buildExtractionPrompt(availableFiles));
|
|
1952
|
+
console.log('\n---');
|
|
1953
|
+
console.log('After the owner confirms, submit with:');
|
|
1954
|
+
console.log(" a2a onboard --submit '<json>'\n");
|
|
1955
|
+
},
|
|
1956
|
+
|
|
1957
|
+
version: () => {
|
|
1958
|
+
const pkg = require('../package.json');
|
|
1959
|
+
console.log(pkg.version);
|
|
1960
|
+
},
|
|
1551
1961
|
|
|
1552
1962
|
help: () => {
|
|
1553
1963
|
console.log(`A2A Calling - Agent-to-Agent Communication
|
|
@@ -1628,6 +2038,10 @@ Server:
|
|
|
1628
2038
|
|
|
1629
2039
|
install Install A2A for OpenClaw
|
|
1630
2040
|
setup Auto setup (gateway-aware dashboard install)
|
|
2041
|
+
uninstall Stop server and remove local config/DB
|
|
2042
|
+
--keep-config Preserve config/DB (for reinstall)
|
|
2043
|
+
--force Skip confirmation prompt
|
|
2044
|
+
version Show installed package version
|
|
1631
2045
|
|
|
1632
2046
|
Examples:
|
|
1633
2047
|
a2a create --name "bappybot" --owner "Benjamin Pollack" --expires 7d
|
|
@@ -1651,6 +2065,8 @@ if (!commands[command]) {
|
|
|
1651
2065
|
process.exit(1);
|
|
1652
2066
|
}
|
|
1653
2067
|
|
|
2068
|
+
enforceOnboarding(command);
|
|
2069
|
+
|
|
1654
2070
|
// Handle async commands
|
|
1655
2071
|
const result = commands[command](args);
|
|
1656
2072
|
if (result instanceof Promise) {
|