antenna-fyi 1.2.1 โ 1.2.2
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/bin/antenna.js +3 -0
- package/lib/cli.js +19 -4
- package/lib/core.js +24 -34
- package/lib/hermes-plugin/__init__.py +55 -2
- package/lib/hermes-plugin/tools.py +4 -1
- package/package.json +1 -1
package/bin/antenna.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
handleAccept,
|
|
8
8
|
handleCheckin,
|
|
9
9
|
handleMatches,
|
|
10
|
+
handleDiscover,
|
|
10
11
|
handleBind,
|
|
11
12
|
handleSetup,
|
|
12
13
|
handleStatus,
|
|
@@ -31,6 +32,8 @@ async function main() {
|
|
|
31
32
|
return handleCheckin(f);
|
|
32
33
|
case "matches":
|
|
33
34
|
return handleMatches(f);
|
|
35
|
+
case "discover":
|
|
36
|
+
return handleDiscover(f);
|
|
34
37
|
case "bind":
|
|
35
38
|
return handleBind(f);
|
|
36
39
|
case "serve": {
|
package/lib/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// antenna CLI command handlers
|
|
2
2
|
|
|
3
|
-
import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken } from "./core.js";
|
|
3
|
+
import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover } from "./core.js";
|
|
4
4
|
import { createInterface } from "readline";
|
|
5
5
|
import { existsSync, mkdirSync, copyFileSync } from "fs";
|
|
6
6
|
import { join, dirname } from "path";
|
|
@@ -24,10 +24,10 @@ export function parseFlags(args) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export async function handleScan(f) {
|
|
27
|
-
if (!f.lat
|
|
27
|
+
if (!f.lat && !f.lng && !f.id) return console.error("Usage: antenna scan --lat 39.99 --lng 116.48 [--radius 500] (max 1000) [--id telegram:123]\n Or just: antenna scan --id telegram:123 (uses saved location from GPS bind)");
|
|
28
28
|
const result = await scan({
|
|
29
|
-
lat: +f.lat,
|
|
30
|
-
lng: +f.lng,
|
|
29
|
+
lat: f.lat ? +f.lat : undefined,
|
|
30
|
+
lng: f.lng ? +f.lng : undefined,
|
|
31
31
|
radius_m: +(f.radius || 500),
|
|
32
32
|
device_id: f.id || null,
|
|
33
33
|
});
|
|
@@ -110,6 +110,21 @@ export async function handleMatches(f) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
export async function handleDiscover(f) {
|
|
114
|
+
if (!f.id) return console.error("Usage: antenna discover --id telegram:123");
|
|
115
|
+
const result = await discover({ device_id: f.id });
|
|
116
|
+
if (result.count === 0) return console.log(result.message || "๐ No global recommendation available right now.");
|
|
117
|
+
console.log(`๐ Global discover:\n`);
|
|
118
|
+
result.profiles.forEach((p) => {
|
|
119
|
+
console.log(` ${p.emoji} ${p.name}`);
|
|
120
|
+
if (p.line1) console.log(` ${p.line1}`);
|
|
121
|
+
if (p.line2) console.log(` ${p.line2}`);
|
|
122
|
+
if (p.line3) console.log(` ${p.line3}`);
|
|
123
|
+
if (p.match_reason) console.log(` โ ${p.match_reason}`);
|
|
124
|
+
console.log(` ref: ${p.ref}\n`);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
113
128
|
export async function handleBind(f) {
|
|
114
129
|
if (!f.id) return console.error("Usage: antenna bind --id telegram:123");
|
|
115
130
|
const result = await createBindToken({ device_id: f.id });
|
package/lib/core.js
CHANGED
|
@@ -10,49 +10,39 @@ const DEFAULT_KEY =
|
|
|
10
10
|
let _client = null;
|
|
11
11
|
let _url = null;
|
|
12
12
|
|
|
13
|
-
// โโโ Embedding
|
|
14
|
-
|
|
15
|
-
const GEMINI_EMBEDDING_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent";
|
|
16
|
-
const GEMINI_FLASH_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent";
|
|
17
|
-
|
|
18
|
-
async function generateMatchReason(myLines, theirLines) {
|
|
19
|
-
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
20
|
-
if (!apiKey) return null;
|
|
21
|
-
|
|
22
|
-
const prompt = `You are matching two people. Person A: "${myLines}". Person B: "${theirLines}". Write ONE short sentence (under 20 words) in the SAME LANGUAGE as the profiles explaining why they might click. Be specific, not generic. No fluff.`;
|
|
13
|
+
// โโโ Embedding & Match Reason (via Supabase Edge Functions) โโโโโโโ
|
|
23
14
|
|
|
15
|
+
async function generateEmbedding(text) {
|
|
24
16
|
try {
|
|
25
|
-
const
|
|
17
|
+
const sb = getClient();
|
|
18
|
+
const res = await fetch(`${_url || DEFAULT_URL}/functions/v1/generate-embedding`, {
|
|
26
19
|
method: "POST",
|
|
27
|
-
headers: {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}),
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
"Authorization": `Bearer ${process.env.ANTENNA_SUPABASE_KEY || process.env.ANTENNA_KEY || DEFAULT_KEY}`,
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({ text }),
|
|
32
25
|
});
|
|
33
26
|
if (!res.ok) return null;
|
|
34
27
|
const data = await res.json();
|
|
35
|
-
return data?.
|
|
28
|
+
return data?.embedding || null;
|
|
36
29
|
} catch { return null; }
|
|
37
30
|
}
|
|
38
31
|
|
|
39
|
-
async function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!res.ok) return null;
|
|
54
|
-
const data = await res.json();
|
|
55
|
-
return data?.embedding?.values || null;
|
|
32
|
+
async function generateMatchReason(myLines, theirLines) {
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`${_url || DEFAULT_URL}/functions/v1/generate-match-reason`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"Authorization": `Bearer ${process.env.ANTENNA_SUPABASE_KEY || process.env.ANTENNA_KEY || DEFAULT_KEY}`,
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ my_lines: myLines, their_lines: theirLines }),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) return null;
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
return data?.reason || null;
|
|
45
|
+
} catch { return null; }
|
|
56
46
|
}
|
|
57
47
|
|
|
58
48
|
export function getClient(url, key) {
|
|
@@ -15,6 +15,7 @@ from .tools import (
|
|
|
15
15
|
handle_bind,
|
|
16
16
|
_sb,
|
|
17
17
|
_device_id,
|
|
18
|
+
_my_device_ids,
|
|
18
19
|
SCAN_SCHEMA,
|
|
19
20
|
PROFILE_SCHEMA,
|
|
20
21
|
ACCEPT_SCHEMA,
|
|
@@ -29,6 +30,11 @@ import time
|
|
|
29
30
|
_last_event_check = 0
|
|
30
31
|
_EVENT_CHECK_INTERVAL = 30 # seconds
|
|
31
32
|
|
|
33
|
+
# Track last match check timestamp
|
|
34
|
+
_last_match_check = 0
|
|
35
|
+
_MATCH_CHECK_INTERVAL = 60 # seconds
|
|
36
|
+
_notified_match_keys: set = set() # "deviceAโdeviceB" already notified
|
|
37
|
+
|
|
32
38
|
|
|
33
39
|
def register(ctx):
|
|
34
40
|
# โโ Tools โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -41,10 +47,57 @@ def register(ctx):
|
|
|
41
47
|
|
|
42
48
|
# โโ Hook: auto-detect location + check web GPS events โโโโโโโโโ
|
|
43
49
|
def on_pre_llm(messages, **kwargs):
|
|
44
|
-
"""Check for location data in messages AND pending web GPS events."""
|
|
45
|
-
global _last_event_check
|
|
50
|
+
"""Check for location data in messages AND pending web GPS events AND new matches."""
|
|
51
|
+
global _last_event_check, _last_match_check
|
|
46
52
|
hints = []
|
|
47
53
|
|
|
54
|
+
now = time.time()
|
|
55
|
+
|
|
56
|
+
# 0. Check for new matches (every 60s)
|
|
57
|
+
if now - _last_match_check > _MATCH_CHECK_INTERVAL and _my_device_ids:
|
|
58
|
+
_last_match_check = now
|
|
59
|
+
try:
|
|
60
|
+
sb = _sb()
|
|
61
|
+
for did in list(_my_device_ids):
|
|
62
|
+
resp = sb.rpc("get_my_matches", {"p_device_id": did}).execute()
|
|
63
|
+
all_matches = resp.data or []
|
|
64
|
+
if not all_matches:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
incoming = [m for m in all_matches if m.get("device_id_b") == did]
|
|
68
|
+
my = [m for m in all_matches if m.get("device_id_a") == did]
|
|
69
|
+
|
|
70
|
+
for m in incoming:
|
|
71
|
+
key = f"{m['device_id_a']}โ{did}"
|
|
72
|
+
if key in _notified_match_keys:
|
|
73
|
+
continue
|
|
74
|
+
_notified_match_keys.add(key)
|
|
75
|
+
|
|
76
|
+
# Get their profile
|
|
77
|
+
prof_resp = sb.rpc("get_profile", {"p_device_id": m["device_id_a"]}).execute()
|
|
78
|
+
p = prof_resp.data or {}
|
|
79
|
+
name = p.get("display_name") or "ๆไบบ"
|
|
80
|
+
emoji = p.get("emoji") or "๐ค"
|
|
81
|
+
|
|
82
|
+
# Check if mutual
|
|
83
|
+
i_accepted = any(
|
|
84
|
+
x.get("device_id_b") == m["device_id_a"] for x in my
|
|
85
|
+
)
|
|
86
|
+
if i_accepted:
|
|
87
|
+
contact = m.get("contact_info_a")
|
|
88
|
+
contact_msg = f"ๅฏนๆน็่็ณปๆนๅผ๏ผ{contact}" if contact else "ๅฏนๆน่ฟๆฒกๆๅไบซ่็ณปๆนๅผใ"
|
|
89
|
+
hints.append(
|
|
90
|
+
f"[Antenna] ๐ ๅๅๅน้
๏ผ{emoji} {name} ไนๆฅๅไบไฝ ๏ผ{contact_msg} "
|
|
91
|
+
f"่ฏท็ซๅณ้็ฅ็จๆท่ฟไธชๅฅฝๆถๆฏ๏ผ"
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
hints.append(
|
|
95
|
+
f"[Antenna] ๐ฉ {emoji} {name} ๆณ่ฎค่ฏไฝ ๏ผ"
|
|
96
|
+
f"่ฏทๅ่ฏ็จๆทๆไบบๆณ่ฎค่ฏ TA๏ผๅนถๅปบ่ฎฎ็จ antenna_check_matches ๆฅ็่ฏฆๆ
ใ"
|
|
97
|
+
)
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
48
101
|
# 1. Check location_events table (web GPS updates)
|
|
49
102
|
now = time.time()
|
|
50
103
|
if now - _last_event_check > _EVENT_CHECK_INTERVAL:
|
|
@@ -29,6 +29,7 @@ _client_url = None
|
|
|
29
29
|
_last_scan: dict[str, float] = {}
|
|
30
30
|
SCAN_DEBOUNCE_S = 30
|
|
31
31
|
_last_ref_map: dict[str, str] = {} # ref โ device_id from last scan
|
|
32
|
+
_my_device_ids: set[str] = set() # track this user's device_ids for match checking
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def _get_url():
|
|
@@ -53,7 +54,9 @@ def _sb():
|
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def _device_id(sender_id: str, channel: str) -> str:
|
|
56
|
-
|
|
57
|
+
did = f"{channel}:{sender_id}"
|
|
58
|
+
_my_device_ids.add(did)
|
|
59
|
+
return did
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
def _fuzzy(lat: float, lng: float) -> tuple[float, float]:
|