antenna-fyi 1.2.0 โ 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 +32 -39
- package/lib/hermes-plugin/__init__.py +55 -2
- package/lib/hermes-plugin/tools.py +4 -1
- package/lib/mcp.js +3 -2
- package/package.json +1 -1
- package/skill/SKILL.md +7 -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) {
|
|
@@ -135,7 +125,7 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
|
|
|
135
125
|
const saveRefs = async (refMap) => {
|
|
136
126
|
if (device_id && Object.keys(refMap).length > 0) {
|
|
137
127
|
try {
|
|
138
|
-
await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs:
|
|
128
|
+
await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: refMap });
|
|
139
129
|
} catch { /* best effort */ }
|
|
140
130
|
}
|
|
141
131
|
};
|
|
@@ -197,6 +187,7 @@ export async function setProfile({
|
|
|
197
187
|
line1,
|
|
198
188
|
line2,
|
|
199
189
|
line3,
|
|
190
|
+
matching_context,
|
|
200
191
|
visible = true,
|
|
201
192
|
supabaseUrl,
|
|
202
193
|
supabaseKey,
|
|
@@ -210,12 +201,14 @@ export async function setProfile({
|
|
|
210
201
|
p_line2: line2 || null,
|
|
211
202
|
p_line3: line3 || null,
|
|
212
203
|
p_visible: visible,
|
|
204
|
+
p_matching_context: matching_context || null,
|
|
213
205
|
});
|
|
214
206
|
if (error) throw new Error(error.message);
|
|
215
207
|
|
|
216
|
-
// Generate embedding for
|
|
208
|
+
// Generate embedding using lines + matching_context for better quality
|
|
217
209
|
try {
|
|
218
|
-
const
|
|
210
|
+
const textParts = [line1, line2, line3, matching_context].filter(Boolean);
|
|
211
|
+
const text = textParts.join(". ");
|
|
219
212
|
if (text) {
|
|
220
213
|
const embedding = await generateEmbedding(text);
|
|
221
214
|
if (embedding) {
|
|
@@ -443,7 +436,7 @@ export async function discover({ device_id, supabaseUrl, supabaseKey }) {
|
|
|
443
436
|
// Persist ref map to DB
|
|
444
437
|
if (device_id && Object.keys(_refMap).length > 0) {
|
|
445
438
|
try {
|
|
446
|
-
await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs:
|
|
439
|
+
await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: _refMap });
|
|
447
440
|
} catch { /* best effort */ }
|
|
448
441
|
}
|
|
449
442
|
|
|
@@ -520,7 +513,7 @@ export async function eventScan({ code, device_id, supabaseUrl, supabaseKey }) {
|
|
|
520
513
|
|
|
521
514
|
// Persist refs
|
|
522
515
|
if (device_id && Object.keys(_refMap).length > 0) {
|
|
523
|
-
try { await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs:
|
|
516
|
+
try { await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: _refMap }); } catch {}
|
|
524
517
|
}
|
|
525
518
|
|
|
526
519
|
return {
|
|
@@ -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]:
|
package/lib/mcp.js
CHANGED
|
@@ -72,16 +72,17 @@ export async function startMcpServer() {
|
|
|
72
72
|
line1: z.string().optional(),
|
|
73
73
|
line2: z.string().optional(),
|
|
74
74
|
line3: z.string().optional(),
|
|
75
|
+
matching_context: z.string().optional().describe("Agent-generated rich context for better matching (not shown to others)"),
|
|
75
76
|
visible: z.boolean().optional().default(true),
|
|
76
77
|
},
|
|
77
|
-
async ({ action, sender_id, channel, display_name, emoji, line1, line2, line3, visible }) => {
|
|
78
|
+
async ({ action, sender_id, channel, display_name, emoji, line1, line2, line3, matching_context, visible }) => {
|
|
78
79
|
const deviceId = deriveDeviceId(sender_id, channel);
|
|
79
80
|
try {
|
|
80
81
|
if (action === "get") {
|
|
81
82
|
const data = await getProfile({ device_id: deviceId });
|
|
82
83
|
return jsonResult(data ? { profile: data } : { profile: null, message: "่ฟๆฒกๆๅ็๏ผๅธฎไฝ ๅๅปบไธไธช๏ผ" });
|
|
83
84
|
}
|
|
84
|
-
const data = await setProfile({ device_id: deviceId, display_name, emoji, line1, line2, line3, visible });
|
|
85
|
+
const data = await setProfile({ device_id: deviceId, display_name, emoji, line1, line2, line3, matching_context, visible });
|
|
85
86
|
return jsonResult({ saved: true, profile: data });
|
|
86
87
|
} catch (e) {
|
|
87
88
|
return jsonResult({ error: e.message });
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -83,7 +83,7 @@ After receiving the nearby profiles, **you decide** who to recommend:
|
|
|
83
83
|
View or update the user's name card.
|
|
84
84
|
- `action`: "get" or "set"
|
|
85
85
|
- `sender_id`, `channel`: from context
|
|
86
|
-
- For "set": `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`
|
|
86
|
+
- For "set": `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`, `matching_context`
|
|
87
87
|
|
|
88
88
|
The name card has:
|
|
89
89
|
- **emoji**: a single emoji that represents them
|
|
@@ -91,6 +91,12 @@ The name card has:
|
|
|
91
91
|
- **line1**: who they are / what they do
|
|
92
92
|
- **line2**: what they're into
|
|
93
93
|
- **line3**: what they're looking for right now
|
|
94
|
+
- **matching_context** (optional, not shown to others): A richer description generated by the agent based on what it knows about the user โ career background, tech stack, interests, projects, personality traits. ~200 words. Only used for embedding-based matching, never displayed to other users.
|
|
95
|
+
|
|
96
|
+
**After saving the profile, generate `matching_context` automatically** based on your knowledge of the user (memory, conversations, context). Don't ask the user to write it โ you write it. Example:
|
|
97
|
+
> "Product designer at a tech company in Beijing, focusing on AI search experience. Interested in music (Sakamoto), swimming, cooking, language learning. Recently exploring AI agent ecosystems and social discovery. Looking to connect with AI builders, indie hackers, and creative technologists."
|
|
98
|
+
- **line2**: what they're into
|
|
99
|
+
- **line3**: what they're looking for right now
|
|
94
100
|
|
|
95
101
|
### `antenna_accept`
|
|
96
102
|
Accept a match after the user sees results. Can optionally include contact info to share.
|