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 +32 -15
- package/lib/hermes-plugin/__init__.py +135 -2
- package/lib/hermes-plugin/schemas.py +9 -9
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
534
|
+
_log(` ๐ Detected OpenClaw โ will push notifications to your channel.`);
|
|
521
535
|
} else if (pushMethod === "hermes") {
|
|
522
|
-
|
|
536
|
+
_log(` ๐ Detected Hermes โ will push notifications to your channel.`);
|
|
523
537
|
} else {
|
|
524
|
-
|
|
538
|
+
_log(` โน๏ธ No agent framework detected โ notifications will print here.`);
|
|
525
539
|
}
|
|
526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
585
|
+
_log(` ${m.emoji || "๐ค"} ${m.name}${m.their_contact ? " โ contact: " + m.their_contact : ""}`);
|
|
569
586
|
}
|
|
570
|
-
|
|
587
|
+
_log();
|
|
571
588
|
}
|
|
572
589
|
if (initial.incoming_accepts?.length) {
|
|
573
|
-
|
|
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
|
-
|
|
594
|
+
_log(` ${m.emoji || "๐ค"} ${m.name} โ ${m.line1 || ""}`);
|
|
578
595
|
}
|
|
579
|
-
|
|
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
|
-
|
|
652
|
+
_log("โ
Connected โ listening for matches in real-time.");
|
|
636
653
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
|
|
637
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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"],
|