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 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
- console.log(` ${p.emoji} ${p.name}${badge}`);
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", { p_device_id: device_id });
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
- message: "发送čŋ™äļŠé“ūæŽĨįŧ™į”Ļ户åœĻ手朚æĩč§ˆå™Ļ打垀åģåŊå…ąäšŦä―į―Ū。",
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. Send this URL to the user so they can "
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", {"p_device_id": did}).execute()
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
- "message": "发送čŋ™äļŠé“ūæŽĨįŧ™į”Ļ户åœĻ手朚æĩč§ˆå™Ļ打垀åģåŊå…ąäšŦä―į―Ū。",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Antenna — nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {
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