antenna-fyi 0.5.2 → 0.6.1

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 CHANGED
@@ -49,6 +49,29 @@ This starts a stdio-based MCP server with tools:
49
49
  - `antenna_accept` — Accept a match
50
50
  - `antenna_check_matches` — Check match status
51
51
 
52
+ ## Hermes Agent Integration
53
+
54
+ ### Option 1: MCP Server (recommended)
55
+
56
+ Add to `~/.hermes/config.yaml`:
57
+
58
+ ```yaml
59
+ mcp_servers:
60
+ antenna:
61
+ command: "antenna"
62
+ args: ["serve"]
63
+ ```
64
+
65
+ Hermes will auto-discover `mcp_antenna_scan`, `mcp_antenna_profile`, etc.
66
+
67
+ ### Option 2: One-step install (Plugin + Skill + deps)
68
+
69
+ ```bash
70
+ antenna install-hermes
71
+ ```
72
+
73
+ Done. Restart Hermes.
74
+
52
75
  ## OpenClaw Integration
53
76
 
54
77
  ### Install Skill (recommended)
package/bin/antenna.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  handleStatus,
12
12
  handleInstallSkill,
13
13
  handleInstallPlugin,
14
+ handleInstallHermesPlugin,
14
15
  printHelp,
15
16
  } from "../lib/cli.js";
16
17
 
