a2acalling 0.6.44 → 0.6.46

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/README.md CHANGED
@@ -49,6 +49,32 @@ a2a call "Alice's Agent" "Hey! Want to collaborate on the a2a protocol?"
49
49
  a2a call "a2a://their-host.com/fed_xyz789" "Hello!"
50
50
  ```
51
51
 
52
+ ## 🖥️ Native macOS App
53
+
54
+ A2A Callbook is also available as a native macOS app that wraps the dashboard in a proper macOS window with native integrations.
55
+
56
+ **Features:**
57
+ - Native macOS notifications for incoming calls
58
+ - Keyboard shortcuts: Cmd+1–5 for tab switching, Cmd+R to refresh
59
+ - Menu bar tray icon with server status
60
+ - Deep link support (`a2a://` URLs open in the app)
61
+ - Cmd+W hides the window (stays in tray), Cmd+Q quits
62
+
63
+ **Auto-installed** on macOS when you run `npm install -g a2acalling`. The app is placed in `~/Applications/`.
64
+
65
+ **Manual install:** Download the `.dmg` from [GitHub Releases](https://github.com/onthegonow/a2a_calling/releases).
66
+
67
+ **Build from source:**
68
+
69
+ ```bash
70
+ # Requires Rust: https://rustup.rs
71
+ cargo install tauri-cli --version "^2"
72
+ cd native/macos/src-tauri
73
+ cargo tauri build --target universal-apple-darwin
74
+ ```
75
+
76
+ To open the app via CLI: `a2a gui` (prefers native app; use `--browser` to force browser).
77
+
52
78
  ## 📦 Installation
53
79
 
54
80
  ```bash
package/bin/cli.js CHANGED
@@ -29,6 +29,7 @@ const ONBOARDING_EXEMPT = new Set([
29
29
  'quickstart',
30
30
  'help',
31
31
  'version',
32
+ 'status',
32
33
  'update',
33
34
  'uninstall',
34
35
  'onboard',
@@ -75,7 +76,7 @@ const store = new TokenStore();
75
76
  // rather than falling through to the interactive quickstart flow.
76
77
  // These are outbound operations often invoked by agents/automation.
77
78
  const ONBOARDING_HARD_FAIL = new Set([
78
- 'call', 'ping', 'status'
79
+ 'call', 'ping'
79
80
  ]);
80
81
 
81
82
  // ── enforceOnboarding ────────────────────────────────────────────────────
@@ -149,6 +150,23 @@ function openInBrowser(url) {
149
150
  }
150
151
  }
151
152
 
153
+ function findNativeApp() {
154
+ if (os.platform() !== 'darwin') return null;
155
+
156
+ const candidates = [
157
+ path.join(os.homedir(), 'Applications', 'A2A Callbook.app'),
158
+ '/Applications/A2A Callbook.app',
159
+ ];
160
+
161
+ for (const appPath of candidates) {
162
+ try {
163
+ if (fs.existsSync(appPath)) return appPath;
164
+ } catch (_) {}
165
+ }
166
+
167
+ return null;
168
+ }
169
+
152
170
  async function findLocalServerPort(preferredPorts = []) {
153
171
  const http = require('http');
154
172
 
@@ -472,6 +490,19 @@ function generateProxyConfig(backendPort) {
472
490
  return { hasNginx, hasCaddy, nginxConfig, caddyConfig };
473
491
  }
474
492
 
493
+ function extractNameFromPersonality(notes) {
494
+ if (!notes || typeof notes !== 'string') return null;
495
+ const patterns = [
496
+ /(?:I'm|I am|My name is|Name:|Owner:)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/,
497
+ /^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(?:is|here|speaking)/
498
+ ];
499
+ for (const p of patterns) {
500
+ const m = notes.match(p);
501
+ if (m && m[1]) return m[1].trim();
502
+ }
503
+ return null;
504
+ }
505
+
475
506
  async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
476
507
  const submitRaw = args.flags.submit;
477
508
  if (!submitRaw) return false;
@@ -520,18 +551,29 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
520
551
 
521
552
  const tiersData = manifest.tiers || {};
522
553
 
554
+ // Derive goals from disclosure objectives (used in tier config and token creation)
555
+ const disclosureObjectives = (tiersData.public?.objectives || [])
556
+ .map(o => typeof o === 'string' ? o : (o && o.objective || ''))
557
+ .map(s => s.trim().toLowerCase().replace(/\s+/g, '-').slice(0, 60))
558
+ .filter(Boolean);
559
+
560
+ const tokenGoals = disclosureObjectives.length > 0
561
+ ? [...new Set(disclosureObjectives)].slice(0, 5)
562
+ : ['grow-network', 'find-collaborators', 'build-in-public'];
563
+
523
564
  try {
524
565
  config.setTier('public', {
525
566
  topics: getTierTopics(tiersData.public),
526
- disclosure: 'public'
567
+ goals: tokenGoals,
568
+ disclosure: 'minimal'
527
569
  });
528
570
  config.setTier('friends', {
529
571
  topics: [...getTierTopics(tiersData.public), ...getTierTopics(tiersData.friends)],
530
- disclosure: 'minimal'
572
+ disclosure: 'standard'
531
573
  });
532
574
  config.setTier('family', {
533
575
  topics: [...getTierTopics(tiersData.public), ...getTierTopics(tiersData.friends), ...getTierTopics(tiersData.family)],
534
- disclosure: 'minimal'
576
+ disclosure: 'full'
535
577
  });
536
578
  } catch (err) {
537
579
  console.error(` Warning: could not sync tier config: ${err.message}`);
@@ -545,21 +587,34 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
545
587
 
546
588
  console.log('\nStep 4 of 4: Generating your first invite...\n');
547
589
 
548
- const agentName = args.flags.name || config.getAgent().name || process.env.A2A_AGENT_NAME || 'my-agent';
590
+ // Extract identity from disclosure submission (parsed = raw JSON, result = validated)
591
+ const ownerName = parsed.owner_name
592
+ || extractNameFromPersonality(result.manifest?.personality_notes)
593
+ || process.env.USER
594
+ || 'Agent Owner';
595
+
596
+ const agentName = args.flags.name
597
+ || parsed.agent_name
598
+ || config.getAgent().name
599
+ || process.env.A2A_AGENT_NAME
600
+ || `${ownerName}'s Agent`;
601
+
602
+ // Save identity to config
603
+ config.setAgent({ name: agentName, owner_name: ownerName });
604
+
549
605
  const hostname = config.getAgent().hostname || process.env.A2A_HOSTNAME || 'localhost';
550
- if (args.flags.name) config.setAgent({ name: agentName });
551
606
 
552
607
  const publicTopics = getTierTopics(tiersData.public);
553
608
 
554
609
  const { token } = store.create({
555
610
  name: agentName,
556
- owner: agentName,
611
+ owner: ownerName,
557
612
  permissions: 'public',
558
613
  disclosure: 'minimal',
559
614
  expires: 'never',
560
615
  maxCalls: null,
561
616
  allowedTopics: publicTopics,
562
- allowedGoals: ['grow-network', 'find-collaborators', 'build-in-public'],
617
+ allowedGoals: tokenGoals,
563
618
  notify: 'all'
564
619
  });
565
620
 
@@ -1476,6 +1531,18 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1476
1531
  return;
1477
1532
  }
1478
1533
 
1534
+ // Prefer native app on macOS (--browser flag forces browser)
1535
+ if (!args.flags.browser) {
1536
+ const nativeApp = findNativeApp();
1537
+ if (nativeApp) {
1538
+ console.log('Opening A2A Callbook native app...');
1539
+ const result = openInBrowser(nativeApp);
1540
+ if (result.attempted) {
1541
+ return;
1542
+ }
1543
+ }
1544
+ }
1545
+
1479
1546
  const preferred = [];
1480
1547
  if (args.flags.port || args.flags.p) preferred.push(args.flags.port || args.flags.p);
1481
1548
  if (process.env.A2A_PORT) preferred.push(process.env.A2A_PORT);
@@ -1508,20 +1575,83 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1508
1575
 
1509
1576
  status: async (args) => {
1510
1577
  const url = args._[1];
1511
- if (!url) {
1512
- console.error('Usage: a2a status <invite_url>');
1513
- process.exit(1);
1578
+
1579
+ // If a URL is provided, check that remote agent's status
1580
+ if (url) {
1581
+ const client = new A2AClient();
1582
+ try {
1583
+ const status = await client.status(url);
1584
+ console.log(`A2A status for ${url}:\n`);
1585
+ console.log(JSON.stringify(status, null, 2));
1586
+ } catch (err) {
1587
+ console.error(`❌ Failed to get status: ${err.message}`);
1588
+ process.exit(1);
1589
+ }
1590
+ return;
1514
1591
  }
1515
1592
 
1516
- const client = new A2AClient();
1517
- try {
1518
- const status = await client.status(url);
1519
- console.log(`A2A status for ${url}:\n`);
1520
- console.log(JSON.stringify(status, null, 2));
1521
- } catch (err) {
1522
- console.error(`❌ Failed to get status: ${err.message}`);
1523
- process.exit(1);
1593
+ // No URL show local server status
1594
+ const { A2AConfig } = require('../src/lib/config');
1595
+ const config = new A2AConfig();
1596
+ const onboarding = config.getOnboarding();
1597
+ const agent = config.getAgent();
1598
+
1599
+ console.log('A2A Local Status\n');
1600
+
1601
+ // Onboarding state
1602
+ const onboarded = onboarding.version === 2 && onboarding.step === 'complete';
1603
+ console.log(` Onboarding: ${onboarded ? '✅ Complete' : `⚠️ ${onboarding.step || 'not started'} (run: a2a quickstart)`}`);
1604
+ console.log(` Agent name: ${agent.name || '(not set)'}`);
1605
+ console.log(` Hostname: ${agent.hostname || '(not set)'}`);
1606
+
1607
+ // Check if server is running
1608
+ const preferred = [];
1609
+ if (onboarding.server_port) preferred.push(onboarding.server_port);
1610
+ const port = await findLocalServerPort(preferred);
1611
+ if (port) {
1612
+ console.log(` Server: ✅ Running on port ${port}`);
1613
+
1614
+ // Fetch dashboard status for more detail
1615
+ const http = require('http');
1616
+ try {
1617
+ const statusData = await new Promise((resolve, reject) => {
1618
+ const req = http.request({
1619
+ hostname: '127.0.0.1', port,
1620
+ path: '/api/a2a/dashboard/status',
1621
+ method: 'GET', timeout: 2000
1622
+ }, (res) => {
1623
+ let body = '';
1624
+ res.on('data', c => body += c);
1625
+ res.on('end', () => {
1626
+ try { resolve(JSON.parse(body)); } catch (e) { reject(e); }
1627
+ });
1628
+ });
1629
+ req.on('error', reject);
1630
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
1631
+ req.end();
1632
+ });
1633
+
1634
+ if (statusData.agent) {
1635
+ if (statusData.agent.owner_name) console.log(` Owner: ${statusData.agent.owner_name}`);
1636
+ }
1637
+ if (statusData.invite_host) {
1638
+ console.log(` Invite host: ${statusData.invite_host}`);
1639
+ }
1640
+ if (statusData.warnings && statusData.warnings.length) {
1641
+ console.log('');
1642
+ for (const w of statusData.warnings) {
1643
+ console.log(` ⚠️ ${w}`);
1644
+ }
1645
+ }
1646
+ } catch (_) {
1647
+ // Dashboard status unavailable — that's fine, we already showed port
1648
+ }
1649
+ } else {
1650
+ console.log(' Server: ❌ Not running');
1651
+ console.log(' Start with: a2a server --port 3001');
1524
1652
  }
1653
+
1654
+ console.log(`\n Tip: a2a status <invite_url> to check a remote agent`);
1525
1655
  },
1526
1656
 
1527
1657
  config: (args) => {
@@ -1916,6 +2046,9 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1916
2046
  // User chose 'continue' on non-standard port — brief reminder
1917
2047
  console.log(`\n ⚠ Running on port ${serverPort} (non-standard).`);
1918
2048
  console.log(` Invite hostname: ${publicHost}`);
2049
+ console.log('');
2050
+ console.log(' ⚠️ Remote agents using your invite URL will try port 80 by default.');
2051
+ console.log(' Without a reverse proxy, inbound calls on port 80 will fail silently.');
1919
2052
  console.log(`\n To set up a reverse proxy later:`);
1920
2053
  console.log(` a2a config --hostname ${externalIp}`);
1921
2054
  console.log(` Then configure nginx/caddy to proxy port 80 → ${serverPort}.`);
@@ -1923,6 +2056,45 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1923
2056
 
1924
2057
  const verifyUrl = `http://${publicHost}/api/a2a/ping`;
1925
2058
  console.log(`\n Verify: curl -s ${verifyUrl}`);
2059
+
2060
+ // Fix 6: Actually run the connectivity check
2061
+ const http = require('http');
2062
+ const verifyOk = await new Promise(resolve => {
2063
+ const req = http.request({
2064
+ hostname: '127.0.0.1',
2065
+ port: serverPort,
2066
+ path: '/api/a2a/ping',
2067
+ method: 'GET',
2068
+ timeout: 2000
2069
+ }, (res) => {
2070
+ res.resume();
2071
+ resolve(res.statusCode === 200);
2072
+ });
2073
+ req.on('error', () => resolve(false));
2074
+ req.on('timeout', () => { req.destroy(); resolve(false); });
2075
+ req.end();
2076
+ });
2077
+
2078
+ if (verifyOk) {
2079
+ console.log(' ✅ Local connectivity verified');
2080
+ } else {
2081
+ console.log(' ⚠️ Local server check failed — server may still be starting');
2082
+ }
2083
+
2084
+ // Fix 7: Surface invite-host warnings during quickstart
2085
+ try {
2086
+ const { resolveInviteHost } = require('../src/lib/invite-host');
2087
+ const resolved = await resolveInviteHost({
2088
+ hostname: publicHost,
2089
+ port: serverPort
2090
+ });
2091
+ if (resolved.warnings && resolved.warnings.length) {
2092
+ console.log('\n ━━━ Network Warnings ━━━');
2093
+ for (const w of resolved.warnings) {
2094
+ console.warn(` ⚠️ ${w}`);
2095
+ }
2096
+ }
2097
+ } catch (_) {}
1926
2098
  }
1927
2099
 
1928
2100
  // Save server config and advance onboarding state to awaiting_disclosure.
@@ -2244,6 +2416,24 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
2244
2416
  console.log('Removing database... ⏭️');
2245
2417
  }
2246
2418
 
2419
+ // Remove native macOS app if present
2420
+ if (os.platform() === 'darwin') {
2421
+ const appCandidates = [
2422
+ path.join(os.homedir(), 'Applications', 'A2A Callbook.app'),
2423
+ '/Applications/A2A Callbook.app',
2424
+ ];
2425
+ for (const appPath of appCandidates) {
2426
+ if (fs.existsSync(appPath)) {
2427
+ try {
2428
+ fs.rmSync(appPath, { recursive: true, force: true });
2429
+ console.log(`Removed ${appPath}`);
2430
+ } catch (err) {
2431
+ console.log(`Could not remove ${appPath}: ${err.message}`);
2432
+ }
2433
+ }
2434
+ }
2435
+ }
2436
+
2247
2437
  console.log('\nTo complete removal:');
2248
2438
  console.log(' npm uninstall -g a2acalling\n');
2249
2439
  console.log(`Config preserved: ${keepConfig ? 'yes' : 'no'}`);
@@ -2466,6 +2656,13 @@ Examples:
2466
2656
 
2467
2657
  // Main
2468
2658
  const args = parseArgs(process.argv);
2659
+
2660
+ // Handle --version flag before command dispatch (standard CLI convention)
2661
+ if (args.flags.version || args.flags.v) {
2662
+ commands.version();
2663
+ process.exit(0);
2664
+ }
2665
+
2469
2666
  const command = args._[0] || 'help';
2470
2667
 
2471
2668
  if (!commands[command]) {