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 +26 -0
- package/bin/cli.js +216 -19
- package/docs/plans/2026-02-16-a2a-callbook-macos-app.md +1660 -0
- package/docs/plans/2026-02-16-bugfixes-22-24.md +246 -0
- package/native/macos/index.html +172 -0
- package/native/macos/package.json +8 -0
- package/native/macos/src-tauri/Cargo.toml +23 -0
- package/native/macos/src-tauri/build.rs +3 -0
- package/native/macos/src-tauri/capabilities/default.json +16 -0
- package/native/macos/src-tauri/icons/128x128.png +0 -0
- package/native/macos/src-tauri/icons/128x128@2x.png +0 -0
- package/native/macos/src-tauri/icons/32x32.png +0 -0
- package/native/macos/src-tauri/icons/icon.icns +0 -0
- package/native/macos/src-tauri/icons/tray-connected.png +0 -0
- package/native/macos/src-tauri/icons/tray-disconnected.png +0 -0
- package/native/macos/src-tauri/src/discovery.rs +86 -0
- package/native/macos/src-tauri/src/health.rs +64 -0
- package/native/macos/src-tauri/src/lib.rs +185 -0
- package/native/macos/src-tauri/src/main.rs +6 -0
- package/native/macos/src-tauri/src/notifications.rs +101 -0
- package/native/macos/src-tauri/src/server.rs +67 -0
- package/native/macos/src-tauri/tauri.conf.json +48 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +49 -0
- package/src/lib/disclosure.js +2 -0
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'
|
|
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
|
-
|
|
567
|
+
goals: tokenGoals,
|
|
568
|
+
disclosure: 'minimal'
|
|
527
569
|
});
|
|
528
570
|
config.setTier('friends', {
|
|
529
571
|
topics: [...getTierTopics(tiersData.public), ...getTierTopics(tiersData.friends)],
|
|
530
|
-
disclosure: '
|
|
572
|
+
disclosure: 'standard'
|
|
531
573
|
});
|
|
532
574
|
config.setTier('family', {
|
|
533
575
|
topics: [...getTierTopics(tiersData.public), ...getTierTopics(tiersData.friends), ...getTierTopics(tiersData.family)],
|
|
534
|
-
disclosure: '
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
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]) {
|