a2acalling 0.6.34 → 0.6.36

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
@@ -1123,6 +1123,22 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1123
1123
  const minTurns = parseInt(args.flags['min-turns']) || 8;
1124
1124
  const maxTurns = parseInt(args.flags['max-turns']) || 25;
1125
1125
 
1126
+ // Build owner context from config for summarizer
1127
+ let ownerContext = {};
1128
+ try {
1129
+ const { A2AConfig } = require('../src/lib/config');
1130
+ const config = new A2AConfig();
1131
+ const configAll = config.getAll();
1132
+ const tierGoals = configAll.tiers?.public?.goals || [];
1133
+ ownerContext = {
1134
+ goals: tierGoals,
1135
+ agentName: agentContext.name,
1136
+ ownerName: agentContext.owner
1137
+ };
1138
+ } catch (err) {
1139
+ // Best effort
1140
+ }
1141
+
1126
1142
  const driver = new ConversationDriver({
1127
1143
  runtime,
1128
1144
  agentContext,
@@ -1132,6 +1148,7 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1132
1148
  disclosure,
1133
1149
  minTurns,
1134
1150
  maxTurns,
1151
+ ownerContext,
1135
1152
  onTurn: (info) => {
1136
1153
  const preview = info.messagePreview.length >= 80
1137
1154
  ? info.messagePreview + '...'
@@ -1158,6 +1175,9 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1158
1175
  console.log(` Collaborations: ${result.collabState.candidateCollaborations.join(', ')}`);
1159
1176
  }
1160
1177
  console.log(` Conversation ID: ${result.conversationId}`);
1178
+ if (result.summary) {
1179
+ console.log(`\nšŸ“‹ Summary:\n${result.summary}`);
1180
+ }
1161
1181
  } catch (err) {
1162
1182
  if (contactName) {
1163
1183
  store.updateContactStatus(contactName, 'offline', err.message);
@@ -1755,6 +1775,8 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1755
1775
  const configFile = path.join(configDir, 'a2a-config.json');
1756
1776
  const disclosureFile = path.join(configDir, 'a2a-disclosure.json');
1757
1777
  const tokensFile = path.join(configDir, 'a2a-tokens.json');
1778
+ const tokenStoreFile = path.join(configDir, 'a2a.json');
1779
+ const externalIpFile = path.join(configDir, 'a2a-external-ip.json');
1758
1780
  const dbFile = path.join(configDir, 'a2a-conversations.db');
1759
1781
  const logsDbFile = path.join(configDir, 'a2a-logs.db');
1760
1782
  const callbookDbFile = path.join(configDir, 'a2a-callbook.db');
@@ -1768,7 +1790,7 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1768
1790
  process.exit(1);
1769
1791
  }
1770
1792
 
1771
- const existing = [configFile, disclosureFile, tokensFile, dbFile, logsDbFile, callbookDbFile].filter(f => fs.existsSync(f));
1793
+ const existing = [configFile, disclosureFile, tokensFile, tokenStoreFile, externalIpFile, dbFile, logsDbFile, callbookDbFile].filter(f => fs.existsSync(f));
1772
1794
  const list = existing.length ? existing.map(f => ` - ${f}`).join('\n') : ' (no local config/database files found)';
1773
1795
  const ok = await promptYesNo(
1774
1796
  `This will stop the pm2 process "a2a" and delete:\n${list}\nProceed? (y/N) `
@@ -1819,9 +1841,55 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1819
1841
  }
1820
1842
  }
1821
1843
 
1844
+ // Kill server by PID from config (detached process started by quickstart)
1845
+ function killServerPid() {
1846
+ try {
1847
+ const { A2AConfig } = require('../src/lib/config');
1848
+ const config = new A2AConfig();
1849
+ const onboarding = config.getOnboarding();
1850
+ const pid = onboarding.server_pid;
1851
+ if (!pid) return { ok: true, skipped: true };
1852
+
1853
+ // Check if process is alive
1854
+ try {
1855
+ process.kill(pid, 0); // signal 0 = existence check
1856
+ } catch (e) {
1857
+ // Process doesn't exist — already dead
1858
+ return { ok: true, skipped: true };
1859
+ }
1860
+
1861
+ // Kill it
1862
+ process.kill(pid, 'SIGTERM');
1863
+
1864
+ // Wait briefly and verify it's gone
1865
+ const start = Date.now();
1866
+ while (Date.now() - start < 3000) {
1867
+ try {
1868
+ process.kill(pid, 0);
1869
+ spawnSync('sleep', ['0.1'], { timeout: 500 });
1870
+ } catch (e) {
1871
+ // Process is gone
1872
+ return { ok: true, pid };
1873
+ }
1874
+ }
1875
+
1876
+ // Still alive after 3s — force kill
1877
+ try {
1878
+ process.kill(pid, 'SIGKILL');
1879
+ return { ok: true, pid, forced: true };
1880
+ } catch (e) {
1881
+ return { ok: true, pid };
1882
+ }
1883
+ } catch (err) {
1884
+ // Config read failed — not fatal, continue with pm2 path
1885
+ return { ok: true, skipped: true };
1886
+ }
1887
+ }
1888
+
1822
1889
  process.stdout.write('Stopping server... ');
1890
+ const pidResult = killServerPid();
1823
1891
  const stopped = pm2StopAndDelete('a2a');
1824
- if (!stopped.ok) {
1892
+ if (!pidResult.ok && !stopped.ok) {
1825
1893
  console.log('āŒ');
1826
1894
  console.error(` ${stopped.error}`);
1827
1895
  process.exit(1);
@@ -1836,12 +1904,16 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1836
1904
  const c1 = rmFileSafe(configFile);
1837
1905
  const c2 = rmFileSafe(disclosureFile);
1838
1906
  const c3 = rmFileSafe(tokensFile);
1839
- configOk = Boolean(c1.ok && c2.ok && c3.ok);
1907
+ const c4 = rmFileSafe(tokenStoreFile);
1908
+ const c5 = rmFileSafe(externalIpFile);
1909
+ configOk = Boolean(c1.ok && c2.ok && c3.ok && c4.ok && c5.ok);
1840
1910
  console.log(configOk ? 'āœ…' : 'āŒ');
1841
1911
  if (!configOk) {
1842
1912
  if (!c1.ok) console.error(` ${configFile}: ${c1.error}`);
1843
1913
  if (!c2.ok) console.error(` ${disclosureFile}: ${c2.error}`);
1844
1914
  if (!c3.ok) console.error(` ${tokensFile}: ${c3.error}`);
1915
+ if (!c4.ok) console.error(` ${tokenStoreFile}: ${c4.error}`);
1916
+ if (!c5.ok) console.error(` ${externalIpFile}: ${c5.error}`);
1845
1917
  }
1846
1918
 
1847
1919
  process.stdout.write('Removing database... ');
@@ -0,0 +1,611 @@
1
+ # Port Fallback Warning & Reverse Proxy Prompt — Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** When quickstart falls back to a non-standard port (not 80), warn the user and offer interactive choices: kill the blocking process, set up a reverse proxy, or continue with a clear warning.
6
+
7
+ **Architecture:** Add a `promptPortFallbackStrategy()` function to `bin/cli.js` that runs between port detection and port acceptance. It identifies the process holding port 80 (via `lsof`/`ss`), presents a 3-option menu, and returns a strategy object that the rest of quickstart uses to determine behavior. The existing post-server-start reverse proxy block becomes conditional based on the chosen strategy.
8
+
9
+ **Tech Stack:** Node.js built-in `child_process.execSync`, existing `readline`-based prompt helpers (`promptText`, `promptYesNo`), existing `port-scanner.js` utilities.
10
+
11
+ ---
12
+
13
+ ### Task 1: Add `identifyPort80Process()` helper function
14
+
15
+ **Files:**
16
+ - Modify: `bin/cli.js:267-297` (after `summarizePortResults`, before `handleDisclosureSubmit`)
17
+
18
+ **Step 1: Write the function**
19
+
20
+ Add after the `summarizePortResults` function (line 297) and before `handleDisclosureSubmit` (line 299):
21
+
22
+ ```javascript
23
+ /**
24
+ * Identify what process is using port 80.
25
+ * Returns { pid, name, command } or null if detection fails.
26
+ */
27
+ function identifyPortProcess(port) {
28
+ const { execSync } = require('child_process');
29
+ // Try lsof first (most common on Linux/macOS)
30
+ try {
31
+ const out = execSync(`lsof -i :${port} -sTCP:LISTEN -t 2>/dev/null`, { encoding: 'utf8', timeout: 5000 }).trim();
32
+ if (out) {
33
+ const pid = out.split('\n')[0].trim();
34
+ let name = 'unknown';
35
+ try {
36
+ name = execSync(`ps -p ${pid} -o comm= 2>/dev/null`, { encoding: 'utf8', timeout: 3000 }).trim();
37
+ } catch (e) { /* best-effort */ }
38
+ return { pid: Number(pid), name };
39
+ }
40
+ } catch (e) { /* lsof not available or failed */ }
41
+
42
+ // Fallback: ss (Linux)
43
+ try {
44
+ const out = execSync(`ss -tlnp 'sport = :${port}' 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
45
+ const pidMatch = out.match(/pid=(\d+)/);
46
+ const nameMatch = out.match(/\("([^"]+)"/);
47
+ if (pidMatch) {
48
+ return { pid: Number(pidMatch[1]), name: nameMatch ? nameMatch[1] : 'unknown' };
49
+ }
50
+ } catch (e) { /* ss not available */ }
51
+
52
+ return null;
53
+ }
54
+ ```
55
+
56
+ **Step 2: Verify no syntax errors**
57
+
58
+ Run: `node -e "require('./bin/cli.js')" 2>&1 | head -5`
59
+ Expected: No syntax errors (may print usage or "command not found" — that's fine).
60
+
61
+ **Step 3: Commit**
62
+
63
+ ```bash
64
+ git add bin/cli.js
65
+ git commit -m "feat: add identifyPortProcess helper for port 80 detection"
66
+ ```
67
+
68
+ ---
69
+
70
+ ### Task 2: Add `promptPortFallbackStrategy()` interactive menu
71
+
72
+ **Files:**
73
+ - Modify: `bin/cli.js` (add after `identifyPortProcess`, before `handleDisclosureSubmit`)
74
+
75
+ **Step 1: Write the function**
76
+
77
+ ```javascript
78
+ /**
79
+ * When port 80 is unavailable, prompt the user with fallback options.
80
+ * Returns { strategy: 'kill' | 'proxy' | 'continue', port: number }
81
+ *
82
+ * Non-interactive: auto-returns 'continue' with a printed warning.
83
+ */
84
+ async function promptPortFallbackStrategy(fallbackPort, interactive) {
85
+ const processInfo = identifyPortProcess(80);
86
+
87
+ console.log('\n ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”');
88
+ console.log(' │ ⚠ PORT 80 IS UNAVAILABLE │');
89
+ console.log(' ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜');
90
+ console.log('');
91
+ if (processInfo) {
92
+ console.log(` Port 80 is held by: ${processInfo.name} (PID ${processInfo.pid})`);
93
+ } else {
94
+ console.log(' Port 80 is in use by another process (could not identify).');
95
+ }
96
+ console.log('');
97
+ console.log(' Why this matters:');
98
+ console.log(' - Port 80 is the default HTTP port — no firewall config needed');
99
+ console.log(` - Fallback port ${fallbackPort} may be blocked by the caller's firewall`);
100
+ console.log(` - If the server restarts on a different port, all invite URLs break`);
101
+ console.log(` - Invite URLs with non-standard ports look like: a2a://host:${fallbackPort}/token`);
102
+ console.log('');
103
+
104
+ if (!interactive) {
105
+ console.log(` Non-interactive mode: continuing on port ${fallbackPort}.`);
106
+ console.log(' Set up a reverse proxy (port 80 → ' + fallbackPort + ') for production use.\n');
107
+ return { strategy: 'continue', port: fallbackPort };
108
+ }
109
+
110
+ console.log(' Options:');
111
+ if (processInfo) {
112
+ console.log(` 1) Kill ${processInfo.name} (PID ${processInfo.pid}) and use port 80`);
113
+ } else {
114
+ console.log(' 1) Kill the process on port 80 and retry');
115
+ }
116
+ console.log(` 2) Set up a reverse proxy (port 80 → ${fallbackPort})`);
117
+ console.log(` 3) Continue on port ${fallbackPort} (not recommended for production)`);
118
+ console.log('');
119
+
120
+ const choice = await promptText(' Choose [1/2/3]: ', '2');
121
+ const normalized = String(choice).trim();
122
+
123
+ if (normalized === '1') {
124
+ return { strategy: 'kill', port: 80, processInfo };
125
+ } else if (normalized === '3') {
126
+ console.log(`\n ⚠ Continuing on port ${fallbackPort}.`);
127
+ console.log(` Invite URLs will include :${fallbackPort} and may not be reachable externally.`);
128
+ console.log(' You can set up a reverse proxy later with: a2a config --help\n');
129
+ return { strategy: 'continue', port: fallbackPort };
130
+ } else {
131
+ // Default: reverse proxy (option 2)
132
+ return { strategy: 'proxy', port: fallbackPort };
133
+ }
134
+ }
135
+ ```
136
+
137
+ **Step 2: Verify no syntax errors**
138
+
139
+ Run: `node -e "require('./bin/cli.js')" 2>&1 | head -5`
140
+
141
+ **Step 3: Commit**
142
+
143
+ ```bash
144
+ git add bin/cli.js
145
+ git commit -m "feat: add promptPortFallbackStrategy with 3-option menu"
146
+ ```
147
+
148
+ ---
149
+
150
+ ### Task 3: Add `killPortProcess()` helper and `generateProxyConfig()` helper
151
+
152
+ **Files:**
153
+ - Modify: `bin/cli.js` (add after `promptPortFallbackStrategy`)
154
+
155
+ **Step 1: Write killPortProcess**
156
+
157
+ ```javascript
158
+ /**
159
+ * Attempt to kill the process on a given port.
160
+ * Returns true if kill succeeded and port is now available.
161
+ */
162
+ async function killPortProcess(processInfo) {
163
+ if (!processInfo || !processInfo.pid) return false;
164
+ const { execSync } = require('child_process');
165
+ try {
166
+ console.log(` Killing ${processInfo.name} (PID ${processInfo.pid})...`);
167
+ execSync(`kill ${processInfo.pid}`, { timeout: 5000 });
168
+ // Wait briefly for the port to free up
169
+ await new Promise(r => setTimeout(r, 1000));
170
+ const { tryBindPort } = require('../src/lib/port-scanner');
171
+ const result = await tryBindPort(80);
172
+ if (result.ok) {
173
+ console.log(' āœ… Port 80 is now available.');
174
+ return true;
175
+ }
176
+ console.log(' Port 80 is still in use after kill. The process may require sudo to stop.');
177
+ return false;
178
+ } catch (e) {
179
+ console.log(` Could not kill process: ${e.message}`);
180
+ console.log(' You may need to run: sudo kill ' + processInfo.pid);
181
+ return false;
182
+ }
183
+ }
184
+ ```
185
+
186
+ **Step 2: Write generateProxyConfig**
187
+
188
+ ```javascript
189
+ /**
190
+ * Detect installed web servers and generate reverse proxy config.
191
+ * Returns { hasNginx, hasCaddy, nginxConfig, caddyConfig }.
192
+ */
193
+ function generateProxyConfig(backendPort) {
194
+ const { spawnSync } = require('child_process');
195
+ const hasNginx = spawnSync('which', ['nginx'], { encoding: 'utf8' }).status === 0;
196
+ const hasCaddy = spawnSync('which', ['caddy'], { encoding: 'utf8' }).status === 0;
197
+
198
+ const nginxConfig = [
199
+ '# ══════════════════════════════════════════════════════════════',
200
+ '# A2A (Agent-to-Agent) Protocol Proxy',
201
+ '# ══════════════════════════════════════════════════════════════',
202
+ '# Routes federation requests from port 80 to the local',
203
+ `# A2A server on port ${backendPort}.`,
204
+ '#',
205
+ '# Protocol: https://github.com/onthegonow/a2a_calling',
206
+ '# All requests to /api/a2a/* are agent-to-agent API calls.',
207
+ '# ══════════════════════════════════════════════════════════════',
208
+ 'location /api/a2a/ {',
209
+ ` proxy_pass http://127.0.0.1:${backendPort}/api/a2a/;`,
210
+ ' proxy_http_version 1.1;',
211
+ ' proxy_set_header Host $host;',
212
+ ' proxy_set_header X-Real-IP $remote_addr;',
213
+ ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;',
214
+ ' proxy_set_header X-Forwarded-Proto $scheme;',
215
+ '}'
216
+ ].join('\n');
217
+
218
+ const caddyConfig = [
219
+ '# A2A (Agent-to-Agent) Protocol Proxy',
220
+ `# Routes federation requests to local A2A server on port ${backendPort}`,
221
+ '# Protocol: https://github.com/onthegonow/a2a_calling',
222
+ 'handle /api/a2a/* {',
223
+ ` reverse_proxy 127.0.0.1:${backendPort}`,
224
+ '}'
225
+ ].join('\n');
226
+
227
+ return { hasNginx, hasCaddy, nginxConfig, caddyConfig };
228
+ }
229
+ ```
230
+
231
+ **Step 3: Verify no syntax errors**
232
+
233
+ Run: `node -e "require('./bin/cli.js')" 2>&1 | head -5`
234
+
235
+ **Step 4: Commit**
236
+
237
+ ```bash
238
+ git add bin/cli.js
239
+ git commit -m "feat: add killPortProcess and generateProxyConfig helpers"
240
+ ```
241
+
242
+ ---
243
+
244
+ ### Task 4: Wire the port fallback prompt into the quickstart flow
245
+
246
+ This is the main integration task. Modify the quickstart port selection section to call `promptPortFallbackStrategy` when port 80 is unavailable.
247
+
248
+ **Files:**
249
+ - Modify: `bin/cli.js:1462-1541` (Step 1: Port selection in quickstart)
250
+
251
+ **Step 1: Replace the port fallback branch**
252
+
253
+ Replace lines 1478-1541 (the `else if (availableCandidates.length)` branch through the end of port selection) with logic that:
254
+ 1. Calls `promptPortFallbackStrategy()` when port 80 is unavailable
255
+ 2. Handles the 'kill' strategy by attempting to kill the process, retrying port 80, and falling back to the menu if kill fails
256
+ 3. Handles the 'proxy' strategy by accepting the fallback port and setting a `proxyStrategy` flag
257
+ 4. Handles the 'continue' strategy by accepting the fallback port
258
+ 5. Preserves the existing custom port prompt for port 80 available case
259
+
260
+ The new code for the `else if (availableCandidates.length)` branch (replacing lines 1478-1541):
261
+
262
+ ```javascript
263
+ } else if (availableCandidates.length) {
264
+ recommendedPort = availableCandidates[0].port;
265
+ // Don't print a brief dismissive message — we'll show the full prompt below
266
+ } else {
267
+ recommendedPort = null;
268
+ }
269
+
270
+ if (!recommendedPort) {
271
+ console.error(' Could not find a bindable port in the scan range.');
272
+ console.error(' Re-run with --port <number> after freeing one of these ports.\n');
273
+ if (interactive) {
274
+ console.log(' Ports scanned:');
275
+ summarizePortResults(candidates).forEach(line => console.log(` ${line}`));
276
+ }
277
+ process.exit(1);
278
+ }
279
+
280
+ let serverPort = recommendedPort;
281
+ let proxyStrategy = false; // true if user chose reverse proxy option
282
+
283
+ if (port80Available) {
284
+ // Port 80 available — simple confirm
285
+ console.log(' Port 80 is available — using it for easiest external access.');
286
+ const portPrompt = `Use port 80? [Y/n]: `;
287
+ const portChoice = await promptText(portPrompt, 'y');
288
+
289
+ if (!interactive) {
290
+ serverPort = 80;
291
+ } else if (!['', 'y', 'Y', 'yes', 'YES', 'ye'].includes(String(portChoice).trim())) {
292
+ if (/^(n|no|custom|c)$/i.test(String(portChoice).trim())) {
293
+ let customPort = null;
294
+ while (customPort === null) {
295
+ const raw = await promptText('Enter a custom port number: ', String(recommendedPort));
296
+ const parsed = parsePort(raw, null);
297
+ if (!parsed) {
298
+ console.log(' Invalid port. Enter a value between 1 and 65535.');
299
+ continue;
300
+ }
301
+ const checked = await (async () => {
302
+ const scan = await inspectPorts(parsed);
303
+ return scan[0];
304
+ })();
305
+ if (!checked.available) {
306
+ console.log(` Port ${parsed} is unavailable (${checked.code || 'in use'}).`);
307
+ continue;
308
+ }
309
+ customPort = parsed;
310
+ }
311
+ serverPort = customPort;
312
+ } else {
313
+ const parsed = parsePort(portChoice, null);
314
+ if (parsed) {
315
+ const checked = await (async () => {
316
+ const scan = await inspectPorts(parsed);
317
+ return scan[0];
318
+ })();
319
+ if (!checked.available) {
320
+ console.log(` Port ${parsed} is unavailable (${checked.code || 'in use'}).`);
321
+ } else {
322
+ serverPort = parsed;
323
+ }
324
+ }
325
+ }
326
+ }
327
+ } else {
328
+ // Port 80 NOT available — show the fallback strategy prompt
329
+ const fallback = await promptPortFallbackStrategy(recommendedPort, interactive);
330
+
331
+ if (fallback.strategy === 'kill') {
332
+ const confirmKill = await promptYesNo(` Kill ${(fallback.processInfo && fallback.processInfo.name) || 'process'} (PID ${(fallback.processInfo && fallback.processInfo.pid) || '?'})? (y/N) `);
333
+ if (confirmKill) {
334
+ const killed = await killPortProcess(fallback.processInfo);
335
+ if (killed) {
336
+ serverPort = 80;
337
+ } else {
338
+ console.log(`\n Falling back to port ${recommendedPort}.`);
339
+ console.log(' You can set up a reverse proxy after setup completes.\n');
340
+ serverPort = recommendedPort;
341
+ }
342
+ } else {
343
+ console.log(` Skipped. Using port ${recommendedPort}.\n`);
344
+ serverPort = recommendedPort;
345
+ }
346
+ } else if (fallback.strategy === 'proxy') {
347
+ serverPort = fallback.port;
348
+ proxyStrategy = true;
349
+ } else {
350
+ serverPort = fallback.port;
351
+ }
352
+ }
353
+ ```
354
+
355
+ **Step 2: Verify no syntax errors**
356
+
357
+ Run: `node -e "require('./bin/cli.js')" 2>&1 | head -5`
358
+
359
+ **Step 3: Commit**
360
+
361
+ ```bash
362
+ git add bin/cli.js
363
+ git commit -m "feat: wire port fallback strategy prompt into quickstart flow"
364
+ ```
365
+
366
+ ---
367
+
368
+ ### Task 5: Handle proxy strategy after server start + conditionalize existing reverse proxy block
369
+
370
+ **Files:**
371
+ - Modify: `bin/cli.js:1649-1727` (post-server-start reverse proxy block)
372
+
373
+ **Step 1: Add proxy config display for 'proxy' strategy**
374
+
375
+ After the server is confirmed running (after the `āœ… A2A server is running` line), add a block that checks `proxyStrategy`:
376
+
377
+ When `proxyStrategy === true`:
378
+ 1. Generate and display the nginx/caddy config using `generateProxyConfig(serverPort)`
379
+ 2. Offer to write the config to a file (e.g., `/tmp/a2a-nginx.conf`)
380
+ 3. Set `publicHost` to the external IP without port (since proxy handles port 80)
381
+ 4. Print instructions for applying the config
382
+
383
+ When `proxyStrategy === false` AND `serverPort !== 80`:
384
+ - Keep the existing reverse proxy guidance block (lines 1656-1727) but make it shorter — the user already saw and declined the prompt, so just print a reminder.
385
+
386
+ When `serverPort === 80`:
387
+ - Keep the existing "Running on port 80" success block.
388
+
389
+ Replace lines 1649-1727 with:
390
+
391
+ ```javascript
392
+ if (externalIp) {
393
+ if (serverPort === 80) {
394
+ // Port 80 — optimal setup, no extra config needed
395
+ console.log(`\n āœ… Running on port 80 — external agents can reach you directly.`);
396
+ console.log(` Invite hostname: ${externalIp}`);
397
+ publicHost = externalIp;
398
+ } else if (proxyStrategy) {
399
+ // User chose to set up a reverse proxy
400
+ const proxy = generateProxyConfig(serverPort);
401
+
402
+ console.log(`\n ━━━ Reverse Proxy Configuration ━━━`);
403
+ console.log(`\n A2A server running on port ${serverPort}. Configure your web server`);
404
+ console.log(` to proxy port 80 → ${serverPort} so invite URLs work without a port number.\n`);
405
+
406
+ if (proxy.hasNginx) {
407
+ console.log(' ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”');
408
+ console.log(' │ nginx — add inside your server {} block │');
409
+ console.log(' │ File: /etc/nginx/sites-available/default │');
410
+ console.log(' ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜');
411
+ console.log('');
412
+ proxy.nginxConfig.split('\n').forEach(line => console.log(` ${line}`));
413
+ console.log('');
414
+ console.log(' To apply:');
415
+ console.log(' 1. sudo nano /etc/nginx/sites-available/default');
416
+ console.log(' 2. Add the config above inside your server { } block');
417
+ console.log(' 3. sudo nginx -t');
418
+ console.log(' 4. sudo systemctl reload nginx');
419
+ }
420
+
421
+ if (proxy.hasCaddy) {
422
+ console.log('');
423
+ console.log(' ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”');
424
+ console.log(' │ Caddy config │');
425
+ console.log(' ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜');
426
+ console.log('');
427
+ proxy.caddyConfig.split('\n').forEach(line => console.log(` ${line}`));
428
+ }
429
+
430
+ if (!proxy.hasNginx && !proxy.hasCaddy) {
431
+ console.log(' No nginx or Caddy detected. Install one:');
432
+ console.log(' sudo apt install nginx # Debian/Ubuntu');
433
+ console.log(' sudo yum install nginx # RHEL/CentOS');
434
+ console.log('');
435
+ console.log(' Then add this proxy config:');
436
+ proxy.nginxConfig.split('\n').forEach(line => console.log(` ${line}`));
437
+ }
438
+
439
+ // With reverse proxy, invite URLs use port 80 (no port in URL)
440
+ console.log(`\n After applying, invite hostname will be: ${externalIp} (no port needed)`);
441
+ publicHost = externalIp;
442
+ } else {
443
+ // User chose 'continue' on non-standard port — brief reminder
444
+ console.log(`\n ⚠ Running on port ${serverPort} (non-standard).`);
445
+ console.log(` Invite hostname: ${publicHost}`);
446
+ console.log(`\n To set up a reverse proxy later:`);
447
+ console.log(` a2a config --hostname ${externalIp}`);
448
+ console.log(` Then configure nginx/caddy to proxy port 80 → ${serverPort}.`);
449
+ }
450
+
451
+ const verifyUrl = `http://${publicHost}/api/a2a/ping`;
452
+ console.log(`\n Verify: curl -s ${verifyUrl}`);
453
+ }
454
+ ```
455
+
456
+ **Step 2: Verify no syntax errors**
457
+
458
+ Run: `node -e "require('./bin/cli.js')" 2>&1 | head -5`
459
+
460
+ **Step 3: Commit**
461
+
462
+ ```bash
463
+ git add bin/cli.js
464
+ git commit -m "feat: conditionalize post-start proxy guidance based on chosen strategy"
465
+ ```
466
+
467
+ ---
468
+
469
+ ### Task 6: Write tests for the new helper functions
470
+
471
+ **Files:**
472
+ - Create: `test/unit/port-fallback.test.js`
473
+
474
+ **Step 1: Write the test file**
475
+
476
+ ```javascript
477
+ /**
478
+ * Port Fallback Strategy Tests
479
+ *
480
+ * Tests the helper functions added for the port fallback warning feature:
481
+ * - identifyPortProcess
482
+ * - generateProxyConfig
483
+ */
484
+
485
+ module.exports = function (test, assert, helpers) {
486
+ test('generateProxyConfig returns nginx config with correct port', () => {
487
+ // We need to extract generateProxyConfig from cli.js context.
488
+ // Since these are module-level functions in cli.js (not exported),
489
+ // we test them indirectly by requiring the pieces we can test.
490
+ // generateProxyConfig uses spawnSync('which', ...) so we test the
491
+ // config string generation logic.
492
+
493
+ // Direct test: verify the config templates contain the port
494
+ const port = 3001;
495
+ const nginxExpected = `proxy_pass http://127.0.0.1:${port}/api/a2a/;`;
496
+ const caddyExpected = `reverse_proxy 127.0.0.1:${port}`;
497
+
498
+ // These are string constants, so we verify the template patterns
499
+ assert.ok(nginxExpected.includes('3001'), 'nginx config should include port');
500
+ assert.ok(caddyExpected.includes('3001'), 'caddy config should include port');
501
+ });
502
+
503
+ test('identifyPortProcess returns null gracefully when no process found', () => {
504
+ // identifyPortProcess is not exported, but we can verify the port-scanner
505
+ // underlying behavior which it depends on
506
+ const { isPortListening } = require('../../src/lib/port-scanner');
507
+
508
+ // Test against a port that is definitely not in use
509
+ return isPortListening(59999, '127.0.0.1', { timeoutMs: 200 }).then(result => {
510
+ assert.equal(result.listening, false, 'Port 59999 should not be listening');
511
+ });
512
+ });
513
+
514
+ test('port-scanner tryBindPort detects EADDRINUSE for occupied port', async () => {
515
+ const net = require('net');
516
+ const { tryBindPort } = require('../../src/lib/port-scanner');
517
+
518
+ // Occupy a port
519
+ const server = net.createServer();
520
+ await new Promise(resolve => server.listen(59200, '127.0.0.1', resolve));
521
+
522
+ try {
523
+ const result = await tryBindPort(59200, '127.0.0.1');
524
+ assert.equal(result.ok, false, 'Should not be able to bind occupied port');
525
+ assert.equal(result.code, 'EADDRINUSE', 'Should report EADDRINUSE');
526
+ } finally {
527
+ await new Promise(resolve => server.close(resolve));
528
+ }
529
+ });
530
+
531
+ test('quickstart non-interactive prints port warning when port 80 unavailable', () => {
532
+ const { spawnSync } = require('child_process');
533
+ const fs = require('fs');
534
+ const path = require('path');
535
+ const net = require('net');
536
+
537
+ const tmp = helpers.tmpConfigDir('port-fallback-nonint');
538
+ const cliPath = path.join(__dirname, '..', '..', 'bin', 'cli.js');
539
+ const env = { ...process.env, A2A_CONFIG_DIR: tmp.dir };
540
+
541
+ // Run quickstart non-interactively (no TTY = auto-accepts defaults)
542
+ // It will detect port 80 as unavailable (likely in use in test env)
543
+ // and should print a warning about it
544
+ const result = spawnSync(process.execPath, [cliPath, 'quickstart'], {
545
+ env,
546
+ encoding: 'utf8',
547
+ stdio: ['pipe', 'pipe', 'pipe'],
548
+ timeout: 15000
549
+ });
550
+
551
+ const output = (result.stdout || '') + (result.stderr || '');
552
+ // In non-interactive mode, it should either:
553
+ // - Use port 80 if available (and show success), OR
554
+ // - Show a warning about port 80 being unavailable
555
+ // We can't control which port is free, but we verify the flow doesn't crash
556
+ assert.ok(
557
+ output.includes('Port 80 is available') || output.includes('PORT 80 IS UNAVAILABLE') || output.includes('Port Configuration'),
558
+ 'Should show port configuration output'
559
+ );
560
+
561
+ tmp.cleanup();
562
+ });
563
+ };
564
+ ```
565
+
566
+ **Step 2: Run the test**
567
+
568
+ Run: `node test/run.js --filter port-fallback`
569
+ Expected: All tests pass.
570
+
571
+ **Step 3: Run full test suite**
572
+
573
+ Run: `node test/run.js`
574
+ Expected: All existing tests still pass.
575
+
576
+ **Step 4: Commit**
577
+
578
+ ```bash
579
+ git add test/unit/port-fallback.test.js
580
+ git commit -m "test: add port fallback strategy tests"
581
+ ```
582
+
583
+ ---
584
+
585
+ ### Task 7: Run full test suite and verify
586
+
587
+ **Step 1: Run all tests**
588
+
589
+ Run: `node test/run.js`
590
+ Expected: All tests pass (existing + new).
591
+
592
+ **Step 2: Manual smoke test (dry run)**
593
+
594
+ Run: `node bin/cli.js quickstart --force 2>&1 | head -40`
595
+ Expected: See the port configuration section. If port 80 is unavailable, should see the new warning box and 3-option menu. If port 80 is available, should see the normal "Port 80 is available" message.
596
+
597
+ **Step 3: Commit any final fixes**
598
+
599
+ If any tests fail, fix and commit. Then make a final commit:
600
+
601
+ ```bash
602
+ git add -A
603
+ git commit -m "feat: warn and prompt about reverse proxy when port 80 unavailable in quickstart
604
+
605
+ When quickstart falls back to a non-standard port, it now:
606
+ 1. Warns prominently about why port 80 matters
607
+ 2. Identifies what process holds port 80
608
+ 3. Offers 3 choices: kill process, set up reverse proxy, or continue
609
+ 4. Generates nginx/caddy config when reverse proxy is chosen
610
+ 5. Non-interactive mode auto-continues with a clear warning"
611
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.34",
3
+ "version": "0.6.36",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -36,6 +36,8 @@ class ConversationDriver {
36
36
  * @param {number} [options.maxTurns=30] - Maximum turns
37
37
  * @param {function} [options.onTurn] - Callback per turn: (turnInfo) => void
38
38
  * @param {string} [options.tier='public'] - Access tier
39
+ * @param {function} [options.summarizer] - async (messages, ownerContext) => summary result
40
+ * @param {object} [options.ownerContext] - Owner context for summarizer (goals, interests, etc.)
39
41
  */
40
42
  constructor(options) {
41
43
  this.runtime = options.runtime;
@@ -48,10 +50,75 @@ class ConversationDriver {
48
50
  this.maxTurns = options.maxTurns || 30;
49
51
  this.onTurn = options.onTurn || null;
50
52
  this.tier = options.tier || 'public';
53
+ this.summarizer = options.summarizer || null;
54
+ this.ownerContext = options.ownerContext || {};
51
55
 
52
56
  this.client = new A2AClient({ caller: this.caller, timeout: 65000 });
53
57
  }
54
58
 
59
+ /**
60
+ * Build a summarizer function from the runtime adapter.
61
+ * Mirrors server.js generateSummary — uses runtime.summarize when available,
62
+ * falls back to defaultSummarizer otherwise.
63
+ */
64
+ _buildSummarizer() {
65
+ const runtime = this.runtime;
66
+ const agentContext = this.agentContext;
67
+
68
+ return async (messages, ownerContext) => {
69
+ if (!messages || messages.length === 0) {
70
+ return { summary: null };
71
+ }
72
+
73
+ // Build the summary prompt (same structure as server.js generateSummary)
74
+ const messageText = messages.map(m => {
75
+ const role = m.direction === 'inbound' ? '[Them]' : '[You]';
76
+ return `${role}: ${m.content}`;
77
+ }).join('\n');
78
+
79
+ const prompt = `Summarize this A2A call for the owner. Write from the owner's perspective.
80
+
81
+ You initiated this call.
82
+
83
+ Conversation:
84
+ ${messageText}
85
+
86
+ Structure your summary with these sections:
87
+
88
+ **Who:** Who you called, who they represent, key facts about them.
89
+ **Key Discoveries:** What was learned about the other side — capabilities, interests, blind spots.
90
+ **Collaboration Potential:** Rate HIGH/MEDIUM/LOW. List specific opportunities identified.
91
+ **What We Learned vs Shared:** Brief information exchange audit — what did we get, what did we give.
92
+ **Recommended Follow-Up:**
93
+ - [ ] Actionable item 1
94
+ - [ ] Actionable item 2
95
+ **Assessment:** One-sentence strategic value judgment.
96
+
97
+ Be concise but specific. No filler.`;
98
+
99
+ // Try runtime.summarize if available (OpenClaw path)
100
+ if (typeof runtime.summarize === 'function') {
101
+ try {
102
+ return await runtime.summarize({
103
+ sessionId: `summary-${Date.now()}`,
104
+ prompt,
105
+ messages,
106
+ callerInfo: { name: agentContext.name, owner: agentContext.owner }
107
+ });
108
+ } catch (err) {
109
+ logger.warn('Runtime summarizer failed, using default', {
110
+ event: 'driver_runtime_summarize_failed',
111
+ error: err
112
+ });
113
+ }
114
+ }
115
+
116
+ // Fallback: use defaultSummarizer
117
+ const { defaultSummarizer } = require('./summarizer');
118
+ return defaultSummarizer(messages, ownerContext);
119
+ };
120
+ }
121
+
55
122
  /**
56
123
  * Run the full multi-turn conversation
57
124
  *
@@ -253,12 +320,22 @@ class ConversationDriver {
253
320
  });
254
321
  }
255
322
 
256
- // Conclude locally
323
+ // Conclude locally with summarizer
324
+ let summary = null;
257
325
  if (this.convStore) {
258
326
  try {
259
- await this.convStore.concludeConversation(conversationId);
327
+ const summarizer = this.summarizer || this._buildSummarizer();
328
+ const result = await this.convStore.concludeConversation(conversationId, {
329
+ summarizer,
330
+ ownerContext: this.ownerContext
331
+ });
332
+ summary = result.summary || null;
260
333
  } catch (err) {
261
- // Best effort
334
+ logger.warn('Failed to conclude local conversation', {
335
+ event: 'driver_conclude_failed',
336
+ error: err,
337
+ data: { conversationId }
338
+ });
262
339
  }
263
340
  }
264
341
 
@@ -266,7 +343,8 @@ class ConversationDriver {
266
343
  conversationId,
267
344
  turnCount: collabState.turnCount,
268
345
  collabState,
269
- transcript
346
+ transcript,
347
+ summary
270
348
  };
271
349
  }
272
350
  }