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 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 || !f.lng) return console.error("Usage: antenna scan --lat 39.99 --lng 116.48 [--radius 500] (max 1000) [--id telegram:123]");
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 res = await fetch(`${GEMINI_FLASH_URL}?key=${apiKey}`, {
17
+ const sb = getClient();
18
+ const res = await fetch(`${_url || DEFAULT_URL}/functions/v1/generate-embedding`, {
26
19
  method: "POST",
27
- headers: { "Content-Type": "application/json" },
28
- body: JSON.stringify({
29
- contents: [{ parts: [{ text: prompt }] }],
30
- generationConfig: { maxOutputTokens: 60, temperature: 0.7 },
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?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || null;
28
+ return data?.embedding || null;
36
29
  } catch { return null; }
37
30
  }
38
31
 
39
- async function generateEmbedding(text) {
40
- const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
41
- if (!apiKey) return null; // silently skip if no key
42
-
43
- const res = await fetch(`${GEMINI_EMBEDDING_URL}?key=${apiKey}`, {
44
- method: "POST",
45
- headers: { "Content-Type": "application/json" },
46
- body: JSON.stringify({
47
- content: { parts: [{ text }] },
48
- taskType: "SEMANTIC_SIMILARITY",
49
- outputDimensionality: 768,
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
- return f"{channel}:{sender_id}"
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Antenna โ€” nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {