antenna-fyi 0.7.0 โ†’ 0.8.1

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
+ handleBind,
10
11
  handleSetup,
11
12
  handleStatus,
12
13
  handleInstallSkill,
@@ -30,6 +31,8 @@ async function main() {
30
31
  return handleCheckin(f);
31
32
  case "matches":
32
33
  return handleMatches(f);
34
+ case "bind":
35
+ return handleBind(f);
33
36
  case "serve": {
34
37
  const { startMcpServer } = await import("../lib/mcp.js");
35
38
  return startMcpServer();
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 } from "./core.js";
3
+ import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken } from "./core.js";
4
4
  import { createInterface } from "readline";
5
5
  import { existsSync, mkdirSync, copyFileSync } from "fs";
6
6
  import { join, dirname } from "path";
@@ -106,6 +106,15 @@ export async function handleMatches(f) {
106
106
  }
107
107
  }
108
108
 
109
+ export async function handleBind(f) {
110
+ if (!f.id) return console.error("Usage: antenna bind --id telegram:123");
111
+ const result = await createBindToken({ device_id: f.id });
112
+ console.log("\n๐Ÿ”— GPS Binding Link:\n");
113
+ console.log(` ${result.url}\n`);
114
+ console.log("Send this to the user. Opening it on their phone will share GPS with their agent.");
115
+ console.log();
116
+ }
117
+
109
118
  export async function handleSetup(f) {
110
119
  const rl = createInterface({ input: process.stdin, output: process.stdout });
111
120
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
@@ -276,6 +285,7 @@ Usage:
276
285
  antenna profile --id telegram:123 [--name Yi --emoji ๐Ÿฆฆ --line1 '...']
277
286
  antenna accept --id telegram:123 --target telegram:789 [--contact 'WeChat: yi']
278
287
  antenna matches --id telegram:123
288
+ antenna bind --id telegram:123
279
289
  antenna serve Start MCP server (stdio transport)
280
290
  antenna setup Interactive profile setup [--id telegram:123]
281
291
  antenna status Show config & status [--id telegram:123]
package/lib/core.js CHANGED
@@ -245,3 +245,17 @@ export async function checkMatches({ device_id, supabaseUrl, supabaseKey }) {
245
245
  message: messages.join("๏ผ›"),
246
246
  };
247
247
  }
248
+
249
+ // โ”€โ”€โ”€ createBindToken โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
250
+
251
+ export async function createBindToken({ device_id, supabaseUrl, supabaseKey }) {
252
+ const sb = getClient(supabaseUrl, supabaseKey);
253
+ const { data, error } = await sb.rpc("create_bind_token", { p_device_id: device_id });
254
+ if (error) throw new Error(error.message);
255
+ const baseUrl = "https://www.antenna.fyi";
256
+ return {
257
+ token: data.token,
258
+ url: `${baseUrl}/locate?token=${data.token}`,
259
+ message: "ๅ‘้€่ฟ™ไธช้“พๆŽฅ็ป™็”จๆˆท๏ผŒๅœจๆ‰‹ๆœบๆต่งˆๅ™จๆ‰“ๅผ€ๅณๅฏๅ…ฑไบซไฝ็ฝฎใ€‚",
260
+ };
261
+ }
@@ -1,7 +1,7 @@
1
1
  """Antenna โ€” Hermes Agent Plugin
2
2
 
3
- Nearby people discovery. Registers 5 tools and a pre_llm_call hook
4
- that auto-detects location data and prompts the agent to scan.
3
+ Nearby people discovery. Registers 6 tools and a pre_llm_call hook
4
+ that auto-detects location data (from messages + web GPS events).
5
5
 
6
6
  Drop this directory into ~/.hermes/plugins/antenna/
7
7
  """
@@ -12,13 +12,22 @@ from .tools import (
12
12
  handle_accept,
13
13
  handle_checkin,
14
14
  handle_check_matches,
15
+ handle_bind,
16
+ _sb,
17
+ _device_id,
15
18
  SCAN_SCHEMA,
16
19
  PROFILE_SCHEMA,
17
20
  ACCEPT_SCHEMA,
18
21
  CHECKIN_SCHEMA,
19
22
  CHECK_MATCHES_SCHEMA,
23
+ BIND_SCHEMA,
20
24
  )
21
25
  import re
26
+ import time
27
+
28
+ # Track last checked timestamp for location events
29
+ _last_event_check = 0
30
+ _EVENT_CHECK_INTERVAL = 30 # seconds
22
31
 
23
32
 
24
33
  def register(ctx):
@@ -28,40 +37,59 @@ def register(ctx):
28
37
  ctx.register_tool("antenna_accept", ACCEPT_SCHEMA, handle_accept)
29
38
  ctx.register_tool("antenna_checkin", CHECKIN_SCHEMA, handle_checkin)
30
39
  ctx.register_tool("antenna_check_matches", CHECK_MATCHES_SCHEMA, handle_check_matches)
40
+ ctx.register_tool("antenna_bind", BIND_SCHEMA, handle_bind)
31
41
 
32
- # โ”€โ”€ Hook: auto-detect location in messages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
42
+ # โ”€โ”€ Hook: auto-detect location + check web GPS events โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
33
43
  def on_pre_llm(messages, **kwargs):
34
- """Check last user message for location data and inject scan hint."""
35
- if not messages:
36
- return None
37
-
38
- last_msg = messages[-1] if isinstance(messages[-1], dict) else {}
39
- content = last_msg.get("content", "")
40
- if not isinstance(content, str):
41
- return None
44
+ """Check for location data in messages AND pending web GPS events."""
45
+ global _last_event_check
46
+ hints = []
42
47
 
43
- # Pattern 1: Hermes gateway may serialize location as text
44
- # e.g. "๐Ÿ“ Location shared: 39.9908, 116.4872"
45
- loc_match = re.search(
46
- r"[Ll]ocation.*?(-?\d+\.?\d*)[,\s]+(-?\d+\.?\d*)", content
47
- )
48
-
49
- # Pattern 2: raw coordinates in message
50
- if not loc_match:
51
- loc_match = re.search(
52
- r"(-?\d{1,3}\.\d{3,})[,\s]+(-?\d{1,3}\.\d{3,})", content
53
- )
48
+ # 1. Check location_events table (web GPS updates)
49
+ now = time.time()
50
+ if now - _last_event_check > _EVENT_CHECK_INTERVAL:
51
+ _last_event_check = now
52
+ try:
53
+ sb = _sb()
54
+ resp = (
55
+ sb.from_("location_events")
56
+ .select("device_id, lat, lng")
57
+ .gt("created_at", "now() - interval '2 minutes'")
58
+ .order("created_at", desc=True)
59
+ .limit(5)
60
+ .execute()
61
+ )
62
+ if resp.data:
63
+ for evt in resp.data:
64
+ hints.append(
65
+ f"[Antenna] ๐Ÿ“ก ็”จๆˆท {evt['device_id']} ้€š่ฟ‡็ฝ‘้กตๅˆ†ไบซไบ†ไฝ็ฝฎ "
66
+ f"({evt['lat']}, {evt['lng']})ใ€‚"
67
+ f"่ฏทไฝฟ็”จ antenna_scan ๆŸฅ็œ‹้™„่ฟ‘ๆœ‰่ฐใ€‚"
68
+ )
69
+ except Exception:
70
+ pass
54
71
 
55
- if loc_match:
56
- lat, lng = loc_match.group(1), loc_match.group(2)
57
- return {
58
- "context": (
59
- f"\n\n[Antenna] ๐Ÿ“ ๆฃ€ๆต‹ๅˆฐไฝ็ฝฎ ({lat}, {lng})ใ€‚"
60
- f"่ฏทไฝฟ็”จ antenna_scan ๅทฅๅ…ทๆŸฅ็œ‹้™„่ฟ‘ๆœ‰่ฐใ€‚"
61
- f"ๅ‚ๆ•ฐ๏ผšlat={lat}, lng={lng}๏ผŒsender_id ๅ’Œ channel ไปŽๆถˆๆฏไธŠไธ‹ๆ–‡่Žทๅ–ใ€‚"
72
+ # 2. Check message content for coordinates
73
+ if messages:
74
+ last_msg = messages[-1] if isinstance(messages[-1], dict) else {}
75
+ content = last_msg.get("content", "")
76
+ if isinstance(content, str):
77
+ loc_match = re.search(
78
+ r"[Ll]ocation.*?(-?\d+\.?\d*)[,\s]+(-?\d+\.?\d*)", content
62
79
  )
63
- }
80
+ if not loc_match:
81
+ loc_match = re.search(
82
+ r"(-?\d{1,3}\.\d{3,})[,\s]+(-?\d{1,3}\.\d{3,})", content
83
+ )
84
+ if loc_match:
85
+ lat, lng = loc_match.group(1), loc_match.group(2)
86
+ hints.append(
87
+ f"[Antenna] ๐Ÿ“ ๆฃ€ๆต‹ๅˆฐไฝ็ฝฎ ({lat}, {lng})ใ€‚"
88
+ f"่ฏทไฝฟ็”จ antenna_scan ๆŸฅ็œ‹้™„่ฟ‘ๆœ‰่ฐใ€‚"
89
+ )
64
90
 
91
+ if hints:
92
+ return {"context": "\n".join(hints)}
65
93
  return None
66
94
 
67
95
  ctx.register_hook("pre_llm_call", on_pre_llm)
@@ -115,3 +115,19 @@ CHECK_MATCHES_SCHEMA = {
115
115
  "required": ["sender_id", "channel"],
116
116
  },
117
117
  }
118
+
119
+ BIND_SCHEMA = {
120
+ "name": "antenna_bind",
121
+ "description": (
122
+ "Generate a GPS binding link. Send this URL to the user so they can "
123
+ "share their phone's location via the web browser."
124
+ ),
125
+ "parameters": {
126
+ "type": "object",
127
+ "properties": {
128
+ "sender_id": {"type": "string"},
129
+ "channel": {"type": "string"},
130
+ },
131
+ "required": ["sender_id", "channel"],
132
+ },
133
+ }
@@ -242,3 +242,22 @@ def handle_check_matches(params: dict) -> str:
242
242
  "incoming_accepts": inc_only,
243
243
  "message": "๏ผ›".join(msgs),
244
244
  })
245
+
246
+
247
+ BASE_URL = "https://www.antenna.fyi"
248
+
249
+
250
+ def handle_bind(params: dict) -> str:
251
+ sb = _sb()
252
+ did = _device_id(params["sender_id"], params["channel"])
253
+
254
+ resp = sb.rpc("create_bind_token", {"p_device_id": did}).execute()
255
+ if not resp.data:
256
+ return _ok({"error": "Failed to create bind token"})
257
+
258
+ token = resp.data.get("token")
259
+ return _ok({
260
+ "token": token,
261
+ "url": f"{BASE_URL}/locate?token={token}",
262
+ "message": "ๅ‘้€่ฟ™ไธช้“พๆŽฅ็ป™็”จๆˆท๏ผŒๅœจๆ‰‹ๆœบๆต่งˆๅ™จๆ‰“ๅผ€ๅณๅฏๅ…ฑไบซไฝ็ฝฎใ€‚",
263
+ })
package/lib/mcp.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  accept,
11
11
  checkMatches,
12
12
  checkin,
13
+ createBindToken,
13
14
  deriveDeviceId,
14
15
  } from "./core.js";
15
16
 
@@ -143,6 +144,25 @@ export async function startMcpServer() {
143
144
  }
144
145
  );
145
146
 
147
+ // โ”€โ”€โ”€ antenna_bind โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
148
+
149
+ server.tool(
150
+ "antenna_bind",
151
+ "Generate a GPS binding link. Send this to the user so they can share their phone's location via the web.",
152
+ {
153
+ sender_id: z.string().describe("The sender's user ID"),
154
+ channel: z.string().describe("Channel name"),
155
+ },
156
+ async ({ sender_id, channel }) => {
157
+ try {
158
+ const result = await createBindToken({ device_id: deriveDeviceId(sender_id, channel) });
159
+ return jsonResult(result);
160
+ } catch (e) {
161
+ return jsonResult({ error: e.message });
162
+ }
163
+ }
164
+ );
165
+
146
166
  const transport = new StdioServerTransport();
147
167
  await server.connect(transport);
148
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Antenna โ€” nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {