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,246 @@
|
|
|
1
|
+
# Bugfix Plan: A2A-22 + A2A-24
|
|
2
|
+
|
|
3
|
+
> **For Claude:** Execute these fixes on branch `fix/bugs-22-23-24` in `/root/a2acalling`
|
|
4
|
+
|
|
5
|
+
**Goal:** Fix quickstart onboarding gaps (identity, disclosure levels, goals) and surface network warnings during quickstart.
|
|
6
|
+
|
|
7
|
+
**A2A-23 is already fixed** in commit d3fe14e. This plan covers A2A-22 and A2A-24 only.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Bug A2A-22: Quickstart onboarding gaps
|
|
12
|
+
|
|
13
|
+
### Fix 1: Disclosure levels inverted on friends/family tiers
|
|
14
|
+
|
|
15
|
+
**File:** `bin/cli.js` ~line 546-553 (inside `handleDisclosureSubmit`)
|
|
16
|
+
|
|
17
|
+
**Problem:** Friends and family tiers are set to `disclosure: 'minimal'` when they should be more open than public.
|
|
18
|
+
|
|
19
|
+
**Fix:** Change disclosure levels to escalate: public→'minimal', friends→'standard', family→'full'.
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
// BEFORE (broken):
|
|
23
|
+
config.setTier('public', { topics: ..., disclosure: 'public' });
|
|
24
|
+
config.setTier('friends', { topics: ..., disclosure: 'minimal' });
|
|
25
|
+
config.setTier('family', { topics: ..., disclosure: 'minimal' });
|
|
26
|
+
|
|
27
|
+
// AFTER (fixed):
|
|
28
|
+
config.setTier('public', { topics: ..., disclosure: 'minimal' });
|
|
29
|
+
config.setTier('friends', { topics: ..., disclosure: 'standard' });
|
|
30
|
+
config.setTier('family', { topics: ..., disclosure: 'full' });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Rationale:** Public tier should be most restrictive (minimal). Friends get more (standard). Family gets full disclosure. This matches the trust hierarchy.
|
|
34
|
+
|
|
35
|
+
### Fix 2: Extract identity from disclosure submission
|
|
36
|
+
|
|
37
|
+
**File:** `bin/cli.js` ~line 564-568 (inside `handleDisclosureSubmit`)
|
|
38
|
+
|
|
39
|
+
**Problem:** Agent name defaults to 'my-agent', owner name is set to agent name. The disclosure manifest often contains the real owner name in `personality_notes` or the submission JSON, but it's never extracted.
|
|
40
|
+
|
|
41
|
+
**Fix:** After parsing the disclosure JSON, extract identity fields:
|
|
42
|
+
1. Check if the submission JSON has `agent_name` or `owner_name` fields (the extraction prompt should request these)
|
|
43
|
+
2. Fall back to extracting from `personality_notes` (often contains "I'm [Name]" or similar)
|
|
44
|
+
3. Fall back to the OS username
|
|
45
|
+
4. Use extracted values for config and token creation
|
|
46
|
+
|
|
47
|
+
In `handleDisclosureSubmit`, after line ~527 where `result` is validated, add:
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
// Extract identity from disclosure submission
|
|
51
|
+
const ownerName = result.owner_name
|
|
52
|
+
|| result.manifest?.owner_name
|
|
53
|
+
|| extractNameFromPersonality(result.manifest?.personality_notes)
|
|
54
|
+
|| process.env.USER
|
|
55
|
+
|| 'Agent Owner';
|
|
56
|
+
|
|
57
|
+
const agentName = args.flags.name
|
|
58
|
+
|| result.agent_name
|
|
59
|
+
|| config.getAgent().name
|
|
60
|
+
|| process.env.A2A_AGENT_NAME
|
|
61
|
+
|| `${ownerName}'s Agent`;
|
|
62
|
+
|
|
63
|
+
// Save identity to config
|
|
64
|
+
config.setAgent({ name: agentName, owner_name: ownerName });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Add a helper function (before `handleDisclosureSubmit`):
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
function extractNameFromPersonality(notes) {
|
|
71
|
+
if (!notes || typeof notes !== 'string') return null;
|
|
72
|
+
// Look for patterns like "I'm Ben", "My name is Ben", "Owner: Ben"
|
|
73
|
+
const patterns = [
|
|
74
|
+
/(?:I'm|I am|My name is|Name:|Owner:)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/,
|
|
75
|
+
/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(?:is|here|speaking)/
|
|
76
|
+
];
|
|
77
|
+
for (const p of patterns) {
|
|
78
|
+
const m = notes.match(p);
|
|
79
|
+
if (m && m[1]) return m[1].trim();
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Fix 3: Use extracted identity in token creation
|
|
86
|
+
|
|
87
|
+
**File:** `bin/cli.js` ~line 572-582
|
|
88
|
+
|
|
89
|
+
**Problem:** Token uses `agentName` as both name and owner, with hardcoded goals.
|
|
90
|
+
|
|
91
|
+
**Fix:** Use the extracted `ownerName` for the token owner field:
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
// BEFORE:
|
|
95
|
+
const { token } = store.create({
|
|
96
|
+
name: agentName,
|
|
97
|
+
owner: agentName, // wrong — should be owner name
|
|
98
|
+
...
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// AFTER:
|
|
102
|
+
const { token } = store.create({
|
|
103
|
+
name: agentName,
|
|
104
|
+
owner: ownerName,
|
|
105
|
+
...
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Fix 4: Sync disclosure objectives to token goals
|
|
110
|
+
|
|
111
|
+
**File:** `bin/cli.js` ~line 579
|
|
112
|
+
|
|
113
|
+
**Problem:** Token goals are hardcoded `['grow-network', 'find-collaborators', 'build-in-public']` instead of derived from disclosure.
|
|
114
|
+
|
|
115
|
+
**Fix:** Extract objectives from the disclosure manifest and use them as goals:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
// Extract goals from disclosure objectives
|
|
119
|
+
const disclosureObjectives = (result.manifest?.objectives || [])
|
|
120
|
+
.map(o => typeof o === 'string' ? o : (o && o.objective || ''))
|
|
121
|
+
.map(s => s.trim().toLowerCase().replace(/\s+/g, '-').slice(0, 60))
|
|
122
|
+
.filter(Boolean);
|
|
123
|
+
|
|
124
|
+
const tokenGoals = disclosureObjectives.length > 0
|
|
125
|
+
? disclosureObjectives.slice(0, 5)
|
|
126
|
+
: ['grow-network', 'find-collaborators', 'build-in-public'];
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Then use `tokenGoals` in the store.create call:
|
|
130
|
+
```javascript
|
|
131
|
+
allowedGoals: tokenGoals,
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Also sync goals to tier config:
|
|
135
|
+
```javascript
|
|
136
|
+
config.setTier('public', {
|
|
137
|
+
topics: getTierTopics(tiersData.public),
|
|
138
|
+
goals: tokenGoals,
|
|
139
|
+
disclosure: 'minimal'
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Fix 5: Update extraction prompt to request identity fields
|
|
144
|
+
|
|
145
|
+
**File:** `src/lib/disclosure.js` — the `buildExtractionPrompt` function
|
|
146
|
+
|
|
147
|
+
**Problem:** The extraction prompt tells the agent what to scan but doesn't ask for `owner_name` or `agent_name` fields in the JSON output.
|
|
148
|
+
|
|
149
|
+
**Fix:** Add `owner_name` and `agent_name` to the required JSON schema in the prompt. Look for the JSON structure section and add:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
"owner_name": "The human owner's real name (extracted from USER.md, git config, etc.)",
|
|
153
|
+
"agent_name": "The agent's display name (extracted from USER.md or workspace context)",
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Bug A2A-24: Surface network warnings during quickstart
|
|
159
|
+
|
|
160
|
+
### Fix 6: Add connectivity check after server start
|
|
161
|
+
|
|
162
|
+
**File:** `bin/cli.js` — quickstart command, after server start (~line 2017-2019)
|
|
163
|
+
|
|
164
|
+
**Problem:** The verify URL is printed but never executed. No connectivity feedback.
|
|
165
|
+
|
|
166
|
+
**Fix:** After the server starts and the verify URL is printed, actually run the connectivity check:
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
// After line: console.log(`\n Verify: curl -s ${verifyUrl}`);
|
|
170
|
+
// Add actual verification:
|
|
171
|
+
const http = require('http');
|
|
172
|
+
const verifyOk = await new Promise(resolve => {
|
|
173
|
+
const req = http.request({
|
|
174
|
+
hostname: '127.0.0.1',
|
|
175
|
+
port: serverPort,
|
|
176
|
+
path: '/api/a2a/ping',
|
|
177
|
+
method: 'GET',
|
|
178
|
+
timeout: 2000
|
|
179
|
+
}, (res) => {
|
|
180
|
+
res.resume();
|
|
181
|
+
resolve(res.statusCode === 200);
|
|
182
|
+
});
|
|
183
|
+
req.on('error', () => resolve(false));
|
|
184
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
185
|
+
req.end();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (verifyOk) {
|
|
189
|
+
console.log(' ✅ Local connectivity verified');
|
|
190
|
+
} else {
|
|
191
|
+
console.log(' ⚠️ Local server check failed — server may still be starting');
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Fix 7: Surface invite-host warnings during quickstart
|
|
196
|
+
|
|
197
|
+
**File:** `bin/cli.js` — quickstart command, after server start
|
|
198
|
+
|
|
199
|
+
**Problem:** `resolveInviteHost()` returns warnings about NAT/firewall, but they're only shown during `a2a create`, not during quickstart.
|
|
200
|
+
|
|
201
|
+
**Fix:** After `publicHost` is set in quickstart, call `resolveInviteHost` and print its warnings:
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
// After publicHost is determined, resolve and show warnings
|
|
205
|
+
try {
|
|
206
|
+
const { resolveInviteHost } = require('../src/lib/invite-host');
|
|
207
|
+
const resolved = await resolveInviteHost({
|
|
208
|
+
hostname: publicHost,
|
|
209
|
+
port: serverPort
|
|
210
|
+
});
|
|
211
|
+
if (resolved.warnings && resolved.warnings.length) {
|
|
212
|
+
console.log('\n ━━━ Network Warnings ━━━');
|
|
213
|
+
for (const w of resolved.warnings) {
|
|
214
|
+
console.warn(` ⚠️ ${w}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (_) {}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Fix 8: Add non-standard port warning with reverse proxy guidance
|
|
221
|
+
|
|
222
|
+
**File:** `bin/cli.js` — quickstart, already partially handled
|
|
223
|
+
|
|
224
|
+
**Assessment:** Looking at the code at lines 2008-2014, this is already partially implemented — when the user chooses 'continue' on a non-standard port, it prints a brief reminder about reverse proxy setup. But it could be more prominent.
|
|
225
|
+
|
|
226
|
+
**Fix:** Enhance the existing warning. After the "Running on port X (non-standard)" message, add:
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(' ⚠️ Remote agents using your invite URL will try port 80 by default.');
|
|
231
|
+
console.log(' Without a reverse proxy, inbound calls on port 80 will fail silently.');
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Commit Strategy
|
|
237
|
+
|
|
238
|
+
1. First commit: A2A-22 fixes (disclosure levels, identity extraction, goals sync)
|
|
239
|
+
2. Second commit: A2A-24 fixes (connectivity check, network warnings)
|
|
240
|
+
3. Run `npm test` after each commit to verify no regressions
|
|
241
|
+
|
|
242
|
+
## Testing
|
|
243
|
+
|
|
244
|
+
After all fixes:
|
|
245
|
+
- `npm test` — all 269+ tests must pass
|
|
246
|
+
- Manual verification of the logic changes (the reviewer should trace through the code paths)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>A2A Callbook</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'IBM Plex Sans', sans-serif;
|
|
11
|
+
background: linear-gradient(180deg, #eef3f8 0%, #f8f9fb 100%);
|
|
12
|
+
color: #1a1a2e;
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
min-height: 100vh;
|
|
17
|
+
}
|
|
18
|
+
.status-card {
|
|
19
|
+
background: #fff;
|
|
20
|
+
border: 1px solid #d0d7de;
|
|
21
|
+
border-radius: 12px;
|
|
22
|
+
padding: 48px;
|
|
23
|
+
text-align: center;
|
|
24
|
+
max-width: 420px;
|
|
25
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
|
26
|
+
}
|
|
27
|
+
h1 { font-size: 20px; font-weight: 600; margin-bottom: 8px; }
|
|
28
|
+
.subtitle { color: #666; font-size: 14px; margin-bottom: 24px; }
|
|
29
|
+
.status-indicator {
|
|
30
|
+
display: inline-block;
|
|
31
|
+
width: 10px; height: 10px;
|
|
32
|
+
border-radius: 50%;
|
|
33
|
+
margin-right: 8px;
|
|
34
|
+
vertical-align: middle;
|
|
35
|
+
}
|
|
36
|
+
.status-indicator.searching { background: #f59e0b; animation: pulse 1.5s infinite; }
|
|
37
|
+
.status-indicator.disconnected { background: #ef4444; }
|
|
38
|
+
.status-indicator.connected { background: #22c55e; }
|
|
39
|
+
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
40
|
+
.status-text { font-size: 14px; margin-bottom: 24px; color: #444; }
|
|
41
|
+
.port-info { font-size: 12px; color: #888; margin-bottom: 16px; font-family: monospace; }
|
|
42
|
+
button {
|
|
43
|
+
background: #1466c1; color: #fff; border: none; border-radius: 8px;
|
|
44
|
+
padding: 10px 24px; font-size: 14px; cursor: pointer; margin: 4px;
|
|
45
|
+
font-family: inherit;
|
|
46
|
+
}
|
|
47
|
+
button:hover { background: #1052a0; }
|
|
48
|
+
button.secondary {
|
|
49
|
+
background: transparent; color: #1466c1; border: 1px solid #1466c1;
|
|
50
|
+
}
|
|
51
|
+
button.secondary:hover { background: #eef3f8; }
|
|
52
|
+
#error-detail { color: #ef4444; font-size: 12px; margin-top: 12px; display: none; }
|
|
53
|
+
</style>
|
|
54
|
+
</head>
|
|
55
|
+
<body>
|
|
56
|
+
<div class="status-card">
|
|
57
|
+
<h1>A2A Callbook</h1>
|
|
58
|
+
<p class="subtitle">Agent-to-agent communication dashboard</p>
|
|
59
|
+
|
|
60
|
+
<div id="status-searching">
|
|
61
|
+
<p class="status-text">
|
|
62
|
+
<span class="status-indicator searching"></span>
|
|
63
|
+
Looking for a2a server...
|
|
64
|
+
</p>
|
|
65
|
+
<p class="port-info" id="port-info">Scanning ports: 3001, 80, 8080, 8443, 9001</p>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div id="status-not-found" style="display:none;">
|
|
69
|
+
<p class="status-text">
|
|
70
|
+
<span class="status-indicator disconnected"></span>
|
|
71
|
+
Server not running
|
|
72
|
+
</p>
|
|
73
|
+
<p class="port-info" id="last-port">No a2a server found on common ports</p>
|
|
74
|
+
<button id="btn-start">Start Server</button>
|
|
75
|
+
<button id="btn-retry" class="secondary">Retry</button>
|
|
76
|
+
<p id="error-detail"></p>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div id="status-connected" style="display:none;">
|
|
80
|
+
<p class="status-text">
|
|
81
|
+
<span class="status-indicator connected"></span>
|
|
82
|
+
Connected to server
|
|
83
|
+
</p>
|
|
84
|
+
<p class="port-info" id="connected-port"></p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<script>
|
|
89
|
+
const { invoke } = window.__TAURI__.core;
|
|
90
|
+
|
|
91
|
+
async function checkServer() {
|
|
92
|
+
show('status-searching');
|
|
93
|
+
try {
|
|
94
|
+
const result = await invoke('discover_server');
|
|
95
|
+
if (result.port) {
|
|
96
|
+
show('status-connected');
|
|
97
|
+
document.getElementById('connected-port').textContent =
|
|
98
|
+
`localhost:${result.port}`;
|
|
99
|
+
// Navigate to live SPA
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
window.location.href =
|
|
102
|
+
`http://127.0.0.1:${result.port}/api/a2a/dashboard/` +
|
|
103
|
+
(window.__TAB_HASH || '');
|
|
104
|
+
}, 400);
|
|
105
|
+
} else {
|
|
106
|
+
show('status-not-found');
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
show('status-not-found');
|
|
110
|
+
const detail = document.getElementById('error-detail');
|
|
111
|
+
detail.textContent = err;
|
|
112
|
+
detail.style.display = 'block';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function show(id) {
|
|
117
|
+
['status-searching', 'status-not-found', 'status-connected']
|
|
118
|
+
.forEach(s => document.getElementById(s).style.display = 'none');
|
|
119
|
+
document.getElementById(id).style.display = 'block';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
document.getElementById('btn-start')?.addEventListener('click', async () => {
|
|
123
|
+
try {
|
|
124
|
+
await invoke('start_server');
|
|
125
|
+
// Wait for server to boot, then retry
|
|
126
|
+
setTimeout(checkServer, 2000);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
const detail = document.getElementById('error-detail');
|
|
129
|
+
detail.textContent = `Failed to start: ${err}`;
|
|
130
|
+
detail.style.display = 'block';
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
document.getElementById('btn-retry')?.addEventListener('click', checkServer);
|
|
135
|
+
|
|
136
|
+
// Start discovery on load
|
|
137
|
+
checkServer();
|
|
138
|
+
|
|
139
|
+
// Listen for server disconnect/reconnect from Tauri backend
|
|
140
|
+
const { listen } = window.__TAURI__.event;
|
|
141
|
+
|
|
142
|
+
listen('server-status', (event) => {
|
|
143
|
+
const { connected, port } = event.payload;
|
|
144
|
+
if (!connected) {
|
|
145
|
+
showReconnectionOverlay();
|
|
146
|
+
} else {
|
|
147
|
+
hideReconnectionOverlay();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
function showReconnectionOverlay() {
|
|
152
|
+
if (document.getElementById('reconnect-overlay')) return;
|
|
153
|
+
const overlay = document.createElement('div');
|
|
154
|
+
overlay.id = 'reconnect-overlay';
|
|
155
|
+
overlay.innerHTML = `
|
|
156
|
+
<div style="position:fixed;top:0;left:0;right:0;z-index:9999;
|
|
157
|
+
background:#fef3c7;border-bottom:2px solid #f59e0b;padding:12px 24px;
|
|
158
|
+
text-align:center;font-family:-apple-system,sans-serif;font-size:14px;color:#92400e;">
|
|
159
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;
|
|
160
|
+
background:#f59e0b;margin-right:8px;animation:pulse 1.5s infinite;vertical-align:middle;"></span>
|
|
161
|
+
Server disconnected — Reconnecting...
|
|
162
|
+
</div>`;
|
|
163
|
+
document.body.appendChild(overlay);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function hideReconnectionOverlay() {
|
|
167
|
+
const overlay = document.getElementById('reconnect-overlay');
|
|
168
|
+
if (overlay) overlay.remove();
|
|
169
|
+
}
|
|
170
|
+
</script>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "a2a-callbook"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
name = "a2a_callbook_lib"
|
|
8
|
+
crate-type = ["lib", "cdylib", "staticlib"]
|
|
9
|
+
|
|
10
|
+
[build-dependencies]
|
|
11
|
+
tauri-build = { version = "2", features = [] }
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
tauri = { version = "2", features = [] }
|
|
15
|
+
tauri-plugin-shell = "2"
|
|
16
|
+
tauri-plugin-notification = "2"
|
|
17
|
+
tauri-plugin-deep-link = "2"
|
|
18
|
+
tauri-plugin-window-state = "2"
|
|
19
|
+
serde = { version = "1", features = ["derive"] }
|
|
20
|
+
serde_json = "1"
|
|
21
|
+
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
|
22
|
+
tokio = { version = "1", features = ["full"] }
|
|
23
|
+
dirs = "6"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-utils/schema.json",
|
|
3
|
+
"identifier": "default",
|
|
4
|
+
"description": "Default capabilities for A2A Callbook",
|
|
5
|
+
"windows": ["main"],
|
|
6
|
+
"permissions": [
|
|
7
|
+
"core:default",
|
|
8
|
+
"shell:allow-open",
|
|
9
|
+
"notification:default",
|
|
10
|
+
"notification:allow-is-permission-granted",
|
|
11
|
+
"notification:allow-request-permission",
|
|
12
|
+
"notification:allow-notify",
|
|
13
|
+
"deep-link:default",
|
|
14
|
+
"window-state:default"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use std::path::PathBuf;
|
|
3
|
+
use std::time::Duration;
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PORTS: &[u16] = &[3001, 80, 8080, 8443, 9001];
|
|
6
|
+
const PROBE_TIMEOUT: Duration = Duration::from_millis(800);
|
|
7
|
+
|
|
8
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
9
|
+
pub struct DiscoveryResult {
|
|
10
|
+
pub port: Option<u16>,
|
|
11
|
+
pub source: String, // "config" | "scan" | "none"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[derive(Debug, Deserialize)]
|
|
15
|
+
struct A2AConfig {
|
|
16
|
+
onboarding: Option<OnboardingConfig>,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[derive(Debug, Deserialize)]
|
|
20
|
+
struct OnboardingConfig {
|
|
21
|
+
server_port: Option<u16>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Read port from ~/.config/openclaw/a2a-config.json
|
|
25
|
+
pub fn read_config_port() -> Option<u16> {
|
|
26
|
+
let config_dir = std::env::var("A2A_CONFIG_DIR")
|
|
27
|
+
.or_else(|_| std::env::var("OPENCLAW_CONFIG_DIR"))
|
|
28
|
+
.map(PathBuf::from)
|
|
29
|
+
.unwrap_or_else(|_| {
|
|
30
|
+
dirs::home_dir()
|
|
31
|
+
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
|
32
|
+
.join(".config")
|
|
33
|
+
.join("openclaw")
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
let config_path = config_dir.join("a2a-config.json");
|
|
37
|
+
let content = std::fs::read_to_string(config_path).ok()?;
|
|
38
|
+
let config: A2AConfig = serde_json::from_str(&content).ok()?;
|
|
39
|
+
config.onboarding?.server_port
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Probe a single port — returns true if a2a server responds
|
|
43
|
+
async fn probe_port(port: u16) -> bool {
|
|
44
|
+
let url = format!("http://127.0.0.1:{}/api/a2a/ping", port);
|
|
45
|
+
let client = reqwest::Client::builder()
|
|
46
|
+
.timeout(PROBE_TIMEOUT)
|
|
47
|
+
.build();
|
|
48
|
+
|
|
49
|
+
let client = match client {
|
|
50
|
+
Ok(c) => c,
|
|
51
|
+
Err(_) => return false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
match client.get(&url).send().await {
|
|
55
|
+
Ok(resp) => resp.status().is_success(),
|
|
56
|
+
Err(_) => false,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Discover the running a2a server
|
|
61
|
+
pub async fn discover_server() -> DiscoveryResult {
|
|
62
|
+
// 1. Try config port first
|
|
63
|
+
if let Some(port) = read_config_port() {
|
|
64
|
+
if probe_port(port).await {
|
|
65
|
+
return DiscoveryResult {
|
|
66
|
+
port: Some(port),
|
|
67
|
+
source: "config".to_string(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. Scan default ports
|
|
73
|
+
for &port in DEFAULT_PORTS {
|
|
74
|
+
if probe_port(port).await {
|
|
75
|
+
return DiscoveryResult {
|
|
76
|
+
port: Some(port),
|
|
77
|
+
source: "scan".to_string(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
DiscoveryResult {
|
|
83
|
+
port: None,
|
|
84
|
+
source: "none".to_string(),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
use std::sync::atomic::{AtomicBool, AtomicU16, Ordering};
|
|
2
|
+
use std::sync::Arc;
|
|
3
|
+
use std::time::Duration;
|
|
4
|
+
use tauri::{Emitter, Manager};
|
|
5
|
+
|
|
6
|
+
static CONNECTED: AtomicBool = AtomicBool::new(false);
|
|
7
|
+
static CURRENT_PORT: AtomicU16 = AtomicU16::new(0);
|
|
8
|
+
|
|
9
|
+
pub fn is_connected() -> bool {
|
|
10
|
+
CONNECTED.load(Ordering::Relaxed)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub fn current_port() -> u16 {
|
|
14
|
+
CURRENT_PORT.load(Ordering::Relaxed)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn set_connected(port: u16) {
|
|
18
|
+
CURRENT_PORT.store(port, Ordering::Relaxed);
|
|
19
|
+
CONNECTED.store(true, Ordering::Relaxed);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Start background health check loop — emits "server-status" events
|
|
23
|
+
pub fn start_health_monitor(app: tauri::AppHandle) {
|
|
24
|
+
let handle = Arc::new(app);
|
|
25
|
+
tokio::spawn(async move {
|
|
26
|
+
loop {
|
|
27
|
+
tokio::time::sleep(Duration::from_secs(3)).await;
|
|
28
|
+
|
|
29
|
+
let port = CURRENT_PORT.load(Ordering::Relaxed);
|
|
30
|
+
if port == 0 {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let url = format!("http://127.0.0.1:{}/api/a2a/ping", port);
|
|
35
|
+
let client = match reqwest::Client::builder()
|
|
36
|
+
.timeout(Duration::from_millis(1500))
|
|
37
|
+
.build() {
|
|
38
|
+
Ok(c) => c,
|
|
39
|
+
Err(_) => continue,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let ok = match client.get(&url).send().await {
|
|
43
|
+
Ok(resp) => resp.status().is_success(),
|
|
44
|
+
Err(_) => false,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let was_connected = CONNECTED.swap(ok, Ordering::Relaxed);
|
|
48
|
+
|
|
49
|
+
// Only emit on state change
|
|
50
|
+
if ok != was_connected {
|
|
51
|
+
let _ = handle.emit("server-status", serde_json::json!({
|
|
52
|
+
"connected": ok,
|
|
53
|
+
"port": port
|
|
54
|
+
}));
|
|
55
|
+
// Navigate back to loader page on disconnect so reconnection UI is shown
|
|
56
|
+
if !ok {
|
|
57
|
+
if let Some(window) = handle.get_webview_window("main") {
|
|
58
|
+
let _ = window.navigate("tauri://localhost".parse().unwrap());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|