antenna-fyi 1.2.27 → 1.2.28

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.
@@ -1,11 +1,18 @@
1
1
  """Antenna — Hermes Agent Plugin
2
2
 
3
- Nearby people discovery. Registers 6 tools and a pre_llm_call hook
4
- that auto-detects location data (from messages + web GPS events).
3
+ Nearby people discovery. Registers tools, a pre_llm_call hook,
4
+ and a background Realtime listener for push notifications.
5
5
 
6
6
  Drop this directory into ~/.hermes/plugins/antenna/
7
7
  """
8
8
 
9
+ import json
10
+ import os
11
+ import re
12
+ import threading
13
+ import time
14
+ from pathlib import Path
15
+
9
16
  from .tools import (
10
17
  handle_scan,
11
18
  handle_profile,
@@ -91,6 +98,18 @@ def register(ctx):
91
98
 
92
99
  now = time.time()
93
100
 
101
+ # -1. Read pending notifications from Realtime listener
102
+ _nf = Path.home() / ".antenna" / "pending_notifications.json"
103
+ try:
104
+ if _nf.exists():
105
+ pending = json.loads(_nf.read_text())
106
+ if pending:
107
+ for n in pending:
108
+ hints.append(f"[Antenna] {n['message']}")
109
+ _nf.write_text("[]")
110
+ except Exception:
111
+ pass
112
+
94
113
  # 0. Check for new matches (every 60s)
95
114
  if now - _last_match_check > _MATCH_CHECK_INTERVAL and _my_device_ids:
96
115
  _last_match_check = now
@@ -185,4 +204,118 @@ def register(ctx):
185
204
 
186
205
  ctx.register_hook("pre_llm_call", on_pre_llm)
187
206
 
207
+ # ── Background Realtime listener for push notifications ──────
208
+ _notif_dir = Path.home() / ".antenna"
209
+ _notif_file = _notif_dir / "pending_notifications.json"
210
+
211
+ def _append_notification(msg: str):
212
+ """Thread-safe append to pending notifications file."""
213
+ try:
214
+ _notif_dir.mkdir(parents=True, exist_ok=True)
215
+ notifications = []
216
+ if _notif_file.exists():
217
+ try:
218
+ notifications = json.loads(_notif_file.read_text())
219
+ except Exception:
220
+ notifications = []
221
+ notifications.append({"message": msg, "timestamp": time.time()})
222
+ # Keep max 50
223
+ notifications = notifications[-50:]
224
+ _notif_file.write_text(json.dumps(notifications, ensure_ascii=False))
225
+ except Exception:
226
+ pass
227
+
228
+ def _realtime_listener():
229
+ """Background thread: listen to Supabase Realtime for matches + event participants."""
230
+ try:
231
+ sb = _sb()
232
+ # Listen to matches INSERT
233
+ sb.channel("hermes-antenna-matches").on(
234
+ "postgres_changes",
235
+ {"event": "INSERT", "schema": "public", "table": "matches"},
236
+ lambda payload: _handle_match_change(payload),
237
+ ).subscribe()
238
+
239
+ # Listen to event_participants INSERT + UPDATE
240
+ sb.channel("hermes-antenna-events").on(
241
+ "postgres_changes",
242
+ {"event": "*", "schema": "public", "table": "event_participants"},
243
+ lambda payload: _handle_event_change(payload),
244
+ ).subscribe()
245
+
246
+ # Keep thread alive
247
+ while True:
248
+ time.sleep(60)
249
+ except Exception as e:
250
+ print(f"[Antenna] Realtime listener failed: {e}")
251
+
252
+ def _handle_match_change(payload):
253
+ try:
254
+ row = payload.get("new") or payload.get("record", {})
255
+ target = row.get("device_id_b")
256
+ if not target or target not in _my_device_ids:
257
+ return
258
+ key = f"{row.get('device_id_a')}→{target}"
259
+ if key in _notified_match_keys:
260
+ return
261
+ _notified_match_keys.add(key)
262
+ sb = _sb()
263
+ prof = sb.rpc("get_profile", {"p_device_id": row.get("device_id_a")}).execute()
264
+ p = prof.data or {}
265
+ name = p.get("display_name") or "有人"
266
+ emoji = p.get("emoji") or "👤"
267
+ _append_notification(f"📩 {emoji} {name} 想认识你!用 antenna_check_matches 查看详情。")
268
+ except Exception:
269
+ pass
270
+
271
+ def _handle_event_change(payload):
272
+ try:
273
+ event_type = payload.get("type") or payload.get("eventType", "")
274
+ row = payload.get("new") or payload.get("record", {})
275
+ old = payload.get("old") or payload.get("old_record", {})
276
+
277
+ if event_type == "INSERT" and row.get("status") == "pending":
278
+ # Someone applied — notify creator
279
+ event_id = row.get("event_id")
280
+ applicant_id = row.get("device_id")
281
+ if not event_id:
282
+ return
283
+ sb = _sb()
284
+ evt = sb.rpc("get_event_by_id", {"p_event_id": event_id}).execute()
285
+ event = evt.data or {}
286
+ if not event.get("found") or event.get("created_by") not in _my_device_ids:
287
+ return
288
+ prof = sb.rpc("get_profile", {"p_device_id": applicant_id}).execute()
289
+ p = prof.data or {}
290
+ name = p.get("display_name") or "某人"
291
+ emoji = p.get("emoji") or "👤"
292
+ _append_notification(f"📩 {emoji} {name} 申请加入你的活动「{event.get('name')}」!用 antenna_event_scan 查看并审批。")
293
+
294
+ elif event_type == "UPDATE":
295
+ device_id = row.get("device_id")
296
+ if device_id not in _my_device_ids:
297
+ return
298
+ old_status = old.get("status")
299
+ new_status = row.get("status")
300
+ if old_status == "pending" and new_status == "active":
301
+ sb = _sb()
302
+ evt = sb.rpc("get_event_by_id", {"p_event_id": row.get("event_id")}).execute()
303
+ event = evt.data or {}
304
+ _append_notification(f"✅ 你的申请已通过!欢迎加入「{event.get('name', '活动')}」")
305
+ elif old_status == "pending" and new_status == "rejected":
306
+ sb = _sb()
307
+ evt = sb.rpc("get_event_by_id", {"p_event_id": row.get("event_id")}).execute()
308
+ event = evt.data or {}
309
+ _append_notification(f"❌ 你的申请未通过「{event.get('name', '活动')}」")
310
+ except Exception:
311
+ pass
312
+
313
+ # Start background listener thread
314
+ try:
315
+ t = threading.Thread(target=_realtime_listener, daemon=True, name="antenna-realtime")
316
+ t.start()
317
+ print("[Antenna] Realtime listener started 📡")
318
+ except Exception as e:
319
+ print(f"[Antenna] Failed to start Realtime listener: {e}")
320
+
188
321
  print("[Antenna] Plugin loaded 📡")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.2.27",
3
+ "version": "1.2.28",
4
4
  "description": "Antenna — nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {