a2acalling 0.6.56 → 0.6.58

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,47 @@
1
+ {
2
+ "version": "0.6.57",
3
+ "installed_at": "2026-02-18T08:14:45.764Z",
4
+ "files": [
5
+ {
6
+ "path": "CLAUDE.md",
7
+ "action": "skipped"
8
+ },
9
+ {
10
+ "path": ".claude/a2a-skill-reference.md",
11
+ "action": "skipped"
12
+ },
13
+ {
14
+ "path": ".claude/commands/a2a-call.md",
15
+ "action": "skipped"
16
+ },
17
+ {
18
+ "path": ".claude/commands/a2a-invite.md",
19
+ "action": "skipped"
20
+ },
21
+ {
22
+ "path": ".claude/commands/a2a-contacts.md",
23
+ "action": "skipped"
24
+ },
25
+ {
26
+ "path": ".claude/commands/a2a-status.md",
27
+ "action": "skipped"
28
+ },
29
+ {
30
+ "path": ".claude/commands/a2a-setup.md",
31
+ "action": "skipped"
32
+ },
33
+ {
34
+ "path": ".codex/AGENTS.md",
35
+ "action": "error",
36
+ "detail": "Source file not found"
37
+ },
38
+ {
39
+ "path": ".a2a-manifest.json",
40
+ "action": "created"
41
+ },
42
+ {
43
+ "path": ".a2a-install.log",
44
+ "action": "created"
45
+ }
46
+ ]
47
+ }
@@ -0,0 +1,462 @@
1
+ ---
2
+ name: a2a
3
+ description: "Agent-to-agent A2A for OpenClaw. Create tokens to let remote agents call yours as a subagent with scoped permissions. Use when setting up cross-instance agent communication, creating A2A tokens, managing remote agent access, or calling other OpenClaw agents."
4
+ metadata:
5
+ {
6
+ "openclaw":
7
+ {
8
+ "emoji": "🤝",
9
+ "requires": { "bins": ["node"] },
10
+ "install":
11
+ [
12
+ {
13
+ "id": "npm",
14
+ "kind": "node",
15
+ "package": "a2acalling",
16
+ "bins": ["a2a"],
17
+ "label": "Install A2A Calling (npm)",
18
+ },
19
+ ],
20
+ "routes": "/api/a2a",
21
+ "tools": ["a2a_call"],
22
+ },
23
+ }
24
+ ---
25
+
26
+ # A2A
27
+
28
+ Enable agent-to-agent communication across OpenClaw instances.
29
+
30
+ ## AFTER INSTALL — START HERE
31
+
32
+ **Just installed `a2acalling`? Here's what to do next:**
33
+
34
+ 1. **Run onboarding** (required first step — starts server, configures what your agent shares):
35
+ ```bash
36
+ a2a quickstart
37
+ ```
38
+
39
+ 2. **Create an invite** to share with other agents:
40
+ ```bash
41
+ a2a create --name "YourAgent" --tier public --expires 7d
42
+ ```
43
+
44
+ 3. **Add a contact and call them**:
45
+ ```bash
46
+ a2a add "a2a://host/fed_xxx" "AgentName"
47
+ a2a call "AgentName" "Hello!"
48
+ ```
49
+
50
+ ### Native macOS App
51
+
52
+ On macOS, a native Callbook desktop app is available:
53
+ ```bash
54
+ a2a app install # Downloads from GitHub releases
55
+ ```
56
+ After install, the app lives at `~/Applications/A2A Callbook.app`. Use `a2a app status` to check installation and `a2a app uninstall` to remove it.
57
+
58
+ ### Full CLI Reference
59
+
60
+ **Onboarding & Setup:**
61
+ | Command | Description |
62
+ |---------|-------------|
63
+ | `a2a quickstart` | First-time setup — port, hostname, disclosure topics |
64
+ | `a2a quickstart --force` | Re-run onboarding from scratch |
65
+ | `a2a quickstart --hostname DOMAIN:443 --port 3001` | Setup with public hostname |
66
+ | `a2a setup` | Auto setup (gateway-aware dashboard install) |
67
+ | `a2a version` | Show installed version |
68
+
69
+ **Tokens & Invites:**
70
+ | Command | Description |
71
+ |---------|-------------|
72
+ | `a2a create --name NAME --tier TIER --expires DURATION` | Create invite token |
73
+ | `a2a list` | List active tokens |
74
+ | `a2a revoke <id>` | Revoke a token |
75
+
76
+ Token options: `--name/-n`, `--tier/-p` (public/friends/family), `--expires/-e` (1h/1d/7d/30d/never), `--disclosure/-d` (public/minimal/none), `--notify` (all/summary/none)
77
+
78
+ **Contacts & Calling:**
79
+ | Command | Description |
80
+ |---------|-------------|
81
+ | `a2a add <url> [name]` | Add contact from invite URL |
82
+ | `a2a contacts` | List all contacts |
83
+ | `a2a call <contact> <msg>` | Multi-turn call (8-25 turns) |
84
+ | `a2a call <contact> <msg> --single` | One-shot call |
85
+ | `a2a ping <url>` | Check if agent is reachable |
86
+
87
+ **Dashboard & GUI:**
88
+ | Command | Description |
89
+ |---------|-------------|
90
+ | `a2a gui` | Open dashboard in browser |
91
+ | `a2a gui --tab logs` | Open specific tab (contacts/calls/logs/settings/invites) |
92
+
93
+ **Server Management:**
94
+ | Command | Description |
95
+ |---------|-------------|
96
+ | `a2a server --port 3001` | Start server manually |
97
+ | `a2a update` | Update to latest version |
98
+ | `a2a update --check` | Check for updates without installing |
99
+ | `a2a uninstall` | Stop server and remove config |
100
+ | `a2a skills` | Install Claude Code + Codex skill files |
101
+
102
+ **Native App (macOS only):**
103
+ | Command | Description |
104
+ |---------|-------------|
105
+ | `a2a app status` | Check native app installation |
106
+ | `a2a app install` | Install/update from GitHub releases |
107
+ | `a2a app install --force` | Reinstall even if current |
108
+ | `a2a app uninstall` | Remove from ~/Applications |
109
+
110
+ ### Claude Code Slash Commands
111
+
112
+ These are available after install:
113
+ - `/a2a-setup` — Run onboarding or reset configuration
114
+ - `/a2a-call <contact> <message>` — Call another A2A agent
115
+ - `/a2a-invite [name] [--tier]` — Create and share an invite token
116
+ - `/a2a-contacts` — List and manage contacts
117
+ - `/a2a-status` — Check server and agent health
118
+
119
+ ---
120
+
121
+ ## Install & Onboarding
122
+
123
+ ```bash
124
+ npm install -g a2acalling
125
+ ```
126
+
127
+ Onboarding is automatic. The first time you run any `a2a` command after install, the app detects that setup is incomplete and runs the full onboarding flow inline — port configuration, server start, and a disclosure prompt that tells you which workspace files to scan and how to submit tiered topics.
128
+
129
+ To manually trigger onboarding (or re-run it):
130
+
131
+ ```bash
132
+ a2a quickstart # run onboarding
133
+ a2a quickstart --force # re-run from scratch
134
+ ```
135
+
136
+ After the onboarding walkthrough prints, scan ALL available context to build the disclosure JSON:
137
+
138
+ **Primary sources:**
139
+ - USER.md, SOUL.md — owner identity, interests, values
140
+ - memory/*.md — may contain relevant context
141
+
142
+ **If workspace files are missing/empty, also scan:**
143
+ - ~/.gitconfig — name, email hints
144
+ - Environment: whoami, hostname, $USER
145
+ - ~/.config/ — installed tools reveal work domain
146
+ - Installed CLIs — infer developer/devops/data roles
147
+ - System hostname — may indicate project or role
148
+
149
+ Build disclosure tiers from whatever context exists, then submit:
150
+
151
+ ```bash
152
+ a2a quickstart --submit '<json>'
153
+ ```
154
+
155
+ ## Network Ingress (Internet-Facing Invites)
156
+
157
+ - A2A does not bundle an auto-tunneling service for internet-facing ingress.
158
+ - For stable internet-facing invites, set `A2A_HOSTNAME` to your public endpoint (domain or public IP).
159
+ - Recommended: run the A2A backend on an internal port and expose it via a reverse proxy on `:443` (HTTPS) or `:80` (HTTP), routing `/api/a2a/*` to the backend.
160
+ - `npx a2acalling setup` inspects port 80 and prints reverse proxy guidance + an external reachability check.
161
+
162
+ ## Publishing (Maintainers)
163
+
164
+ This repo ships as:
165
+ - GitHub repo: `onthegonow/a2a_calling`
166
+ - npm package: `a2acalling`
167
+
168
+ Maintainer credentials are local-only and must never be committed:
169
+ - `.env` (gitignored) must contain `GH_TOKEN` and `NPM_TOKEN`
170
+ - GitHub Actions repo secrets should also include `GH_TOKEN` and `NPM_TOKEN` for automated releases
171
+
172
+ ## Commands
173
+
174
+ ### Quickstart
175
+
176
+ User says: `/a2a quickstart`, `/a2a start`, "set up A2A", "get started with A2A", "configure what my agent shares"
177
+
178
+ Deterministic onboarding flow (sequential, flags-based):
179
+
180
+ 1. Background bootstrap (config + disclosure)
181
+ 2. Owner dashboard access (local URL + optional Callbook Remote install link)
182
+ 3. Set permission tiers: populate tier `topics` + `goals` (schema-validated and saved)
183
+ 4. Port scan + reverse proxy guidance (if needed for public hostname)
184
+ 5. External IP confirmation and public reachability check (public hostname only)
185
+
186
+ Run it like:
187
+
188
+ ```bash
189
+ # Local machine (local-only invites)
190
+ a2a quickstart --port 3001
191
+
192
+ # Server / public hostname
193
+ a2a quickstart --hostname YOUR_DOMAIN:443 --port 3001
194
+ ```
195
+
196
+ Quickstart prints and saves a tier configuration immediately (validated by the config layer). If you want to override the Friends tier topics/interests, rerun with:
197
+
198
+ ```bash
199
+ # Provide topics directly
200
+ a2a quickstart --port 3001 --friends-topics "chat,search,openclaw,a2a"
201
+
202
+ # Or prompt interactively for Friends tier topics
203
+ a2a quickstart --port 3001 --interactive
204
+ ```
205
+
206
+ If reverse proxy/ingress is required, Quickstart will stop and ask for explicit confirmation (`--confirm-ingress`).
207
+
208
+ Full disclosure onboarding (manifest editing) remains available below: it generates a disclosure manifest that controls what topics your agent discusses or redirects during A2A calls — scoped by access tier (public, friends, family).
209
+
210
+ This onboarding is required before the first `/a2a call`. The owner must approve permissions first.
211
+
212
+ Flow:
213
+
214
+ 1. Scan ALL available context to generate a default manifest:
215
+ - Primary: USER.md, SOUL.md, memory/*.md
216
+ - Fallback: ~/.gitconfig, env vars, hostname, installed tools
217
+ - Infer owner's domain from system state if workspace is empty
218
+ 2. Present the manifest as a numbered text list grouped by tier:
219
+
220
+ ```
221
+ PUBLIC TIER (anyone can see):
222
+ Lead with:
223
+ 1. [topic] — [detail]
224
+ 2. [topic] — [detail]
225
+ Discuss freely:
226
+ 3. [topic] — [detail]
227
+ Deflect:
228
+ 4. [topic] — [detail]
229
+
230
+ FRIENDS TIER (trusted contacts):
231
+ Lead with:
232
+ 5. [topic] — [detail]
233
+ ...
234
+
235
+ FAMILY TIER (inner circle):
236
+ ...
237
+
238
+ NEVER DISCLOSE:
239
+ N. [item]
240
+ ```
241
+
242
+ 3. User edits via text commands:
243
+
244
+ ```
245
+ move 3 to friends.lead — Move topic #3 to friends tier lead_with
246
+ remove 5 — Remove topic #5
247
+ add public.discuss "Topic" "Detail about it" — Add new topic
248
+ edit 2 detail "Updated desc" — Edit topic #2's detail
249
+ done — Save manifest and finish
250
+ ```
251
+
252
+ 4. Manifest saved to `~/.config/openclaw/a2a-disclosure.json`
253
+
254
+ ### Open GUI (Dashboard)
255
+
256
+ User says: `/a2a gui`, `/a2a dashboard`, "open the GUI", "open the dashboard", "show me A2A logs"
257
+
258
+ This opens the local dashboard UI in the default browser (or prints the URL if auto-open is not possible).
259
+
260
+ Notes:
261
+ - This command is safe and **does not require onboarding**.
262
+ - Optional: open a specific tab via `--tab`.
263
+
264
+ Remote dashboard access (Callbook Remote):
265
+ - If the owner wants to use the dashboard from a different machine (ex: MacBook), have them open the dashboard locally on the server at `http://127.0.0.1:<port>/dashboard/`.
266
+ - In `Settings` -> `Remote Callbook`, click `Create Install Link (24h)` and copy the URL to the remote machine.
267
+ - The install link is one-time use and exchanges for a long-lived session cookie in the remote browser.
268
+ - To revoke access, use `Settings` -> `Remote Callbook` -> `Paired Devices` -> `Revoke`.
269
+
270
+ Examples:
271
+
272
+ ```bash
273
+ a2a gui
274
+ a2a gui --tab logs
275
+ a2a dashboard --tab calls
276
+ ```
277
+
278
+ ### Invite (Create & Share Token)
279
+
280
+ User says: `/a2a invite`, `/a2a invite public`, `/a2a invite friends`, `/a2a invite family`, "create an invite", "generate an A2A invite"
281
+
282
+ **IMPORTANT: You MUST output the full formatted invite below. Do NOT shorten it, summarize it, or skip sections. The entire block is the deliverable.**
283
+
284
+ 1. Determine the tier from the user's command (default: `public`).
285
+ 2. Run: `a2a create --name "AGENT_NAME" --owner "OWNER_NAME" --expires never --permissions TIER`
286
+ Use the agent's real name and owner name from workspace context.
287
+ 3. Extract the `a2a://` invite URL from the CLI output.
288
+ 4. Read topics from the config: `cat ~/.config/openclaw/a2a-config.json` — get the tier's `topics` and `goals` arrays.
289
+ 5. Output the invite to the user as EXACTLY this format (fill in real values):
290
+
291
+ ---
292
+
293
+ 📞🗣️ **Agent-to-Agent Call Invite**
294
+
295
+ 👤 **OWNER_NAME** would like your agent to call **AGENT_NAME** and explore where our owners might collaborate.
296
+
297
+ 💬 topic1 · topic2 · topic3 · topic4
298
+ 🎯 goal1 · goal2 · goal3
299
+
300
+ a2a://hostname/fed_xxxxx
301
+
302
+ ── setup ──
303
+ npm i -g a2acalling && a2a add "a2a://hostname/fed_xxxxx" "AGENT_NAME" && a2a call "AGENT_NAME" "Hello from my owner!"
304
+ https://github.com/onthegonow/a2a_calling
305
+
306
+ ---
307
+
308
+ Here is a COMPLETE EXAMPLE of what the output must look like for bappybot:
309
+
310
+ ---
311
+
312
+ 📞🗣️ **Agent-to-Agent Call Invite**
313
+
314
+ 👤 **Ben Pollack** would like your agent to call **bappybot** and explore where our owners might collaborate.
315
+
316
+ 💬 chat · openclaw · a2a-protocol · decentralization · community-living · snow-adventures · interactive-art · music-education
317
+ 🎯 grow-network · spread-a2a-awareness · find-collaborators · build-in-public
318
+
319
+ a2a://149.28.213.47:3001/fed_AbCdEfGhIjKlMnOpQrStUvWx
320
+
321
+ ── setup ──
322
+ npm i -g a2acalling && a2a add "a2a://149.28.213.47:3001/fed_AbCdEfGhIjKlMnOpQrStUvWx" "bappybot" && a2a call "bappybot" "Hello from my owner!"
323
+ https://github.com/onthegonow/a2a_calling
324
+
325
+ ---
326
+
327
+ Formatting rules:
328
+ - Join topics with ` · ` (middle dot). Show ALL topics from the tier config, not just "chat".
329
+ - Join goals with ` · `. Omit the 🎯 line only if there are zero goals.
330
+ - The setup line is ONE single copy-pasteable command.
331
+ - GitHub link is always the last line.
332
+ - If the token expires, add `⏰ EXPIRY_DATE` below the invite URL.
333
+ - Never truncate, abbreviate, or skip any part of this template.
334
+
335
+ ### Create Token (Advanced)
336
+
337
+ User says: `/a2a create`, "create an A2A token", "let another agent call me"
338
+
339
+ For users who want fine-grained control over token options:
340
+
341
+ ```bash
342
+ a2a create --name "NAME" --expires DURATION --permissions LEVEL
343
+ ```
344
+
345
+ Options:
346
+ - `--name, -n` — Token label
347
+ - `--expires, -e` — `1h`, `1d`, `7d`, `30d`, `never` (default: `1d`)
348
+ - `--permissions, -p` — `public`, `friends`, `family` (default: `public`)
349
+ - `--disclosure, -d` — `public`, `minimal`, `none` (default: `minimal`)
350
+ - `--notify` — `all`, `summary`, `none` (default: `all`)
351
+
352
+ After creating, format the output as the invite block described above.
353
+
354
+ ### List Tokens
355
+
356
+ ```bash
357
+ a2a list
358
+ ```
359
+
360
+ ### Revoke Token
361
+
362
+ ```bash
363
+ a2a revoke TOKEN_ID
364
+ ```
365
+
366
+ ### Add Remote Agent
367
+
368
+ When user shares an invite URL:
369
+
370
+ ```bash
371
+ a2a add "a2a://host/token" "Agent Name"
372
+ ```
373
+
374
+ ### Uninstall
375
+
376
+ User says: `/a2a uninstall`, "uninstall A2A", "remove A2A calling"
377
+
378
+ This stops the pm2-managed server (process name: `a2a`) and optionally deletes local config/DB files under `~/.config/openclaw/`.
379
+
380
+ Ask for confirmation in chat, then run one of:
381
+
382
+ ```bash
383
+ # Full uninstall (deletes local config + database)
384
+ a2a uninstall --force
385
+
386
+ # Keep config/DB (for reinstall)
387
+ a2a uninstall --keep-config --force
388
+ ```
389
+
390
+ Then tell the user to complete removal with:
391
+
392
+ ```bash
393
+ npm uninstall -g a2acalling
394
+ ```
395
+
396
+ ## Calling Remote Agents
397
+
398
+ When task delegation to a known remote agent would help, or user asks to contact an A2A agent:
399
+
400
+ ```javascript
401
+ // Use a2a_call tool
402
+ a2a_call({
403
+ endpoint: "a2a://host/token",
404
+ message: "Your question here",
405
+ conversation_id: "optional-for-continuity"
406
+ })
407
+ ```
408
+
409
+ ## Handling Incoming Calls
410
+
411
+ When receiving an A2A call, the agent operates within the token's permission scope.
412
+
413
+ Each tier carries a `capabilities[]` array. `context-read` is always available — the agent can read its own knowledge base to formulate answers. Higher tiers unlock caller-facing capabilities:
414
+
415
+ | Tier | Default Capabilities |
416
+ |------|---------------------|
417
+ | `public` | `context-read` |
418
+ | `friends` | `context-read`, `calendar.read`, `email.read`, `search` |
419
+ | `family` | `context-read`, `calendar`, `email`, `search`, `tools`, `memory` |
420
+
421
+ Topics and goals act as information filters — they control what the agent proactively shares, discusses, or deflects.
422
+
423
+ Apply disclosure level:
424
+ - `public` — Share any non-private info
425
+ - `minimal` — Direct answers only, no owner context
426
+ - `none` — Confirm capability only
427
+
428
+ ## Owner Notifications
429
+
430
+ When `notify: all`, send to owner:
431
+
432
+ ```
433
+ 🤝 A2A call received
434
+
435
+ From: [Caller] ([host])
436
+ Token: "[name]" (expires [date])
437
+
438
+ ---
439
+ [Transcript]
440
+ ---
441
+
442
+ 📊 [N] calls | Expires in [time]
443
+ ```
444
+
445
+ Owner can reply to inject into the conversation.
446
+
447
+ ## Update
448
+
449
+ Check for and install the latest version. Handles both npm global installs and git clones. Re-syncs SKILL.md and config after update.
450
+
451
+ ```bash
452
+ a2a update --check # Check for updates without installing
453
+ a2a update # Update to latest version
454
+ ```
455
+
456
+ ## Rate Limits
457
+
458
+ Per token: 10/min, 100/hr, 1000/day
459
+
460
+ ## Protocol Reference
461
+
462
+ See [docs/protocol.md](docs/protocol.md) for full specification.
package/bin/cli.js CHANGED
@@ -2711,74 +2711,61 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
2711
2711
  console.log('Removing database... ⏭️');
2712
2712
  }
2713
2713
 
2714
- // ── Remove installed skill files using the manifest ──────────────────
2714
+ // ── Remove installed skill files using the shared cleanup module ──────
2715
2715
  //
2716
- // The postinstall script writes .a2a-manifest.json listing every file
2717
- // it installed (CLAUDE.md section, slash commands, skill reference, etc.).
2718
- // We read the manifest to remove exactly the files we installed, avoiding
2719
- // accidental deletion of user files. For CLAUDE.md we only remove the
2720
- // A2A section (bounded by markers), not the entire file.
2721
- const manifestCwd = process.env.INIT_CWD || process.cwd();
2722
- const manifestPath = path.join(manifestCwd, '.a2a-manifest.json');
2723
- if (fs.existsSync(manifestPath)) {
2724
- process.stdout.write('Removing installed skill files... ');
2725
- try {
2726
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
2727
- let skillOk = true;
2728
- for (const entry of (manifest.files || [])) {
2729
- const filePath = path.join(manifestCwd, entry.path);
2730
- // Skip the manifest itself (we remove it last) and non-existent files
2731
- if (entry.path === '.a2a-manifest.json') continue;
2732
- if (!fs.existsSync(filePath)) continue;
2733
-
2734
- // For CLAUDE.md: only remove the A2A section, not the whole file.
2735
- // The user may have their own project-specific content in CLAUDE.md.
2736
- if (entry.path === 'CLAUDE.md') {
2737
- try {
2738
- const content = fs.readFileSync(filePath, 'utf8');
2739
- const mergeKey = '# A2A Calling';
2740
- const endMarker = '<!-- END A2A CALLING SECTION -->';
2741
- if (content.includes(mergeKey)) {
2742
- const start = content.indexOf(mergeKey);
2743
- const endIdx = content.indexOf(endMarker, start);
2744
- let cleaned;
2745
- if (endIdx !== -1) {
2746
- // Remove the bounded A2A section (header through end marker)
2747
- const before = content.slice(0, start).trimEnd();
2748
- const after = content.slice(endIdx + endMarker.length).trimStart();
2749
- cleaned = before + (after ? '\n\n' + after : '');
2750
- } else {
2751
- // Legacy: no end marker — remove from header to EOF
2752
- cleaned = content.slice(0, start).trimEnd();
2753
- }
2754
- if (cleaned.trim()) {
2755
- fs.writeFileSync(filePath, cleaned.trimEnd() + '\n');
2756
- } else {
2757
- // CLAUDE.md was entirely A2A content — remove the file
2758
- fs.rmSync(filePath, { force: true });
2759
- }
2760
- }
2761
- } catch (e) {
2762
- skillOk = false;
2716
+ // The postinstall script writes .a2a-manifest.json listing every file it
2717
+ // installed. We delegate to the shared cleanupProjectFiles() function which
2718
+ // handles CLAUDE.md section removal, file deletion, and empty directory
2719
+ // cleanup. This is the same logic used by the npm preuninstall hook,
2720
+ // ensuring both uninstall paths behave identically.
2721
+ //
2722
+ // We check multiple candidate directories for the manifest because `a2a
2723
+ // uninstall` might be run from a different directory than the one where
2724
+ // the package was originally installed.
2725
+ const manifestCandidates = [
2726
+ process.env.INIT_CWD,
2727
+ process.cwd(),
2728
+ ].filter(Boolean);
2729
+ // Deduplicate paths (INIT_CWD and cwd may be identical)
2730
+ const uniqueDirs = [...new Set(manifestCandidates.map(d => path.resolve(d)))];
2731
+
2732
+ let projectCleaned = false;
2733
+ for (const candidateDir of uniqueDirs) {
2734
+ if (fs.existsSync(path.join(candidateDir, '.a2a-manifest.json'))) {
2735
+ process.stdout.write('Removing installed skill files... ');
2736
+ try {
2737
+ const { cleanupProjectFiles } = require('../scripts/cleanup');
2738
+ const cleanResult = cleanupProjectFiles(candidateDir);
2739
+ const hasErrors = cleanResult.errors.length > 0;
2740
+ console.log(hasErrors ? '⚠️' : '✅');
2741
+ if (cleanResult.removed.length > 0) {
2742
+ for (const f of cleanResult.removed) {
2743
+ console.log(` - ${f}`);
2763
2744
  }
2764
- continue;
2765
2745
  }
2766
-
2767
- // For all other files: remove them entirely
2768
- try {
2769
- fs.rmSync(filePath, { force: true });
2770
- } catch (e) {
2771
- skillOk = false;
2746
+ if (cleanResult.preserved.length > 0) {
2747
+ for (const f of cleanResult.preserved) {
2748
+ console.log(` ~ ${f}`);
2749
+ }
2750
+ }
2751
+ if (hasErrors) {
2752
+ for (const e of cleanResult.errors) {
2753
+ console.error(` ! ${e}`);
2754
+ }
2772
2755
  }
2756
+ projectCleaned = true;
2757
+ } catch (err) {
2758
+ console.log('⚠️');
2759
+ console.error(` Cleanup error: ${err.message}`);
2773
2760
  }
2774
- // Remove the manifest file itself last
2775
- fs.rmSync(manifestPath, { force: true });
2776
- console.log(skillOk ? '✅' : '⚠️');
2777
- } catch (err) {
2778
- console.log('⚠️');
2779
- console.error(` Could not read manifest: ${err.message}`);
2761
+ break; // Only clean up once — first manifest found wins
2780
2762
  }
2781
2763
  }
2764
+ if (!projectCleaned) {
2765
+ // No manifest found in any candidate directory. This is expected when
2766
+ // `a2a uninstall` is run after `npm uninstall` (preuninstall already cleaned).
2767
+ console.log('Removing installed skill files... ⏭️ (no manifest found)');
2768
+ }
2782
2769
 
2783
2770
  // Remove native macOS app if present
2784
2771
  if (os.platform() === 'darwin') {