antenna-fyi 1.2.26 โ†’ 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.
package/lib/cli.js CHANGED
@@ -515,19 +515,36 @@ export async function handleWatch(f) {
515
515
  } catch { /* hermes not installed */ }
516
516
  }
517
517
 
518
- console.log(`๐Ÿ“ก Watching for new matches for ${id}...`);
518
+ // Force stdout blocking mode for non-TTY environments (Hermes exec)
519
+ try { if (process.stdout._handle) process.stdout._handle.setBlocking(true); } catch {}
520
+
521
+ // Log to file as fallback
522
+ const logDir = path.join(os.homedir(), '.antenna');
523
+ const logFile = path.join(logDir, 'watch.log');
524
+ try { if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true }); } catch {}
525
+ const logStream = await import('fs').then(fs => fs.createWriteStream(logFile, { flags: 'a' }));
526
+ const _log = (msg) => {
527
+ const line = `[${new Date().toISOString()}] ${msg}`;
528
+ console.log(msg);
529
+ try { logStream.write(line + '\n'); } catch {}
530
+ };
531
+
532
+ _log(`๐Ÿ“ก Watching for new matches for ${id}...`);
519
533
  if (pushMethod === "openclaw") {
520
- console.log(` ๐Ÿ”— Detected OpenClaw โ€” will push notifications to your channel.`);
534
+ _log(` ๐Ÿ”— Detected OpenClaw โ€” will push notifications to your channel.`);
521
535
  } else if (pushMethod === "hermes") {
522
- console.log(` ๐Ÿ”— Detected Hermes โ€” will push notifications to your channel.`);
536
+ _log(` ๐Ÿ”— Detected Hermes โ€” will push notifications to your channel.`);
523
537
  } else {
524
- console.log(` โ„น๏ธ No agent framework detected โ€” notifications will print here.`);
538
+ _log(` โ„น๏ธ No agent framework detected โ€” notifications will print here.`);
525
539
  }
526
- console.log(` Press Ctrl+C to stop.\n`);
540
+ _log(` Press Ctrl+C to stop.\n`);
541
+
542
+ // Keep process alive (prevent exit when backgrounded)
543
+ process.stdin.resume();
527
544
 
528
545
  // Push notification helper
