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 +3 -0
- package/lib/cli.js +11 -1
- package/lib/core.js +14 -0
- package/lib/hermes-plugin/__init__.py +58 -30
- package/lib/hermes-plugin/schemas.py +16 -0
- package/lib/hermes-plugin/tools.py +19 -0
- package/lib/mcp.js +20 -0
- 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
|
+
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
|
|
4
|
-
that auto-detects location data
|
|
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
|
|
42
|
+
# โโ Hook: auto-detect location + check web GPS events โโโโโโโโโ
|
|
33
43
|
def on_pre_llm(messages, **kwargs):
|
|
34
|
-
"""Check
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
}
|