@@ -41,6 +42,8 @@ async function main() {
41
42
  return handleInstallSkill();
42
43
  case "install-plugin":
43
44
  return handleInstallPlugin();
45
+ case "install-hermes":
46
+ return handleInstallHermesPlugin();
44
47
  case "help":
45
48
  default:
46
49
  return printHelp();
package/lib/cli.js CHANGED
@@ -6,6 +6,7 @@ import { existsSync, mkdirSync, copyFileSync } from "fs";
6
6
  import { join, dirname } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import { homedir } from "os";
9
+ import { execSync } from "child_process";
9
10
 
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = dirname(__filename);
@@ -162,16 +163,36 @@ export async function handleStatus(f) {
162
163
  }
163
164
 
164
165
  export function handleInstallSkill() {
165
- const skillDir = join(homedir(), ".openclaw", "skills", "antenna");
166
166
  const skillSrc = join(__dirname, "..", "skill", "SKILL.md");
167
+ let installed = 0;
167
168
 
168
- if (!existsSync(skillDir)) {
169
- mkdirSync(skillDir, { recursive: true });
169
+ // OpenClaw
170
+ const openclawDir = join(homedir(), ".openclaw", "skills", "antenna");
171
+ if (existsSync(join(homedir(), ".openclaw"))) {
172
+ if (!existsSync(openclawDir)) mkdirSync(openclawDir, { recursive: true });
173
+ copyFileSync(skillSrc, join(openclawDir, "SKILL.md"));
174
+ console.log("✅ SKILL.md installed to ~/.openclaw/skills/antenna/");
175
+ installed++;
170
176
  }
171
177
 
172
- copyFileSync(skillSrc, join(skillDir, "SKILL.md"));
173
- console.log("✅ SKILL.md installed to ~/.openclaw/skills/antenna/");
174
- console.log(" Restart OpenClaw to pick it up.");
178
+ // Hermes
179
+ const hermesDir = join(homedir(), ".hermes", "skills", "antenna");
180
+ if (existsSync(join(homedir(), ".hermes"))) {
181
+ if (!existsSync(hermesDir)) mkdirSync(hermesDir, { recursive: true });
182
+ copyFileSync(skillSrc, join(hermesDir, "SKILL.md"));
183
+ console.log("✅ SKILL.md installed to ~/.hermes/skills/antenna/");
184
+ installed++;
185
+ }
186
+
187
+ if (installed === 0) {
188
+ // Neither found, default to OpenClaw path
189
+ if (!existsSync(openclawDir)) mkdirSync(openclawDir, { recursive: true });
190
+ copyFileSync(skillSrc, join(openclawDir, "SKILL.md"));
191
+ console.log("✅ SKILL.md installed to ~/.openclaw/skills/antenna/");
192
+ console.log(" (Neither ~/.openclaw nor ~/.hermes detected — defaulted to OpenClaw)");
193
+ }
194
+
195
+ console.log(" Restart your agent to pick it up.");
175
196
  }
176
197
 
177
198
  export function handleInstallPlugin() {
@@ -189,13 +210,63 @@ export function handleInstallPlugin() {
189
210
  }
190
211
  }
191
212
 
192
- console.log("\n✅ Plugin template files copied to current directory.");
213
+ console.log("\n✅ OpenClaw Plugin template copied to current directory.");
193
214
  console.log(" Next steps:");
194
215
  console.log(" 1. Run: npm install");
195
216
  console.log(" 2. Run: openclaw plugins install .");
196
217
  console.log();
197
218
  }
198
219
 
220
+ export function handleInstallHermesPlugin() {
221
+ const templateDir = join(__dirname, "hermes-plugin");
222
+ const hermesHome = join(homedir(), ".hermes");
223
+ const pluginDir = join(hermesHome, "plugins", "antenna");
224
+ const skillDir = join(hermesHome, "skills", "antenna");
225
+ const skillSrc = join(__dirname, "..", "skill", "SKILL.md");
226
+ const pluginFiles = ["plugin.yaml", "__init__.py", "schemas.py", "tools.py"];
227
+
228
+ if (!existsSync(hermesHome)) {
229
+ console.error("❌ ~/.hermes not found. Is Hermes Agent installed?");
230
+ console.error(" Install: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash");
231
+ return;
232
+ }
233
+
234
+ // 1. Install Plugin
235
+ if (!existsSync(pluginDir)) mkdirSync(pluginDir, { recursive: true });
236
+ for (const file of pluginFiles) {
237
+ const src = join(templateDir, file);
238
+ const dest = join(pluginDir, file);
239
+ if (existsSync(src)) {
240
+ copyFileSync(src, dest);
241
+ console.log(`📄 Plugin: ${file}`);
242
+ }
243
+ }
244
+
245
+ // 2. Install Skill
246
+ if (!existsSync(skillDir)) mkdirSync(skillDir, { recursive: true });
247
+ copyFileSync(skillSrc, join(skillDir, "SKILL.md"));
248
+ console.log("📄 Skill: SKILL.md");
249
+
250
+ // 3. Auto-install supabase-py
251
+ console.log("\n📦 Installing supabase-py...");
252
+ const hermesAgent = join(hermesHome, "hermes-agent");
253
+ try {
254
+ if (existsSync(hermesAgent)) {
255
+ execSync("uv pip install supabase", { cwd: hermesAgent, stdio: "inherit", timeout: 60_000 });
256
+ } else {
257
+ execSync("pip install supabase", { stdio: "inherit", timeout: 60_000 });
258
+ }
259
+ console.log("✅ supabase-py installed");
260
+ } catch {
261
+ console.log("⚠️ Could not auto-install supabase-py. Run manually:");
262
+ console.log(" cd ~/.hermes/hermes-agent && uv pip install supabase");
263
+ }
264
+
265
+ console.log("\n✅ Antenna installed for Hermes! (Plugin + Skill + deps)");
266
+ console.log(" Restart Hermes to activate.");
267
+ console.log();
268
+ }
269
+
199
270
  export function printHelp() {
200
271
  console.log(`📡 Antenna — nearby people discovery
201
272
 
@@ -208,8 +279,9 @@ Usage:
208
279
  antenna serve Start MCP server (stdio transport)
209
280
  antenna setup Interactive profile setup [--id telegram:123]
210
281
  antenna status Show config & status [--id telegram:123]
211
- antenna install-skill Install SKILL.md to ~/.openclaw/skills/antenna/
212
- antenna install-plugin Copy plugin template to current directory
282
+ antenna install-skill Install SKILL.md (detects OpenClaw + Hermes)
283
+ antenna install-plugin Copy OpenClaw plugin template to cwd
284
+ antenna install-hermes One-step Hermes setup (Plugin + Skill + deps)
213
285
  antenna help Show this help
214
286
 
215
287
  Environment:
@@ -0,0 +1,69 @@
1
+ """Antenna — Hermes Agent Plugin
2
+
3
+ Nearby people discovery. Registers 5 tools and a pre_llm_call hook
4
+ that auto-detects location data and prompts the agent to scan.
5
+
6
+ Drop this directory into ~/.hermes/plugins/antenna/
7
+ """
8
+
9
+ from .tools import (
10
+ handle_scan,
11
+ handle_profile,
12
+ handle_accept,
13
+ handle_checkin,
14
+ handle_check_matches,
15
+ SCAN_SCHEMA,
16
+ PROFILE_SCHEMA,
17
+ ACCEPT_SCHEMA,
18
+ CHECKIN_SCHEMA,
19
+ CHECK_MATCHES_SCHEMA,
20
+ )
21
+ import re
22
+
23
+
24
+ def register(ctx):
25
+ # ── Tools ─────────────────────────────────────────────────────
26
+ ctx.register_tool("antenna_scan", SCAN_SCHEMA, handle_scan)
27
+ ctx.register_tool("antenna_profile", PROFILE_SCHEMA, handle_profile)
28
+ ctx.register_tool("antenna_accept", ACCEPT_SCHEMA, handle_accept)
29
+ ctx.register_tool("antenna_checkin", CHECKIN_SCHEMA, handle_checkin)
30
+ ctx.register_tool("antenna_check_matches", CHECK_MATCHES_SCHEMA, handle_check_matches)
31
+
32
+ # ── Hook: auto-detect location in messages ────────────────────
33
+ def on_pre_llm(messages, **kwargs):
34
+ """Check last user message for location data and inject scan hint."""
35
+ if not messages:
36
+ return None
37
+
38
+ last_msg = messages[-1] if isinstance(messages[-1], dict) else {}
39
+ content = last_msg.get("content", "")
40
+ if not isinstance(content, str):
41
+ return None
42
+
43
+ # Pattern 1: Hermes gateway may serialize location as text
44
+ # e.g. "📍 Location shared: 39.9908, 116.4872"
45
+ loc_match = re.search(
46
+ r"[Ll]ocation.*?(-?\d+\.?\d*)[,\s]+(-?\d+\.?\d*)", content
47
+ )
48
+
49
+ # Pattern 2: raw coordinates in message
50
+ if not loc_match:
51
+ loc_match = re.search(
52
+ r"(-?\d{1,3}\.\d{3,})[,\s]+(-?\d{1,3}\.\d{3,})", content
53
+ )
54
+
55
+ if loc_match:
56
+ lat, lng = loc_match.group(1), loc_match.group(2)
57
+ return {
58
+ "context": (
59
+ f"\n\n[Antenna] 📍 检测到位置 ({lat}, {lng})。"
60
+ f"请使用 antenna_scan 工具查看附近有谁。"
61
+ f"参数:lat={lat}, lng={lng},sender_id 和 channel 从消息上下文获取。"
62
+ )
63
+ }
64
+
65
+ return None
66
+
67
+ ctx.register_hook("pre_llm_call", on_pre_llm)
68
+
69
+ print("[Antenna] Plugin loaded 📡")
@@ -0,0 +1,7 @@
1
+ name: antenna
2
+ version: "0.5.2"
3
+ description: |
4
+ Nearby people discovery — scan for people around you, set up your profile card,
5
+ accept matches, and check match status. Uses Supabase as shared backend.
6
+ Triggers automatically when location data is detected.
7
+ requires_env: []
@@ -0,0 +1,117 @@
1
+ """Antenna tool schemas — what the LLM sees."""
2
+
3
+ SCAN_SCHEMA = {
4
+ "name": "antenna_scan",
5
+ "description": (
6
+ "Scan for nearby people at a given location. Returns raw profile cards "
7
+ "of nearby people — the agent should read these cards and decide who to "
8
+ "recommend based on its understanding of the user."
9
+ ),
10
+ "parameters": {
11
+ "type": "object",
12
+ "properties": {
13
+ "lat": {"type": "number", "description": "Latitude"},
14
+ "lng": {"type": "number", "description": "Longitude"},
15
+ "radius_m": {
16
+ "type": "number",
17
+ "description": "Search radius in meters (default: 500)",
18
+ },
19
+ "sender_id": {
20
+ "type": "string",
21
+ "description": "The sender's user ID (from message context)",
22
+ },
23
+ "channel": {
24
+ "type": "string",
25
+ "description": "Platform name (telegram, discord, etc.)",
26
+ },
27
+ },
28
+ "required": ["lat", "lng", "sender_id", "channel"],
29
+ },
30
+ }
31
+
32
+ PROFILE_SCHEMA = {
33
+ "name": "antenna_profile",
34
+ "description": (
35
+ "View or update the user's Antenna profile (name card). "
36
+ "The profile has a display name, emoji, and three lines."
37
+ ),
38
+ "parameters": {
39
+ "type": "object",
40
+ "properties": {
41
+ "action": {
42
+ "type": "string",
43
+ "enum": ["get", "set"],
44
+ "description": "'get' to view, 'set' to update",
45
+ },
46
+ "sender_id": {"type": "string"},
47
+ "channel": {"type": "string"},
48
+ "display_name": {"type": "string", "description": "Display name"},
49
+ "emoji": {"type": "string", "description": "Profile emoji"},
50
+ "line1": {"type": "string", "description": "Who you are / what you do"},
51
+ "line2": {"type": "string", "description": "What you're into"},
52
+ "line3": {"type": "string", "description": "What you're looking for"},
53
+ "visible": {"type": "boolean", "description": "Visible to others"},
54
+ },
55
+ "required": ["action", "sender_id", "channel"],
56
+ },
57
+ }
58
+
59
+ ACCEPT_SCHEMA = {
60
+ "name": "antenna_accept",
61
+ "description": (
62
+ "Accept a match. Optionally share contact info. "
63
+ "If both sides accept, they can exchange contact info."
64
+ ),
65
+ "parameters": {
66
+ "type": "object",
67
+ "properties": {
68
+ "sender_id": {"type": "string"},
69
+ "channel": {"type": "string"},
70
+ "target_device_id": {
71
+ "type": "string",
72
+ "description": "Device ID of the person to accept",
73
+ },
74
+ "contact_info": {
75
+ "type": "string",
76
+ "description": "Contact info to share (e.g. 'WeChat: yi')",
77
+ },
78
+ },
79
+ "required": ["sender_id", "channel", "target_device_id"],
80
+ },
81
+ }
82
+
83
+ CHECKIN_SCHEMA = {
84
+ "name": "antenna_checkin",
85
+ "description": (
86
+ "Check in at a location — update your position so others can find you."
87
+ ),
88
+ "parameters": {
89
+ "type": "object",
90
+ "properties": {
91
+ "lat": {"type": "number", "description": "Latitude"},
92
+ "lng": {"type": "number", "description": "Longitude"},
93
+ "sender_id": {"type": "string"},
94
+ "channel": {"type": "string"},
95
+ "place_name": {
96
+ "type": "string",
97
+ "description": "Name of the place (optional)",
98
+ },
99
+ },
100
+ "required": ["lat", "lng", "sender_id", "channel"],
101
+ },
102
+ }
103
+
104
+ CHECK_MATCHES_SCHEMA = {
105
+ "name": "antenna_check_matches",
106
+ "description": (
107
+ "Check for mutual matches and incoming accepts."
108
+ ),
109
+ "parameters": {
110
+ "type": "object",
111
+ "properties": {
112
+ "sender_id": {"type": "string"},
113
+ "channel": {"type": "string"},
114
+ },
115
+ "required": ["sender_id", "channel"],
116
+ },
117
+ }
@@ -0,0 +1,244 @@
1
+ """Antenna tool handlers — what runs when called.
2
+
3
+ Uses the Supabase REST API via supabase-py. Falls back to the built-in
4
+ shared backend if no env vars are set.
5
+ """
6
+
7
+ import json
8
+ import math
9
+ import os
10
+ import time
11
+
12
+ try:
13
+ from supabase import create_client
14
+ except ImportError:
15
+ create_client = None # Will fail at runtime with helpful message
16
+
17
+ # ─── Config ───────────────────────────────────────────────────────────
18
+
19
+ BUILTIN_URL = "https://bcudjloikmpcqwcptuyd.supabase.co"
20
+ BUILTIN_KEY = (
21
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
22
+ "eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJjdWRqbG9pa21wY3F3Y3B0dXlkIiwi"
23
+ "cm9sZSI6ImFub24iLCJpYXQiOjE3NzQ0MTg1NDgsImV4cCI6MjA4OTk5NDU0OH0."
24
+ "FaoC3QfpfHP1npNGjRchJAoAp2PdZtQe_WhP-t-GN1o"
25
+ )
26
+
27
+ _client = None
28
+ _client_url = None
29
+ _last_scan: dict[str, float] = {}
30
+ SCAN_DEBOUNCE_S = 30
31
+
32
+
33
+ def _get_url():
34
+ return os.environ.get("ANTENNA_SUPABASE_URL") or os.environ.get("ANTENNA_URL") or BUILTIN_URL
35
+
36
+
37
+ def _get_key():
38
+ return os.environ.get("ANTENNA_SUPABASE_KEY") or os.environ.get("ANTENNA_KEY") or BUILTIN_KEY
39
+
40
+
41
+ def _sb():
42
+ global _client, _client_url
43
+ if create_client is None:
44
+ raise RuntimeError(
45
+ "supabase-py not installed. Run: pip install supabase"
46
+ )
47
+ url = _get_url()
48
+ if _client is None or _client_url != url:
49
+ _client = create_client(url, _get_key())
50
+ _client_url = url
51
+ return _client
52
+
53
+
54
+ def _device_id(sender_id: str, channel: str) -> str:
55
+ return f"{channel}:{sender_id}"
56
+
57
+
58
+ def _fuzzy(lat: float, lng: float) -> tuple[float, float]:
59
+ return round(lat * 1000) / 1000, round(lng * 1000) / 1000
60
+
61
+
62
+ def _ok(data) -> str:
63
+ return json.dumps(data, ensure_ascii=False)
64
+
65
+
66
+ # ─── Handlers ─────────────────────────────────────────────────────────
67
+
68
+ def handle_scan(params: dict) -> str:
69
+ sb = _sb()
70
+ did = _device_id(params["sender_id"], params["channel"])
71
+ radius = params.get("radius_m", 500)
72
+
73
+ # Rate limit
74
+ now = time.time()
75
+ if did in _last_scan and now - _last_scan[did] < SCAN_DEBOUNCE_S:
76
+ return _ok({"nearby": [], "message": "刚刚才扫描过,稍等一会儿再试。", "rate_limited": True})
77
+ _last_scan[did] = now
78
+
79
+ flat, flng = _fuzzy(params["lat"], params["lng"])
80
+
81
+ # Update own location
82
+ sb.rpc("upsert_profile_location", {
83
+ "p_device_id": did, "p_lng": flng, "p_lat": flat,
84
+ }).execute()
85
+
86
+ # Query nearby
87
+ resp = sb.rpc("nearby_profiles", {
88
+ "p_lat": flat, "p_lng": flng, "p_radius_m": radius,
89
+ }).execute()
90
+
91
+ others = [p for p in (resp.data or []) if p.get("device_id") != did]
92
+
93
+ if not others:
94
+ return _ok({"nearby": [], "message": f"在 {radius}m 范围内没有发现其他人。"})
95
+
96
+ return _ok({
97
+ "nearby": [
98
+ {
99
+ "device_id": p.get("device_id"),
100
+ "emoji": p.get("emoji") or "👤",
101
+ "name": p.get("display_name") or "匿名",
102
+ "line1": p.get("line1"),
103
+ "line2": p.get("line2"),
104
+ "line3": p.get("line3"),
105
+ }
106
+ for p in others
107
+ ],
108
+ "total": len(others),
109
+ "radius_m": radius,
110
+ "instruction": "根据你对用户的了解,判断哪些人值得推荐,为每个推荐写一句个性化的匹配理由。",
111
+ })
112
+
113
+
114
+ def handle_profile(params: dict) -> str:
115
+ sb = _sb()
116
+ did = _device_id(params["sender_id"], params["channel"])
117
+
118
+ if params["action"] == "get":
119
+ resp = sb.rpc("get_profile", {"p_device_id": did}).execute()
120
+ if not resp.data:
121
+ return _ok({"exists": False, "message": "你还没有名片。告诉我你的名字、emoji、三句话,我帮你创建。"})
122
+ return _ok({"exists": True, "profile": resp.data})
123
+
124
+ # set
125
+ resp = sb.rpc("upsert_profile", {
126
+ "p_device_id": did,
127
+ "p_display_name": params.get("display_name"),
128
+ "p_emoji": params.get("emoji"),
129
+ "p_line1": params.get("line1"),
130
+ "p_line2": params.get("line2"),
131
+ "p_line3": params.get("line3"),
132
+ "p_visible": params.get("visible", True),
133
+ }).execute()
134
+
135
+ if resp.data:
136
+ return _ok({"updated": True, "profile": resp.data})
137
+ return _ok({"error": "upsert_profile failed"})
138
+
139
+
140
+ def handle_accept(params: dict) -> str:
141
+ sb = _sb()
142
+ did = _device_id(params["sender_id"], params["channel"])
143
+ target = params["target_device_id"]
144
+
145
+ sb.rpc("upsert_match", {
146
+ "p_device_id_a": did,
147
+ "p_device_id_b": target,
148
+ "p_status": "accepted",
149
+ "p_contact_info": params.get("contact_info"),
150
+ }).execute()
151
+
152
+ # Check mutual
153
+ resp = sb.rpc("get_my_matches", {"p_device_id": did}).execute()
154
+ matches = resp.data or []
155
+ reverse = next(
156
+ (m for m in matches if m.get("device_id_a") == target and m.get("device_id_b") == did),
157
+ None,
158
+ )
159
+
160
+ if reverse:
161
+ contact = reverse.get("contact_info_a")
162
+ msg = f"双方都接受了!对方分享的联系方式:{contact}" if contact else "双方都接受了!但对方还没有分享联系方式。"
163
+ return _ok({"accepted": True, "mutual": True, "their_contact": contact, "message": msg})
164
+
165
+ return _ok({
166
+ "accepted": True,
167
+ "mutual": False,
168
+ "message": "已接受。等对方也接受后,你们就可以交换联系方式了。",
169
+ })
170
+
171
+
172
+ def handle_checkin(params: dict) -> str:
173
+ sb = _sb()
174
+ did = _device_id(params["sender_id"], params["channel"])
175
+ flat, flng = _fuzzy(params["lat"], params["lng"])
176
+
177
+ # Check profile exists
178
+ prof = sb.rpc("get_profile", {"p_device_id": did}).execute()
179
+ if not prof.data:
180
+ return _ok({"checked_in": False, "message": "你还没有名片,先创建一个吧。"})
181
+
182
+ sb.rpc("upsert_profile_location", {
183
+ "p_device_id": did, "p_lng": flng, "p_lat": flat,
184
+ }).execute()
185
+
186
+ place = f" ({params['place_name']})" if params.get("place_name") else ""
187
+ return _ok({"checked_in": True, "message": f"已签到{place} 📍 现在附近的人扫描就能看到你了。"})
188
+
189
+
190
+ def handle_check_matches(params: dict) -> str:
191
+ sb = _sb()
192
+ did = _device_id(params["sender_id"], params["channel"])
193
+
194
+ resp = sb.rpc("get_my_matches", {"p_device_id": did}).execute()
195
+ all_matches = resp.data or []
196
+
197
+ if not all_matches:
198
+ return _ok({"mutual_matches": [], "incoming_accepts": [], "message": "目前没有进行中的匹配。"})
199
+
200
+ my = [m for m in all_matches if m.get("device_id_a") == did]
201
+ incoming = [m for m in all_matches if m.get("device_id_b") == did]
202
+
203
+ # Mutual
204
+ mutual = []
205
+ for m in my:
206
+ rev = next((i for i in incoming if i.get("device_id_a") == m.get("device_id_b")), None)
207
+ if rev:
208
+ prof = sb.rpc("get_profile", {"p_device_id": m["device_id_b"]}).execute()
209
+ p = prof.data or {}
210
+ mutual.append({
211
+ "device_id": m["device_id_b"],
212
+ "name": p.get("display_name") or "匿名",
213
+ "emoji": p.get("emoji") or "👤",
214
+ "their_contact": rev.get("contact_info_a"),
215
+ "you_shared": m.get("contact_info_a"),
216
+ })
217
+
218
+ # Incoming only
219
+ inc_only = []
220
+ for m in incoming:
221
+ already = next((x for x in my if x.get("device_id_b") == m.get("device_id_a")), None)
222
+ if not already:
223
+ prof = sb.rpc("get_profile", {"p_device_id": m["device_id_a"]}).execute()
224
+ p = prof.data or {}
225
+ inc_only.append({
226
+ "device_id": m["device_id_a"],
227
+ "name": p.get("display_name") or "匿名",
228
+ "emoji": p.get("emoji") or "👤",
229
+ "line1": p.get("line1"),
230
+ })
231
+
232
+ msgs = []
233
+ if mutual:
234
+ msgs.append(f"{len(mutual)} 个双向匹配!可以交换联系方式了")
235
+ if inc_only:
236
+ msgs.append(f"{len(inc_only)} 个人想认识你,等你回应")
237
+ if not msgs:
238
+ msgs.append("你接受了一些匹配,但对方还没有回应。耐心等等 ⏳")
239
+
240
+ return _ok({
241
+ "mutual_matches": mutual,
242
+ "incoming_accepts": inc_only,
243
+ "message": ";".join(msgs),
244
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "Antenna — nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {