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 +410 -220
- package/package.json +1 -1
- package/scripts/postinstall.js +10 -0
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
|
|
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 {
|
|
952
|
-
const { buildExtractionPrompt
|
|
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
|
|
1249
|
+
console.log(" Then submit with: a2a quickstart --submit '<json>'\n");
|
|
983
1250
|
return;
|
|
984
1251
|
}
|
|
985
1252
|
|
|
986
|
-
|
|
987
|
-
|
|
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
|
-
|
|
995
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (
|
|
1004
|
-
console.
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1321
|
+
printSection('Hostname Configuration');
|
|
1322
|
+
const ipResult = await getExternalIp();
|
|
1323
|
+
const externalIp = ipResult.ip || null;
|
|
1324
|
+
let publicHost = `localhost:${serverPort}`;
|
|
1048
1325
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
1055
|
-
|
|
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
|
-
|
|
1060
|
-
console.log(
|
|
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
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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(
|
|
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
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1098
|
-
|
|
1099
|
-
config.setOnboarding({ server_pid:
|
|
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
|
-
|
|
1110
|
-
const publicHost = externalIp
|
|
1111
|
-
? (serverPort === 80 ? externalIp : `${externalIp}:${serverPort}`)
|
|
1112
|
-
: `localhost:${serverPort}`;
|
|
1415
|
+
console.log(' ✅ A2A server is running');
|
|
1113
1416
|
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
|
1125
|
-
console.log(
|
|
1126
|
-
console.log('
|
|
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('
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
package/scripts/postinstall.js
CHANGED
|
@@ -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.
|