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.
@@ -0,0 +1,185 @@
1
+ use tauri::{Manager, RunEvent, WindowEvent};
2
+ use tauri::menu::{Menu, MenuItem, Submenu, PredefinedMenuItem, AboutMetadata};
3
+ use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
4
+ use tauri_plugin_deep_link::DeepLinkExt;
5
+
6
+ mod discovery;
7
+ mod health;
8
+ mod notifications;
9
+ mod server;
10
+
11
+ #[tauri::command]
12
+ async fn discover_server() -> Result<discovery::DiscoveryResult, String> {
13
+ let result = discovery::discover_server().await;
14
+ if let Some(port) = result.port {
15
+ health::set_connected(port);
16
+ }
17
+ Ok(result)
18
+ }
19
+
20
+ #[tauri::command]
21
+ fn start_server() -> Result<server::StartResult, String> {
22
+ Ok(server::start_server())
23
+ }
24
+
25
+ fn build_menu(app: &tauri::AppHandle) -> tauri::Result<Menu<tauri::Wry>> {
26
+ let about = PredefinedMenuItem::about(app, Some("About A2A Callbook"), Some(AboutMetadata {
27
+ name: Some("A2A Callbook".into()),
28
+ version: Some(env!("CARGO_PKG_VERSION").into()),
29
+ ..Default::default()
30
+ }))?;
31
+ let quit = PredefinedMenuItem::quit(app, Some("Quit A2A Callbook"))?;
32
+ let hide = PredefinedMenuItem::hide(app, Some("Hide A2A Callbook"))?;
33
+ let preferences = MenuItem::with_id(app, "preferences", "Preferences\u{2026}", true, Some("CmdOrCtrl+,"))?;
34
+ let separator = PredefinedMenuItem::separator(app)?;
35
+
36
+ let app_menu = Submenu::with_items(app, "A2A Callbook", true, &[
37
+ &about, &separator, &preferences, &separator, &hide, &separator, &quit,
38
+ ])?;
39
+
40
+ // View menu with tab shortcuts
41
+ let contacts = MenuItem::with_id(app, "tab-contacts", "Contacts", true, Some("CmdOrCtrl+1"))?;
42
+ let calls = MenuItem::with_id(app, "tab-calls", "Calls", true, Some("CmdOrCtrl+2"))?;
43
+ let logs = MenuItem::with_id(app, "tab-logs", "Logs", true, Some("CmdOrCtrl+3"))?;
44
+ let settings = MenuItem::with_id(app, "tab-settings", "Settings", true, Some("CmdOrCtrl+4"))?;
45
+ let invites = MenuItem::with_id(app, "tab-invites", "Invites", true, Some("CmdOrCtrl+5"))?;
46
+ let sep2 = PredefinedMenuItem::separator(app)?;
47
+ let refresh = MenuItem::with_id(app, "refresh", "Refresh", true, Some("CmdOrCtrl+R"))?;
48
+
49
+ let view_menu = Submenu::with_items(app, "View", true, &[
50
+ &contacts, &calls, &logs, &settings, &invites, &sep2, &refresh,
51
+ ])?;
52
+
53
+ // Edit menu (standard macOS)
54
+ let copy = PredefinedMenuItem::copy(app, None)?;
55
+ let paste = PredefinedMenuItem::paste(app, None)?;
56
+ let cut = PredefinedMenuItem::cut(app, None)?;
57
+ let select_all = PredefinedMenuItem::select_all(app, None)?;
58
+ let edit_menu = Submenu::with_items(app, "Edit", true, &[
59
+ &cut, &copy, &paste, &select_all,
60
+ ])?;
61
+
62
+ let window_menu = Submenu::with_items(app, "Window", true, &[
63
+ &PredefinedMenuItem::minimize(app, None)?,
64
+ &PredefinedMenuItem::close_window(app, Some("Hide Window"))?,
65
+ ])?;
66
+
67
+ Menu::with_items(app, &[&app_menu, &edit_menu, &view_menu, &window_menu])
68
+ }
69
+
70
+ #[cfg_attr(mobile, tauri::mobile_entry_point)]
71
+ pub fn run() {
72
+ let app = tauri::Builder::default()
73
+ .plugin(tauri_plugin_shell::init())
74
+ .plugin(tauri_plugin_notification::init())
75
+ .plugin(tauri_plugin_deep_link::init())
76
+ .plugin(tauri_plugin_window_state::Builder::new().build())
77
+ .invoke_handler(tauri::generate_handler![discover_server, start_server])
78
+ .setup(|app| {
79
+ let menu = build_menu(app.handle())?;
80
+ app.set_menu(menu)?;
81
+
82
+ // Handle menu events
83
+ let app_handle = app.handle().clone();
84
+ app.on_menu_event(move |_app, event| {
85
+ let id = event.id().0.as_str();
86
+ let tab = match id {
87
+ "tab-contacts" => Some("contacts"),
88
+ "tab-calls" => Some("calls"),
89
+ "tab-logs" => Some("logs"),
90
+ "tab-settings" => Some("settings"),
91
+ "tab-invites" => Some("invites"),
92
+ "preferences" => Some("settings"),
93
+ "refresh" => {
94
+ if let Some(window) = app_handle.get_webview_window("main") {
95
+ let _ = window.eval("window.location.reload()");
96
+ }
97
+ None
98
+ }
99
+ _ => None,
100
+ };
101
+
102
+ if let Some(tab_name) = tab {
103
+ if let Some(window) = app_handle.get_webview_window("main") {
104
+ let js = format!("window.location.hash = '{}'", tab_name);
105
+ let _ = window.eval(&js);
106
+ }
107
+ }
108
+ });
109
+
110
+ // Start background health monitor
111
+ health::start_health_monitor(app.handle().clone());
112
+
113
+ // Start notification poller
114
+ notifications::start_notification_poller(app.handle().clone());
115
+
116
+ // Menu bar tray icon
117
+ let show = MenuItem::with_id(app, "show", "Show A2A Callbook", true, None::<&str>)?;
118
+ let tray_quit = MenuItem::with_id(app, "tray-quit", "Quit", true, None::<&str>)?;
119
+ let tray_menu = Menu::with_items(app, &[&show, &tray_quit])?;
120
+
121
+ let _tray = TrayIconBuilder::new()
122
+ .tooltip("A2A Callbook")
123
+ .menu(&tray_menu)
124
+ .on_menu_event(|app, event| {
125
+ match event.id().0.as_str() {
126
+ "show" => {
127
+ if let Some(window) = app.get_webview_window("main") {
128
+ let _ = window.show();
129
+ let _ = window.set_focus();
130
+ }
131
+ }
132
+ "tray-quit" => {
133
+ app.exit(0);
134
+ }
135
+ _ => {}
136
+ }
137
+ })
138
+ .on_tray_icon_event(|tray, event| {
139
+ if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event {
140
+ let app = tray.app_handle();
141
+ if let Some(window) = app.get_webview_window("main") {
142
+ let _ = window.show();
143
+ let _ = window.set_focus();
144
+ }
145
+ }
146
+ })
147
+ .build(app)?;
148
+
149
+ // Handle a2a:// deep links
150
+ let deep_link_handle = app.handle().clone();
151
+ app.deep_link().on_open_url(move |event| {
152
+ let urls = event.urls();
153
+ for url in urls {
154
+ let url_str = url.to_string();
155
+ // a2a://host/callbook/CODE or a2a://host/fed_TOKEN
156
+ if let Some(window) = deep_link_handle.get_webview_window("main") {
157
+ let _ = window.show();
158
+ let _ = window.set_focus();
159
+ // Pass URL to the SPA via JS
160
+ let js = format!(
161
+ "window.__A2A_DEEP_LINK = '{}'; \
162
+ window.dispatchEvent(new CustomEvent('a2a-deep-link', {{ detail: '{}' }}))",
163
+ url_str.replace('\\', "\\\\").replace('\'', "\\'"),
164
+ url_str.replace('\\', "\\\\").replace('\'', "\\'")
165
+ );
166
+ let _ = window.eval(&js);
167
+ }
168
+ }
169
+ });
170
+
171
+ Ok(())
172
+ })
173
+ .build(tauri::generate_context!())
174
+ .expect("error building A2A Callbook");
175
+
176
+ // Cmd+W hides window instead of quitting
177
+ app.run(|app_handle, event| {
178
+ if let RunEvent::WindowEvent { label, event: WindowEvent::CloseRequested { api, .. }, .. } = &event {
179
+ api.prevent_close();
180
+ if let Some(window) = app_handle.get_webview_window(label) {
181
+ let _ = window.hide();
182
+ }
183
+ }
184
+ });
185
+ }
@@ -0,0 +1,6 @@
1
+ // Prevents additional console window on Windows in release
2
+ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3
+
4
+ fn main() {
5
+ a2a_callbook_lib::run()
6
+ }
@@ -0,0 +1,101 @@
1
+ use std::collections::HashSet;
2
+ use std::sync::Mutex;
3
+ use std::time::Duration;
4
+ use tauri::Manager;
5
+ use tauri_plugin_notification::NotificationExt;
6
+
7
+ static SEEN_CALLS: Mutex<Option<HashSet<String>>> = Mutex::new(None);
8
+
9
+ #[derive(Debug, serde::Deserialize)]
10
+ struct CallsResponse {
11
+ success: bool,
12
+ calls: Option<Vec<Conversation>>,
13
+ }
14
+
15
+ #[derive(Debug, serde::Deserialize)]
16
+ struct Conversation {
17
+ id: String,
18
+ contact_name: Option<String>,
19
+ summary: Option<String>,
20
+ status: Option<String>,
21
+ started_at: Option<String>,
22
+ }
23
+
24
+ /// Poll for new inbound calls and fire native notifications
25
+ pub fn start_notification_poller(app: tauri::AppHandle) {
26
+ // Initialize seen set
27
+ {
28
+ let mut seen = SEEN_CALLS.lock().unwrap();
29
+ *seen = Some(HashSet::new());
30
+ }
31
+
32
+ tokio::spawn(async move {
33
+ // Wait for initial server discovery
34
+ tokio::time::sleep(Duration::from_secs(10)).await;
35
+
36
+ loop {
37
+ tokio::time::sleep(Duration::from_secs(15)).await;
38
+
39
+ let port = crate::health::current_port();
40
+ if port == 0 || !crate::health::is_connected() {
41
+ continue;
42
+ }
43
+
44
+ let url = format!(
45
+ "http://127.0.0.1:{}/api/a2a/dashboard/calls?status=active",
46
+ port
47
+ );
48
+
49
+ let client = reqwest::Client::builder()
50
+ .timeout(Duration::from_secs(5))
51
+ .build();
52
+
53
+ let client = match client {
54
+ Ok(c) => c,
55
+ Err(_) => continue,
56
+ };
57
+
58
+ let resp = match client.get(&url).send().await {
59
+ Ok(r) => r,
60
+ Err(_) => continue,
61
+ };
62
+
63
+ let data: CallsResponse = match resp.json().await {
64
+ Ok(d) => d,
65
+ Err(_) => continue,
66
+ };
67
+
68
+ if !data.success {
69
+ continue;
70
+ }
71
+
72
+ let conversations = data.calls.unwrap_or_default();
73
+ let mut seen = SEEN_CALLS.lock().unwrap();
74
+ let seen_set = seen.as_mut().unwrap();
75
+
76
+ for conv in &conversations {
77
+ if seen_set.contains(&conv.id) {
78
+ continue;
79
+ }
80
+ seen_set.insert(conv.id.clone());
81
+
82
+ let caller = conv.contact_name.as_deref().unwrap_or("Unknown agent");
83
+ let summary = conv.summary.as_deref().unwrap_or("New inbound call");
84
+
85
+ let _ = app.notification()
86
+ .builder()
87
+ .title(&format!("Inbound call from {}", caller))
88
+ .body(summary)
89
+ .show();
90
+ }
91
+
92
+ // Prevent unbounded memory growth — cap at 1000 entries
93
+ if seen_set.len() > 1000 {
94
+ seen_set.clear();
95
+ for conv in &conversations {
96
+ seen_set.insert(conv.id.clone());
97
+ }
98
+ }
99
+ }
100
+ });
101
+ }
@@ -0,0 +1,67 @@
1
+ use serde::Serialize;
2
+ use std::process::Command;
3
+
4
+ #[derive(Debug, Serialize)]
5
+ pub struct StartResult {
6
+ pub success: bool,
7
+ pub message: String,
8
+ }
9
+
10
+ /// Find the `a2a` CLI binary
11
+ fn find_a2a_binary() -> Option<String> {
12
+ // Check common locations
13
+ let candidates = [
14
+ "a2a", // In PATH
15
+ ];
16
+
17
+ for candidate in &candidates {
18
+ let result = Command::new("which")
19
+ .arg(candidate)
20
+ .output();
21
+
22
+ if let Ok(output) = result {
23
+ if output.status.success() {
24
+ let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
25
+ if !path.is_empty() {
26
+ return Some(path);
27
+ }
28
+ }
29
+ }
30
+ }
31
+
32
+ None
33
+ }
34
+
35
+ /// Start the a2a server as a detached process
36
+ pub fn start_server() -> StartResult {
37
+ let binary = match find_a2a_binary() {
38
+ Some(b) => b,
39
+ None => {
40
+ return StartResult {
41
+ success: false,
42
+ message: "Could not find 'a2a' CLI. Is a2acalling installed? Run: npm install -g a2acalling".to_string(),
43
+ };
44
+ }
45
+ };
46
+
47
+ let port = crate::discovery::read_config_port().unwrap_or(3001);
48
+ let port_str = port.to_string();
49
+
50
+ let result = Command::new(&binary)
51
+ .args(["server", "--port", &port_str])
52
+ .stdout(std::process::Stdio::null())
53
+ .stderr(std::process::Stdio::null())
54
+ .stdin(std::process::Stdio::null())
55
+ .spawn();
56
+
57
+ match result {
58
+ Ok(_child) => StartResult {
59
+ success: true,
60
+ message: format!("Server starting on port {}...", port),
61
+ },
62
+ Err(err) => StartResult {
63
+ success: false,
64
+ message: format!("Failed to start server: {}", err),
65
+ },
66
+ }
67
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
3
+ "productName": "A2A Callbook",
4
+ "version": "0.1.0",
5
+ "identifier": "com.openclaw.a2a-callbook",
6
+ "build": {
7
+ "frontendDist": "../index.html"
8
+ },
9
+ "app": {
10
+ "windows": [
11
+ {
12
+ "title": "A2A Callbook",
13
+ "width": 1024,
14
+ "height": 720,
15
+ "minWidth": 480,
16
+ "minHeight": 600,
17
+ "resizable": true
18
+ }
19
+ ],
20
+ "security": {
21
+ "dangerousRemoteUrlAccess": [
22
+ { "url": "http://127.0.0.1:**" },
23
+ { "url": "http://localhost:**" }
24
+ ]
25
+ }
26
+ },
27
+ "bundle": {
28
+ "active": true,
29
+ "targets": ["dmg", "app"],
30
+ "icon": [
31
+ "icons/32x32.png",
32
+ "icons/128x128.png",
33
+ "icons/128x128@2x.png",
34
+ "icons/icon.icns"
35
+ ],
36
+ "macOS": {
37
+ "minimumSystemVersion": "12.0",
38
+ "frameworks": []
39
+ }
40
+ },
41
+ "plugins": {
42
+ "deep-link": {
43
+ "desktop": {
44
+ "schemes": ["a2a"]
45
+ }
46
+ }
47
+ }
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.44",
3
+ "version": "0.6.46",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -43,7 +43,56 @@ const result = spawnSync(process.execPath, [cliPath, 'quickstart'], {
43
43
 
44
44
  if (result.error) {
45
45
  // Don't fail the install — the agent will get onboarding when it runs `a2a`.
46
+ installMacOSApp();
46
47
  process.exit(0);
47
48
  }
48
49
 
50
+ installMacOSApp();
49
51
  process.exit(result.status || 0);
52
+
53
+ // Download and install the native macOS app from GitHub Releases
54
+ function installMacOSApp() {
55
+ const os = require('os');
56
+ const fs = require('fs');
57
+
58
+ if (os.platform() !== 'darwin') return;
59
+
60
+ try {
61
+ const version = require('../package.json').version;
62
+ const appDir = path.join(os.homedir(), 'Applications');
63
+ const appPath = path.join(appDir, 'A2A Callbook.app');
64
+
65
+ // Skip if already installed at same version
66
+ const plistPath = path.join(appPath, 'Contents', 'Info.plist');
67
+ if (fs.existsSync(plistPath)) {
68
+ try {
69
+ const plist = fs.readFileSync(plistPath, 'utf8');
70
+ if (plist.includes(version)) {
71
+ return; // Same version already installed
72
+ }
73
+ } catch (_) {}
74
+ }
75
+
76
+ const tarUrl = `https://github.com/onthegonow/a2a_calling/releases/download/v${version}/A2A-Callbook-${version}.app.tar.gz`;
77
+ const tmpFile = path.join(os.tmpdir(), `a2a-callbook-${version}.tar.gz`);
78
+
79
+ // Download
80
+ const { execFileSync } = require('child_process');
81
+ execFileSync('curl', ['-sL', '-o', tmpFile, tarUrl], { timeout: 30000 });
82
+
83
+ if (!fs.existsSync(tmpFile) || fs.statSync(tmpFile).size < 1000) {
84
+ return; // Download failed or too small — skip silently
85
+ }
86
+
87
+ // Ensure ~/Applications exists
88
+ fs.mkdirSync(appDir, { recursive: true });
89
+
90
+ // Extract
91
+ execFileSync('tar', ['-xzf', tmpFile, '-C', appDir], { timeout: 15000 });
92
+
93
+ // Cleanup
94
+ try { fs.unlinkSync(tmpFile); } catch (_) {}
95
+ } catch (_) {
96
+ // Silently fail — native app is optional
97
+ }
98
+ }
@@ -608,6 +608,8 @@ Use ALL available context to build a reasonable disclosure profile. If truly not
608
608
 
609
609
  const jsonBlock = `\`\`\`json
610
610
  {
611
+ "owner_name": "The human owner's real name (extracted from USER.md, git config, etc.)",
612
+ "agent_name": "The agent's display name (extracted from USER.md or workspace context)",
611
613
  "tiers": {
612
614
  "public": {
613
615
  "topics": [