antenna-fyi 1.2.11 → 1.2.13
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 +2 -2
- package/lib/core.js +99 -3
- package/lib/hermes-plugin/__init__.py +12 -0
- package/lib/hermes-plugin/schemas.py +73 -2
- package/lib/hermes-plugin/tools.py +115 -5
- package/lib/mcp.js +97 -6
- package/package.json +1 -1
- package/skill/EVENTS.md +96 -0
- package/skill/SKILL.md +44 -2
package/lib/cli.js
CHANGED
|
@@ -179,7 +179,7 @@ export async function handleEvent(f) {
|
|
|
179
179
|
|
|
180
180
|
if (f.create || (!f.join && !f.scan && !f.end && f.name)) {
|
|
181
181
|
if (!f.name) return console.error("Usage: antenna event --create --name 'AI Meetup' [--desc 'description'] [--og-image 'url']");
|
|
182
|
-
const result = await createEvent({ name: f.name, device_id: f.id || null, lat: f.lat ? +f.lat : undefined, lng: f.lng ? +f.lng : undefined, description: f.desc || undefined, og_image: f['og-image'] || undefined });
|
|
182
|
+
const result = await createEvent({ name: f.name, device_id: f.id || null, lat: f.lat ? +f.lat : undefined, lng: f.lng ? +f.lng : undefined, description: f.desc || undefined, og_image: f['og-image'] || undefined, requires_approval: f['requires-approval'] === true || f['requires-approval'] === 'true' || undefined, screening_questions: f['screening-questions'] ? f['screening-questions'].split('|') : undefined });
|
|
183
183
|
console.log(`\n🎉 Event created!\n`);
|
|
184
184
|
console.log(` Name: ${result.name}`);
|
|
185
185
|
console.log(` Code: ${result.code}`);
|
|
@@ -190,7 +190,7 @@ export async function handleEvent(f) {
|
|
|
190
190
|
|
|
191
191
|
if (f.join) {
|
|
192
192
|
if (!f.code || !f.id) return console.error("Usage: antenna event --join --code abc123 --id telegram:123");
|
|
193
|
-
const result = await joinEvent({ code: f.code, device_id: f.id });
|
|
193
|
+
const result = await joinEvent({ code: f.code, device_id: f.id, lat: f.lat ? +f.lat : undefined, lng: f.lng ? +f.lng : undefined, application_context: f['application-context'] || undefined });
|
|
194
194
|
if (result.joined) {
|
|
195
195
|
console.log(`\n✅ Joined "${result.name}" (${result.code})\n`);
|
|
196
196
|
} else {
|
package/lib/core.js
CHANGED
|
@@ -490,7 +490,7 @@ export async function uploadEventImage({ image_data, content_type, event_code, s
|
|
|
490
490
|
return data.publicUrl;
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
export async function createEvent({ name, lat, lng, device_id, starts_at, ends_at, description, og_image, supabaseUrl, supabaseKey }) {
|
|
493
|
+
export async function createEvent({ name, lat, lng, device_id, starts_at, ends_at, description, og_image, requires_approval, screening_questions, supabaseUrl, supabaseKey }) {
|
|
494
494
|
const sb = getClient(supabaseUrl, supabaseKey);
|
|
495
495
|
const { data, error } = await sb.rpc("create_event", {
|
|
496
496
|
p_name: name,
|
|
@@ -501,6 +501,8 @@ export async function createEvent({ name, lat, lng, device_id, starts_at, ends_a
|
|
|
501
501
|
p_ends_at: ends_at || new Date(Date.now() + 12 * 60 * 60 * 1000).toISOString(),
|
|
502
502
|
p_description: description || null,
|
|
503
503
|
p_og_image: og_image || null,
|
|
504
|
+
p_requires_approval: requires_approval || false,
|
|
505
|
+
p_screening_questions: screening_questions || null,
|
|
504
506
|
});
|
|
505
507
|
if (error) throw new Error(error.message);
|
|
506
508
|
return data;
|
|
@@ -533,10 +535,71 @@ export async function eventCheckin({ code, device_id, lat, lng, supabaseUrl, sup
|
|
|
533
535
|
return data;
|
|
534
536
|
}
|
|
535
537
|
|
|
536
|
-
export async function joinEvent({ code, device_id, supabaseUrl, supabaseKey }) {
|
|
538
|
+
export async function joinEvent({ code, device_id, lat, lng, application_context, supabaseUrl, supabaseKey }) {
|
|
537
539
|
const sb = getClient(supabaseUrl, supabaseKey);
|
|
538
|
-
|
|
540
|
+
|
|
541
|
+
// Profile gate: check if user has a profile before joining
|
|
542
|
+
const profile = await getProfile({ device_id, supabaseUrl, supabaseKey });
|
|
543
|
+
if (!profile) {
|
|
544
|
+
return { joined: false, error: "Create a profile first before joining events" };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Auto-read profile location if not provided
|
|
548
|
+
if (lat == null || lng == null) {
|
|
549
|
+
try {
|
|
550
|
+
const { data: loc } = await sb.rpc("get_profile_location", { p_device_id: device_id });
|
|
551
|
+
if (loc?.lat && loc?.lng) { lat = loc.lat; lng = loc.lng; }
|
|
552
|
+
} catch {}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const { data, error } = await sb.rpc("join_event", {
|
|
556
|
+
p_code: code,
|
|
557
|
+
p_device_id: device_id,
|
|
558
|
+
p_lat: (lat != null && lng != null) ? fuzzyCoord(lat, lng).lat : null,
|
|
559
|
+
p_lng: (lat != null && lng != null) ? fuzzyCoord(lat, lng).lng : null,
|
|
560
|
+
p_application_context: application_context || null,
|
|
561
|
+
});
|
|
539
562
|
if (error) throw new Error(error.message);
|
|
563
|
+
if (!data?.joined) return data;
|
|
564
|
+
|
|
565
|
+
// Auto-checkin if event has already started and we have GPS
|
|
566
|
+
if (lat != null && lng != null) {
|
|
567
|
+
try {
|
|
568
|
+
const event = await getEvent({ code, supabaseUrl, supabaseKey });
|
|
569
|
+
const startsAt = event?.starts_at ? new Date(event.starts_at) : null;
|
|
570
|
+
if (startsAt && startsAt <= new Date()) {
|
|
571
|
+
// Event has started — attempt auto-checkin
|
|
572
|
+
if (event.lat != null && event.lng != null) {
|
|
573
|
+
// Calculate distance (Haversine)
|
|
574
|
+
const R = 6371000;
|
|
575
|
+
const dLat = (event.lat - lat) * Math.PI / 180;
|
|
576
|
+
const dLng = (event.lng - lng) * Math.PI / 180;
|
|
577
|
+
const a = Math.sin(dLat/2)**2 + Math.cos(lat*Math.PI/180)*Math.cos(event.lat*Math.PI/180)*Math.sin(dLng/2)**2;
|
|
578
|
+
const dist = R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
579
|
+
|
|
580
|
+
if (dist <= 1000) {
|
|
581
|
+
await eventCheckin({ code, device_id, lat, lng, supabaseUrl, supabaseKey });
|
|
582
|
+
data.checked_in = true;
|
|
583
|
+
} else {
|
|
584
|
+
data.checked_in = false;
|
|
585
|
+
data.checkin_reason = "too far";
|
|
586
|
+
data.distance_m = Math.round(dist);
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
// Event has no GPS — checkin without distance check
|
|
590
|
+
await eventCheckin({ code, device_id, lat, lng, supabaseUrl, supabaseKey });
|
|
591
|
+
data.checked_in = true;
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
data.checked_in = false;
|
|
595
|
+
data.checkin_reason = "event not started yet";
|
|
596
|
+
}
|
|
597
|
+
} catch {
|
|
598
|
+
data.checked_in = false;
|
|
599
|
+
data.checkin_reason = "checkin failed";
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
540
603
|
return data;
|
|
541
604
|
}
|
|
542
605
|
|
|
@@ -586,6 +649,39 @@ export async function getEvent({ code, supabaseUrl, supabaseKey }) {
|
|
|
586
649
|
return data;
|
|
587
650
|
}
|
|
588
651
|
|
|
652
|
+
export async function updateEvent({ code, device_id, name, description, og_image, lat, lng, starts_at, ends_at, supabaseUrl, supabaseKey }) {
|
|
653
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
654
|
+
const { data, error } = await sb.rpc("update_event", {
|
|
655
|
+
p_code: code, p_device_id: device_id,
|
|
656
|
+
p_name: name || null, p_description: description || null,
|
|
657
|
+
p_og_image: og_image || null, p_lat: lat || null, p_lng: lng || null,
|
|
658
|
+
p_starts_at: starts_at || null, p_ends_at: ends_at || null,
|
|
659
|
+
});
|
|
660
|
+
if (error) throw new Error(error.message);
|
|
661
|
+
return data;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export async function approveParticipant({ code, device_id, ref, supabaseUrl, supabaseKey }) {
|
|
665
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
666
|
+
const { data, error } = await sb.rpc("approve_participant", { p_code: code, p_device_id: device_id, p_target_ref: ref });
|
|
667
|
+
if (error) throw new Error(error.message);
|
|
668
|
+
return data;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export async function rejectParticipant({ code, device_id, ref, supabaseUrl, supabaseKey }) {
|
|
672
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
673
|
+
const { data, error } = await sb.rpc("reject_participant", { p_code: code, p_device_id: device_id, p_target_ref: ref });
|
|
674
|
+
if (error) throw new Error(error.message);
|
|
675
|
+
return data;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export async function addCohost({ code, device_id, ref, supabaseUrl, supabaseKey }) {
|
|
679
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
680
|
+
const { data, error } = await sb.rpc("add_cohost", { p_code: code, p_device_id: device_id, p_target_ref: ref });
|
|
681
|
+
if (error) throw new Error(error.message);
|
|
682
|
+
return data;
|
|
683
|
+
}
|
|
684
|
+
|
|
589
685
|
export async function createBindToken({ device_id, purpose, event_code, supabaseUrl, supabaseKey }) {
|
|
590
686
|
const sb = getClient(supabaseUrl, supabaseKey);
|
|
591
687
|
const { data, error } = await sb.rpc("create_bind_token", {
|
|
@@ -21,6 +21,10 @@ from .tools import (
|
|
|
21
21
|
handle_event_end,
|
|
22
22
|
handle_event_checkin,
|
|
23
23
|
handle_event_upload_image,
|
|
24
|
+
handle_event_update,
|
|
25
|
+
handle_event_approve,
|
|
26
|
+
handle_event_reject,
|
|
27
|
+
handle_event_add_host,
|
|
24
28
|
_sb,
|
|
25
29
|
_device_id,
|
|
26
30
|
_my_device_ids,
|
|
@@ -40,6 +44,10 @@ from .schemas import (
|
|
|
40
44
|
EVENT_END_SCHEMA,
|
|
41
45
|
EVENT_CHECKIN_SCHEMA,
|
|
42
46
|
EVENT_UPLOAD_IMAGE_SCHEMA,
|
|
47
|
+
EVENT_UPDATE_SCHEMA,
|
|
48
|
+
EVENT_APPROVE_SCHEMA,
|
|
49
|
+
EVENT_REJECT_SCHEMA,
|
|
50
|
+
EVENT_ADD_HOST_SCHEMA,
|
|
43
51
|
)
|
|
44
52
|
import re
|
|
45
53
|
import time
|
|
@@ -70,6 +78,10 @@ def register(ctx):
|
|
|
70
78
|
ctx.register_tool("antenna_event_end", EVENT_END_SCHEMA, handle_event_end)
|
|
71
79
|
ctx.register_tool("antenna_event_checkin", EVENT_CHECKIN_SCHEMA, handle_event_checkin)
|
|
72
80
|
ctx.register_tool("antenna_event_upload_image", EVENT_UPLOAD_IMAGE_SCHEMA, handle_event_upload_image)
|
|
81
|
+
ctx.register_tool("antenna_event_update", EVENT_UPDATE_SCHEMA, handle_event_update)
|
|
82
|
+
ctx.register_tool("antenna_event_approve", EVENT_APPROVE_SCHEMA, handle_event_approve)
|
|
83
|
+
ctx.register_tool("antenna_event_reject", EVENT_REJECT_SCHEMA, handle_event_reject)
|
|
84
|
+
ctx.register_tool("antenna_event_add_host", EVENT_ADD_HOST_SCHEMA, handle_event_add_host)
|
|
73
85
|
|
|
74
86
|
# ── Hook: auto-detect location + check web GPS events ─────────
|
|
75
87
|
def on_pre_llm(messages, **kwargs):
|
|
@@ -176,7 +176,7 @@ DISCOVER_SCHEMA = {
|
|
|
176
176
|
EVENT_CREATE_SCHEMA = {
|
|
177
177
|
"name": "antenna_event_create",
|
|
178
178
|
"description": (
|
|
179
|
-
"Create an event. Returns a shareable link (antenna.fyi/
|
|
179
|
+
"Create an event. Returns a shareable link (antenna.fyi/events/CODE) "
|
|
180
180
|
"for participants to join. Optionally include a description and OG image URL."
|
|
181
181
|
),
|
|
182
182
|
"parameters": {
|
|
@@ -191,6 +191,8 @@ EVENT_CREATE_SCHEMA = {
|
|
|
191
191
|
"ends_at": {"type": "string", "description": "End time ISO"},
|
|
192
192
|
"description": {"type": "string", "description": "Event description"},
|
|
193
193
|
"og_image": {"type": "string", "description": "OG image URL for social sharing"},
|
|
194
|
+
"requires_approval": {"type": "boolean", "description": "Require host approval to join (default false)"},
|
|
195
|
+
"screening_questions": {"type": "array", "items": {"type": "string"}, "description": "Screening questions for applicants"},
|
|
194
196
|
},
|
|
195
197
|
"required": ["name", "sender_id", "channel"],
|
|
196
198
|
},
|
|
@@ -198,13 +200,16 @@ EVENT_CREATE_SCHEMA = {
|
|
|
198
200
|
|
|
199
201
|
EVENT_JOIN_SCHEMA = {
|
|
200
202
|
"name": "antenna_event_join",
|
|
201
|
-
"description": "Join an event by its code from the event URL.",
|
|
203
|
+
"description": "Join an event by its code from the event URL. Auto-checks in if event has started and you're within 1km.",
|
|
202
204
|
"parameters": {
|
|
203
205
|
"type": "object",
|
|
204
206
|
"properties": {
|
|
205
207
|
"code": {"type": "string", "description": "Event code"},
|
|
206
208
|
"sender_id": {"type": "string", "description": "The sender's user ID"},
|
|
207
209
|
"channel": {"type": "string", "description": "Platform name"},
|
|
210
|
+
"lat": {"type": "number", "description": "Latitude (optional, for auto-checkin)"},
|
|
211
|
+
"lng": {"type": "number", "description": "Longitude (optional, for auto-checkin)"},
|
|
212
|
+
"application_context": {"type": "string", "description": "Application context from screening conversation"},
|
|
208
213
|
},
|
|
209
214
|
"required": ["code", "sender_id", "channel"],
|
|
210
215
|
},
|
|
@@ -267,3 +272,69 @@ EVENT_CHECKIN_SCHEMA = {
|
|
|
267
272
|
"required": ["code", "sender_id", "channel"],
|
|
268
273
|
},
|
|
269
274
|
}
|
|
275
|
+
|
|
276
|
+
EVENT_UPDATE_SCHEMA = {
|
|
277
|
+
"name": "antenna_event_update",
|
|
278
|
+
"description": "Update event info. Only creator or co-host can update.",
|
|
279
|
+
"parameters": {
|
|
280
|
+
"type": "object",
|
|
281
|
+
"properties": {
|
|
282
|
+
"code": {"type": "string"},
|
|
283
|
+
"sender_id": {"type": "string"},
|
|
284
|
+
"channel": {"type": "string"},
|
|
285
|
+
"name": {"type": "string"},
|
|
286
|
+
"description": {"type": "string"},
|
|
287
|
+
"og_image": {"type": "string"},
|
|
288
|
+
"lat": {"type": "number"},
|
|
289
|
+
"lng": {"type": "number"},
|
|
290
|
+
"starts_at": {"type": "string"},
|
|
291
|
+
"ends_at": {"type": "string"},
|
|
292
|
+
},
|
|
293
|
+
"required": ["code", "sender_id", "channel"],
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
EVENT_APPROVE_SCHEMA = {
|
|
298
|
+
"name": "antenna_event_approve",
|
|
299
|
+
"description": "Approve a pending participant. Only creator or co-host.",
|
|
300
|
+
"parameters": {
|
|
301
|
+
"type": "object",
|
|
302
|
+
"properties": {
|
|
303
|
+
"code": {"type": "string"},
|
|
304
|
+
"sender_id": {"type": "string"},
|
|
305
|
+
"channel": {"type": "string"},
|
|
306
|
+
"ref": {"type": "string"},
|
|
307
|
+
},
|
|
308
|
+
"required": ["code", "sender_id", "channel", "ref"],
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
EVENT_REJECT_SCHEMA = {
|
|
313
|
+
"name": "antenna_event_reject",
|
|
314
|
+
"description": "Reject a pending participant. Only creator or co-host.",
|
|
315
|
+
"parameters": {
|
|
316
|
+
"type": "object",
|
|
317
|
+
"properties": {
|
|
318
|
+
"code": {"type": "string"},
|
|
319
|
+
"sender_id": {"type": "string"},
|
|
320
|
+
"channel": {"type": "string"},
|
|
321
|
+
"ref": {"type": "string"},
|
|
322
|
+
},
|
|
323
|
+
"required": ["code", "sender_id", "channel", "ref"],
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
EVENT_ADD_HOST_SCHEMA = {
|
|
328
|
+
"name": "antenna_event_add_host",
|
|
329
|
+
"description": "Add a co-host to the event. Only creator can add.",
|
|
330
|
+
"parameters": {
|
|
331
|
+
"type": "object",
|
|
332
|
+
"properties": {
|
|
333
|
+
"code": {"type": "string"},
|
|
334
|
+
"sender_id": {"type": "string"},
|
|
335
|
+
"channel": {"type": "string"},
|
|
336
|
+
"ref": {"type": "string"},
|
|
337
|
+
},
|
|
338
|
+
"required": ["code", "sender_id", "channel", "ref"],
|
|
339
|
+
},
|
|
340
|
+
}
|
|
@@ -383,6 +383,10 @@ def handle_event_create(params: dict) -> str:
|
|
|
383
383
|
rpc_params["p_description"] = params["description"]
|
|
384
384
|
if params.get("og_image"):
|
|
385
385
|
rpc_params["p_og_image"] = params["og_image"]
|
|
386
|
+
if params.get("requires_approval"):
|
|
387
|
+
rpc_params["p_requires_approval"] = params["requires_approval"]
|
|
388
|
+
if params.get("screening_questions"):
|
|
389
|
+
rpc_params["p_screening_questions"] = params["screening_questions"]
|
|
386
390
|
|
|
387
391
|
resp = sb.rpc("create_event", rpc_params).execute()
|
|
388
392
|
data = resp.data or {}
|
|
@@ -392,8 +396,8 @@ def handle_event_create(params: dict) -> str:
|
|
|
392
396
|
"created": True,
|
|
393
397
|
"name": params["name"],
|
|
394
398
|
"code": code,
|
|
395
|
-
"url": f"{BASE_URL}/
|
|
396
|
-
"message": f"活动已创建!分享链接给参加的人:{BASE_URL}/
|
|
399
|
+
"url": f"{BASE_URL}/events/{code}",
|
|
400
|
+
"message": f"活动已创建!分享链接给参加的人:{BASE_URL}/events/{code}",
|
|
397
401
|
})
|
|
398
402
|
|
|
399
403
|
|
|
@@ -401,15 +405,81 @@ def handle_event_join(params: dict) -> str:
|
|
|
401
405
|
sb = _sb()
|
|
402
406
|
did = _device_id(params["sender_id"], params["channel"])
|
|
403
407
|
|
|
408
|
+
# Profile gate
|
|
409
|
+
prof = sb.rpc("get_profile", {"p_device_id": did}).execute()
|
|
410
|
+
if not prof.data:
|
|
411
|
+
return _ok({"joined": False, "error": "Create a profile first before joining events"})
|
|
412
|
+
|
|
413
|
+
lat = params.get("lat")
|
|
414
|
+
lng = params.get("lng")
|
|
415
|
+
|
|
416
|
+
# Auto-read profile location if not provided
|
|
417
|
+
if lat is None or lng is None:
|
|
418
|
+
try:
|
|
419
|
+
loc_resp = sb.rpc("get_profile_location", {"p_device_id": did}).execute()
|
|
420
|
+
loc = loc_resp.data if loc_resp.data else {}
|
|
421
|
+
if loc.get("found"):
|
|
422
|
+
lat = loc["lat"]
|
|
423
|
+
lng = loc["lng"]
|
|
424
|
+
except Exception:
|
|
425
|
+
pass
|
|
426
|
+
|
|
404
427
|
resp = sb.rpc("join_event", {
|
|
405
428
|
"p_device_id": did,
|
|
406
429
|
"p_code": params["code"],
|
|
430
|
+
"p_lat": lat,
|
|
431
|
+
"p_lng": lng,
|
|
432
|
+
"p_application_context": params.get("application_context"),
|
|
407
433
|
}).execute()
|
|
408
434
|
data = resp.data or {}
|
|
409
435
|
|
|
410
|
-
if data.get("joined"):
|
|
411
|
-
return _ok({"joined":
|
|
412
|
-
|
|
436
|
+
if not data.get("joined"):
|
|
437
|
+
return _ok({"joined": False, "error": data.get("error", "加入失败")})
|
|
438
|
+
|
|
439
|
+
# Auto-checkin if event started and we have GPS
|
|
440
|
+
if lat is not None and lng is not None:
|
|
441
|
+
try:
|
|
442
|
+
evt_resp = sb.rpc("get_event", {"p_code": params["code"]}).execute()
|
|
443
|
+
evt = evt_resp.data or {}
|
|
444
|
+
import datetime
|
|
445
|
+
starts_at = evt.get("starts_at")
|
|
446
|
+
if starts_at:
|
|
447
|
+
# Parse ISO datetime
|
|
448
|
+
sa = datetime.datetime.fromisoformat(starts_at.replace("Z", "+00:00"))
|
|
449
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
450
|
+
if sa <= now:
|
|
451
|
+
evt_lat = evt.get("lat")
|
|
452
|
+
evt_lng = evt.get("lng")
|
|
453
|
+
do_checkin = True
|
|
454
|
+
if evt_lat is not None and evt_lng is not None:
|
|
455
|
+
# Haversine distance
|
|
456
|
+
R = 6371000
|
|
457
|
+
d_lat = math.radians(evt_lat - lat)
|
|
458
|
+
d_lng = math.radians(evt_lng - lng)
|
|
459
|
+
a = math.sin(d_lat/2)**2 + math.cos(math.radians(lat))*math.cos(math.radians(evt_lat))*math.sin(d_lng/2)**2
|
|
460
|
+
dist = R * 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
|
461
|
+
if dist > 1000:
|
|
462
|
+
do_checkin = False
|
|
463
|
+
data["checked_in"] = False
|
|
464
|
+
data["checkin_reason"] = "too far"
|
|
465
|
+
data["distance_m"] = round(dist)
|
|
466
|
+
if do_checkin:
|
|
467
|
+
flat, flng = _fuzzy(lat, lng)
|
|
468
|
+
sb.rpc("event_checkin", {
|
|
469
|
+
"p_code": params["code"],
|
|
470
|
+
"p_device_id": did,
|
|
471
|
+
"p_lat": flat,
|
|
472
|
+
"p_lng": flng,
|
|
473
|
+
}).execute()
|
|
474
|
+
data["checked_in"] = True
|
|
475
|
+
else:
|
|
476
|
+
data["checked_in"] = False
|
|
477
|
+
data["checkin_reason"] = "event not started yet"
|
|
478
|
+
except Exception:
|
|
479
|
+
data["checked_in"] = False
|
|
480
|
+
data["checkin_reason"] = "checkin failed"
|
|
481
|
+
|
|
482
|
+
return _ok(data)
|
|
413
483
|
|
|
414
484
|
|
|
415
485
|
def handle_event_scan(params: dict) -> str:
|
|
@@ -522,3 +592,43 @@ def handle_event_checkin(params: dict) -> str:
|
|
|
522
592
|
"p_lng": flng,
|
|
523
593
|
}).execute()
|
|
524
594
|
return _ok(resp.data or {})
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def handle_event_update(params: dict) -> str:
|
|
598
|
+
sb = _sb()
|
|
599
|
+
did = _device_id(params["sender_id"], params["channel"])
|
|
600
|
+
resp = sb.rpc("update_event", {
|
|
601
|
+
"p_code": params["code"], "p_device_id": did,
|
|
602
|
+
"p_name": params.get("name"), "p_description": params.get("description"),
|
|
603
|
+
"p_og_image": params.get("og_image"), "p_lat": params.get("lat"),
|
|
604
|
+
"p_lng": params.get("lng"), "p_starts_at": params.get("starts_at"),
|
|
605
|
+
"p_ends_at": params.get("ends_at"),
|
|
606
|
+
}).execute()
|
|
607
|
+
return _ok(resp.data or {"error": "update failed"})
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def handle_event_approve(params: dict) -> str:
|
|
611
|
+
sb = _sb()
|
|
612
|
+
did = _device_id(params["sender_id"], params["channel"])
|
|
613
|
+
resp = sb.rpc("approve_participant", {
|
|
614
|
+
"p_code": params["code"], "p_device_id": did, "p_target_ref": params["ref"],
|
|
615
|
+
}).execute()
|
|
616
|
+
return _ok(resp.data or {"error": "approve failed"})
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def handle_event_reject(params: dict) -> str:
|
|
620
|
+
sb = _sb()
|
|
621
|
+
did = _device_id(params["sender_id"], params["channel"])
|
|
622
|
+
resp = sb.rpc("reject_participant", {
|
|
623
|
+
"p_code": params["code"], "p_device_id": did, "p_target_ref": params["ref"],
|
|
624
|
+
}).execute()
|
|
625
|
+
return _ok(resp.data or {"error": "reject failed"})
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def handle_event_add_host(params: dict) -> str:
|
|
629
|
+
sb = _sb()
|
|
630
|
+
did = _device_id(params["sender_id"], params["channel"])
|
|
631
|
+
resp = sb.rpc("add_cohost", {
|
|
632
|
+
"p_code": params["code"], "p_device_id": did, "p_target_ref": params["ref"],
|
|
633
|
+
}).execute()
|
|
634
|
+
return _ok(resp.data or {"error": "add_cohost failed"})
|
package/lib/mcp.js
CHANGED
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
joinEvent,
|
|
20
20
|
eventScan,
|
|
21
21
|
uploadEventImage,
|
|
22
|
+
updateEvent,
|
|
23
|
+
approveParticipant,
|
|
24
|
+
rejectParticipant,
|
|
25
|
+
addCohost,
|
|
22
26
|
deriveDeviceId,
|
|
23
27
|
} from "./core.js";
|
|
24
28
|
|
|
@@ -270,7 +274,7 @@ export async function startMcpServer() {
|
|
|
270
274
|
|
|
271
275
|
server.tool(
|
|
272
276
|
"antenna_event_create",
|
|
273
|
-
"Create an event. Returns a shareable link (antenna.fyi/
|
|
277
|
+
"Create an event. Returns a shareable link (antenna.fyi/events/CODE) for participants to join.",
|
|
274
278
|
{
|
|
275
279
|
name: z.string().describe("Event name"),
|
|
276
280
|
sender_id: z.string().describe("Creator's user ID"),
|
|
@@ -281,10 +285,12 @@ export async function startMcpServer() {
|
|
|
281
285
|
ends_at: z.string().optional().describe("End time ISO string"),
|
|
282
286
|
description: z.string().optional().describe("Event description"),
|
|
283
287
|
og_image: z.string().optional().describe("OG image URL for social sharing"),
|
|
288
|
+
requires_approval: z.boolean().optional().describe("Require host approval to join (default false)"),
|
|
289
|
+
screening_questions: z.array(z.string()).optional().describe("Screening questions for applicants"),
|
|
284
290
|
},
|
|
285
|
-
async ({ name, sender_id, channel, lat, lng, starts_at, ends_at, description, og_image }) => {
|
|
291
|
+
async ({ name, sender_id, channel, lat, lng, starts_at, ends_at, description, og_image, requires_approval, screening_questions }) => {
|
|
286
292
|
try {
|
|
287
|
-
const result = await createEvent({ name, lat, lng, device_id: deriveDeviceId(sender_id, channel), starts_at, ends_at, description, og_image });
|
|
293
|
+
const result = await createEvent({ name, lat, lng, device_id: deriveDeviceId(sender_id, channel), starts_at, ends_at, description, og_image, requires_approval, screening_questions });
|
|
288
294
|
return jsonResult(result);
|
|
289
295
|
} catch (e) { return jsonResult({ error: e.message }); }
|
|
290
296
|
}
|
|
@@ -330,15 +336,18 @@ export async function startMcpServer() {
|
|
|
330
336
|
|
|
331
337
|
server.tool(
|
|
332
338
|
"antenna_event_join",
|
|
333
|
-
"Join an event by its code. The code is from the event URL (antenna.fyi/
|
|
339
|
+
"Join an event by its code. The code is from the event URL (antenna.fyi/events/CODE). Auto-checks in if the event has already started and you're within 1km.",
|
|
334
340
|
{
|
|
335
341
|
code: z.string().describe("Event code"),
|
|
336
342
|
sender_id: z.string().describe("The sender's user ID"),
|
|
337
343
|
channel: z.string().describe("Channel name"),
|
|
344
|
+
lat: z.number().optional().describe("Latitude (optional, for auto-checkin)"),
|
|
345
|
+
lng: z.number().optional().describe("Longitude (optional, for auto-checkin)"),
|
|
346
|
+
application_context: z.string().optional().describe("Application context from screening conversation"),
|
|
338
347
|
},
|
|
339
|
-
async ({ code, sender_id, channel }) => {
|
|
348
|
+
async ({ code, sender_id, channel, lat, lng, application_context }) => {
|
|
340
349
|
try {
|
|
341
|
-
const result = await joinEvent({ code, device_id: deriveDeviceId(sender_id, channel) });
|
|
350
|
+
const result = await joinEvent({ code, device_id: deriveDeviceId(sender_id, channel), lat, lng, application_context });
|
|
342
351
|
return jsonResult(result);
|
|
343
352
|
} catch (e) { return jsonResult({ error: e.message }); }
|
|
344
353
|
}
|
|
@@ -387,6 +396,88 @@ export async function startMcpServer() {
|
|
|
387
396
|
}
|
|
388
397
|
);
|
|
389
398
|
|
|
399
|
+
// ─── antenna_event_update ──────────────────────────────────
|
|
400
|
+
|
|
401
|
+
server.tool(
|
|
402
|
+
"antenna_event_update",
|
|
403
|
+
"Update event info. Only the creator or co-host can update.",
|
|
404
|
+
{
|
|
405
|
+
code: z.string().describe("Event code"),
|
|
406
|
+
sender_id: z.string().describe("The sender's user ID"),
|
|
407
|
+
channel: z.string().describe("Channel name"),
|
|
408
|
+
name: z.string().optional().describe("New event name"),
|
|
409
|
+
description: z.string().optional().describe("New event description"),
|
|
410
|
+
og_image: z.string().optional().describe("New OG image URL"),
|
|
411
|
+
lat: z.number().optional().describe("New event latitude"),
|
|
412
|
+
lng: z.number().optional().describe("New event longitude"),
|
|
413
|
+
starts_at: z.string().optional().describe("New start time ISO"),
|
|
414
|
+
ends_at: z.string().optional().describe("New end time ISO"),
|
|
415
|
+
},
|
|
416
|
+
async ({ code, sender_id, channel, name, description, og_image, lat, lng, starts_at, ends_at }) => {
|
|
417
|
+
try {
|
|
418
|
+
const result = await updateEvent({ code, device_id: deriveDeviceId(sender_id, channel), name, description, og_image, lat, lng, starts_at, ends_at });
|
|
419
|
+
return jsonResult(result);
|
|
420
|
+
} catch (e) { return jsonResult({ error: e.message }); }
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// ─── antenna_event_approve ─────────────────────────────────
|
|
425
|
+
|
|
426
|
+
server.tool(
|
|
427
|
+
"antenna_event_approve",
|
|
428
|
+
"Approve a pending participant. Only the creator or co-host can approve.",
|
|
429
|
+
{
|
|
430
|
+
code: z.string().describe("Event code"),
|
|
431
|
+
sender_id: z.string().describe("The sender's user ID"),
|
|
432
|
+
channel: z.string().describe("Channel name"),
|
|
433
|
+
ref: z.string().describe("Ref number of the participant to approve"),
|
|
434
|
+
},
|
|
435
|
+
async ({ code, sender_id, channel, ref }) => {
|
|
436
|
+
try {
|
|
437
|
+
const result = await approveParticipant({ code, device_id: deriveDeviceId(sender_id, channel), ref });
|
|
438
|
+
return jsonResult(result);
|
|
439
|
+
} catch (e) { return jsonResult({ error: e.message }); }
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// ─── antenna_event_reject ──────────────────────────────────
|
|
444
|
+
|
|
445
|
+
server.tool(
|
|
446
|
+
"antenna_event_reject",
|
|
447
|
+
"Reject a pending participant. Only the creator or co-host can reject.",
|
|
448
|
+
{
|
|
449
|
+
code: z.string().describe("Event code"),
|
|
450
|
+
sender_id: z.string().describe("The sender's user ID"),
|
|
451
|
+
channel: z.string().describe("Channel name"),
|
|
452
|
+
ref: z.string().describe("Ref number of the participant to reject"),
|
|
453
|
+
},
|
|
454
|
+
async ({ code, sender_id, channel, ref }) => {
|
|
455
|
+
try {
|
|
456
|
+
const result = await rejectParticipant({ code, device_id: deriveDeviceId(sender_id, channel), ref });
|
|
457
|
+
return jsonResult(result);
|
|
458
|
+
} catch (e) { return jsonResult({ error: e.message }); }
|
|
459
|
+
}
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
// ─── antenna_event_add_host ────────────────────────────────
|
|
463
|
+
|
|
464
|
+
server.tool(
|
|
465
|
+
"antenna_event_add_host",
|
|
466
|
+
"Add a co-host to an event. Only the creator can add co-hosts.",
|
|
467
|
+
{
|
|
468
|
+
code: z.string().describe("Event code"),
|
|
469
|
+
sender_id: z.string().describe("The sender's user ID"),
|
|
470
|
+
channel: z.string().describe("Channel name"),
|
|
471
|
+
ref: z.string().describe("Ref number of the participant to make co-host"),
|
|
472
|
+
},
|
|
473
|
+
async ({ code, sender_id, channel, ref }) => {
|
|
474
|
+
try {
|
|
475
|
+
const result = await addCohost({ code, device_id: deriveDeviceId(sender_id, channel), ref });
|
|
476
|
+
return jsonResult(result);
|
|
477
|
+
} catch (e) { return jsonResult({ error: e.message }); }
|
|
478
|
+
}
|
|
479
|
+
);
|
|
480
|
+
|
|
390
481
|
const transport = new StdioServerTransport();
|
|
391
482
|
await server.connect(transport);
|
|
392
483
|
}
|
package/package.json
CHANGED
package/skill/EVENTS.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: antenna-events
|
|
3
|
+
description: "Antenna Event Mode — create events, manage participants, GPS check-in. Use when a user wants to create an event, join an event, check in, scan event participants, or manage event settings."
|
|
4
|
+
tools:
|
|
5
|
+
- antenna_event_create
|
|
6
|
+
- antenna_event_join
|
|
7
|
+
- antenna_event_scan
|
|
8
|
+
- antenna_event_end
|
|
9
|
+
- antenna_event_checkin
|
|
10
|
+
- antenna_event_upload_image
|
|
11
|
+
- antenna_bind
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Antenna Event Mode
|
|
15
|
+
|
|
16
|
+
Create real-world events where everyone's AI agent handles the networking.
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
1. **Create**: `antenna_event_create(name, description)` → get shareable link
|
|
21
|
+
2. **Share**: Send `antenna.fyi/events/CODE` to attendees
|
|
22
|
+
3. **Join**: Attendees' agents call `antenna_event_join(code)`
|
|
23
|
+
4. **Check in**: At the venue, `antenna_event_checkin(code)` — GPS verified ≤1km
|
|
24
|
+
5. **Discover**: `antenna_event_scan(code)` — no distance limit inside events
|
|
25
|
+
|
|
26
|
+
## Tools
|
|
27
|
+
|
|
28
|
+
### `antenna_event_create`
|
|
29
|
+
Create an event. Returns a shareable link (antenna.fyi/events/CODE).
|
|
30
|
+
- `name`: event name
|
|
31
|
+
- `sender_id`, `channel`: from context
|
|
32
|
+
- `lat`, `lng`: optional event location
|
|
33
|
+
- `starts_at`, `ends_at`: optional time range (default: now to +12h)
|
|
34
|
+
- `description`: optional event description
|
|
35
|
+
- `og_image`: optional OG image URL for social sharing preview
|
|
36
|
+
|
|
37
|
+
**GPS flow for events:** If the user doesn't provide coordinates, generate a bind link (`antenna_bind` with `purpose="event"` and `event_code`) and ask them to open it at the event location. The GPS will update the event's coordinates, NOT the user's profile.
|
|
38
|
+
|
|
39
|
+
### `antenna_event_join`
|
|
40
|
+
Join an event by code. Auto-checks in if event already started and user GPS is within 1km.
|
|
41
|
+
- `code`: from the event URL (antenna.fyi/events/CODE)
|
|
42
|
+
- `sender_id`, `channel`: from context
|
|
43
|
+
- **Requires profile** — user must have a profile before joining
|
|
44
|
+
- If event has started + user has GPS + within 1km → auto check-in
|
|
45
|
+
|
|
46
|
+
### `antenna_event_scan`
|
|
47
|
+
Scan people in an event. No distance limit — returns all participants.
|
|
48
|
+
- `code`: event code
|
|
49
|
+
- `sender_id`, `channel`: from context
|
|
50
|
+
- Returns profiles with `checked_in` status and `role` (creator/participant)
|
|
51
|
+
- Header shows "X joined, Y checked in"
|
|
52
|
+
|
|
53
|
+
### `antenna_event_end`
|
|
54
|
+
End an event. Only the creator can end it.
|
|
55
|
+
- `code`: event code
|
|
56
|
+
- `sender_id`, `channel`: from context
|
|
57
|
+
|
|
58
|
+
### `antenna_event_checkin`
|
|
59
|
+
Check in at an event — marks you as present at the event location.
|
|
60
|
+
- `code`: event code
|
|
61
|
+
- `sender_id`, `channel`: from context
|
|
62
|
+
- `lat`, `lng`: optional GPS (auto-reads profile location if not provided)
|
|
63
|
+
- GPS verified: must be within 1km of event location
|
|
64
|
+
- Event must have GPS set for check-in to work
|
|
65
|
+
|
|
66
|
+
### `antenna_event_upload_image`
|
|
67
|
+
Upload an image for an event OG preview. Returns a public URL.
|
|
68
|
+
- `image_base64`: base64-encoded image data
|
|
69
|
+
- `content_type`: MIME type (default image/png)
|
|
70
|
+
- `event_code`: event code
|
|
71
|
+
|
|
72
|
+
### `antenna_bind` (for events)
|
|
73
|
+
Generate a GPS link for setting event location.
|
|
74
|
+
- `purpose`: set to `"event"`
|
|
75
|
+
- `event_code`: the event code
|
|
76
|
+
- GPS from this link writes to the event, not the user's profile
|
|
77
|
+
|
|
78
|
+
## Agent Behavior
|
|
79
|
+
|
|
80
|
+
### When someone says "create an event"
|
|
81
|
+
1. Ask for event name (required) and description (optional)
|
|
82
|
+
2. Call `antenna_event_create`
|
|
83
|
+
3. If no GPS provided, call `antenna_bind(purpose="event", event_code=CODE)` and send the link
|
|
84
|
+
4. Share the event URL with the user
|
|
85
|
+
|
|
86
|
+
### When someone shares an event link
|
|
87
|
+
1. Extract the code from `antenna.fyi/events/CODE`
|
|
88
|
+
2. Call `antenna_event_join(code)` — this will auto-check in if applicable
|
|
89
|
+
3. If join fails with "Create a profile first", guide profile creation then retry
|
|
90
|
+
|
|
91
|
+
### When someone says "who's here" at an event
|
|
92
|
+
1. Call `antenna_event_scan(code)`
|
|
93
|
+
2. Analyze profiles against what you know about the user
|
|
94
|
+
3. Recommend who they should meet and why
|
|
95
|
+
4. Creator appears with organizer badge
|
|
96
|
+
|
package/skill/SKILL.md
CHANGED
|
@@ -73,6 +73,24 @@ Scan for nearby people **and events**. Returns raw profile cards + active events
|
|
|
73
73
|
- `channel`: the channel name (telegram, whatsapp, discord, etc.)
|
|
74
74
|
- Returns `profiles` (nearby people) + `nearby_events` (active events with name, participants count, code)
|
|
75
75
|
|
|
76
|
+
**Location staleness:** Before scanning, check if the user's GPS is recent. If `last_seen_at` is older than 2 hours, prompt the user to update their location (`antenna_bind` or `antenna_checkin`). Stale GPS = wrong results.
|
|
77
|
+
|
|
78
|
+
## GPS Logic
|
|
79
|
+
|
|
80
|
+
**Profile GPS** — the user's location ("where am I")
|
|
81
|
+
- Updated via `antenna_bind(purpose="profile")` or `antenna_checkin`
|
|
82
|
+
- Fuzzy-hashed to ~150m for privacy
|
|
83
|
+
- Used for: `antenna_scan` (nearby people/events), `antenna_event_checkin` (distance check)
|
|
84
|
+
- Has `last_seen_at` timestamp. **Expires conceptually after 2h** — agent should prompt refresh
|
|
85
|
+
|
|
86
|
+
**Event GPS** — the event's location ("where is the event")
|
|
87
|
+
- Set via `antenna_bind(purpose="event")` or `antenna_event_create(lat, lng)`
|
|
88
|
+
- Precise coordinates (NOT blurred)
|
|
89
|
+
- Used for: check-in distance verification (≤1km), `nearby_events` discovery (5km)
|
|
90
|
+
- Does not expire — event location is fixed
|
|
91
|
+
|
|
92
|
+
**Relationship:** check-in = compare profile GPS vs event GPS. scan = use profile GPS to find nearby people + events.
|
|
93
|
+
|
|
76
94
|
After receiving the nearby profiles, **you decide** who to recommend:
|
|
77
95
|
- Use everything you know about the user: their SOUL.md, memory, recent conversations, interests, current mood
|
|
78
96
|
- Compare each nearby person's three-line card against your understanding of the user
|
|
@@ -277,7 +295,7 @@ Plugin 自带后台服务,每 10 分钟轮询一次 Supabase 查新的 mutual
|
|
|
277
295
|
用户不需要主动问,agent 会自动收到通知。
|
|
278
296
|
|
|
279
297
|
### `antenna_event_create`
|
|
280
|
-
Create an event. Returns a shareable link (antenna.fyi/
|
|
298
|
+
Create an event. Returns a shareable link (antenna.fyi/events/CODE).
|
|
281
299
|
- `name`: event name
|
|
282
300
|
- `sender_id`, `channel`: from context
|
|
283
301
|
- `lat`, `lng`: optional event location
|
|
@@ -294,7 +312,7 @@ End an event. Only the creator can end it.
|
|
|
294
312
|
|
|
295
313
|
### `antenna_event_join`
|
|
296
314
|
Join an event by code.
|
|
297
|
-
- `code`: from the event URL (antenna.fyi/
|
|
315
|
+
- `code`: from the event URL (antenna.fyi/events/CODE)
|
|
298
316
|
- `sender_id`, `channel`: from context
|
|
299
317
|
|
|
300
318
|
### `antenna_event_scan`
|
|
@@ -314,3 +332,27 @@ Upload an image for an event OG preview. Returns a public URL.
|
|
|
314
332
|
- `image_base64`: base64-encoded image data
|
|
315
333
|
- `content_type`: MIME type (default image/png)
|
|
316
334
|
- `event_code`: event code
|
|
335
|
+
|
|
336
|
+
### `antenna_event_update`
|
|
337
|
+
Update event info. Only creator or co-host can update.
|
|
338
|
+
- `code`: event code
|
|
339
|
+
- `sender_id`, `channel`: from context
|
|
340
|
+
- `name`, `description`, `og_image`, `lat`, `lng`, `starts_at`, `ends_at`: all optional, only provided fields are updated
|
|
341
|
+
|
|
342
|
+
### `antenna_event_approve`
|
|
343
|
+
Approve a pending participant. Only creator or co-host.
|
|
344
|
+
- `code`: event code
|
|
345
|
+
- `sender_id`, `channel`: from context
|
|
346
|
+
- `ref`: participant ref number from scan
|
|
347
|
+
|
|
348
|
+
### `antenna_event_reject`
|
|
349
|
+
Reject a pending participant. Only creator or co-host.
|
|
350
|
+
- `code`: event code
|
|
351
|
+
- `sender_id`, `channel`: from context
|
|
352
|
+
- `ref`: participant ref number from scan
|
|
353
|
+
|
|
354
|
+
### `antenna_event_add_host`
|
|
355
|
+
Add a co-host to the event. Only creator can add.
|
|
356
|
+
- `code`: event code
|
|
357
|
+
- `sender_id`, `channel`: from context
|
|
358
|
+
- `ref`: participant ref number to promote to co-host
|