antenna-fyi 1.2.7 â 1.2.9
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/lib/cli.js +22 -7
- package/lib/core.js +31 -3
- package/lib/hermes-plugin/__init__.py +3 -0
- package/lib/hermes-plugin/schemas.py +17 -2
- package/lib/hermes-plugin/tools.py +26 -2
- package/lib/mcp.js +24 -3
- package/package.json +1 -1
- package/skill/SKILL.md +11 -0
package/lib/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// antenna CLI command handlers
|
|
2
2
|
|
|
3
|
-
import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover, createEvent, endEvent, eventCheckin, joinEvent, eventScan, pass as passUser } from "./core.js";
|
|
3
|
+
import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover, createEvent, endEvent, eventCheckin, joinEvent, eventScan, pass as passUser, uploadEventImage } from "./core.js";
|
|
4
4
|
import { createInterface } from "readline";
|
|
5
|
-
import { existsSync, mkdirSync, copyFileSync } from "fs";
|
|
6
|
-
import { join, dirname } from "path";
|
|
5
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync } from "fs";
|
|
6
|
+
import { join, dirname, extname } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { homedir } from "os";
|
|
9
9
|
import { execSync } from "child_process";
|
|
@@ -141,7 +141,20 @@ export async function handleDiscover(f) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
export async function handleEvent(f) {
|
|
144
|
-
const sub = f._?.[0] || Object.keys(f).find(k => ["create", "join", "scan", "end", "checkin"].includes(k));
|
|
144
|
+
const sub = f._?.[0] || Object.keys(f).find(k => ["create", "join", "scan", "end", "checkin", "upload-image"].includes(k));
|
|
145
|
+
|
|
146
|
+
if (f['upload-image']) {
|
|
147
|
+
if (!f.code || !f.file) return console.error("Usage: antenna event --upload-image --code abc123 --file /path/to/image.png");
|
|
148
|
+
const fileBuf = readFileSync(f.file);
|
|
149
|
+
const image_data = fileBuf.toString("base64");
|
|
150
|
+
const ext = extname(f.file).slice(1) || "png";
|
|
151
|
+
const mimeMap = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
|
|
152
|
+
const content_type = mimeMap[ext] || "image/png";
|
|
153
|
+
const url = await uploadEventImage({ image_data, content_type, event_code: f.code });
|
|
154
|
+
console.log(`\nâ
Image uploaded!\n`);
|
|
155
|
+
console.log(` URL: ${url}\n`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
145
158
|
|
|
146
159
|
if (f.end) {
|
|
147
160
|
if (!f.code || !f.id) return console.error("Usage: antenna event --end --code abc123 --id telegram:123");
|
|
@@ -190,7 +203,8 @@ export async function handleEvent(f) {
|
|
|
190
203
|
console.log(`\nðïļ ${result.count} joined, ${result.checked_in_count || 0} checked in:\n`);
|
|
191
204
|
result.profiles.forEach((p) => {
|
|
192
205
|
const badge = p.checked_in ? " â
" : "";
|
|
193
|
-
|
|
206
|
+
const creatorTag = p.role === "creator" ? " [äļŧå]" : "";
|
|
207
|
+
console.log(` ${p.emoji} ${p.name}${creatorTag}${badge}`);
|
|
194
208
|
if (p.line1) console.log(` ${p.line1}`);
|
|
195
209
|
console.log(` ref: ${p.ref}\n`);
|
|
196
210
|
});
|
|
@@ -202,7 +216,8 @@ export async function handleEvent(f) {
|
|
|
202
216
|
antenna event --join --code abc123 --id telegram:123
|
|
203
217
|
antenna event --scan --code abc123 [--id telegram:123]
|
|
204
218
|
antenna event --checkin --code abc123 --id telegram:123 [--lat 34.05 --lng -118.24]
|
|
205
|
-
antenna event --end --code abc123 --id telegram:123
|
|
219
|
+
antenna event --end --code abc123 --id telegram:123
|
|
220
|
+
antenna event --upload-image --code abc123 --file /path/to/image.png`);
|
|
206
221
|
}
|
|
207
222
|
|
|
208
223
|
export async function handleBind(f) {
|
|
@@ -397,7 +412,7 @@ Usage:
|
|
|
397
412
|
antenna pass --id telegram:123 --target telegram:789 (or --ref 1)
|
|
398
413
|
antenna matches --id telegram:123
|
|
399
414
|
antenna discover --id telegram:123
|
|
400
|
-
antenna event --create --name 'AI Meetup' [--desc '...'] [--og-image 'url'] | --join --code abc123 | --scan --code abc123 | --end --code abc123 --id telegram:123
|
|
415
|
+
antenna event --create --name 'AI Meetup' [--desc '...'] [--og-image 'url'] | --join --code abc123 | --scan --code abc123 | --end --code abc123 --id telegram:123 | --upload-image --code abc123 --file /path/to/image.png
|
|
401
416
|
antenna bind --id telegram:123
|
|
402
417
|
antenna serve Start MCP server (stdio transport)
|
|
403
418
|
antenna setup Interactive profile setup [--id telegram:123]
|
package/lib/core.js
CHANGED
|
@@ -479,6 +479,17 @@ export async function pass({ device_id, target_device_id, ref, supabaseUrl, supa
|
|
|
479
479
|
|
|
480
480
|
// âââ events âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
481
481
|
|
|
482
|
+
export async function uploadEventImage({ image_data, content_type, event_code, supabaseUrl, supabaseKey }) {
|
|
483
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
484
|
+
const ext = (content_type || "image/png").split("/")[1] || "png";
|
|
485
|
+
const path = `${event_code || Date.now()}.${ext}`;
|
|
486
|
+
const buf = typeof image_data === "string" ? Buffer.from(image_data, "base64") : image_data;
|
|
487
|
+
const { error } = await sb.storage.from("event-images").upload(path, buf, { contentType: content_type || "image/png", upsert: true });
|
|
488
|
+
if (error) throw new Error(error.message);
|
|
489
|
+
const { data } = sb.storage.from("event-images").getPublicUrl(path);
|
|
490
|
+
return data.publicUrl;
|
|
491
|
+
}
|
|
492
|
+
|
|
482
493
|
export async function createEvent({ name, lat, lng, device_id, starts_at, ends_at, description, og_image, supabaseUrl, supabaseKey }) {
|
|
483
494
|
const sb = getClient(supabaseUrl, supabaseKey);
|
|
484
495
|
const { data, error } = await sb.rpc("create_event", {
|
|
@@ -504,6 +515,15 @@ export async function endEvent({ code, device_id, supabaseUrl, supabaseKey }) {
|
|
|
504
515
|
|
|
505
516
|
export async function eventCheckin({ code, device_id, lat, lng, supabaseUrl, supabaseKey }) {
|
|
506
517
|
const sb = getClient(supabaseUrl, supabaseKey);
|
|
518
|
+
|
|
519
|
+
// Auto-read profile location if not provided
|
|
520
|
+
if (lat == null || lng == null) {
|
|
521
|
+
try {
|
|
522
|
+
const { data: loc } = await sb.rpc("get_profile_location", { p_device_id: device_id });
|
|
523
|
+
if (loc?.lat && loc?.lng) { lat = loc.lat; lng = loc.lng; }
|
|
524
|
+
} catch {}
|
|
525
|
+
}
|
|
526
|
+
|
|
507
527
|
const fuzzy = (lat != null && lng != null) ? fuzzyCoord(lat, lng) : { lat: null, lng: null };
|
|
508
528
|
const { data, error } = await sb.rpc("event_checkin", {
|
|
509
529
|
p_code: code, p_device_id: device_id,
|
|
@@ -540,6 +560,7 @@ export async function eventScan({ code, device_id, supabaseUrl, supabaseKey }) {
|
|
|
540
560
|
line2: p.line2,
|
|
541
561
|
line3: p.line3,
|
|
542
562
|
checked_in: !!p.checked_in,
|
|
563
|
+
role: p.role || "participant",
|
|
543
564
|
source: "event",
|
|
544
565
|
};
|
|
545
566
|
});
|
|
@@ -565,14 +586,21 @@ export async function getEvent({ code, supabaseUrl, supabaseKey }) {
|
|
|
565
586
|
return data;
|
|
566
587
|
}
|
|
567
588
|
|
|
568
|
-
export async function createBindToken({ device_id, supabaseUrl, supabaseKey }) {
|
|
589
|
+
export async function createBindToken({ device_id, purpose, event_code, supabaseUrl, supabaseKey }) {
|
|
569
590
|
const sb = getClient(supabaseUrl, supabaseKey);
|
|
570
|
-
const { data, error } = await sb.rpc("create_bind_token", {
|
|
591
|
+
const { data, error } = await sb.rpc("create_bind_token", {
|
|
592
|
+
p_device_id: device_id,
|
|
593
|
+
p_purpose: purpose || "profile",
|
|
594
|
+
p_event_code: event_code || null,
|
|
595
|
+
});
|
|
571
596
|
if (error) throw new Error(error.message);
|
|
572
597
|
const baseUrl = "https://www.antenna.fyi";
|
|
573
598
|
return {
|
|
574
599
|
token: data.token,
|
|
575
600
|
url: `${baseUrl}/locate?token=${data.token}`,
|
|
576
|
-
|
|
601
|
+
purpose: purpose || "profile",
|
|
602
|
+
message: purpose === "event"
|
|
603
|
+
? "åéčŋäļŠéūæĨįŧæīŧåĻååŧšč
ïžåĻæīŧåĻå°įđæåžåģåŊčŪūåŪæīŧåĻä―į―Ūã"
|
|
604
|
+
: "åéčŋäļŠéūæĨįŧįĻæ·ïžåĻææšæĩč§åĻæåžåģåŊå
ąäšŦä―į―Ūã",
|
|
577
605
|
};
|
|
578
606
|
}
|
|
@@ -20,6 +20,7 @@ from .tools import (
|
|
|
20
20
|
handle_event_scan,
|
|
21
21
|
handle_event_end,
|
|
22
22
|
handle_event_checkin,
|
|
23
|
+
handle_event_upload_image,
|
|
23
24
|
_sb,
|
|
24
25
|
_device_id,
|
|
25
26
|
_my_device_ids,
|
|
@@ -38,6 +39,7 @@ from .schemas import (
|
|
|
38
39
|
EVENT_SCAN_SCHEMA,
|
|
39
40
|
EVENT_END_SCHEMA,
|
|
40
41
|
EVENT_CHECKIN_SCHEMA,
|
|
42
|
+
EVENT_UPLOAD_IMAGE_SCHEMA,
|
|
41
43
|
)
|
|
42
44
|
import re
|
|
43
45
|
import time
|
|
@@ -67,6 +69,7 @@ def register(ctx):
|
|
|
67
69
|
ctx.register_tool("antenna_event_scan", EVENT_SCAN_SCHEMA, handle_event_scan)
|
|
68
70
|
ctx.register_tool("antenna_event_end", EVENT_END_SCHEMA, handle_event_end)
|
|
69
71
|
ctx.register_tool("antenna_event_checkin", EVENT_CHECKIN_SCHEMA, handle_event_checkin)
|
|
72
|
+
ctx.register_tool("antenna_event_upload_image", EVENT_UPLOAD_IMAGE_SCHEMA, handle_event_upload_image)
|
|
70
73
|
|
|
71
74
|
# ââ Hook: auto-detect location + check web GPS events âââââââââ
|
|
72
75
|
def on_pre_llm(messages, **kwargs):
|
|
@@ -122,14 +122,15 @@ CHECK_MATCHES_SCHEMA = {
|
|
|
122
122
|
BIND_SCHEMA = {
|
|
123
123
|
"name": "antenna_bind",
|
|
124
124
|
"description": (
|
|
125
|
-
"Generate a GPS binding link.
|
|
126
|
-
"share their phone's location via the web browser."
|
|
125
|
+
"Generate a GPS binding link. Use purpose='event' + event_code when setting an event's location."
|
|
127
126
|
),
|
|
128
127
|
"parameters": {
|
|
129
128
|
"type": "object",
|
|
130
129
|
"properties": {
|
|
131
130
|
"sender_id": {"type": "string"},
|
|
132
131
|
"channel": {"type": "string"},
|
|
132
|
+
"purpose": {"type": "string", "description": "'profile' (default) or 'event'"},
|
|
133
|
+
"event_code": {"type": "string", "description": "Event code (when purpose=event)"},
|
|
133
134
|
},
|
|
134
135
|
"required": ["sender_id", "channel"],
|
|
135
136
|
},
|
|
@@ -237,6 +238,20 @@ EVENT_END_SCHEMA = {
|
|
|
237
238
|
},
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
EVENT_UPLOAD_IMAGE_SCHEMA = {
|
|
242
|
+
"name": "antenna_event_upload_image",
|
|
243
|
+
"description": "Upload an image for an event OG preview. Returns a public URL.",
|
|
244
|
+
"parameters": {
|
|
245
|
+
"type": "object",
|
|
246
|
+
"properties": {
|
|
247
|
+
"image_base64": {"type": "string", "description": "Base64-encoded image data"},
|
|
248
|
+
"content_type": {"type": "string", "description": "MIME type (default image/png)"},
|
|
249
|
+
"event_code": {"type": "string", "description": "Event code"},
|
|
250
|
+
},
|
|
251
|
+
"required": ["image_base64", "event_code"],
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
240
255
|
EVENT_CHECKIN_SCHEMA = {
|
|
241
256
|
"name": "antenna_event_checkin",
|
|
242
257
|
"description": "Check in at an event \u2014 marks you as present at the event location. Optionally updates GPS.",
|
|
@@ -452,16 +452,28 @@ def handle_event_scan(params: dict) -> str:
|
|
|
452
452
|
def handle_bind(params: dict) -> str:
|
|
453
453
|
sb = _sb()
|
|
454
454
|
did = _device_id(params["sender_id"], params["channel"])
|
|
455
|
+
purpose = params.get("purpose", "profile")
|
|
456
|
+
event_code = params.get("event_code")
|
|
455
457
|
|
|
456
|
-
resp = sb.rpc("create_bind_token", {
|
|
458
|
+
resp = sb.rpc("create_bind_token", {
|
|
459
|
+
"p_device_id": did,
|
|
460
|
+
"p_purpose": purpose,
|
|
461
|
+
"p_event_code": event_code,
|
|
462
|
+
}).execute()
|
|
457
463
|
if not resp.data:
|
|
458
464
|
return _ok({"error": "Failed to create bind token"})
|
|
459
465
|
|
|
460
466
|
token = resp.data.get("token")
|
|
467
|
+
msg = (
|
|
468
|
+
"åéčŋäļŠéūæĨįŧæīŧåĻååŧšč
ïžåĻæīŧåĻå°įđæåžåģåŊčŪūåŪæīŧåĻä―į―Ūã"
|
|
469
|
+
if purpose == "event"
|
|
470
|
+
else "åéčŋäļŠéūæĨįŧįĻæ·ïžåĻææšæĩč§åĻæåžåģåŊå
ąäšŦä―į―Ūã"
|
|
471
|
+
)
|
|
461
472
|
return _ok({
|
|
462
473
|
"token": token,
|
|
463
474
|
"url": f"{BASE_URL}/locate?token={token}",
|
|
464
|
-
"
|
|
475
|
+
"purpose": purpose,
|
|
476
|
+
"message": msg,
|
|
465
477
|
})
|
|
466
478
|
|
|
467
479
|
|
|
@@ -480,6 +492,18 @@ def handle_event_end(params: dict) -> str:
|
|
|
480
492
|
return _ok({"ended": False, "error": data.get("error", "įŧææīŧåĻåĪąčīĨ")})
|
|
481
493
|
|
|
482
494
|
|
|
495
|
+
def handle_event_upload_image(params: dict) -> str:
|
|
496
|
+
import base64 as b64mod
|
|
497
|
+
sb = _sb()
|
|
498
|
+
content_type = params.get("content_type") or "image/png"
|
|
499
|
+
ext = content_type.split("/")[1] if "/" in content_type else "png"
|
|
500
|
+
path = f"{params['event_code']}.{ext}"
|
|
501
|
+
buf = b64mod.b64decode(params["image_base64"])
|
|
502
|
+
resp = sb.storage.from_("event-images").upload(path, buf, {"content-type": content_type, "upsert": "true"})
|
|
503
|
+
pub = sb.storage.from_("event-images").get_public_url(path)
|
|
504
|
+
return _ok({"url": pub})
|
|
505
|
+
|
|
506
|
+
|
|
483
507
|
def handle_event_checkin(params: dict) -> str:
|
|
484
508
|
sb = _sb()
|
|
485
509
|
did = _device_id(params["sender_id"], params["channel"])
|
package/lib/mcp.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
eventCheckin,
|
|
19
19
|
joinEvent,
|
|
20
20
|
eventScan,
|
|
21
|
+
uploadEventImage,
|
|
21
22
|
deriveDeviceId,
|
|
22
23
|
} from "./core.js";
|
|
23
24
|
|
|
@@ -162,14 +163,16 @@ export async function startMcpServer() {
|
|
|
162
163
|
|
|
163
164
|
server.tool(
|
|
164
165
|
"antenna_bind",
|
|
165
|
-
"Generate a GPS binding link. Send this to the user so they can share their phone's location via the web.",
|
|
166
|
+
"Generate a GPS binding link. Send this to the user so they can share their phone's location via the web. Use purpose='event' + event_code when creating a link for setting an event's location.",
|
|
166
167
|
{
|
|
167
168
|
sender_id: z.string().describe("The sender's user ID"),
|
|
168
169
|
channel: z.string().describe("Channel name"),
|
|
170
|
+
purpose: z.string().optional().describe("'profile' (default) or 'event'"),
|
|
171
|
+
event_code: z.string().optional().describe("Event code (required when purpose=event)"),
|
|
169
172
|
},
|
|
170
|
-
async ({ sender_id, channel }) => {
|
|
173
|
+
async ({ sender_id, channel, purpose, event_code }) => {
|
|
171
174
|
try {
|
|
172
|
-
const result = await createBindToken({ device_id: deriveDeviceId(sender_id, channel) });
|
|
175
|
+
const result = await createBindToken({ device_id: deriveDeviceId(sender_id, channel), purpose, event_code });
|
|
173
176
|
return jsonResult(result);
|
|
174
177
|
} catch (e) {
|
|
175
178
|
return jsonResult({ error: e.message });
|
|
@@ -246,6 +249,24 @@ export async function startMcpServer() {
|
|
|
246
249
|
}
|
|
247
250
|
);
|
|
248
251
|
|
|
252
|
+
// âââ antenna_event_upload_image ââââââââââââââââââââââââââââââ
|
|
253
|
+
|
|
254
|
+
server.tool(
|
|
255
|
+
"antenna_event_upload_image",
|
|
256
|
+
"Upload an image for an event. Returns a public URL to use as og_image.",
|
|
257
|
+
{
|
|
258
|
+
image_base64: z.string().describe("Base64-encoded image data"),
|
|
259
|
+
content_type: z.string().optional().describe("MIME type (default image/png)"),
|
|
260
|
+
event_code: z.string().describe("Event code to associate the image with"),
|
|
261
|
+
},
|
|
262
|
+
async ({ image_base64, content_type, event_code }) => {
|
|
263
|
+
try {
|
|
264
|
+
const url = await uploadEventImage({ image_data: image_base64, content_type, event_code });
|
|
265
|
+
return jsonResult({ url });
|
|
266
|
+
} catch (e) { return jsonResult({ error: e.message }); }
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
|
|
249
270
|
// âââ antenna_event_end âââââââââââââââââââââââââââââââââââââââ
|
|
250
271
|
|
|
251
272
|
server.tool(
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -112,9 +112,12 @@ Check for mutual matches and contact info updates.
|
|
|
112
112
|
### `antenna_bind`
|
|
113
113
|
Generate a GPS binding link. **You MUST call this immediately after saving a profile.** Do not skip this step.
|
|
114
114
|
- `sender_id`, `channel`: from context
|
|
115
|
+
- `purpose`: optional â `'profile'` (default) updates user location; `'event'` sets event location
|
|
116
|
+
- `event_code`: required when `purpose='event'`
|
|
115
117
|
- Returns a URL like `https://www.antenna.fyi/locate?token=xxx`
|
|
116
118
|
- Send this link to the user â they open it on their phone, allow GPS, and their location is automatically shared
|
|
117
119
|
- **MANDATORY after profile save. Do not wait for user to ask.**
|
|
120
|
+
- **For events:** When a creator needs to set event location, call with `purpose='event'` and `event_code`. The GPS will update the event's coordinates, NOT the user's profile.
|
|
118
121
|
|
|
119
122
|
### `antenna_discover`
|
|
120
123
|
Get today's global recommendation â the person most similar to you worldwide. 1 per day, no repeats.
|
|
@@ -282,6 +285,8 @@ Create an event. Returns a shareable link (antenna.fyi/e/CODE).
|
|
|
282
285
|
- `description`: optional event description
|
|
283
286
|
- `og_image`: optional OG image URL for social sharing preview
|
|
284
287
|
|
|
288
|
+
**GPS flow for events:** If the user doesn't provide coordinates, generate a bind link (`antenna_bind`) and ask them to open it at the event location. Once GPS comes in, use those coordinates for the event's `lat`/`lng` â do NOT treat this as the user's personal location. The bind link GPS for event creation goes to the event, not the user's profile. Only use `antenna_checkin` when the user wants to update their own location.
|
|
289
|
+
|
|
285
290
|
### `antenna_event_end`
|
|
286
291
|
End an event. Only the creator can end it.
|
|
287
292
|
- `code`: event code
|
|
@@ -303,3 +308,9 @@ Check in at an event â marks you as present at the event location. Optionally
|
|
|
303
308
|
- `code`: event code
|
|
304
309
|
- `sender_id`, `channel`: from context
|
|
305
310
|
- `lat`, `lng`: optional GPS coordinates
|
|
311
|
+
|
|
312
|
+
### `antenna_event_upload_image`
|
|
313
|
+
Upload an image for an event OG preview. Returns a public URL.
|
|
314
|
+
- `image_base64`: base64-encoded image data
|
|
315
|
+
- `content_type`: MIME type (default image/png)
|
|
316
|
+
- `event_code`: event code
|