a2acalling 0.6.8 → 0.6.10

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
@@ -81,7 +81,7 @@ function enforceOnboarding(command) {
81
81
  const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
82
82
  if (cfg.onboarding?.step === 'awaiting_disclosure') {
83
83
  console.log('\nA2A setup in progress. Disclosure topics not yet submitted.\n');
84
- console.log("Next: run `a2a onboard --submit '<json>'`\n");
84
+ console.log("Next: run `a2a quickstart --submit '<json>'` (or `a2a onboard --submit`)\n");
85
85
  process.exit(1);
86
86
  }
87
87
  } catch (e) {
@@ -200,16 +200,275 @@ function parseArgs(argv) {
200
200
  }
201
201
 
202
202
  async function promptYesNo(question) {
203
+ const defaultValue = question.includes('[Y/n]') || question.includes('[y/N]') || question.includes('[Y/N]')
204
+ ? question.includes('[Y/n]') || question.includes('[y/n]')
205
+ ? true
206
+ : false
207
+ : true;
208
+
209
+ if (!isInteractiveShell()) {
210
+ return defaultValue;
211
+ }
212
+
203
213
  return await new Promise(resolve => {
204
214
  const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
205
215
  rl.question(question, (answer) => {
206
216
  rl.close();
207
217
  const normalized = String(answer || '').trim().toLowerCase();
218
+ if (!normalized) return resolve(defaultValue);
208
219
  resolve(normalized === 'y' || normalized === 'yes');
209
220
  });
210
221
  });
211
222
  }
212
223
 
224
+ function isInteractiveShell() {
225
+ return Boolean(process.stdin && process.stdout && process.stdin.isTTY && process.stdout.isTTY);
226
+ }
227
+
228
+ async function promptText(question, defaultValue = '') {
229
+ if (!isInteractiveShell()) {
230
+ return defaultValue;
231
+ }
232
+ return await new Promise(resolve => {
233
+ const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
234
+ rl.question(question, (answer) => {
235
+ rl.close();
236
+ const cleaned = String(answer || '').trim();
237
+ resolve(cleaned || defaultValue);
238
+ });
239
+ });
240
+ }
241
+
242
+ function parsePort(raw, fallback = null) {
243
+ const parsed = Number.parseInt(String(raw || '').trim(), 10);
244
+ if (Number.isFinite(parsed) && parsed > 0 && parsed <= 65535) {
245
+ return parsed;
246
+ }
247
+ return fallback;
248
+ }
249
+
250
+ function printStepHeader(label) {
251
+ const clean = String(label || '').trim();
252
+ const innerWidth = Math.max(62, clean.length + 12);
253
+ const padding = Math.max(0, innerWidth - clean.length);
254
+ const left = Math.floor(padding / 2);
255
+ const right = Math.max(0, padding - left);
256
+ console.log('\n' + '╔' + '═'.repeat(innerWidth) + '╗');
257
+ console.log(`║${' '.repeat(left)}${clean}${' '.repeat(right)}║`);
258
+ console.log('╚' + '═'.repeat(innerWidth) + '╝');
259
+ }
260
+
261
+ function printSection(title) {
262
+ console.log('\n━━━ ' + title + ' ━━━');
263
+ }
264
+
265
+ function readWorkspaceContext(baseDir = process.cwd()) {
266
+ const base = baseDir || process.cwd();
267
+ const workspaceFiles = {
268
+ USER: { filename: 'USER.md' },
269
+ SOUL: { filename: 'SOUL.md' },
270
+ HEARTBEAT: { filename: 'HEARTBEAT.md' },
271
+ SKILL: { filename: 'SKILL.md' },
272
+ CLAUDE: { filename: 'CLAUDE.md' }
273
+ };
274
+
275
+ const found = {};
276
+ for (const key of Object.keys(workspaceFiles)) {
277
+ found[key] = fs.existsSync(path.join(base, workspaceFiles[key].filename));
278
+ }
279
+
280
+ const memoryDir = path.join(base, 'memory');
281
+ let memoryCount = 0;
282
+ if (fs.existsSync(memoryDir)) {
283
+ try {
284
+ memoryCount = fs.readdirSync(memoryDir).filter(item => item.endsWith('.md')).length;
285
+ } catch (err) {
286
+ memoryCount = 0;
287
+ }
288
+ }
289
+ found.MEMORY = memoryCount;
290
+
291
+ return {
292
+ workspace: base,
293
+ found,
294
+ memoryCount
295
+ };
296
+ }
297
+
298
+ function printWorkspaceScan(context) {
299
+ console.log('Scanning workspace for context...');
300
+ console.log(` ${context.found.USER ? '✅' : '⚠️ '} ${context.found.USER ? 'Found USER.md' : 'No USER.md'}${context.found.USER ? ' — identity hints found' : ''}`);
301
+ console.log(` ${context.found.SOUL ? '✅' : '⚠️ '} ${context.found.SOUL ? 'Found SOUL.md' : 'No SOUL.md'}${context.found.SOUL ? ' — personality notes available' : ''}`);
302
+ console.log(` ${context.found.HEARTBEAT ? '✅' : '⚠️ '} ${context.found.HEARTBEAT ? 'Found HEARTBEAT.md' : 'No HEARTBEAT.md (skipped)'}`);
303
+ console.log(` ${context.found.SKILL ? '✅' : '⚠️ '} ${context.found.SKILL ? 'Found SKILL.md' : 'No SKILL.md (skipped)'}`
304
+ );
305
+ console.log(` ${context.found.CLAUDE ? '✅' : '⚠️ '} ${context.found.CLAUDE ? 'Found CLAUDE.md' : 'No CLAUDE.md (skipped)'}`);
306
+ if (context.memoryCount > 0) {
307
+ console.log(` ✅ Found ${context.memoryCount} memory file(s)`);
308
+ } else {
309
+ console.log(' ⚠️ No memory/*.md files');
310
+ }
311
+ }
312
+
313
+ function getDisclosurePromptFiles(context) {
314
+ return {
315
+ 'USER.md': context.found.USER,
316
+ 'SOUL.md': context.found.SOUL,
317
+ 'HEARTBEAT.md': context.found.HEARTBEAT,
318
+ 'SKILL.md': context.found.SKILL,
319
+ 'CLAUDE.md': context.found.CLAUDE,
320
+ 'memory/*.md': context.memoryCount > 0
321
+ };
322
+ }
323
+
324
+ async function inspectPorts(preferredPort = null) {
325
+ const candidates = [];
326
+ if (preferredPort) {
327
+ candidates.push(preferredPort);
328
+ }
329
+ candidates.push(80);
330
+ for (let p = 3001; p < 3021; p += 1) {
331
+ if (!candidates.includes(p)) candidates.push(p);
332
+ }
333
+
334
+ const { tryBindPort } = require('../src/lib/port-scanner');
335
+ const results = [];
336
+ for (const port of candidates) {
337
+ const r = await tryBindPort(port);
338
+ results.push({
339
+ port,
340
+ available: Boolean(r.ok),
341
+ blocked: !r.ok && r.code === 'EACCES',
342
+ code: r.code || null
343
+ });
344
+ }
345
+ return results;
346
+ }
347
+
348
+ function summarizePortResults(portResults) {
349
+ return portResults.map(item => {
350
+ if (item.available) return `Port ${item.port}: available ✓`;
351
+ if (item.blocked) return `Port ${item.port}: requires elevated privileges`;
352
+ return `Port ${item.port}: in use`;
353
+ });
354
+ }
355
+
356
+ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
357
+ const submitRaw = args.flags.submit;
358
+ if (!submitRaw) return false;
359
+
360
+ const { A2AConfig } = require('../src/lib/config');
361
+ const {
362
+ validateDisclosureSubmission,
363
+ saveManifest,
364
+ MANIFEST_FILE
365
+ } = require('../src/lib/disclosure');
366
+
367
+ const config = new A2AConfig();
368
+ const submitCommand = commandLabel === 'quickstart'
369
+ ? 'a2a quickstart --submit'
370
+ : 'a2a onboard --submit';
371
+
372
+ let parsed;
373
+ try {
374
+ parsed = JSON.parse(String(submitRaw));
375
+ } catch (e) {
376
+ console.error('\nInvalid JSON in --submit flag.');
377
+ console.error(` Parse error: ${e.message}\n`);
378
+ process.exit(1);
379
+ }
380
+
381
+ const result = validateDisclosureSubmission(parsed);
382
+ if (!result.valid) {
383
+ console.error('\nDisclosure submission validation failed:\n');
384
+ result.errors.forEach(err => console.error(` - ${err}`));
385
+ console.error(`\nFix the errors above and resubmit with: ${submitCommand} '<json>'\n`);
386
+ process.exit(1);
387
+ }
388
+
389
+ saveManifest(result.manifest);
390
+ console.log('\nStep 3 of 4: Disclosure manifest saved.');
391
+ console.log(` Manifest: ${MANIFEST_FILE}`);
392
+
393
+ // Sync tier config from manifest
394
+ const manifest = result.manifest;
395
+ function flattenTopics(sections) {
396
+ const out = [];
397
+ for (const section of sections) {
398
+ for (const item of section) {
399
+ const t = String(item && item.topic || '').trim();
400
+ if (t && !out.includes(t)) out.push(t);
401
+ }
402
+ }
403
+ return out;
404
+ }
405
+
406
+ try {
407
+ config.setTier('public', {
408
+ topics: flattenTopics([manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect]),
409
+ disclosure: 'public'
410
+ });
411
+ config.setTier('friends', {
412
+ topics: flattenTopics([
413
+ manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect,
414
+ manifest.topics.friends.lead_with, manifest.topics.friends.discuss_freely, manifest.topics.friends.deflect
415
+ ]),
416
+ disclosure: 'minimal'
417
+ });
418
+ config.setTier('family', {
419
+ topics: flattenTopics([
420
+ manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect,
421
+ manifest.topics.friends.lead_with, manifest.topics.friends.discuss_freely, manifest.topics.friends.deflect,
422
+ manifest.topics.family.lead_with, manifest.topics.family.discuss_freely, manifest.topics.family.deflect
423
+ ]),
424
+ disclosure: 'minimal'
425
+ });
426
+ } catch (err) {
427
+ console.error(` Warning: could not sync tier config: ${err.message}`);
428
+ }
429
+
430
+ // If already onboarded, this is a topic update — no invite generation needed
431
+ if (config.isOnboarded()) {
432
+ console.log('\nDisclosure topics updated. Your agent will use these on the next inbound call.\n');
433
+ return true;
434
+ }
435
+
436
+ console.log('\nStep 4 of 4: Generating your first invite...\n');
437
+
438
+ const agentName = args.flags.name || config.getAgent().name || process.env.A2A_AGENT_NAME || 'my-agent';
439
+ const hostname = config.getAgent().hostname || process.env.A2A_HOSTNAME || 'localhost';
440
+ if (args.flags.name) config.setAgent({ name: agentName });
441
+
442
+ const publicTopics = flattenTopics([
443
+ manifest.topics.public.lead_with,
444
+ manifest.topics.public.discuss_freely
445
+ ]);
446
+
447
+ const { token } = store.create({
448
+ name: agentName,
449
+ owner: agentName,
450
+ permissions: 'public',
451
+ disclosure: 'minimal',
452
+ expires: 'never',
453
+ maxCalls: null,
454
+ allowedTopics: publicTopics,
455
+ allowedGoals: ['grow-network', 'find-collaborators', 'build-in-public'],
456
+ notify: 'all'
457
+ });
458
+
459
+ const inviteUrl = `a2a://${hostname}/${token}`;
460
+ console.log(` Invite URL: ${inviteUrl}`);
461
+ console.log(' Share this invite to let other agents call you.\n');
462
+
463
+ config.completeOnboarding();
464
+ console.log('Onboarding complete.\n');
465
+ console.log(` Config: ${CONFIG_PATH}`);
466
+ console.log(` Disclosure: ${MANIFEST_FILE}`);
467
+ console.log(` Invite: ${inviteUrl}\n`);
468
+
469
+ return true;
470
+ }
471
+
213
472
  async function resolveInviteHostname() {
214
473
  const { resolveInviteHost } = require('../src/lib/invite-host');
215
474
 
@@ -948,11 +1207,16 @@ https://github.com/onthegonow/a2a_calling`;
948
1207
 
949
1208
  quickstart: async (args) => {
950
1209
  const { A2AConfig } = require('../src/lib/config');
951
- const { tryBindPort, findAvailablePort, isPortListening } = require('../src/lib/port-scanner');
952
- const { buildExtractionPrompt, MANIFEST_FILE } = require('../src/lib/disclosure');
1210
+ const { isPortListening } = require('../src/lib/port-scanner');
1211
+ const { buildExtractionPrompt } = require('../src/lib/disclosure');
953
1212
  const { getExternalIp } = require('../src/lib/external-ip');
954
1213
 
955
1214
  const config = new A2AConfig();
1215
+ const interactive = isInteractiveShell();
1216
+
1217
+ if (await handleDisclosureSubmit(args, 'quickstart')) {
1218
+ return;
1219
+ }
956
1220
 
957
1221
  if (args.flags.force) {
958
1222
  config.resetOnboarding();
@@ -964,6 +1228,9 @@ https://github.com/onthegonow/a2a_calling`;
964
1228
  return;
965
1229
  }
966
1230
 
1231
+ const context = readWorkspaceContext(process.env.A2A_WORKSPACE || process.cwd());
1232
+ const availableFiles = getDisclosurePromptFiles(context);
1233
+
967
1234
  // If server is already running and awaiting disclosure, skip to Step 2
968
1235
  let currentStep = 'not_started';
969
1236
  try {
@@ -977,100 +1244,150 @@ https://github.com/onthegonow/a2a_calling`;
977
1244
  if (currentStep === 'awaiting_disclosure' && !args.flags.force) {
978
1245
  console.log('\nStep 1 already complete. Server is running.\n');
979
1246
  console.log('Step 2 of 4: Configure disclosure topics\n');
980
- console.log(buildExtractionPrompt());
1247
+ console.log(buildExtractionPrompt(availableFiles));
981
1248
  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");
1249
+ console.log(" Then submit with: a2a quickstart --submit '<json>'\n");
983
1250
  return;
984
1251
  }
985
1252
 
986
- function parsePort(raw, fallback) {
987
- const parsed = Number.parseInt(String(raw || '').trim(), 10);
988
- if (Number.isFinite(parsed) && parsed > 0 && parsed <= 65535) {
989
- return parsed;
990
- }
991
- return fallback;
992
- }
1253
+ printStepHeader('🤝 A2A Calling — First-Time Setup');
1254
+ printWorkspaceScan(context);
993
1255
 
994
- // ── Step 1 of 4: Setting up A2A server ──────────────────
995
- console.log('\nStep 1 of 4: Setting up A2A server\n');
1256
+ const continueSetup = await promptYesNo('Continue with setup? [Y/n] ');
1257
+ if (!continueSetup) {
1258
+ console.log('\nSetup cancelled.\n');
1259
+ return;
1260
+ }
996
1261
 
1262
+ printSection('Port Configuration');
997
1263
  const preferredPort = parsePort(args.flags.port || args.flags.p, null);
1264
+ const candidates = await inspectPorts(preferredPort);
1265
+ const availableCandidates = candidates.filter(c => c.available);
1266
+ const recommendedPort = availableCandidates.length ? availableCandidates[0].port : null;
998
1267
 
999
- // If user specified a port, try that first
1000
- let serverPort;
1001
- let usingAlternatePort = false;
1002
-
1003
- if (preferredPort) {
1004
- console.log(` 1a. Checking preferred port ${preferredPort}...`);
1005
- const preferredResult = await tryBindPort(preferredPort);
1006
- if (preferredResult.ok) {
1007
- console.log(` Port ${preferredPort} available.`);
1008
- serverPort = preferredPort;
1009
- usingAlternatePort = preferredPort !== 80;
1010
- } else if (preferredResult.code === 'EACCES') {
1011
- console.log(` Port ${preferredPort} requires elevated privileges.`);
1012
- console.log(' Rerun with: sudo npm install -g a2acalling\n');
1013
- process.exit(1);
1268
+ summarizePortResults(candidates).forEach(line => {
1269
+ console.log(` ${line}`);
1270
+ });
1271
+
1272
+ if (!recommendedPort) {
1273
+ console.error(' Could not find a bindable port in the scan range.');
1274
+ console.error(' Re-run with --port <number> after freeing one of these ports.\n');
1275
+ process.exit(1);
1276
+ }
1277
+
1278
+ console.log(`\n Recommended: ${recommendedPort}`);
1279
+ let serverPort = recommendedPort;
1280
+ const portChoice = await promptText(`Use port ${recommendedPort}? [Y/n/custom]: `, 'y');
1281
+ if (!interactive) {
1282
+ // explicit default for non-interactive mode
1283
+ serverPort = recommendedPort;
1284
+ } else if (!['', 'y', 'Y', 'yes', 'YES', 'ye'].includes(String(portChoice).trim())) {
1285
+ if (/^(n|no|custom|c)$/i.test(String(portChoice).trim())) {
1286
+ let customPort = null;
1287
+ while (customPort === null) {
1288
+ const raw = await promptText('Enter a custom port number: ', String(recommendedPort));
1289
+ const parsed = parsePort(raw, null);
1290
+ if (!parsed) {
1291
+ console.log(' Invalid port. Enter a value between 1 and 65535.');
1292
+ continue;
1293
+ }
1294
+ const checked = await (async () => {
1295
+ const scan = await inspectPorts(parsed);
1296
+ return scan[0];
1297
+ })();
1298
+ if (!checked.available) {
1299
+ console.log(` Port ${parsed} is unavailable (${checked.code || 'in use'}).`);
1300
+ continue;
1301
+ }
1302
+ customPort = parsed;
1303
+ }
1304
+ serverPort = customPort;
1014
1305
  } 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);
1306
+ const parsed = parsePort(portChoice, null);
1307
+ if (parsed) {
1308
+ const checked = await (async () => {
1309
+ const scan = await inspectPorts(parsed);
1310
+ return scan[0];
1311
+ })();
1312
+ if (!checked.available) {
1313
+ console.log(` Port ${parsed} is unavailable (${checked.code || 'in use'}).`);
1314
+ } else {
1315
+ serverPort = parsed;
1316
+ }
1023
1317
  }
1024
- console.log(` Port ${serverPort} available.`);
1025
- usingAlternatePort = true;
1026
1318
  }
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...');
1319
+ }
1044
1320
 
1045
- const candidates = [];
1046
- for (let p = 3001; p < 3101; p++) candidates.push(p);
1047
- serverPort = await findAvailablePort(candidates);
1321
+ printSection('Hostname Configuration');
1322
+ const ipResult = await getExternalIp();
1323
+ const externalIp = ipResult.ip || null;
1324
+ let publicHost = `localhost:${serverPort}`;
1048
1325
 
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);
1326
+ if (externalIp) {
1327
+ const detectedHost = serverPort === 80 ? externalIp : `${externalIp}:${serverPort}`;
1328
+ console.log(` Detected external IP: ${detectedHost}`);
1329
+ if (interactive) {
1330
+ const hostChoiceRaw = await promptText(
1331
+ 'How should other agents reach you?\n'
1332
+ + ' 1. Use IP directly\n'
1333
+ + ' 2. Enter a domain name\n'
1334
+ + ' 3. Skip (configure later)\n'
1335
+ + 'Choice [1/2/3]: ',
1336
+ '1'
1337
+ );
1338
+ const hostChoice = String(hostChoiceRaw || '').trim();
1339
+ if (hostChoice === '2') {
1340
+ const manualHost = await promptText('Enter your public hostname: ', '');
1341
+ if (manualHost) publicHost = String(manualHost).trim();
1342
+ } else if (hostChoice === '3') {
1343
+ publicHost = process.env.A2A_HOSTNAME || `localhost:${serverPort}`;
1344
+ } else {
1345
+ publicHost = detectedHost;
1053
1346
  }
1054
- console.log(` Port ${serverPort} available.`);
1055
- usingAlternatePort = true;
1347
+ } else {
1348
+ publicHost = detectedHost;
1349
+ }
1350
+ } else if (interactive) {
1351
+ const hostChoiceRaw = await promptText(
1352
+ 'External IP unavailable.\nHow should other agents reach you?\n'
1353
+ + ' 1. Enter a domain name\n'
1354
+ + ' 2. Skip (use localhost)\n'
1355
+ + 'Choice [1/2]: ',
1356
+ '2'
1357
+ );
1358
+ const hostChoice = String(hostChoiceRaw || '').trim();
1359
+ if (hostChoice === '1') {
1360
+ const manualHost = await promptText('Enter your public hostname: ', '');
1361
+ if (manualHost) publicHost = String(manualHost).trim();
1056
1362
  }
1363
+ } else if (ipResult.error) {
1364
+ console.log(` External IP lookup failed: ${ipResult.error}`);
1057
1365
  }
1058
1366
 
1059
- // Start server
1060
- console.log(` Starting A2A server on port ${serverPort}...`);
1367
+ printSection('Starting Server');
1368
+ console.log(' Configuration summary:');
1369
+ console.log(` Port: ${serverPort}`);
1370
+ console.log(` Public host: ${publicHost}`);
1371
+ const startServer = await promptYesNo('Start the A2A server now? [Y/n] ');
1372
+ if (!startServer) {
1373
+ console.log('\nServer not started. Run with:\n a2a server --port <port> --hostname <host>\n');
1374
+ return;
1375
+ }
1061
1376
 
1062
- async function startServer(port) {
1063
- const listening = await isPortListening(port, '127.0.0.1', { timeoutMs: 250 });
1064
- if (listening.listening) return { started: false, existing: true };
1377
+ // Start server
1378
+ const isAlreadyListening = await isPortListening(serverPort, '127.0.0.1', { timeoutMs: 250 });
1379
+ let serverPid = null;
1380
+ if (!isAlreadyListening.listening) {
1065
1381
  const serverScript = path.join(__dirname, '../src/server.js');
1066
1382
  const child = spawn(process.execPath, [serverScript], {
1067
- env: { ...process.env, PORT: String(port) },
1383
+ env: { ...process.env, PORT: String(serverPort) },
1068
1384
  detached: true,
1069
1385
  stdio: 'ignore'
1070
1386
  });
1387
+ serverPid = child.pid;
1071
1388
  child.unref();
1072
- await new Promise(r => setTimeout(r, 300));
1073
- return { started: true, pid: child.pid };
1389
+ } else {
1390
+ console.log(' Existing server detected on this port.');
1074
1391
  }
1075
1392
 
1076
1393
  async function waitForServer(port) {
@@ -1082,66 +1399,45 @@ https://github.com/onthegonow/a2a_calling`;
1082
1399
  return false;
1083
1400
  }
1084
1401
 
1085
- const serverResult = await startServer(serverPort);
1086
- if (serverResult.existing) {
1087
- console.log(' Existing server detected on this port.');
1088
- }
1089
1402
  const serverUp = await waitForServer(serverPort);
1090
1403
  if (!serverUp) {
1091
1404
  console.log(' Server failed to start. Check logs and retry:');
1092
1405
  console.log(` PORT=${serverPort} node ${path.join(__dirname, '../src/server.js')}`);
1093
1406
  process.exit(1);
1094
1407
  }
1095
- console.log(' Server running.\n');
1096
1408
 
1097
- // Store server PID for cleanup
1098
- if (serverResult.pid) {
1099
- config.setOnboarding({ server_pid: serverResult.pid, server_port: serverPort });
1100
- }
1101
-
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`);
1409
+ if (serverPid) {
1410
+ console.log(' Server started.');
1411
+ config.setOnboarding({ server_pid: serverPid, server_port: serverPort });
1412
+ } else {
1413
+ console.log(' Using existing server.');
1108
1414
  }
1109
- const externalIp = ipResult.ip || null;
1110
- const publicHost = externalIp
1111
- ? (serverPort === 80 ? externalIp : `${externalIp}:${serverPort}`)
1112
- : `localhost:${serverPort}`;
1415
+ console.log(' ✅ A2A server is running');
1113
1416
 
1114
- // Save server config
1115
- config.setAgent({ hostname: publicHost });
1116
-
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');
1417
+ if (serverPort !== 80 && externalIp) {
1418
+ console.log('\n External access required because port 80 is not in use.');
1121
1419
  console.log(' Option A (recommended): Set up a reverse proxy (HTTP or HTTPS).');
1122
1420
  console.log(` Configure your web server to forward /api/a2a/* to localhost:${serverPort}.`);
1123
1421
  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');
1422
+ console.log(' A reverse proxy avoids firewall changes entirely.');
1423
+ console.log(`\n Option B: Open port ${serverPort} in your firewall (e.g. ufw allow ${serverPort}).`);
1424
+ console.log(' Most users prefer not to modify firewall settings.\n');
1129
1425
  }
1130
-
1131
1426
  if (externalIp) {
1132
1427
  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');
1428
+ console.log(' External ping check:\n curl -s ' + verifyUrl);
1136
1429
  }
1137
1430
 
1431
+ // Save server config and advance onboarding
1432
+ config.setAgent({ hostname: publicHost });
1138
1433
  config.setOnboarding({ step: 'awaiting_disclosure' });
1139
1434
 
1140
- // ── Step 2 of 4: Configure disclosure topics ────────────
1435
+ // Step 2 of 4: Configure disclosure topics
1436
+ printSection('Disclosure Topic Extraction');
1141
1437
  console.log('Step 2 of 4: Configure disclosure topics\n');
1142
- console.log(buildExtractionPrompt());
1438
+ console.log(buildExtractionPrompt(availableFiles));
1143
1439
  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");
1440
+ console.log(" Then submit with: a2a quickstart --submit '<json>'\n");
1145
1441
  },
1146
1442
 
1147
1443
 
@@ -1391,114 +1687,7 @@ https://github.com/onthegonow/a2a_calling`;
1391
1687
  },
1392
1688
 
1393
1689
  onboard: async (args) => {
1394
- const { A2AConfig } = require('../src/lib/config');
1395
- const {
1396
- validateDisclosureSubmission,
1397
- saveManifest,
1398
- MANIFEST_FILE
1399
- } = require('../src/lib/disclosure');
1400
- const config = new A2AConfig();
1401
-
1402
- // ── Submit mode: agent sends structured JSON ──────────────
1403
- const submitRaw = args.flags.submit;
1404
- if (submitRaw) {
1405
- let parsed;
1406
- try {
1407
- parsed = JSON.parse(String(submitRaw));
1408
- } catch (e) {
1409
- console.error('\nInvalid JSON in --submit flag.');
1410
- console.error(` Parse error: ${e.message}\n`);
1411
- process.exit(1);
1412
- }
1413
-
1414
- const result = validateDisclosureSubmission(parsed);
1415
- if (!result.valid) {
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");
1419
- process.exit(1);
1420
- }
1421
-
1422
- saveManifest(result.manifest);
1423
- console.log('\nStep 3 of 4: Disclosure manifest saved.');
1424
- console.log(` Manifest: ${MANIFEST_FILE}`);
1425
-
1426
- // Sync tier config from manifest
1427
- const manifest = result.manifest;
1428
- function flattenTopics(sections) {
1429
- const out = [];
1430
- for (const section of sections) {
1431
- for (const item of section) {
1432
- const t = String(item && item.topic || '').trim();
1433
- if (t && !out.includes(t)) out.push(t);
1434
- }
1435
- }
1436
- return out;
1437
- }
1438
-
1439
- try {
1440
- config.setTier('public', {
1441
- topics: flattenTopics([manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect]),
1442
- disclosure: 'public'
1443
- });
1444
- config.setTier('friends', {
1445
- topics: flattenTopics([
1446
- manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect,
1447
- manifest.topics.friends.lead_with, manifest.topics.friends.discuss_freely, manifest.topics.friends.deflect
1448
- ]),
1449
- disclosure: 'minimal'
1450
- });
1451
- config.setTier('family', {
1452
- topics: flattenTopics([
1453
- manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect,
1454
- manifest.topics.friends.lead_with, manifest.topics.friends.discuss_freely, manifest.topics.friends.deflect,
1455
- manifest.topics.family.lead_with, manifest.topics.family.discuss_freely, manifest.topics.family.deflect
1456
- ]),
1457
- disclosure: 'minimal'
1458
- });
1459
- } catch (err) {
1460
- console.error(` Warning: could not sync tier config: ${err.message}`);
1461
- }
1462
-
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
- }
1468
-
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
- });
1492
-
1493
- const inviteUrl = `a2a://${hostname}/${token}`;
1494
- console.log(` Invite URL: ${inviteUrl}`);
1495
- console.log(' Share this invite to let other agents call you.\n');
1496
-
1497
- config.completeOnboarding();
1498
- console.log('Onboarding complete.\n');
1499
- console.log(` Config: ${CONFIG_PATH}`);
1500
- console.log(` Disclosure: ${MANIFEST_FILE}`);
1501
- console.log(` Invite: ${inviteUrl}\n`);
1690
+ if (await handleDisclosureSubmit(args, 'onboard')) {
1502
1691
  return;
1503
1692
  }
1504
1693
 
@@ -1571,6 +1760,7 @@ Server:
1571
1760
 
1572
1761
  quickstart Set up A2A server and start onboarding
1573
1762
  --port, -p Preferred server port (default: 80, fallback: 3001+)
1763
+ --submit '<json>' Submit disclosure JSON (Step 3 of onboarding)
1574
1764
  --force Reset onboarding and re-run from scratch
1575
1765
 
1576
1766
  onboard Submit disclosure topics or resume quickstart
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -6,6 +6,16 @@ if (process.env.DOCKER) process.exit(0);
6
6
  if (process.env.npm_config_global !== 'true') process.exit(0);
7
7
 
8
8
  const { spawnSync } = require('child_process');
9
+ const isInteractive = Boolean(process.stdout && process.stderr && process.stdin &&
10
+ process.stdout.isTTY && process.stderr.isTTY && process.stdin.isTTY);
11
+
12
+ function warnSuppressedOutput() {
13
+ if (!isInteractive) {
14
+ console.warn('\n⚠️ Output may be suppressed. Run \'a2a quickstart\' manually if you don\'t see prompts.');
15
+ }
16
+ }
17
+
18
+ warnSuppressedOutput();
9
19
 
10
20
  // Launch quickstart directly — stdio: 'inherit' forces foreground output
11
21
  // even when npm v10+ suppresses postinstall stdout by default.