antenna-fyi 0.5.0

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/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # ๐Ÿ“ก Antenna
2
+
3
+ Nearby people discovery โ€” find interesting people around you.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g antenna-fyi
9
+ ```
10
+
11
+ ## CLI Usage
12
+
13
+ ```bash
14
+ # Create your profile card
15
+ antenna setup --id telegram:123
16
+
17
+ # Scan for nearby people
18
+ antenna scan --lat 39.99 --lng 116.48 --radius 500 --id telegram:123
19
+
20
+ # Check in at a location
21
+ antenna checkin --id telegram:123 --lat 39.99 --lng 116.48
22
+
23
+ # View/edit your profile
24
+ antenna profile --id telegram:123
25
+ antenna profile --id telegram:123 --name Yi --emoji ๐Ÿฆฆ --line1 'Product Designer'
26
+
27
+ # Accept a match
28
+ antenna accept --id telegram:123 --target telegram:789 --contact 'WeChat: yi'
29
+
30
+ # Check match status
31
+ antenna matches --id telegram:123
32
+
33
+ # Show status
34
+ antenna status --id telegram:123
35
+ ```
36
+
37
+ ## MCP Server
38
+
39
+ Start the MCP server for AI agent integration:
40
+
41
+ ```bash
42
+ antenna serve
43
+ ```
44
+
45
+ This starts a stdio-based MCP server with tools:
46
+ - `antenna_scan` โ€” Scan for nearby people
47
+ - `antenna_profile` โ€” Get/set profile card
48
+ - `antenna_checkin` โ€” Check in at a location
49
+ - `antenna_accept` โ€” Accept a match
50
+ - `antenna_check_matches` โ€” Check match status
51
+
52
+ ## OpenClaw Integration
53
+
54
+ ### Install Skill (recommended)
55
+
56
+ ```bash
57
+ antenna install-skill
58
+ ```
59
+
60
+ Copies the SKILL.md to `~/.openclaw/skills/antenna/` so your agent knows how to use Antenna.
61
+
62
+ ### Install Plugin (advanced)
63
+
64
+ ```bash
65
+ mkdir my-antenna-plugin && cd my-antenna-plugin
66
+ antenna install-plugin
67
+ npm install
68
+ openclaw plugins install .
69
+ ```
70
+
71
+ The plugin adds automatic location-triggered scanning, match polling, and real-time notifications.
72
+
73
+ ## Environment Variables
74
+
75
+ | Variable | Description | Default |
76
+ |---|---|---|
77
+ | `ANTENNA_SUPABASE_URL` | Supabase project URL | Built-in |
78
+ | `ANTENNA_SUPABASE_KEY` | Supabase anon key | Built-in |
79
+
80
+ ## How It Works
81
+
82
+ 1. **Create a profile card** โ€” emoji, name, 3 lines about you
83
+ 2. **Scan nearby** โ€” find people within radius at your location
84
+ 3. **Accept matches** โ€” if both sides accept, exchange contact info
85
+ 4. **Everything expires in 24h** โ€” ephemeral by design
86
+
87
+ ## License
88
+
89
+ MIT
package/bin/antenna.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ parseFlags,
5
+ handleScan,
6
+ handleProfile,
7
+ handleAccept,
8
+ handleCheckin,
9
+ handleMatches,
10
+ handleSetup,
11
+ handleStatus,
12
+ handleInstallSkill,
13
+ handleInstallPlugin,
14
+ printHelp,
15
+ } from "../lib/cli.js";
16
+
17
+ const [,, cmd, ...args] = process.argv;
18
+ const f = parseFlags(args);
19
+
20
+ async function main() {
21
+ switch (cmd) {
22
+ case "scan":
23
+ return handleScan(f);
24
+ case "profile":
25
+ return handleProfile(f);
26
+ case "accept":
27
+ return handleAccept(f);
28
+ case "checkin":
29
+ return handleCheckin(f);
30
+ case "matches":
31
+ return handleMatches(f);
32
+ case "serve": {
33
+ const { startMcpServer } = await import("../lib/mcp.js");
34
+ return startMcpServer();
35
+ }
36
+ case "setup":
37
+ return handleSetup(f);
38
+ case "status":
39
+ return handleStatus(f);
40
+ case "install-skill":
41
+ return handleInstallSkill();
42
+ case "install-plugin":
43
+ return handleInstallPlugin();
44
+ case "help":
45
+ default:
46
+ return printHelp();
47
+ }
48
+ }
49
+
50
+ main().catch((e) => {
51
+ console.error(e.message);
52
+ process.exit(1);
53
+ });
package/install.js ADDED
@@ -0,0 +1,19 @@
1
+ // postinstall โ€” welcome banner
2
+
3
+ console.log(`
4
+ โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
5
+ โ•‘ ๐Ÿ“ก Antenna โ€” Nearby People Discovery โ•‘
6
+ โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
7
+ โ•‘ โ•‘
8
+ โ•‘ Quick start: โ•‘
9
+ โ•‘ antenna setup Create your card โ•‘
10
+ โ•‘ antenna scan --lat โ€ฆ --lng โ€ฆ Find people โ•‘
11
+ โ•‘ antenna serve Start MCP server โ•‘
12
+ โ•‘ โ•‘
13
+ โ•‘ OpenClaw integration: โ•‘
14
+ โ•‘ antenna install-skill Install SKILL.md โ•‘
15
+ โ•‘ antenna install-plugin Get plugin files โ•‘
16
+ โ•‘ โ•‘
17
+ โ•‘ antenna help Full usage info โ•‘
18
+ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
19
+ `);
package/lib/cli.js ADDED
@@ -0,0 +1,221 @@
1
+ // antenna CLI command handlers
2
+
3
+ import { scan, getProfile, setProfile, accept, checkMatches, checkin } from "./core.js";
4
+ import { createInterface } from "readline";
5
+ import { existsSync, mkdirSync, copyFileSync } from "fs";
6
+ import { join, dirname } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { homedir } from "os";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ export function parseFlags(args) {
14
+ const flags = {};
15
+ for (let i = 0; i < args.length; i++) {
16
+ if (args[i].startsWith("--")) {
17
+ const key = args[i].slice(2);
18
+ flags[key] = args[i + 1] || true;
19
+ i++;
20
+ }
21
+ }
22
+ return flags;
23
+ }
24
+
25
+ export async function handleScan(f) {
26
+ if (!f.lat || !f.lng) return console.error("Usage: antenna scan --lat 39.99 --lng 116.48 [--radius 500] [--id telegram:123]");
27
+ const result = await scan({
28
+ lat: +f.lat,
29
+ lng: +f.lng,
30
+ radius_m: +(f.radius || 500),
31
+ device_id: f.id || null,
32
+ });
33
+ if (result.count === 0) return console.log("๐Ÿ“ก No one nearby within " + result.radius_m + "m");
34
+ console.log(`๐Ÿ“ก ${result.count} people within ${result.radius_m}m:\n`);
35
+ result.profiles.forEach((p) => {
36
+ console.log(` ${p.emoji} ${p.name}${p.distance_m != null ? ` (${Math.round(p.distance_m)}m)` : ""}`);
37
+ if (p.line1) console.log(` ${p.line1}`);
38
+ if (p.line2) console.log(` ${p.line2}`);
39
+ if (p.line3) console.log(` ${p.line3}`);
40
+ console.log(` id: ${p.device_id}\n`);
41
+ });
42
+ }
43
+
44
+ export async function handleProfile(f) {
45
+ if (!f.id) return console.error("Usage: antenna profile --id telegram:123 [--name Yi --emoji ๐Ÿฆฆ --line1 '...' --line2 '...' --line3 '...']");
46
+ if (f.name || f.line1 || f.line2 || f.line3) {
47
+ const data = await setProfile({
48
+ device_id: f.id,
49
+ display_name: f.name,
50
+ emoji: f.emoji || "๐Ÿ‘ค",
51
+ line1: f.line1,
52
+ line2: f.line2,
53
+ line3: f.line3,
54
+ });
55
+ console.log("โœ… Profile saved");
56
+ console.log(JSON.stringify(data, null, 2));
57
+ } else {
58
+ const data = await getProfile({ device_id: f.id });
59
+ if (!data) return console.log("No profile yet. Create one with --name and --line1/2/3");
60
+ console.log(`${data.emoji || "๐Ÿ‘ค"} ${data.display_name || "Anonymous"}`);
61
+ if (data.line1) console.log(` ${data.line1}`);
62
+ if (data.line2) console.log(` ${data.line2}`);
63
+ if (data.line3) console.log(` ${data.line3}`);
64
+ }
65
+ }
66
+
67
+ export async function handleAccept(f) {
68
+ if (!f.id || !f.target) return console.error("Usage: antenna accept --id telegram:123 --target telegram:789 [--contact 'WeChat: yi']");
69
+ const result = await accept({
70
+ device_id: f.id,
71
+ target_device_id: f.target,
72
+ contact_info: f.contact,
73
+ });
74
+ console.log("โœ… " + result.message);
75
+ if (result.mutual && result.their_contact) console.log("๐Ÿ“‡ Their contact: " + result.their_contact);
76
+ }
77
+
78
+ export async function handleCheckin(f) {
79
+ if (!f.id || !f.lat || !f.lng) return console.error("Usage: antenna checkin --id telegram:123 --lat 39.99 --lng 116.48 [--place 'ไธ‰้‡Œๅฑฏ']");
80
+ const result = await checkin({
81
+ lat: +f.lat,
82
+ lng: +f.lng,
83
+ device_id: f.id,
84
+ });
85
+ console.log(result.checked_in ? "โœ… " + result.message : "โŒ " + result.message);
86
+ }
87
+
88
+ export async function handleMatches(f) {
89
+ if (!f.id) return console.error("Usage: antenna matches --id telegram:123");
90
+ const result = await checkMatches({ device_id: f.id });
91
+ if (!result.mutual_matches.length && !result.incoming_accepts.length) {
92
+ return console.log(result.message);
93
+ }
94
+ for (const m of result.mutual_matches) {
95
+ console.log(`๐ŸŽ‰ MUTUAL: ${m.emoji} ${m.name}`);
96
+ if (m.their_contact) console.log(` Their contact: ${m.their_contact}`);
97
+ if (m.you_shared) console.log(` You shared: ${m.you_shared}`);
98
+ console.log();
99
+ }
100
+ for (const m of result.incoming_accepts) {
101
+ console.log(`๐Ÿ“ฉ WANTS TO MEET YOU: ${m.emoji} ${m.name}`);
102
+ if (m.line1) console.log(` ${m.line1}`);
103
+ console.log(` Accept: antenna accept --id ${f.id} --target ${m.device_id}`);
104
+ console.log();
105
+ }
106
+ }
107
+
108
+ export async function handleSetup(f) {
109
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
110
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
111
+
112
+ console.log("\n๐Ÿ“ก Antenna Setup โ€” ๅˆ›ๅปบไฝ ็š„ๅ็‰‡\n");
113
+
114
+ const id = f.id || await ask("Your device ID (e.g. telegram:123): ");
115
+ if (!id) { rl.close(); return console.error("Device ID is required."); }
116
+
117
+ const name = await ask("Display name: ");
118
+ const emoji = (await ask("Emoji (default ๐Ÿ‘ค): ")) || "๐Ÿ‘ค";
119
+ const line1 = await ask("Line 1 โ€” who you are / what you do: ");
120
+ const line2 = await ask("Line 2 โ€” what you're into: ");
121
+ const line3 = await ask("Line 3 โ€” what you're looking for: ");
122
+
123
+ rl.close();
124
+
125
+ const data = await setProfile({
126
+ device_id: id,
127
+ display_name: name || null,
128
+ emoji,
129
+ line1: line1 || null,
130
+ line2: line2 || null,
131
+ line3: line3 || null,
132
+ });
133
+
134
+ console.log("\nโœ… Profile saved!\n");
135
+ console.log(` ${emoji} ${name || "Anonymous"}`);
136
+ if (line1) console.log(` ${line1}`);
137
+ if (line2) console.log(` ${line2}`);
138
+ if (line3) console.log(` ${line3}`);
139
+ console.log();
140
+ }
141
+
142
+ export async function handleStatus(f) {
143
+ const supabaseUrl = process.env.ANTENNA_SUPABASE_URL || process.env.ANTENNA_URL || "https://bcudjloikmpcqwcptuyd.supabase.co";
144
+ console.log("๐Ÿ“ก Antenna Status\n");
145
+ console.log(` Supabase URL: ${supabaseUrl}`);
146
+
147
+ if (f.id) {
148
+ const profile = await getProfile({ device_id: f.id });
149
+ if (profile) {
150
+ console.log(` Profile: โœ… ${profile.emoji || "๐Ÿ‘ค"} ${profile.display_name || "Anonymous"}`);
151
+ } else {
152
+ console.log(" Profile: โŒ Not created yet");
153
+ }
154
+ const matches = await checkMatches({ device_id: f.id });
155
+ console.log(` Mutual matches: ${matches.mutual_matches.length}`);
156
+ console.log(` Incoming accepts: ${matches.incoming_accepts.length}`);
157
+ } else {
158
+ console.log(" Profile: (pass --id to check)");
159
+ console.log(" Matches: (pass --id to check)");
160
+ }
161
+ console.log();
162
+ }
163
+
164
+ export function handleInstallSkill() {
165
+ const skillDir = join(homedir(), ".openclaw", "skills", "antenna");
166
+ const skillSrc = join(__dirname, "..", "skill", "SKILL.md");
167
+
168
+ if (!existsSync(skillDir)) {
169
+ mkdirSync(skillDir, { recursive: true });
170
+ }
171
+
172
+ copyFileSync(skillSrc, join(skillDir, "SKILL.md"));
173
+ console.log("โœ… SKILL.md installed to ~/.openclaw/skills/antenna/");
174
+ console.log(" Restart OpenClaw to pick it up.");
175
+ }
176
+
177
+ export function handleInstallPlugin() {
178
+ const templateDir = join(__dirname, "plugin-template");
179
+ const files = ["index.ts", "openclaw.plugin.json", "package.json"];
180
+
181
+ for (const file of files) {
182
+ const src = join(templateDir, file);
183
+ const dest = join(process.cwd(), file);
184
+ if (existsSync(dest)) {
185
+ console.log(`โš ๏ธ ${file} already exists, skipping.`);
186
+ } else {
187
+ copyFileSync(src, dest);
188
+ console.log(`๐Ÿ“„ Copied ${file}`);
189
+ }
190
+ }
191
+
192
+ console.log("\nโœ… Plugin template files copied to current directory.");
193
+ console.log(" Next steps:");
194
+ console.log(" 1. Run: npm install");
195
+ console.log(" 2. Run: openclaw plugins install .");
196
+ console.log();
197
+ }
198
+
199
+ export function printHelp() {
200
+ console.log(`๐Ÿ“ก Antenna โ€” nearby people discovery
201
+
202
+ Usage:
203
+ antenna scan --lat 39.99 --lng 116.48 [--radius 500] [--id telegram:123]
204
+ antenna checkin --id telegram:123 --lat 39.99 --lng 116.48
205
+ antenna profile --id telegram:123 [--name Yi --emoji ๐Ÿฆฆ --line1 '...']
206
+ antenna accept --id telegram:123 --target telegram:789 [--contact 'WeChat: yi']
207
+ antenna matches --id telegram:123
208
+ antenna serve Start MCP server (stdio transport)
209
+ antenna setup Interactive profile setup [--id telegram:123]
210
+ antenna status Show config & status [--id telegram:123]
211
+ antenna install-skill Install SKILL.md to ~/.openclaw/skills/antenna/
212
+ antenna install-plugin Copy plugin template to current directory
213
+ antenna help Show this help
214
+
215
+ Environment:
216
+ ANTENNA_SUPABASE_URL Supabase project URL (optional, has default)
217
+ ANTENNA_SUPABASE_KEY Supabase anon key (optional, has default)
218
+
219
+ Install: npm install -g antenna-fyi
220
+ Or: npx antenna-fyi scan --lat 39.99 --lng 116.48`);
221
+ }
package/lib/core.js ADDED
@@ -0,0 +1,247 @@
1
+ // antenna-core โ€” shared logic for CLI, MCP, and Plugin
2
+ // All three import this instead of duplicating Supabase calls.
3
+
4
+ import { createClient } from "@supabase/supabase-js";
5
+
6
+ const DEFAULT_URL = "https://bcudjloikmpcqwcptuyd.supabase.co";
7
+ const DEFAULT_KEY =
8
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJjdWRqbG9pa21wY3F3Y3B0dXlkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ0MTg1NDgsImV4cCI6MjA4OTk5NDU0OH0.FaoC3QfpfHP1npNGjRchJAoAp2PdZtQe_WhP-t-GN1o";
9
+
10
+ let _client = null;
11
+ let _url = null;
12
+
13
+ export function getClient(url, key) {
14
+ const u = url || process.env.ANTENNA_SUPABASE_URL || process.env.ANTENNA_URL || DEFAULT_URL;
15
+ const k = key || process.env.ANTENNA_SUPABASE_KEY || process.env.ANTENNA_KEY || DEFAULT_KEY;
16
+ if (!_client || _url !== u) {
17
+ _client = createClient(u, k);
18
+ _url = u;
19
+ }
20
+ return _client;
21
+ }
22
+
23
+ export function deriveDeviceId(senderId, channel) {
24
+ return `${channel}:${senderId}`;
25
+ }
26
+
27
+ export function fuzzyCoord(lat, lng) {
28
+ return {
29
+ lat: Math.round(lat * 1000) / 1000,
30
+ lng: Math.round(lng * 1000) / 1000,
31
+ };
32
+ }
33
+
34
+ // โ”€โ”€โ”€ scan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
35
+
36
+ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, supabaseKey }) {
37
+ const sb = getClient(supabaseUrl, supabaseKey);
38
+ const fuzzy = fuzzyCoord(lat, lng);
39
+
40
+ if (device_id) {
41
+ await sb.rpc("upsert_profile_location", {
42
+ p_device_id: device_id,
43
+ p_lng: fuzzy.lng,
44
+ p_lat: fuzzy.lat,
45
+ });
46
+ }
47
+
48
+ const { data, error } = await sb.rpc("nearby_profiles", {
49
+ p_lat: fuzzy.lat,
50
+ p_lng: fuzzy.lng,
51
+ p_radius_m: radius_m,
52
+ });
53
+
54
+ if (error) throw new Error(error.message);
55
+
56
+ const others = device_id
57
+ ? (data || []).filter((p) => p.device_id !== device_id)
58
+ : data || [];
59
+
60
+ return {
61
+ count: others.length,
62
+ radius_m,
63
+ profiles: others.map((p) => ({
64
+ device_id: p.device_id,
65
+ name: p.display_name || "ๅŒฟๅ",
66
+ emoji: p.emoji || "๐Ÿ‘ค",
67
+ line1: p.line1,
68
+ line2: p.line2,
69
+ line3: p.line3,
70
+ distance_m: p.distance_m ?? p.dist_meters ?? null,
71
+ })),
72
+ };
73
+ }
74
+
75
+ // โ”€โ”€โ”€ getProfile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
76
+
77
+ export async function getProfile({ device_id, supabaseUrl, supabaseKey }) {
78
+ const sb = getClient(supabaseUrl, supabaseKey);
79
+ const { data, error } = await sb.rpc("get_profile", { p_device_id: device_id });
80
+ if (error) throw new Error(error.message);
81
+ return data || null;
82
+ }
83
+
84
+ // โ”€โ”€โ”€ setProfile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
85
+
86
+ export async function setProfile({
87
+ device_id,
88
+ display_name,
89
+ emoji = "๐Ÿ‘ค",
90
+ line1,
91
+ line2,
92
+ line3,
93
+ visible = true,
94
+ supabaseUrl,
95
+ supabaseKey,
96
+ }) {
97
+ const sb = getClient(supabaseUrl, supabaseKey);
98
+ const { data, error } = await sb.rpc("upsert_profile", {
99
+ p_device_id: device_id,
100
+ p_display_name: display_name || null,
101
+ p_emoji: emoji,
102
+ p_line1: line1 || null,
103
+ p_line2: line2 || null,
104
+ p_line3: line3 || null,
105
+ p_visible: visible,
106
+ });
107
+ if (error) throw new Error(error.message);
108
+ return data;
109
+ }
110
+
111
+ // โ”€โ”€โ”€ accept โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
112
+
113
+ export async function accept({
114
+ device_id,
115
+ target_device_id,
116
+ contact_info,
117
+ supabaseUrl,
118
+ supabaseKey,
119
+ }) {
120
+ const sb = getClient(supabaseUrl, supabaseKey);
121
+
122
+ const { error } = await sb.rpc("upsert_match", {
123
+ p_device_id_a: device_id,
124
+ p_device_id_b: target_device_id,
125
+ p_reason: "",
126
+ p_score: 0,
127
+ p_status: "accepted",
128
+ p_contact_info: contact_info || null,
129
+ p_expires_hours: 24,
130
+ });
131
+ if (error) throw new Error(error.message);
132
+
133
+ // Check mutual
134
+ const { data: reverse } = await sb
135
+ .from("matches")
136
+ .select("status, contact_info_a")
137
+ .eq("device_id_a", target_device_id)
138
+ .eq("device_id_b", device_id)
139
+ .eq("status", "accepted")
140
+ .single();
141
+
142
+ const mutual = !!reverse;
143
+
144
+ return {
145
+ accepted: true,
146
+ mutual,
147
+ their_contact: mutual ? reverse?.contact_info_a || null : null,
148
+ message: mutual
149
+ ? "ๅŒๅ‘ๅŒน้…ๆˆๅŠŸ๏ผ๐ŸŽ‰"
150
+ : "ๅทฒๆŽฅๅ—ใ€‚็ญ‰ๅฏนๆ–นไนŸๆŽฅๅ—ๅŽ๏ผŒไฝ ไปฌๅฐฑๅฏไปฅไบคๆข่”็ณปๆ–นๅผไบ†ใ€‚",
151
+ };
152
+ }
153
+
154
+ // โ”€โ”€โ”€ checkin โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
155
+
156
+ export async function checkin({ lat, lng, device_id, supabaseUrl, supabaseKey }) {
157
+ const sb = getClient(supabaseUrl, supabaseKey);
158
+ const fuzzy = fuzzyCoord(lat, lng);
159
+
160
+ // Check profile exists
161
+ const profile = await getProfile({ device_id, supabaseUrl, supabaseKey });
162
+ if (!profile) {
163
+ return {
164
+ checked_in: false,
165
+ message: "ไฝ ่ฟ˜ๆฒกๆœ‰ๅ็‰‡๏ผŒๅ…ˆๅˆ›ๅปบไธ€ไธชๅงใ€‚",
166
+ };
167
+ }
168
+
169
+ const { error } = await sb.rpc("upsert_profile_location", {
170
+ p_device_id: device_id,
171
+ p_lng: fuzzy.lng,
172
+ p_lat: fuzzy.lat,
173
+ });
174
+ if (error) throw new Error(error.message);
175
+
176
+ return {
177
+ checked_in: true,
178
+ message: "ๅทฒ็ญพๅˆฐ ๐Ÿ“ ็Žฐๅœจ้™„่ฟ‘็š„ไบบๆ‰ซๆๅฐฑ่ƒฝ็œ‹ๅˆฐไฝ ไบ†ใ€‚",
179
+ };
180
+ }
181
+
182
+ // โ”€โ”€โ”€ checkMatches โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
183
+
184
+ export async function checkMatches({ device_id, supabaseUrl, supabaseKey }) {
185
+ const sb = getClient(supabaseUrl, supabaseKey);
186
+
187
+ const { data: allMatches, error } = await sb.rpc("get_my_matches", { p_device_id: device_id });
188
+ if (error) throw new Error(error.message);
189
+
190
+ if (!allMatches?.length) {
191
+ return {
192
+ mutual_matches: [],
193
+ incoming_accepts: [],
194
+ message: "็›ฎๅ‰ๆฒกๆœ‰่ฟ›่กŒไธญ็š„ๅŒน้…ใ€‚",
195
+ };
196
+ }
197
+
198
+ const myMatches = allMatches.filter((m) => m.device_id_a === device_id);
199
+ const incomingMatches = allMatches.filter((m) => m.device_id_b === device_id);
200
+
201
+ // Mutual
202
+ const mutualMatches = [];
203
+ for (const match of myMatches) {
204
+ const reverse = incomingMatches.find((m) => m.device_id_a === match.device_id_b);
205
+ if (reverse) {
206
+ const profile = await getProfile({ device_id: match.device_id_b, supabaseUrl, supabaseKey });
207
+ mutualMatches.push({
208
+ device_id: match.device_id_b,
209
+ name: profile?.display_name || "ๅŒฟๅ",
210
+ emoji: profile?.emoji || "๐Ÿ‘ค",
211
+ line1: profile?.line1,
212
+ line2: profile?.line2,
213
+ line3: profile?.line3,
214
+ their_contact: reverse.contact_info_a || null,
215
+ you_shared: match.contact_info_a || null,
216
+ });
217
+ }
218
+ }
219
+
220
+ // Incoming only
221
+ const incomingAccepts = [];
222
+ for (const match of incomingMatches) {
223
+ const iAccepted = myMatches.find((m) => m.device_id_b === match.device_id_a);
224
+ if (!iAccepted) {
225
+ const profile = await getProfile({ device_id: match.device_id_a, supabaseUrl, supabaseKey });
226
+ incomingAccepts.push({
227
+ device_id: match.device_id_a,
228
+ name: profile?.display_name || "ๅŒฟๅ",
229
+ emoji: profile?.emoji || "๐Ÿ‘ค",
230
+ line1: profile?.line1,
231
+ line2: profile?.line2,
232
+ line3: profile?.line3,
233
+ });
234
+ }
235
+ }
236
+
237
+ const messages = [];
238
+ if (mutualMatches.length > 0) messages.push(`${mutualMatches.length} ไธชๅŒๅ‘ๅŒน้…๏ผๅฏไปฅไบคๆข่”็ณปๆ–นๅผไบ†`);
239
+ if (incomingAccepts.length > 0) messages.push(`${incomingAccepts.length} ไธชไบบๆƒณ่ฎค่ฏ†ไฝ ๏ผŒ็ญ‰ไฝ ๅ›žๅบ”`);
240
+ if (messages.length === 0) messages.push("ไฝ ๆŽฅๅ—ไบ†ไธ€ไบ›ๅŒน้…๏ผŒไฝ†ๅฏนๆ–น่ฟ˜ๆฒกๆœ‰ๅ›žๅบ”ใ€‚่€ๅฟƒ็ญ‰็ญ‰ โณ");
241
+
242
+ return {
243
+ mutual_matches: mutualMatches,
244
+ incoming_accepts: incomingAccepts,
245
+ message: messages.join("๏ผ›"),
246
+ };
247
+ }