529
546
  async function pushNotify(message) {
530
- console.log(message); // always print to terminal
547
+ _log(message); // always print to terminal
531
548
 
532
549
  if (pushMethod === "openclaw") {
533
550
  try {
@@ -561,22 +578,22 @@ export async function handleWatch(f) {
561
578
  // Initial check
562
579
  const initial = await checkMatches({ device_id: id });
563
580
  if (initial.mutual_matches?.length) {
564
- console.log(`๐ŸŽ‰ You have ${initial.mutual_matches.length} mutual match(es)!`);
581
+ _log(`๐ŸŽ‰ You have ${initial.mutual_matches.length} mutual match(es)!`);
565
582
  for (const m of initial.mutual_matches) {
566
583
  const key = `mutual:${m.device_id}`;
567
584
  notified.add(key);
568
- console.log(` ${m.emoji || "๐Ÿ‘ค"} ${m.name}${m.their_contact ? " โ€” contact: " + m.their_contact : ""}`);
585
+ _log(` ${m.emoji || "๐Ÿ‘ค"} ${m.name}${m.their_contact ? " โ€” contact: " + m.their_contact : ""}`);
569
586
  }
570
- console.log();
587
+ _log();
571
588
  }
572
589
  if (initial.incoming_accepts?.length) {
573
- console.log(`๐Ÿ“ฉ ${initial.incoming_accepts.length} person(s) want to meet you!`);
590
+ _log(`๐Ÿ“ฉ ${initial.incoming_accepts.length} person(s) want to meet you!`);
574
591
  for (const m of initial.incoming_accepts) {
575
592
  const key = `incoming:${m.device_id}`;
576
593
  notified.add(key);
577
- console.log(` ${m.emoji || "๐Ÿ‘ค"} ${m.name} โ€” ${m.line1 || ""}`);
594
+ _log(` ${m.emoji || "๐Ÿ‘ค"} ${m.name} โ€” ${m.line1 || ""}`);
578
595
  }
579
- console.log();
596
+ _log();
580
597
  }
581
598
 
582
599
  // Subscribe to realtime changes on matches table
@@ -632,9 +649,9 @@ export async function handleWatch(f) {
632
649
  )
633
650
  .subscribe((status) => {
634
651
  if (status === "SUBSCRIBED") {
635
- console.log("โœ… Connected โ€” listening for matches in real-time.");
652
+ _log("โœ… Connected โ€” listening for matches in real-time.");
636
653
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
637
- console.log(`โš ๏ธ Connection issue (${status}), retrying...`);
654
+ _log(`โš ๏ธ Connection issue (${status}), retrying...`);
638
655
  }
639
656
  });
640
657
 
@@ -678,7 +695,7 @@ export async function handleWatch(f) {
678
695
  )
679
696
  .subscribe((status) => {
680
697
  if (status === "SUBSCRIBED") {
681
- console.log("โœ… Connected โ€” listening for event notifications.");
698
+ _log("โœ… Connected โ€” listening for event notifications.");
682
699
  }
683
700
  });
684
701
 
@@ -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 ๐Ÿ“ก")
@@ -44,7 +44,7 @@ PROFILE_SCHEMA = {
44
44
  },
45
45
  "sender_id": {"type": "string"},
46
46
  "channel": {"type": "string"},
47
- "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
47
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
48
48
  "display_name": {"type": "string", "description": "Display name"},
49
49
  "emoji": {"type": "string", "description": "Profile emoji"},
50
50
  "line1": {"type": "string", "description": "Who you are / what you do"},
@@ -67,7 +67,7 @@ ACCEPT_SCHEMA = {
67
67
  "properties": {
68
68
  "sender_id": {"type": "string"},
69
69
  "channel": {"type": "string"},
70
- "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
70
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
71
71
  "ref": {
72
72
  "type": "string",
73
73
  "description": "Ref number from scan results (e.g. '1')",
@@ -97,7 +97,7 @@ CHECKIN_SCHEMA = {
97
97
  "lng": {"type": "number", "description": "Longitude"},
98
98
  "sender_id": {"type": "string"},
99
99
  "channel": {"type": "string"},
100
- "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
100
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
101
101
  "place_name": {
102
102
  "type": "string",
103
103
  "description": "Name of the place (optional)",
@@ -117,7 +117,7 @@ CHECK_MATCHES_SCHEMA = {
117
117
  "properties": {
118
118
  "sender_id": {"type": "string"},
119
119
  "channel": {"type": "string"},
120
- "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
120
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
121
121
  },
122
122
  "required": ["sender_id", "channel"],
123
123
  },
@@ -133,7 +133,7 @@ BIND_SCHEMA = {
133
133
  "properties": {
134
134
  "sender_id": {"type": "string"},
135
135
  "channel": {"type": "string"},
136
- "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
136
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
137
137
  "purpose": {"type": "string", "description": "'profile' (default) or 'event'"},
138
138
  "event_code": {"type": "string", "description": "Event code (when purpose=event)"},
139
139
  },
@@ -287,7 +287,7 @@ EVENT_UPDATE_SCHEMA = {
287
287
  "code": {"type": "string"},
288
288
  "sender_id": {"type": "string"},
289
289
  "channel": {"type": "string"},
290
- "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
290
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
291
291
  "name": {"type": "string"},
292
292
  "description": {"type": "string"},
293
293
  "og_image": {"type": "string"},
@@ -309,7 +309,7 @@ EVENT_APPROVE_SCHEMA = {
309
309
  "code": {"type": "string"},
310
310
  "sender_id": {"type": "string"},
311
311
  "channel": {"type": "string"},
312
- "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
312
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
313
313
  "ref": {"type": "string"},
314
314
  },
315
315
  "required": ["code", "sender_id", "channel", "ref"],
@@ -325,7 +325,7 @@ EVENT_REJECT_SCHEMA = {
325
325
  "code": {"type": "string"},
326
326
  "sender_id": {"type": "string"},
327
327
  "channel": {"type": "string"},
328
- "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
328
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
329
329
  "ref": {"type": "string"},
330
330
  },
331
331
  "required": ["code", "sender_id", "channel", "ref"],
@@ -341,7 +341,7 @@ EVENT_ADD_HOST_SCHEMA = {
341
341
  "code": {"type": "string"},
342
342
  "sender_id": {"type": "string"},
343
343
  "channel": {"type": "string"},
344
- "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
344
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
345
345
  "ref": {"type": "string"},
346
346
  },
347
347
  "required": ["code", "sender_id", "channel", "ref"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.2.26",
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": {