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
|
@@ -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, ©, &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,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
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
+
}
|
package/src/lib/disclosure.js
CHANGED
|
@@ -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": [
|