antenna-fyi 1.2.8 โ 1.2.10
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 +193 -1
- package/lib/core.js +19 -3
- package/lib/hermes-plugin/schemas.py +3 -2
- package/lib/hermes-plugin/tools.py +14 -2
- package/lib/mcp.js +55 -12
- package/package.json +1 -1
- package/skill/SKILL.md +4 -1
package/bin/antenna.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
handleEvent,
|
|
12
12
|
handleBind,
|
|
13
13
|
handlePass,
|
|
14
|
+
handleWatch,
|
|
14
15
|
handleSetup,
|
|
15
16
|
handleStatus,
|
|
16
17
|
handleInstallSkill,
|
|
@@ -42,6 +43,8 @@ async function main() {
|
|
|
42
43
|
return handleBind(f);
|
|
43
44
|
case "pass":
|
|
44
45
|
return handlePass(f);
|
|
46
|
+
case "watch":
|
|
47
|
+
return handleWatch(f);
|
|
45
48
|
case "serve": {
|
|
46
49
|
const { startMcpServer } = await import("../lib/mcp.js");
|
|
47
50
|
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, createBindToken, discover, createEvent, endEvent, eventCheckin, joinEvent, eventScan, pass as passUser, uploadEventImage } from "./core.js";
|
|
3
|
+
import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover, createEvent, endEvent, eventCheckin, joinEvent, eventScan, pass as passUser, uploadEventImage, getClient } from "./core.js";
|
|
4
4
|
import { createInterface } from "readline";
|
|
5
5
|
import { existsSync, mkdirSync, copyFileSync, readFileSync } from "fs";
|
|
6
6
|
import { join, dirname, extname } from "path";
|
|
@@ -401,6 +401,197 @@ export function handleInstallHermesPlugin() {
|
|
|
401
401
|
console.log();
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
+
export async function handleWatch(f) {
|
|
405
|
+
const id = f.id;
|
|
406
|
+
if (!id) {
|
|
407
|
+
console.error("โ --id required (e.g. --id telegram:123)");
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const sb = getClient();
|
|
412
|
+
const notified = new Set();
|
|
413
|
+
|
|
414
|
+
// Detect local agent framework for push notifications
|
|
415
|
+
let pushMethod = "terminal"; // default: just print
|
|
416
|
+
try {
|
|
417
|
+
execSync("which openclaw", { stdio: "pipe" });
|
|
418
|
+
// Verify gateway is running
|
|
419
|
+
try {
|
|
420
|
+
execSync("openclaw gateway health", { stdio: "pipe", timeout: 5000 });
|
|
421
|
+
pushMethod = "openclaw";
|
|
422
|
+
} catch { /* gateway not running */ }
|
|
423
|
+
} catch { /* openclaw not installed */ }
|
|
424
|
+
|
|
425
|
+
if (pushMethod === "terminal") {
|
|
426
|
+
try {
|
|
427
|
+
execSync("which hermes", { stdio: "pipe" });
|
|
428
|
+
try {
|
|
429
|
+
execSync("hermes gateway status", { stdio: "pipe", timeout: 5000 });
|
|
430
|
+
pushMethod = "hermes";
|
|
431
|
+
} catch { /* hermes gateway not running */ }
|
|
432
|
+
} catch { /* hermes not installed */ }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log(`๐ก Watching for new matches for ${id}...`);
|
|
436
|
+
if (pushMethod === "openclaw") {
|
|
437
|
+
console.log(` ๐ Detected OpenClaw โ will push notifications to your channel.`);
|
|
438
|
+
} else if (pushMethod === "hermes") {
|
|
439
|
+
console.log(` ๐ Detected Hermes โ will push notifications to your channel.`);
|
|
440
|
+
} else {
|
|
441
|
+
console.log(` โน๏ธ No agent framework detected โ notifications will print here.`);
|
|
442
|
+
}
|
|
443
|
+
console.log(` Press Ctrl+C to stop.\n`);
|
|
444
|
+
|
|
445
|
+
// Push notification helper
|
|
446
|
+
function pushNotify(message) {
|
|
447
|
+
console.log(message); // always print to terminal
|
|
448
|
+
|
|
449
|
+
if (pushMethod === "openclaw") {
|
|
450
|
+
try {
|
|
451
|
+
const parts = id.split(":");
|
|
452
|
+
const channel = parts[0];
|
|
453
|
+
const userId = parts.slice(1).join(":");
|
|
454
|
+
execSync(
|
|
455
|
+
`openclaw agent` +
|
|
456
|
+
` --message ${JSON.stringify(message)}` +
|
|
457
|
+
` --deliver` +
|
|
458
|
+
` --reply-channel ${channel}` +
|
|
459
|
+
` --reply-to "${userId}"`,
|
|
460
|
+
{ timeout: 30_000, stdio: "pipe" }
|
|
461
|
+
);
|
|
462
|
+
} catch (err) {
|
|
463
|
+
// silent โ terminal output is the fallback
|
|
464
|
+
}
|
|
465
|
+
} else if (pushMethod === "hermes") {
|
|
466
|
+
try {
|
|
467
|
+
// Use hermes cron to create a one-shot notification
|
|
468
|
+
const parts = id.split(":");
|
|
469
|
+
const channel = parts[0];
|
|
470
|
+
execSync(
|
|
471
|
+
`hermes cron create` +
|
|
472
|
+
` --name "Antenna notification"` +
|
|
473
|
+
` --run-now` +
|
|
474
|
+
` --once` +
|
|
475
|
+
` --message ${JSON.stringify(message)}` +
|
|
476
|
+
` --deliver ${channel}`,
|
|
477
|
+
{ timeout: 30_000, stdio: "pipe" }
|
|
478
|
+
);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
// silent โ terminal output is the fallback
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Initial check
|
|
486
|
+
const initial = await checkMatches({ device_id: id });
|
|
487
|
+
if (initial.mutual_matches?.length) {
|
|
488
|
+
console.log(`๐ You have ${initial.mutual_matches.length} mutual match(es)!`);
|
|
489
|
+
for (const m of initial.mutual_matches) {
|
|
490
|
+
const key = `mutual:${m.device_id}`;
|
|
491
|
+
notified.add(key);
|
|
492
|
+
console.log(` ${m.emoji || "๐ค"} ${m.name}${m.their_contact ? " โ contact: " + m.their_contact : ""}`);
|
|
493
|
+
}
|
|
494
|
+
console.log();
|
|
495
|
+
}
|
|
496
|
+
if (initial.incoming_accepts?.length) {
|
|
497
|
+
console.log(`๐ฉ ${initial.incoming_accepts.length} person(s) want to meet you!`);
|
|
498
|
+
for (const m of initial.incoming_accepts) {
|
|
499
|
+
const key = `incoming:${m.device_id}`;
|
|
500
|
+
notified.add(key);
|
|
501
|
+
console.log(` ${m.emoji || "๐ค"} ${m.name} โ ${m.line1 || ""}`);
|
|
502
|
+
}
|
|
503
|
+
console.log();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Subscribe to realtime changes on matches table
|
|
507
|
+
const channel = sb
|
|
508
|
+
.channel("antenna-cli-watch")
|
|
509
|
+
.on("postgres_changes",
|
|
510
|
+
{ event: "INSERT", schema: "public", table: "matches" },
|
|
511
|
+
async (payload) => {
|
|
512
|
+
try {
|
|
513
|
+
const row = payload.new;
|
|
514
|
+
if (!row) return;
|
|
515
|
+
|
|
516
|
+
// Someone accepted me
|
|
517
|
+
if (row.device_id_b === id) {
|
|
518
|
+
const key = `incoming:${row.device_id_a}`;
|
|
519
|
+
if (notified.has(key)) return;
|
|
520
|
+
notified.add(key);
|
|
521
|
+
|
|
522
|
+
const profile = await getProfile({ device_id: row.device_id_a });
|
|
523
|
+
const name = profile?.display_name || "Someone";
|
|
524
|
+
const emoji = profile?.emoji || "๐ค";
|
|
525
|
+
|
|
526
|
+
// Check if mutual
|
|
527
|
+
const matches = await checkMatches({ device_id: id });
|
|
528
|
+
const isMutual = matches.mutual_matches?.some(m => m.device_id === row.device_id_a);
|
|
529
|
+
|
|
530
|
+
if (isMutual) {
|
|
531
|
+
const mutualKey = `mutual:${row.device_id_a}`;
|
|
532
|
+
notified.add(mutualKey);
|
|
533
|
+
const contact = row.contact_info_a;
|
|
534
|
+
pushNotify(`๐ MUTUAL MATCH! ${emoji} ${name} also accepted you!${contact ? " Contact: " + contact : ""}`);
|
|
535
|
+
} else {
|
|
536
|
+
pushNotify(`๐ฉ ${emoji} ${name} wants to meet you! Run: antenna accept --id ${id} --target ${row.device_id_a}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// I accepted someone and they also accepted me
|
|
541
|
+
if (row.device_id_a === id) {
|
|
542
|
+
const matches = await checkMatches({ device_id: id });
|
|
543
|
+
const mutual = matches.mutual_matches?.find(m => m.device_id === row.device_id_b);
|
|
544
|
+
if (mutual) {
|
|
545
|
+
const mutualKey = `mutual:${row.device_id_b}`;
|
|
546
|
+
if (!notified.has(mutualKey)) {
|
|
547
|
+
notified.add(mutualKey);
|
|
548
|
+
pushNotify(`๐ MUTUAL MATCH! ${mutual.emoji || "๐ค"} ${mutual.name}!${mutual.their_contact ? " Contact: " + mutual.their_contact : ""}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
} catch (err) {
|
|
553
|
+
// silent
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
.subscribe((status) => {
|
|
558
|
+
if (status === "SUBSCRIBED") {
|
|
559
|
+
console.log("โ
Connected โ listening for matches in real-time.");
|
|
560
|
+
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
|
|
561
|
+
console.log(`โ ๏ธ Connection issue (${status}), retrying...`);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Keep alive โ also poll every 2 minutes as fallback
|
|
566
|
+
const pollInterval = setInterval(async () => {
|
|
567
|
+
try {
|
|
568
|
+
const result = await checkMatches({ device_id: id });
|
|
569
|
+
for (const m of (result.mutual_matches || [])) {
|
|
570
|
+
const key = `mutual:${m.device_id}`;
|
|
571
|
+
if (!notified.has(key)) {
|
|
572
|
+
notified.add(key);
|
|
573
|
+
pushNotify(`๐ MUTUAL MATCH! ${m.emoji || "๐ค"} ${m.name}!${m.their_contact ? " Contact: " + m.their_contact : ""}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
for (const m of (result.incoming_accepts || [])) {
|
|
577
|
+
const key = `incoming:${m.device_id}`;
|
|
578
|
+
if (!notified.has(key)) {
|
|
579
|
+
notified.add(key);
|
|
580
|
+
pushNotify(`๐ฉ ${m.emoji || "๐ค"} ${m.name} wants to meet you!`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} catch { /* silent */ }
|
|
584
|
+
}, 2 * 60 * 1000);
|
|
585
|
+
|
|
586
|
+
// Handle Ctrl+C
|
|
587
|
+
process.on("SIGINT", () => {
|
|
588
|
+
console.log("\n๐ Stopped watching.");
|
|
589
|
+
clearInterval(pollInterval);
|
|
590
|
+
sb.removeChannel(channel);
|
|
591
|
+
process.exit(0);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
404
595
|
export function printHelp() {
|
|
405
596
|
console.log(`๐ก Antenna โ nearby people discovery
|
|
406
597
|
|
|
@@ -413,6 +604,7 @@ Usage:
|
|
|
413
604
|
antenna matches --id telegram:123
|
|
414
605
|
antenna discover --id telegram:123
|
|
415
606
|
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
|
|
607
|
+
antenna watch --id telegram:123 Watch for new matches in real-time (Ctrl+C to stop)
|
|
416
608
|
antenna bind --id telegram:123
|
|
417
609
|
antenna serve Start MCP server (stdio transport)
|
|
418
610
|
antenna setup Interactive profile setup [--id telegram:123]
|
package/lib/core.js
CHANGED
|
@@ -515,6 +515,15 @@ export async function endEvent({ code, device_id, supabaseUrl, supabaseKey }) {
|
|
|
515
515
|
|
|
516
516
|
export async function eventCheckin({ code, device_id, lat, lng, supabaseUrl, supabaseKey }) {
|
|
517
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
|
+
|
|
518
527
|
const fuzzy = (lat != null && lng != null) ? fuzzyCoord(lat, lng) : { lat: null, lng: null };
|
|
519
528
|
const { data, error } = await sb.rpc("event_checkin", {
|
|
520
529
|
p_code: code, p_device_id: device_id,
|
|
@@ -577,14 +586,21 @@ export async function getEvent({ code, supabaseUrl, supabaseKey }) {
|
|
|
577
586
|
return data;
|
|
578
587
|
}
|
|
579
588
|
|
|
580
|
-
export async function createBindToken({ device_id, supabaseUrl, supabaseKey }) {
|
|
589
|
+
export async function createBindToken({ device_id, purpose, event_code, supabaseUrl, supabaseKey }) {
|
|
581
590
|
const sb = getClient(supabaseUrl, supabaseKey);
|
|
582
|
-
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
|
+
});
|
|
583
596
|
if (error) throw new Error(error.message);
|
|
584
597
|
const baseUrl = "https://www.antenna.fyi";
|
|
585
598
|
return {
|
|
586
599
|
token: data.token,
|
|
587
600
|
url: `${baseUrl}/locate?token=${data.token}`,
|
|
588
|
-
|
|
601
|
+
purpose: purpose || "profile",
|
|
602
|
+
message: purpose === "event"
|
|
603
|
+
? "ๅ้่ฟไธช้พๆฅ็ปๆดปๅจๅๅปบ่
๏ผๅจๆดปๅจๅฐ็นๆๅผๅณๅฏ่ฎพๅฎๆดปๅจไฝ็ฝฎใ"
|
|
604
|
+
: "ๅ้่ฟไธช้พๆฅ็ป็จๆท๏ผๅจๆๆบๆต่งๅจๆๅผๅณๅฏๅ
ฑไบซไฝ็ฝฎใ",
|
|
589
605
|
};
|
|
590
606
|
}
|
|
@@ -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
|
},
|
|
@@ -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
|
|
package/lib/mcp.js
CHANGED
|
@@ -37,6 +37,39 @@ export async function startMcpServer() {
|
|
|
37
37
|
// Store last scan ref map for resolving refs in accept
|
|
38
38
|
let _lastRefMap = {};
|
|
39
39
|
|
|
40
|
+
// Track known device_ids and last notified matches for piggyback notifications
|
|
41
|
+
const _knownDeviceIds = new Set();
|
|
42
|
+
const _notifiedMatches = new Set();
|
|
43
|
+
|
|
44
|
+
// Piggyback match check: append pending notifications to any tool response
|
|
45
|
+
async function withMatchNotifications(deviceId, result) {
|
|
46
|
+
_knownDeviceIds.add(deviceId);
|
|
47
|
+
try {
|
|
48
|
+
const matches = await checkMatches({ device_id: deviceId });
|
|
49
|
+
const notifications = [];
|
|
50
|
+
|
|
51
|
+
for (const m of (matches.mutual_matches || [])) {
|
|
52
|
+
const key = `mutual:${m.device_id}`;
|
|
53
|
+
if (!_notifiedMatches.has(key)) {
|
|
54
|
+
_notifiedMatches.add(key);
|
|
55
|
+
notifications.push(`๐ ๅๅๅน้
๏ผ${m.emoji || "๐ค"} ${m.name} ไนๆฅๅไบไฝ ๏ผ${m.their_contact ? "่็ณปๆนๅผ๏ผ" + m.their_contact : ""}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const m of (matches.incoming_accepts || [])) {
|
|
59
|
+
const key = `incoming:${m.device_id}`;
|
|
60
|
+
if (!_notifiedMatches.has(key)) {
|
|
61
|
+
_notifiedMatches.add(key);
|
|
62
|
+
notifications.push(`๐ฉ ${m.emoji || "๐ค"} ${m.name} ๆณ่ฎค่ฏไฝ ๏ผ็จ antenna_check_matches ๆฅ็่ฏฆๆ
ใ`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (notifications.length > 0) {
|
|
67
|
+
result._pending_notifications = notifications;
|
|
68
|
+
}
|
|
69
|
+
} catch { /* silent */ }
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
40
73
|
// โโโ antenna_scan โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
41
74
|
|
|
42
75
|
server.tool(
|
|
@@ -51,10 +84,11 @@ export async function startMcpServer() {
|
|
|
51
84
|
},
|
|
52
85
|
async ({ lat, lng, radius_m, sender_id, channel }) => {
|
|
53
86
|
try {
|
|
54
|
-
const
|
|
87
|
+
const deviceId = deriveDeviceId(sender_id, channel);
|
|
88
|
+
const result = await scan({ lat, lng, radius_m, device_id: deviceId });
|
|
55
89
|
_lastRefMap = result._ref_map || {};
|
|
56
90
|
const { _ref_map, ...clean } = result;
|
|
57
|
-
return jsonResult(clean);
|
|
91
|
+
return jsonResult(await withMatchNotifications(deviceId, clean));
|
|
58
92
|
} catch (e) {
|
|
59
93
|
return jsonResult({ error: e.message });
|
|
60
94
|
}
|
|
@@ -83,10 +117,11 @@ export async function startMcpServer() {
|
|
|
83
117
|
try {
|
|
84
118
|
if (action === "get") {
|
|
85
119
|
const data = await getProfile({ device_id: deviceId });
|
|
86
|
-
|
|
120
|
+
const result = data ? { profile: data } : { profile: null, message: "่ฟๆฒกๆๅ็๏ผๅธฎไฝ ๅๅปบไธไธช๏ผ" };
|
|
121
|
+
return jsonResult(await withMatchNotifications(deviceId, result));
|
|
87
122
|
}
|
|
88
123
|
const data = await setProfile({ device_id: deviceId, display_name, emoji, line1, line2, line3, matching_context, visible });
|
|
89
|
-
return jsonResult({ saved: true, profile: data });
|
|
124
|
+
return jsonResult(await withMatchNotifications(deviceId, { saved: true, profile: data }));
|
|
90
125
|
} catch (e) {
|
|
91
126
|
return jsonResult({ error: e.message });
|
|
92
127
|
}
|
|
@@ -107,8 +142,9 @@ export async function startMcpServer() {
|
|
|
107
142
|
},
|
|
108
143
|
async ({ sender_id, channel, ref, target_device_id, contact_info }) => {
|
|
109
144
|
try {
|
|
110
|
-
const
|
|
111
|
-
|
|
145
|
+
const deviceId = deriveDeviceId(sender_id, channel);
|
|
146
|
+
const result = await accept({ device_id: deviceId, target_device_id, ref, contact_info });
|
|
147
|
+
return jsonResult(await withMatchNotifications(deviceId, result));
|
|
112
148
|
} catch (e) {
|
|
113
149
|
return jsonResult({ error: e.message });
|
|
114
150
|
}
|
|
@@ -129,11 +165,12 @@ export async function startMcpServer() {
|
|
|
129
165
|
},
|
|
130
166
|
async ({ lat, lng, sender_id, channel, place_name }) => {
|
|
131
167
|
try {
|
|
132
|
-
const
|
|
168
|
+
const deviceId = deriveDeviceId(sender_id, channel);
|
|
169
|
+
const result = await checkin({ lat, lng, device_id: deviceId });
|
|
133
170
|
if (result.checked_in && place_name) {
|
|
134
171
|
result.message = `ๅทฒ็ญพๅฐ (${place_name}) ๐ ็ฐๅจ้่ฟ็ไบบๆซๆๅฐฑ่ฝ็ๅฐไฝ ไบใ`;
|
|
135
172
|
}
|
|
136
|
-
return jsonResult(result);
|
|
173
|
+
return jsonResult(await withMatchNotifications(deviceId, result));
|
|
137
174
|
} catch (e) {
|
|
138
175
|
return jsonResult({ error: e.message });
|
|
139
176
|
}
|
|
@@ -151,7 +188,11 @@ export async function startMcpServer() {
|
|
|
151
188
|
},
|
|
152
189
|
async ({ sender_id, channel }) => {
|
|
153
190
|
try {
|
|
154
|
-
const
|
|
191
|
+
const deviceId = deriveDeviceId(sender_id, channel);
|
|
192
|
+
const result = await checkMatches({ device_id: deviceId });
|
|
193
|
+
// Mark all as notified since user explicitly checked
|
|
194
|
+
for (const m of (result.mutual_matches || [])) _notifiedMatches.add(`mutual:${m.device_id}`);
|
|
195
|
+
for (const m of (result.incoming_accepts || [])) _notifiedMatches.add(`incoming:${m.device_id}`);
|
|
155
196
|
return jsonResult(result);
|
|
156
197
|
} catch (e) {
|
|
157
198
|
return jsonResult({ error: e.message });
|
|
@@ -163,14 +204,16 @@ export async function startMcpServer() {
|
|
|
163
204
|
|
|
164
205
|
server.tool(
|
|
165
206
|
"antenna_bind",
|
|
166
|
-
"Generate a GPS binding link. Send this to the user so they can share their phone's location via the web.",
|
|
207
|
+
"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.",
|
|
167
208
|
{
|
|
168
209
|
sender_id: z.string().describe("The sender's user ID"),
|
|
169
210
|
channel: z.string().describe("Channel name"),
|
|
211
|
+
purpose: z.string().optional().describe("'profile' (default) or 'event'"),
|
|
212
|
+
event_code: z.string().optional().describe("Event code (required when purpose=event)"),
|
|
170
213
|
},
|
|
171
|
-
async ({ sender_id, channel }) => {
|
|
214
|
+
async ({ sender_id, channel, purpose, event_code }) => {
|
|
172
215
|
try {
|
|
173
|
-
const result = await createBindToken({ device_id: deriveDeviceId(sender_id, channel) });
|
|
216
|
+
const result = await createBindToken({ device_id: deriveDeviceId(sender_id, channel), purpose, event_code });
|
|
174
217
|
return jsonResult(result);
|
|
175
218
|
} catch (e) {
|
|
176
219
|
return jsonResult({ error: e.message });
|
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,7 +285,7 @@ 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
|
|
|
285
|
-
**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 `lat`/`lng
|
|
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.
|
|
286
289
|
|
|
287
290
|
### `antenna_event_end`
|
|
288
291
|
End an event. Only the creator can end it.
|