a2acalling 0.6.6 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +253 -708
- package/package.json +1 -1
- package/scripts/postinstall.js +21 -20
- package/src/lib/disclosure.js +21 -8
package/bin/cli.js
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const os = require('os');
|
|
20
20
|
const path = require('path');
|
|
21
|
-
const readline = require('readline');
|
|
22
21
|
const { spawn } = require('child_process');
|
|
23
22
|
const { TokenStore } = require('../src/lib/tokens');
|
|
24
23
|
const { A2AClient } = require('../src/lib/client');
|
|
@@ -77,13 +76,22 @@ function enforceOnboarding(command) {
|
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
if (!isOnboarded()) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log('\nA2A not configured yet.\n');
|
|
94
|
+
console.log('Next: run `a2a quickstart`\n');
|
|
87
95
|
process.exit(1);
|
|
88
96
|
}
|
|
89
97
|
}
|
|
@@ -193,7 +201,7 @@ function parseArgs(argv) {
|
|
|
193
201
|
|
|
194
202
|
async function promptYesNo(question) {
|
|
195
203
|
return await new Promise(resolve => {
|
|
196
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
204
|
+
const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
|
|
197
205
|
rl.question(question, (answer) => {
|
|
198
206
|
rl.close();
|
|
199
207
|
const normalized = String(answer || '').trim().toLowerCase();
|
|
@@ -939,708 +947,204 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
939
947
|
},
|
|
940
948
|
|
|
941
949
|
quickstart: async (args) => {
|
|
942
|
-
const http = require('http');
|
|
943
950
|
const { A2AConfig } = require('../src/lib/config');
|
|
944
|
-
const { isPortListening } = require('../src/lib/port-scanner');
|
|
945
|
-
const {
|
|
946
|
-
|
|
947
|
-
generateDefaultManifest,
|
|
948
|
-
saveManifest
|
|
949
|
-
} = require('../src/lib/disclosure');
|
|
950
|
-
const {
|
|
951
|
-
normalizeHostInput,
|
|
952
|
-
splitHostPort,
|
|
953
|
-
isLocalOrUnroutableHost
|
|
954
|
-
} = require('../src/lib/invite-host');
|
|
951
|
+
const { tryBindPort, findAvailablePort, isPortListening } = require('../src/lib/port-scanner');
|
|
952
|
+
const { buildExtractionPrompt, MANIFEST_FILE } = require('../src/lib/disclosure');
|
|
953
|
+
const { getExternalIp } = require('../src/lib/external-ip');
|
|
955
954
|
|
|
956
955
|
const config = new A2AConfig();
|
|
957
|
-
const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
|
|
958
956
|
|
|
959
957
|
if (args.flags.force) {
|
|
960
958
|
config.resetOnboarding();
|
|
961
959
|
}
|
|
962
960
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
return fallback;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function uniqueNonEmpty(values, limit = 80) {
|
|
972
|
-
const normalizeValue = (value) => {
|
|
973
|
-
if (typeof value === 'string') {
|
|
974
|
-
return String(value || '').trim();
|
|
975
|
-
}
|
|
976
|
-
if (value && typeof value === 'object' && !Array.isArray(value) && 'topic' in value) {
|
|
977
|
-
return String(value.topic || '').trim();
|
|
978
|
-
}
|
|
979
|
-
return '';
|
|
980
|
-
};
|
|
981
|
-
|
|
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;
|
|
992
|
-
}
|
|
993
|
-
return out;
|
|
994
|
-
}
|
|
995
|
-
|
|
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
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
function uniqueTopicRecords(values, limit = 80) {
|
|
1010
|
-
const out = [];
|
|
1011
|
-
const seen = new Set();
|
|
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);
|
|
1019
|
-
if (out.length >= limit) break;
|
|
1020
|
-
}
|
|
1021
|
-
return out;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function sanitizeSectionItems(values, limit = 80) {
|
|
1025
|
-
return uniqueTopicRecords(values, limit).map(item => ({
|
|
1026
|
-
topic: item.topic,
|
|
1027
|
-
detail: item.detail || ''
|
|
1028
|
-
}));
|
|
1029
|
-
}
|
|
1030
|
-
|
|
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
|
-
};
|
|
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;
|
|
1054
965
|
}
|
|
1055
966
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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}`);
|
|
1062
975
|
}
|
|
1063
|
-
return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
|
|
1064
976
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
return 0;
|
|
1073
|
-
}
|
|
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;
|
|
1074
984
|
}
|
|
1075
985
|
|
|
1076
|
-
function
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
];
|
|
1087
|
-
|
|
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}`);
|
|
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;
|
|
1093
990
|
}
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1096
|
-
|
|
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
|
-
}
|
|
1123
|
-
|
|
1124
|
-
function renderDraft(draft, neverDisclose) {
|
|
1125
|
-
console.log('\n📋 Proposed Permission Tiers');
|
|
1126
|
-
console.log('═'.repeat(60));
|
|
1127
|
-
|
|
1128
|
-
let index = 1;
|
|
1129
|
-
const titleByTier = {
|
|
1130
|
-
public: 'PUBLIC (anyone can see):',
|
|
1131
|
-
friends: 'FRIENDS (trusted contacts):',
|
|
1132
|
-
family: 'FAMILY (inner circle):'
|
|
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);
|
|
1157
|
-
}
|
|
1158
|
-
|
|
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 };
|
|
1175
|
-
}
|
|
1176
|
-
|
|
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
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function findByIndex(draft, index) {
|
|
1193
|
-
const target = flattenDraft(draft).find(item => item.index === index);
|
|
1194
|
-
return target || null;
|
|
991
|
+
return fallback;
|
|
1195
992
|
}
|
|
1196
993
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
for (const line of lines) {
|
|
1200
|
-
const trimmed = String(line || '').trim();
|
|
1201
|
-
if (!trimmed) continue;
|
|
994
|
+
// ── Step 1 of 4: Setting up A2A server ──────────────────
|
|
995
|
+
console.log('\nStep 1 of 4: Setting up A2A server\n');
|
|
1202
996
|
|
|
1203
|
-
|
|
1204
|
-
if (nameMatch && nameMatch[1]) {
|
|
1205
|
-
return String(nameMatch[1]).trim();
|
|
1206
|
-
}
|
|
997
|
+
const preferredPort = parsePort(args.flags.port || args.flags.p, null);
|
|
1207
998
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
if (trimmed.startsWith('-') || trimmed.startsWith('*') || trimmed.startsWith('#')) {
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
999
|
+
// If user specified a port, try that first
|
|
1000
|
+
let serverPort;
|
|
1001
|
+
let usingAlternatePort = false;
|
|
1216
1002
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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);
|
|
1222
1023
|
}
|
|
1024
|
+
console.log(` Port ${serverPort} available.`);
|
|
1025
|
+
usingAlternatePort = true;
|
|
1223
1026
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
console.log('
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
-
}
|
|
1027
|
+
} else {
|
|
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...');
|
|
1373
1044
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
if (!listening.listening) {
|
|
1378
|
-
await new Promise(r => setTimeout(r, 250));
|
|
1379
|
-
continue;
|
|
1380
|
-
}
|
|
1045
|
+
const candidates = [];
|
|
1046
|
+
for (let p = 3001; p < 3101; p++) candidates.push(p);
|
|
1047
|
+
serverPort = await findAvailablePort(candidates);
|
|
1381
1048
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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);
|
|
1385
1053
|
}
|
|
1054
|
+
console.log(` Port ${serverPort} available.`);
|
|
1055
|
+
usingAlternatePort = true;
|
|
1386
1056
|
}
|
|
1387
|
-
return false;
|
|
1388
1057
|
}
|
|
1389
1058
|
|
|
1059
|
+
// Start server
|
|
1060
|
+
console.log(` Starting A2A server on port ${serverPort}...`);
|
|
1061
|
+
|
|
1390
1062
|
async function startServer(port) {
|
|
1391
1063
|
const listening = await isPortListening(port, '127.0.0.1', { timeoutMs: 250 });
|
|
1392
|
-
if (listening.listening) {
|
|
1393
|
-
return false;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1064
|
+
if (listening.listening) return { started: false, existing: true };
|
|
1396
1065
|
const serverScript = path.join(__dirname, '../src/server.js');
|
|
1397
1066
|
const child = spawn(process.execPath, [serverScript], {
|
|
1398
|
-
env: {
|
|
1399
|
-
...process.env,
|
|
1400
|
-
PORT: String(port),
|
|
1401
|
-
A2A_WORKSPACE: workspaceDir
|
|
1402
|
-
},
|
|
1067
|
+
env: { ...process.env, PORT: String(port) },
|
|
1403
1068
|
detached: true,
|
|
1404
1069
|
stdio: 'ignore'
|
|
1405
1070
|
});
|
|
1406
1071
|
child.unref();
|
|
1407
1072
|
await new Promise(r => setTimeout(r, 300));
|
|
1408
|
-
return true;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
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');
|
|
1073
|
+
return { started: true, pid: child.pid };
|
|
1417
1074
|
}
|
|
1418
1075
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
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;
|
|
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));
|
|
1458
1081
|
}
|
|
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'
|
|
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;
|
|
1082
|
+
return false;
|
|
1526
1083
|
}
|
|
1527
1084
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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}`);
|
|
1537
|
-
} else {
|
|
1538
|
-
console.log('✅ Server running!');
|
|
1539
|
-
if (started) {
|
|
1540
|
-
console.log('🟢 Local server started automatically.');
|
|
1541
|
-
}
|
|
1085
|
+
const serverResult = await startServer(serverPort);
|
|
1086
|
+
if (serverResult.existing) {
|
|
1087
|
+
console.log(' Existing server detected on this port.');
|
|
1542
1088
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
-
});
|
|
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
|
-
}
|
|
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);
|
|
1580
1094
|
}
|
|
1095
|
+
console.log(' Server running.\n');
|
|
1581
1096
|
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
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
|
-
});
|
|
1097
|
+
// Store server PID for cleanup
|
|
1098
|
+
if (serverResult.pid) {
|
|
1099
|
+
config.setOnboarding({ server_pid: serverResult.pid, server_port: serverPort });
|
|
1100
|
+
}
|
|
1607
1101
|
|
|
1608
|
-
|
|
1609
|
-
const
|
|
1610
|
-
|
|
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`);
|
|
1108
|
+
}
|
|
1109
|
+
const externalIp = ipResult.ip || null;
|
|
1110
|
+
const publicHost = externalIp
|
|
1111
|
+
? (serverPort === 80 ? externalIp : `${externalIp}:${serverPort}`)
|
|
1112
|
+
: `localhost:${serverPort}`;
|
|
1611
1113
|
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
const inviteText = `📞🗣️ **Agent-to-Agent Call Invite**
|
|
1114
|
+
// Save server config
|
|
1115
|
+
config.setAgent({ hostname: publicHost });
|
|
1615
1116
|
|
|
1616
|
-
|
|
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');
|
|
1129
|
+
}
|
|
1617
1130
|
|
|
1618
|
-
|
|
1619
|
-
|
|
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');
|
|
1136
|
+
}
|
|
1620
1137
|
|
|
1621
|
-
|
|
1138
|
+
config.setOnboarding({ step: 'awaiting_disclosure' });
|
|
1622
1139
|
|
|
1623
|
-
──
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
console.log(
|
|
1627
|
-
console.log('
|
|
1628
|
-
console.log('Share this invite to let other agents call you!\n');
|
|
1629
|
-
|
|
1630
|
-
config.completeOnboarding();
|
|
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! 🤝');
|
|
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");
|
|
1642
1145
|
},
|
|
1643
1146
|
|
|
1147
|
+
|
|
1644
1148
|
install: () => {
|
|
1645
1149
|
require('../scripts/install-openclaw.js');
|
|
1646
1150
|
},
|
|
@@ -1886,11 +1390,9 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1886
1390
|
}
|
|
1887
1391
|
},
|
|
1888
1392
|
|
|
1889
|
-
onboard: (args) => {
|
|
1393
|
+
onboard: async (args) => {
|
|
1890
1394
|
const { A2AConfig } = require('../src/lib/config');
|
|
1891
1395
|
const {
|
|
1892
|
-
readContextFiles,
|
|
1893
|
-
buildExtractionPrompt,
|
|
1894
1396
|
validateDisclosureSubmission,
|
|
1895
1397
|
saveManifest,
|
|
1896
1398
|
MANIFEST_FILE
|
|
@@ -1904,54 +1406,104 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1904
1406
|
try {
|
|
1905
1407
|
parsed = JSON.parse(String(submitRaw));
|
|
1906
1408
|
} catch (e) {
|
|
1907
|
-
console.error('\
|
|
1908
|
-
console.error(`
|
|
1409
|
+
console.error('\nInvalid JSON in --submit flag.');
|
|
1410
|
+
console.error(` Parse error: ${e.message}\n`);
|
|
1909
1411
|
process.exit(1);
|
|
1910
1412
|
}
|
|
1911
1413
|
|
|
1912
1414
|
const result = validateDisclosureSubmission(parsed);
|
|
1913
1415
|
if (!result.valid) {
|
|
1914
|
-
console.error('\
|
|
1915
|
-
result.errors.forEach(err => console.error(`
|
|
1916
|
-
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");
|
|
1917
1419
|
process.exit(1);
|
|
1918
1420
|
}
|
|
1919
1421
|
|
|
1920
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
|
+
}
|
|
1921
1438
|
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
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
|
+
}
|
|
1926
1462
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
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
|
+
}
|
|
1932
1468
|
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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
|
+
});
|
|
1938
1492
|
|
|
1939
|
-
|
|
1940
|
-
|
|
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');
|
|
1941
1496
|
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
};
|
|
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`);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1950
1504
|
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
console.log('After the owner confirms, submit with:');
|
|
1954
|
-
console.log(" a2a onboard --submit '<json>'\n");
|
|
1505
|
+
// ── No --submit: same as quickstart ───────────────────────
|
|
1506
|
+
return commands.quickstart(args);
|
|
1955
1507
|
},
|
|
1956
1508
|
|
|
1957
1509
|
version: () => {
|
|
@@ -2017,21 +1569,14 @@ Server:
|
|
|
2017
1569
|
server Start the A2A server
|
|
2018
1570
|
--port, -p Port to listen on (default: 3001)
|
|
2019
1571
|
|
|
2020
|
-
quickstart
|
|
2021
|
-
--
|
|
2022
|
-
--
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
--
|
|
2026
|
-
--
|
|
2027
|
-
--skip-verify Skip external reachability check (not recommended)
|
|
2028
|
-
--force Reset onboarding + regenerate disclosure manifest
|
|
2029
|
-
--regen-manifest Regenerate disclosure manifest (no onboarding reset)
|
|
2030
|
-
|
|
2031
|
-
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
|
|
2032
1579
|
--force Re-run even if already onboarded
|
|
2033
|
-
--name Agent name
|
|
2034
|
-
--hostname Agent hostname
|
|
2035
1580
|
|
|
2036
1581
|
update Update A2A to latest version (npm or git pull)
|
|
2037
1582
|
--check, -c Check for updates without installing
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// Only
|
|
3
|
+
// Only run for global installs; skip in CI, dev, and Docker builds.
|
|
4
4
|
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) process.exit(0);
|
|
5
|
+
if (process.env.DOCKER) process.exit(0);
|
|
5
6
|
if (process.env.npm_config_global !== 'true') process.exit(0);
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
// Launch quickstart directly — stdio: 'inherit' forces foreground output
|
|
11
|
+
// even when npm v10+ suppresses postinstall stdout by default.
|
|
12
|
+
const result = spawnSync('a2a', ['quickstart'], {
|
|
13
|
+
stdio: 'inherit',
|
|
14
|
+
shell: true,
|
|
15
|
+
cwd: process.env.HOME || process.cwd()
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (result.error || result.status === 127) {
|
|
19
|
+
// spawn error or shell couldn't find the a2a binary
|
|
20
|
+
const reason = result.error ? result.error.message : 'a2a not found in PATH';
|
|
21
|
+
console.error('Could not auto-launch onboarding:', reason);
|
|
22
|
+
console.log('\nRun manually: a2a quickstart');
|
|
23
|
+
process.exit(0); // don't fail the install
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
process.exit(result.status || 0);
|
package/src/lib/disclosure.js
CHANGED
|
@@ -500,11 +500,25 @@ function readContextFiles(workspaceDir) {
|
|
|
500
500
|
* @param {Object} [availableFiles] - Map of filename to truthy if present
|
|
501
501
|
* @returns {string} The instruction prompt for the agent
|
|
502
502
|
*/
|
|
503
|
-
function buildExtractionPrompt(availableFiles
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
503
|
+
function buildExtractionPrompt(availableFiles) {
|
|
504
|
+
let fileSection;
|
|
505
|
+
if (availableFiles && Object.keys(availableFiles).length > 0) {
|
|
506
|
+
const fileList = Object.entries(availableFiles)
|
|
507
|
+
.filter(([, present]) => present)
|
|
508
|
+
.map(([name]) => ` - ${name}`)
|
|
509
|
+
.join('\n') || ' (none detected)';
|
|
510
|
+
fileSection = `### Available workspace files\n${fileList}\n\nRead the available files above and extract disclosure topics.`;
|
|
511
|
+
} else {
|
|
512
|
+
fileSection = `### Workspace files to look for
|
|
513
|
+
- USER.md — owner identity, bio, interests
|
|
514
|
+
- SOUL.md — values, personality, communication style
|
|
515
|
+
- HEARTBEAT.md — skip this (contains agent tasks, not disclosure topics)
|
|
516
|
+
- SKILL.md — skip this (contains agent instructions)
|
|
517
|
+
- CLAUDE.md — skip this (contains agent instructions)
|
|
518
|
+
- memory/*.md — may contain relevant context
|
|
519
|
+
|
|
520
|
+
Look for these files in your workspace directory and read the ones that exist. Extract disclosure topics from USER.md and SOUL.md primarily.`;
|
|
521
|
+
}
|
|
508
522
|
|
|
509
523
|
const jsonBlock = '```json\n{\n "topics": {\n "public": {\n "lead_with": [\n { "topic": "Short label (max 160 chars)", "detail": "Longer description of the topic" }\n ],\n "discuss_freely": [],\n "deflect": []\n },\n "friends": {\n "lead_with": [],\n "discuss_freely": [],\n "deflect": []\n },\n "family": {\n "lead_with": [],\n "discuss_freely": [],\n "deflect": []\n }\n },\n "never_disclose": ["API keys", "Credentials", "Financial figures"],\n "personality_notes": "Brief description of communication style"\n}\n```';
|
|
510
524
|
|
|
@@ -512,10 +526,9 @@ function buildExtractionPrompt(availableFiles = {}) {
|
|
|
512
526
|
|
|
513
527
|
You are helping the owner set up their A2A disclosure profile — the topics and information their agent is willing to discuss with other agents at different trust levels.
|
|
514
528
|
|
|
515
|
-
|
|
516
|
-
${fileList}
|
|
529
|
+
${fileSection}
|
|
517
530
|
|
|
518
|
-
|
|
531
|
+
Focus on what the OWNER cares about, works on, and wants to discuss — NOT on agent instructions, code documentation, or operational tasks.
|
|
519
532
|
|
|
520
533
|
### What to extract
|
|
521
534
|
|