a2acalling 0.6.6 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -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
- 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');
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
- readContextFiles,
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
- 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;
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
- 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';
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
- 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
- }
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 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
- ];
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
- console.log('');
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
- 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;
994
+ // ── Step 1 of 4: Setting up A2A server ──────────────────
995
+ console.log('\nStep 1 of 4: Setting up A2A server\n');
1202
996
 
1203
- const nameMatch = trimmed.match(/^\*{0,2}Name:\*{0,2}\s*(.+)$/i);
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
- 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
- }
999
+ // If user specified a port, try that first
1000
+ let serverPort;
1001
+ let usingAlternatePort = false;
1216
1002
 
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
- }
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
- return '';
1225
- }
1226
-
1227
- function flattenTopicStrings(section) {
1228
- return uniqueNonEmpty((section || []).map(item => String(item && item.topic || '').trim()), 200)
1229
- .filter(Boolean);
1230
- }
1231
-
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;
1238
- }
1239
-
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
- }
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
- 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
- }
1045
+ const candidates = [];
1046
+ for (let p = 3001; p < 3101; p++) candidates.push(p);
1047
+ serverPort = await findAvailablePort(candidates);
1381
1048
 
1382
- const probe = await probePing(port);
1383
- if (probe.ok) {
1384
- return true;
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
- // 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;
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
- console.log('\n🚀 Starting A2A server...');
1529
- console.log(`Port: ${backendPort}`);
1530
- console.log(`Hostname: ${inviteHost}`);
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
- 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
- });
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
- 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
- });
1097
+ // Store server PID for cleanup
1098
+ if (serverResult.pid) {
1099
+ config.setOnboarding({ server_pid: serverResult.pid, server_port: serverPort });
1100
+ }
1607
1101
 
1608
- const inviteUrl = `a2a://${inviteHost}/${token}`;
1609
- const topicLine = publicTopicsForInvite.length > 0 ? publicTopicsForInvite.slice(0, 6).join(' · ') : 'chat';
1610
- const goalLine = goalItems.join(' · ');
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
- console.log('\n📞 Your first invite (public tier):\n');
1613
- console.log('─'.repeat(60));
1614
- const inviteText = `📞🗣️ **Agent-to-Agent Call Invite**
1114
+ // Save server config
1115
+ config.setAgent({ hostname: publicHost });
1615
1116
 
1616
- 👤 **${ownerName}** would like your agent to call **${peerName}**
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
- 💬 ${topicLine}
1619
- 🎯 ${goalLine}
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
- ${inviteUrl}
1138
+ config.setOnboarding({ step: 'awaiting_disclosure' });
1622
1139
 
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
-
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('\n\u274c Invalid JSON in --submit flag.');
1908
- console.error(` Parse error: ${e.message}\n`);
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('\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`);
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
- 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 });
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
- console.log('\n\u2705 Disclosure manifest saved.');
1928
- console.log(` Manifest: ${MANIFEST_FILE}`);
1929
- console.log(' Next: a2a quickstart\n');
1930
- return;
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
- // ── 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
- }
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
- const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
1940
- const contextFiles = readContextFiles(workspaceDir);
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
- 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
- };
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
- 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");
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 Onboarding (access tiers ingress → verify)
2021
- --hostname Public hostname for remote access (e.g. myserver.com:443)
2022
- --public-port Port to assume when --hostname omits a port (default: 443)
2023
- --port A2A server port to run locally (default: 3001)
2024
- --friends-topics Override Friends tier topics/interests (comma or newline-separated)
2025
- --interactive Prompt for Friends tier topics if needed
2026
- --confirm-ingress Confirm reverse proxy/ingress is configured and continue
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,25 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Only show the banner for global installs; skip in CI, dev, and Docker builds.
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
5
  if (process.env.npm_config_global !== 'true') process.exit(0);
6
6
 
7
- console.log(`
8
- ╔══════════════════════════════════════════════════════════╗
9
- ║ 🤝 A2A Calling installed successfully! ║
10
- ╠══════════════════════════════════════════════════════════╣
11
- ║ ║
12
- ║ Next step: ║
13
- ║ a2a quickstart --hostname YOUR_DOMAIN:PORT ║
14
- ║ ║
15
- ║ Example: ║
16
- ║ a2a quickstart --hostname myserver.com:3001 ║
17
- ║ ║
18
- ║ This will: ║
19
- ║ • Configure your agent's disclosure topics ║
20
- ║ • Set up permission tiers (public/friends/family) ║
21
- ║ • Start the A2A server ║
22
- ║ • Generate your first invite to share ║
23
- ║ ║
24
- ╚══════════════════════════════════════════════════════════╝
25
- `);
7
+ console.log('\n a2acalling installed successfully.\n');
8
+ console.log(' To get started, run:\n');
9
+ console.log(' a2a quickstart\n');
@@ -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
- const fileList = Object.entries(availableFiles)
505
- .filter(([, present]) => present)
506
- .map(([name]) => ` - ${name}`)
507
- .join('\n') || ' (no workspace files detected)';
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
- ### Available workspace files
516
- ${fileList}
529
+ ${fileSection}
517
530
 
518
- Read the available files above and extract disclosure topics. Focus on what the OWNER cares about, works on, and wants to discuss — NOT on agent instructions, code documentation, or operational tasks.
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