a2acalling 0.6.9 → 0.6.11

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
@@ -200,16 +200,162 @@ function parseArgs(argv) {
200
200
  }
201
201
 
202
202
  async function promptYesNo(question) {
203
+ const q = String(question || '');
204
+ // Support both bracket and paren styles: [Y/n], (y/N), etc.
205
+ // Convention: uppercase letter is the default when user presses Enter.
206
+ const defaultValue = q.includes('y/N')
207
+ ? false
208
+ : q.includes('Y/n')
209
+ ? true
210
+ : true;
211
+
212
+ if (!isInteractiveShell()) {
213
+ return defaultValue;
214
+ }
215
+
203
216
  return await new Promise(resolve => {
204
217
  const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
205
218
  rl.question(question, (answer) => {
206
219
  rl.close();
207
220
  const normalized = String(answer || '').trim().toLowerCase();
221
+ if (!normalized) return resolve(defaultValue);
208
222
  resolve(normalized === 'y' || normalized === 'yes');
209
223
  });
210
224
  });
211
225
  }
212
226
 
227
+ function isInteractiveShell() {
228
+ return Boolean(process.stdin && process.stdout && process.stdin.isTTY && process.stdout.isTTY);
229
+ }
230
+
231
+ async function promptText(question, defaultValue = '') {
232
+ if (!isInteractiveShell()) {
233
+ return defaultValue;
234
+ }
235
+ return await new Promise(resolve => {
236
+ const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
237
+ rl.question(question, (answer) => {
238
+ rl.close();
239
+ const cleaned = String(answer || '').trim();
240
+ resolve(cleaned || defaultValue);
241
+ });
242
+ });
243
+ }
244
+
245
+ function parsePort(raw, fallback = null) {
246
+ const parsed = Number.parseInt(String(raw || '').trim(), 10);
247
+ if (Number.isFinite(parsed) && parsed > 0 && parsed <= 65535) {
248
+ return parsed;
249
+ }
250
+ return fallback;
251
+ }
252
+
253
+ function printStepHeader(label) {
254
+ const clean = String(label || '').trim();
255
+ const innerWidth = Math.max(62, clean.length + 12);
256
+ const padding = Math.max(0, innerWidth - clean.length);
257
+ const left = Math.floor(padding / 2);
258
+ const right = Math.max(0, padding - left);
259
+ console.log('\n' + '╔' + '═'.repeat(innerWidth) + '╗');
260
+ console.log(`║${' '.repeat(left)}${clean}${' '.repeat(right)}║`);
261
+ console.log('╚' + '═'.repeat(innerWidth) + '╝');
262
+ }
263
+
264
+ function printSection(title) {
265
+ console.log('\n━━━ ' + title + ' ━━━');
266
+ }
267
+
268
+ function readWorkspaceContext(baseDir = process.cwd()) {
269
+ const base = baseDir || process.cwd();
270
+ const workspaceFiles = {
271
+ USER: { filename: 'USER.md' },
272
+ SOUL: { filename: 'SOUL.md' },
273
+ HEARTBEAT: { filename: 'HEARTBEAT.md' },
274
+ SKILL: { filename: 'SKILL.md' },
275
+ CLAUDE: { filename: 'CLAUDE.md' }
276
+ };
277
+
278
+ const found = {};
279
+ for (const key of Object.keys(workspaceFiles)) {
280
+ found[key] = fs.existsSync(path.join(base, workspaceFiles[key].filename));
281
+ }
282
+
283
+ const memoryDir = path.join(base, 'memory');
284
+ let memoryCount = 0;
285
+ if (fs.existsSync(memoryDir)) {
286
+ try {
287
+ memoryCount = fs.readdirSync(memoryDir).filter(item => item.endsWith('.md')).length;
288
+ } catch (err) {
289
+ memoryCount = 0;
290
+ }
291
+ }
292
+ found.MEMORY = memoryCount;
293
+
294
+ return {
295
+ workspace: base,
296
+ found,
297
+ memoryCount
298
+ };
299
+ }
300
+
301
+ function printWorkspaceScan(context) {
302
+ console.log('Scanning workspace for context...');
303
+ console.log(` ${context.found.USER ? '✅' : '⚠️ '} ${context.found.USER ? 'Found USER.md' : 'No USER.md'}${context.found.USER ? ' — identity hints found' : ''}`);
304
+ console.log(` ${context.found.SOUL ? '✅' : '⚠️ '} ${context.found.SOUL ? 'Found SOUL.md' : 'No SOUL.md'}${context.found.SOUL ? ' — personality notes available' : ''}`);
305
+ console.log(` ${context.found.HEARTBEAT ? '✅' : '⚠️ '} ${context.found.HEARTBEAT ? 'Found HEARTBEAT.md' : 'No HEARTBEAT.md (skipped)'}`);
306
+ console.log(` ${context.found.SKILL ? '✅' : '⚠️ '} ${context.found.SKILL ? 'Found SKILL.md' : 'No SKILL.md (skipped)'}`
307
+ );
308
+ console.log(` ${context.found.CLAUDE ? '✅' : '⚠️ '} ${context.found.CLAUDE ? 'Found CLAUDE.md' : 'No CLAUDE.md (skipped)'}`);
309
+ if (context.memoryCount > 0) {
310
+ console.log(` ✅ Found ${context.memoryCount} memory file(s)`);
311
+ } else {
312
+ console.log(' ⚠️ No memory/*.md files');
313
+ }
314
+ }
315
+
316
+ function getDisclosurePromptFiles(context) {
317
+ return {
318
+ 'USER.md': context.found.USER,
319
+ 'SOUL.md': context.found.SOUL,
320
+ 'HEARTBEAT.md': context.found.HEARTBEAT,
321
+ 'SKILL.md': context.found.SKILL,
322
+ 'CLAUDE.md': context.found.CLAUDE,
323
+ 'memory/*.md': context.memoryCount > 0
324
+ };
325
+ }
326
+
327
+ async function inspectPorts(preferredPort = null) {
328
+ const candidates = [];
329
+ if (preferredPort) {
330
+ candidates.push(preferredPort);
331
+ }
332
+ candidates.push(80);
333
+ for (let p = 3001; p < 3021; p += 1) {
334
+ if (!candidates.includes(p)) candidates.push(p);
335
+ }
336
+
337
+ const { tryBindPort } = require('../src/lib/port-scanner');
338
+ const results = [];
339
+ for (const port of candidates) {
340
+ const r = await tryBindPort(port);
341
+ results.push({
342
+ port,
343
+ available: Boolean(r.ok),
344
+ blocked: !r.ok && r.code === 'EACCES',
345
+ code: r.code || null
346
+ });
347
+ }
348
+ return results;
349
+ }
350
+
351
+ function summarizePortResults(portResults) {
352
+ return portResults.map(item => {
353
+ if (item.available) return `Port ${item.port}: available ✓`;
354
+ if (item.blocked) return `Port ${item.port}: requires elevated privileges`;
355
+ return `Port ${item.port}: in use`;
356
+ });
357
+ }
358
+
213
359
  async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
214
360
  const submitRaw = args.flags.submit;
215
361
  if (!submitRaw) return false;
@@ -1064,11 +1210,12 @@ https://github.com/onthegonow/a2a_calling`;
1064
1210
 
1065
1211
  quickstart: async (args) => {
1066
1212
  const { A2AConfig } = require('../src/lib/config');
1067
- const { tryBindPort, findAvailablePort, isPortListening } = require('../src/lib/port-scanner');
1213
+ const { isPortListening } = require('../src/lib/port-scanner');
1068
1214
  const { buildExtractionPrompt } = require('../src/lib/disclosure');
1069
1215
  const { getExternalIp } = require('../src/lib/external-ip');
1070
1216
 
1071
1217
  const config = new A2AConfig();
1218
+ const interactive = isInteractiveShell();
1072
1219
 
1073
1220
  if (await handleDisclosureSubmit(args, 'quickstart')) {
1074
1221
  return;
@@ -1084,6 +1231,9 @@ https://github.com/onthegonow/a2a_calling`;
1084
1231
  return;
1085
1232
  }
1086
1233
 
1234
+ const context = readWorkspaceContext(process.env.A2A_WORKSPACE || process.cwd());
1235
+ const availableFiles = getDisclosurePromptFiles(context);
1236
+
1087
1237
  // If server is already running and awaiting disclosure, skip to Step 2
1088
1238
  let currentStep = 'not_started';
1089
1239
  try {
@@ -1097,100 +1247,160 @@ https://github.com/onthegonow/a2a_calling`;
1097
1247
  if (currentStep === 'awaiting_disclosure' && !args.flags.force) {
1098
1248
  console.log('\nStep 1 already complete. Server is running.\n');
1099
1249
  console.log('Step 2 of 4: Configure disclosure topics\n');
1100
- console.log(buildExtractionPrompt());
1250
+ console.log(buildExtractionPrompt(availableFiles));
1101
1251
  console.log('\n Read your workspace files, extract topics, and present to your owner for review.');
1102
1252
  console.log(" Then submit with: a2a quickstart --submit '<json>'\n");
1103
1253
  return;
1104
1254
  }
1105
1255
 
1106
- function parsePort(raw, fallback) {
1107
- const parsed = Number.parseInt(String(raw || '').trim(), 10);
1108
- if (Number.isFinite(parsed) && parsed > 0 && parsed <= 65535) {
1109
- return parsed;
1110
- }
1111
- return fallback;
1112
- }
1256
+ printStepHeader('🤝 A2A Calling — First-Time Setup');
1257
+ printWorkspaceScan(context);
1113
1258
 
1114
- // ── Step 1 of 4: Setting up A2A server ──────────────────
1115
- console.log('\nStep 1 of 4: Setting up A2A server\n');
1259
+ const continueSetup = await promptYesNo('Continue with setup? [Y/n] ');
1260
+ if (!continueSetup) {
1261
+ console.log('\nSetup cancelled.\n');
1262
+ return;
1263
+ }
1116
1264
 
1265
+ printSection('Port Configuration');
1117
1266
  const preferredPort = parsePort(args.flags.port || args.flags.p, null);
1267
+ const candidates = await inspectPorts(preferredPort);
1268
+ const availableCandidates = candidates.filter(c => c.available);
1269
+ const recommendedPort = availableCandidates.length ? availableCandidates[0].port : null;
1118
1270
 
1119
- // If user specified a port, try that first
1120
- let serverPort;
1121
- let usingAlternatePort = false;
1122
-
1123
- if (preferredPort) {
1124
- console.log(` 1a. Checking preferred port ${preferredPort}...`);
1125
- const preferredResult = await tryBindPort(preferredPort);
1126
- if (preferredResult.ok) {
1127
- console.log(` Port ${preferredPort} available.`);
1128
- serverPort = preferredPort;
1129
- usingAlternatePort = preferredPort !== 80;
1130
- } else if (preferredResult.code === 'EACCES') {
1131
- console.log(` Port ${preferredPort} requires elevated privileges.`);
1132
- console.log(' Rerun with: sudo npm install -g a2acalling\n');
1133
- process.exit(1);
1271
+ summarizePortResults(candidates).forEach(line => {
1272
+ console.log(` ${line}`);
1273
+ });
1274
+
1275
+ if (!recommendedPort) {
1276
+ console.error(' Could not find a bindable port in the scan range.');
1277
+ console.error(' Re-run with --port <number> after freeing one of these ports.\n');
1278
+ process.exit(1);
1279
+ }
1280
+
1281
+ console.log(`\n Recommended: ${recommendedPort}`);
1282
+ let serverPort = recommendedPort;
1283
+ const portChoice = await promptText(`Use port ${recommendedPort}? [Y/n/custom]: `, 'y');
1284
+ if (!interactive) {
1285
+ // explicit default for non-interactive mode
1286
+ serverPort = recommendedPort;
1287
+ } else if (!['', 'y', 'Y', 'yes', 'YES', 'ye'].includes(String(portChoice).trim())) {
1288
+ if (/^(n|no|custom|c)$/i.test(String(portChoice).trim())) {
1289
+ let customPort = null;
1290
+ while (customPort === null) {
1291
+ const raw = await promptText('Enter a custom port number: ', String(recommendedPort));
1292
+ const parsed = parsePort(raw, null);
1293
+ if (!parsed) {
1294
+ console.log(' Invalid port. Enter a value between 1 and 65535.');
1295
+ continue;
1296
+ }
1297
+ const checked = await (async () => {
1298
+ const scan = await inspectPorts(parsed);
1299
+ return scan[0];
1300
+ })();
1301
+ if (!checked.available) {
1302
+ console.log(` Port ${parsed} is unavailable (${checked.code || 'in use'}).`);
1303
+ continue;
1304
+ }
1305
+ customPort = parsed;
1306
+ }
1307
+ serverPort = customPort;
1134
1308
  } else {
1135
- console.log(` Port ${preferredPort} is in use. Scanning for alternatives...`);
1136
- const candidates = [];
1137
- for (let p = 3001; p < 3101; p++) candidates.push(p);
1138
- serverPort = await findAvailablePort(candidates);
1139
- if (!serverPort) {
1140
- console.log(' Could not find a bindable port. Rerun with elevated privileges:');
1141
- console.log(' sudo npm install -g a2acalling');
1142
- process.exit(1);
1309
+ const parsed = parsePort(portChoice, null);
1310
+ if (parsed) {
1311
+ const checked = await (async () => {
1312
+ const scan = await inspectPorts(parsed);
1313
+ return scan[0];
1314
+ })();
1315
+ if (!checked.available) {
1316
+ console.log(` Port ${parsed} is unavailable (${checked.code || 'in use'}).`);
1317
+ } else {
1318
+ serverPort = parsed;
1319
+ }
1143
1320
  }
1144
- console.log(` Port ${serverPort} available.`);
1145
- usingAlternatePort = true;
1146
1321
  }
1147
- } else {
1148
- // Default: check port 80 first, then scan
1149
- console.log(' 1a. Checking port 80...');
1150
- const port80Result = await tryBindPort(80);
1151
-
1152
- if (port80Result.ok) {
1153
- console.log(' Port 80 available.');
1154
- serverPort = 80;
1155
- } else if (port80Result.code === 'EACCES') {
1156
- console.log(' Port 80 is available but requires elevated privileges.');
1157
- console.log(' A2A needs to bind to a port to function. Rerun with:');
1158
- console.log(' sudo npm install -g a2acalling\n');
1159
- console.log(' Onboarding cannot continue without a bound port.');
1160
- process.exit(1);
1161
- } else {
1162
- console.log(' Port 80 is in use by another process.');
1163
- console.log(' 1b. Scanning for available port...');
1322
+ }
1164
1323
 
1165
- const candidates = [];
1166
- for (let p = 3001; p < 3101; p++) candidates.push(p);
1167
- serverPort = await findAvailablePort(candidates);
1324
+ printSection('Hostname Configuration');
1325
+ const ipResult = await getExternalIp();
1326
+ const externalIp = ipResult.ip || null;
1327
+ let publicHost = `localhost:${serverPort}`;
1168
1328
 
1169
- if (!serverPort) {
1170
- console.log(' Could not find a bindable port. Rerun with elevated privileges:');
1171
- console.log(' sudo npm install -g a2acalling');
1172
- process.exit(1);
1329
+ if (externalIp) {
1330
+ const detectedHost = serverPort === 80 ? externalIp : `${externalIp}:${serverPort}`;
1331
+ console.log(` Detected external IP: ${detectedHost}`);
1332
+ if (interactive) {
1333
+ const hostChoiceRaw = await promptText(
1334
+ 'How should other agents reach you?\n'
1335
+ + ' 1. Use IP directly\n'
1336
+ + ' 2. Enter a domain name\n'
1337
+ + ' 3. Skip (configure later)\n'
1338
+ + 'Choice [1/2/3]: ',
1339
+ '1'
1340
+ );
1341
+ const hostChoice = String(hostChoiceRaw || '').trim();
1342
+ if (hostChoice === '2') {
1343
+ const manualHost = await promptText('Enter your public hostname: ', '');
1344
+ if (manualHost) publicHost = String(manualHost).trim();
1345
+ } else if (hostChoice === '3') {
1346
+ publicHost = process.env.A2A_HOSTNAME || `localhost:${serverPort}`;
1347
+ } else {
1348
+ publicHost = detectedHost;
1173
1349
  }
1174
- console.log(` Port ${serverPort} available.`);
1175
- usingAlternatePort = true;
1350
+ } else {
1351
+ publicHost = detectedHost;
1176
1352
  }
1353
+ } else if (interactive) {
1354
+ const hostChoiceRaw = await promptText(
1355
+ 'External IP unavailable.\nHow should other agents reach you?\n'
1356
+ + ' 1. Enter a domain name\n'
1357
+ + ' 2. Skip (use localhost)\n'
1358
+ + 'Choice [1/2]: ',
1359
+ '2'
1360
+ );
1361
+ const hostChoice = String(hostChoiceRaw || '').trim();
1362
+ if (hostChoice === '1') {
1363
+ const manualHost = await promptText('Enter your public hostname: ', '');
1364
+ if (manualHost) publicHost = String(manualHost).trim();
1365
+ }
1366
+ } else if (ipResult.error) {
1367
+ console.log(` External IP lookup failed: ${ipResult.error}`);
1177
1368
  }
1178
1369
 
1179
- // Start server
1180
- console.log(` Starting A2A server on port ${serverPort}...`);
1370
+ printSection('Starting Server');
1371
+ console.log(' Configuration summary:');
1372
+ console.log(` Port: ${serverPort}`);
1373
+ console.log(` Public host: ${publicHost}`);
1374
+
1375
+ if (!interactive) {
1376
+ console.log('\n Non-interactive mode detected (no TTY).');
1377
+ console.log(' Not starting the server automatically.\n');
1378
+ console.log(' Next steps:');
1379
+ console.log(' 1. Re-run in a terminal: a2a quickstart');
1380
+ console.log(` 2. Or start manually: a2a server --port ${serverPort}\n`);
1381
+ return;
1382
+ }
1181
1383
 
1182
- async function startServer(port) {
1183
- const listening = await isPortListening(port, '127.0.0.1', { timeoutMs: 250 });
1184
- if (listening.listening) return { started: false, existing: true };
1384
+ const startServer = await promptYesNo('Start the A2A server now? [Y/n] ');
1385
+ if (!startServer) {
1386
+ console.log('\nServer not started. Run with:\n a2a server --port <port> --hostname <host>\n');
1387
+ return;
1388
+ }
1389
+
1390
+ // Start server
1391
+ const isAlreadyListening = await isPortListening(serverPort, '127.0.0.1', { timeoutMs: 250 });
1392
+ let serverPid = null;
1393
+ if (!isAlreadyListening.listening) {
1185
1394
  const serverScript = path.join(__dirname, '../src/server.js');
1186
1395
  const child = spawn(process.execPath, [serverScript], {
1187
- env: { ...process.env, PORT: String(port) },
1396
+ env: { ...process.env, PORT: String(serverPort) },
1188
1397
  detached: true,
1189
1398
  stdio: 'ignore'
1190
1399
  });
1400
+ serverPid = child.pid;
1191
1401
  child.unref();
1192
- await new Promise(r => setTimeout(r, 300));
1193
- return { started: true, pid: child.pid };
1402
+ } else {
1403
+ console.log(' Existing server detected on this port.');
1194
1404
  }
1195
1405
 
1196
1406
  async function waitForServer(port) {
@@ -1202,64 +1412,43 @@ https://github.com/onthegonow/a2a_calling`;
1202
1412
  return false;
1203
1413
  }
1204
1414
 
1205
- const serverResult = await startServer(serverPort);
1206
- if (serverResult.existing) {
1207
- console.log(' Existing server detected on this port.');
1208
- }
1209
1415
  const serverUp = await waitForServer(serverPort);
1210
1416
  if (!serverUp) {
1211
1417
  console.log(' Server failed to start. Check logs and retry:');
1212
1418
  console.log(` PORT=${serverPort} node ${path.join(__dirname, '../src/server.js')}`);
1213
1419
  process.exit(1);
1214
1420
  }
1215
- console.log(' Server running.\n');
1216
1421
 
1217
- // Store server PID for cleanup
1218
- if (serverResult.pid) {
1219
- config.setOnboarding({ server_pid: serverResult.pid, server_port: serverPort });
1422
+ if (serverPid) {
1423
+ console.log(' Server started.');
1424
+ config.setOnboarding({ server_pid: serverPid, server_port: serverPort });
1425
+ } else {
1426
+ console.log(' Using existing server.');
1220
1427
  }
1428
+ console.log(' ✅ A2A server is running');
1221
1429
 
1222
- // Detect external IP
1223
- const ipResult = await getExternalIp();
1224
- if (!ipResult.ip) {
1225
- console.log(' Warning: Could not detect external IP address.');
1226
- console.log(' Set your hostname via environment variable and re-run:');
1227
- console.log(` A2A_HOSTNAME=YOUR_IP${serverPort !== 80 ? ':' + serverPort : ''} a2a quickstart --force\n`);
1228
- }
1229
- const externalIp = ipResult.ip || null;
1230
- const publicHost = externalIp
1231
- ? (serverPort === 80 ? externalIp : `${externalIp}:${serverPort}`)
1232
- : `localhost:${serverPort}`;
1233
-
1234
- // Save server config
1235
- config.setAgent({ hostname: publicHost });
1236
-
1237
- if (usingAlternatePort) {
1238
- console.log(' External access required.');
1239
- console.log(' Something is already bound to port 80 on this machine.');
1240
- console.log(' Two options to make your A2A server reachable:\n');
1430
+ if (serverPort !== 80 && externalIp) {
1431
+ console.log('\n External access required because port 80 is not in use.');
1241
1432
  console.log(' Option A (recommended): Set up a reverse proxy (HTTP or HTTPS).');
1242
1433
  console.log(` Configure your web server to forward /api/a2a/* to localhost:${serverPort}.`);
1243
1434
  console.log(' If you serve HTTPS on port 443, proxy from there instead.');
1244
- console.log(' A reverse proxy avoids firewall changes entirely.\n');
1245
- console.log(` Option B: Open port ${serverPort} in your firewall.`);
1246
- console.log(' This requires the owner to manually allow inbound traffic on');
1247
- console.log(` port ${serverPort} (e.g. ufw allow ${serverPort}, or cloud provider security group).`);
1248
- console.log(' Most users prefer not to modify firewall settings.\n');
1435
+ console.log(' A reverse proxy avoids firewall changes entirely.');
1436
+ console.log(`\n Option B: Open port ${serverPort} in your firewall (e.g. ufw allow ${serverPort}).`);
1437
+ console.log(' Most users prefer not to modify firewall settings.\n');
1249
1438
  }
1250
-
1251
1439
  if (externalIp) {
1252
1440
  const verifyUrl = `http://${publicHost}/api/a2a/ping`;
1253
- console.log(' Verify externally:');
1254
- console.log(` curl -s ${verifyUrl}`);
1255
- console.log(' Or ask your owner to check: https://canyouseeme.org/\n');
1441
+ console.log(' External ping check:\n curl -s ' + verifyUrl);
1256
1442
  }
1257
1443
 
1444
+ // Save server config and advance onboarding
1445
+ config.setAgent({ hostname: publicHost });
1258
1446
  config.setOnboarding({ step: 'awaiting_disclosure' });
1259
1447
 
1260
- // ── Step 2 of 4: Configure disclosure topics ────────────
1448
+ // Step 2 of 4: Configure disclosure topics
1449
+ printSection('Disclosure Topic Extraction');
1261
1450
  console.log('Step 2 of 4: Configure disclosure topics\n');
1262
- console.log(buildExtractionPrompt());
1451
+ console.log(buildExtractionPrompt(availableFiles));
1263
1452
  console.log('\n Read your workspace files, extract topics, and present to your owner for review.');
1264
1453
  console.log(" Then submit with: a2a quickstart --submit '<json>'\n");
1265
1454
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -5,22 +5,73 @@ if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) process.exit(0);
5
5
  if (process.env.DOCKER) process.exit(0);
6
6
  if (process.env.npm_config_global !== 'true') process.exit(0);
7
7
 
8
+ const fs = require('fs');
9
+ const path = require('path');
8
10
  const { spawnSync } = require('child_process');
9
11
 
10
- // Launch quickstart directly — stdio: 'inherit' forces foreground output
11
- // even when npm v10+ suppresses postinstall stdout by default.
12
- const result = spawnSync('a2a', ['quickstart'], {
13
- stdio: 'inherit',
14
- shell: true,
15
- cwd: process.env.HOME || process.cwd()
12
+ function openDevTty() {
13
+ if (process.env.A2A_POSTINSTALL_DISABLE_TTY === '1') return null;
14
+ if (process.platform === 'win32') return null;
15
+
16
+ try {
17
+ // npm may pipe lifecycle stdio even when the user ran npm in a terminal.
18
+ // /dev/tty lets us talk to the actual interactive terminal when present.
19
+ const fdIn = fs.openSync('/dev/tty', 'r');
20
+ const fdOut = fs.openSync('/dev/tty', 'w');
21
+ return { fdIn, fdOut };
22
+ } catch (err) {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ const initCwd = process.env.INIT_CWD || process.env.HOME || process.cwd();
28
+ const tty = openDevTty();
29
+
30
+ if (!tty) {
31
+ // Do NOT auto-start a background server in non-interactive installs.
32
+ console.warn('\n⚠️ A2A Calling installed.');
33
+ console.warn(' Setup requires an interactive terminal.');
34
+ console.warn(' Next: a2a quickstart\n');
35
+ process.exit(0);
36
+ }
37
+
38
+ function writeTty(message) {
39
+ try {
40
+ fs.writeSync(tty.fdOut, String(message));
41
+ } catch (err) {
42
+ // ignore
43
+ }
44
+ }
45
+
46
+ const cliPath = path.join(__dirname, '..', 'bin', 'cli.js');
47
+
48
+ // Launch quickstart attached to /dev/tty so prompts and output are visible
49
+ // even when npm suppresses postinstall output.
50
+ const result = spawnSync(process.execPath, [cliPath, 'quickstart'], {
51
+ stdio: [tty.fdIn, tty.fdOut, tty.fdOut],
52
+ cwd: initCwd,
53
+ env: {
54
+ ...process.env,
55
+ A2A_WORKSPACE: process.env.A2A_WORKSPACE || initCwd
56
+ }
16
57
  });
17
58
 
18
- if (result.error || result.status === 127) {
19
- // spawn error or shell couldn't find the a2a binary
20
- const reason = result.error ? result.error.message : 'a2a not found in PATH';
21
- console.error('Could not auto-launch onboarding:', reason);
22
- console.log('\nRun manually: a2a quickstart');
59
+ if (result.error) {
60
+ writeTty('\nCould not auto-launch onboarding.\n');
61
+ writeTty(`Reason: ${result.error.message}\n`);
62
+ writeTty('\nRun manually: a2a quickstart\n');
63
+ try {
64
+ fs.closeSync(tty.fdIn);
65
+ fs.closeSync(tty.fdOut);
66
+ } catch (err) {}
23
67
  process.exit(0); // don't fail the install
24
68
  }
25
69
 
70
+ try {
71
+ fs.closeSync(tty.fdIn);
72
+ fs.closeSync(tty.fdOut);
73
+ } catch (err) {
74
+ // Best-effort cleanup.
75
+ }
76
+
26
77
  process.exit(result.status || 0);