antenna-fyi 1.2.6 β†’ 1.2.8

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");
@@ -187,9 +200,11 @@ export async function handleEvent(f) {
187
200
  if (!f.code) return console.error("Usage: antenna event --scan --code abc123 [--id telegram:123]");
188
201
  const result = await eventScan({ code: f.code, device_id: f.id || null });
189
202
  if (result.count === 0) return console.log("\n🏟️ No participants yet.\n");
190
- console.log(`\n🏟️ ${result.count} people in this event:\n`);
203
+ console.log(`\n🏟️ ${result.count} joined, ${result.checked_in_count || 0} checked in:\n`);
191
204
  result.profiles.forEach((p) => {
192
- console.log(` ${p.emoji} ${p.name}`);
205
+ const badge = p.checked_in ? " βœ…" : "";
206
+ const creatorTag = p.role === "creator" ? " [主办]" : "";
207
+ console.log(` ${p.emoji} ${p.name}${creatorTag}${badge}`);
193
208
  if (p.line1) console.log(` ${p.line1}`);
194
209
  console.log(` ref: ${p.ref}\n`);
195
210
  });
@@ -201,7 +216,8 @@ export async function handleEvent(f) {
201
216
  antenna event --join --code abc123 --id telegram:123
202
217
  antenna event --scan --code abc123 [--id telegram:123]
203
218
  antenna event --checkin --code abc123 --id telegram:123 [--lat 34.05 --lng -118.24]
204
- 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`);
205
221
  }
206
222
 
207
223
  export async function handleBind(f) {
@@ -396,7 +412,7 @@ Usage:
396
412
  antenna pass --id telegram:123 --target telegram:789 (or --ref 1)
397
413
  antenna matches --id telegram:123
398
414
  antenna discover --id telegram:123
399
- 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
400
416
  antenna bind --id telegram:123
401
417
  antenna serve Start MCP server (stdio transport)
402
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", {
@@ -527,9 +538,11 @@ export async function eventScan({ code, device_id, supabaseUrl, supabaseKey }) {
527
538
 
528
539
  const others = data || [];
529
540
  const _refMap = {};
541
+ let checkedInCount = 0;
530
542
  const profiles = others.map((p, i) => {
531
543
  const ref = String(i + 1);
532
544
  _refMap[ref] = p.device_id;
545
+ if (p.checked_in) checkedInCount++;
533
546
  return {
534
547
  ref,
535
548
  name: p.display_name || "匿名",
@@ -537,6 +550,8 @@ export async function eventScan({ code, device_id, supabaseUrl, supabaseKey }) {
537
550
  line1: p.line1,
538
551
  line2: p.line2,
539
552
  line3: p.line3,
553
+ checked_in: !!p.checked_in,
554
+ role: p.role || "participant",
540
555
  source: "event",
541
556
  };
542
557
  });
@@ -548,6 +563,7 @@ export async function eventScan({ code, device_id, supabaseUrl, supabaseKey }) {
548
563
 
549
564
  return {
550
565
  count: profiles.length,
566
+ checked_in_count: checkedInCount,
551
567
  profiles,
552
568
  _ref_map: _refMap,
553
569
  event: true,
@@ -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):
@@ -237,6 +237,20 @@ EVENT_END_SCHEMA = {
237
237
  },
238
238
  }
239
239
 
240
+ EVENT_UPLOAD_IMAGE_SCHEMA = {
241
+ "name": "antenna_event_upload_image",
242
+ "description": "Upload an image for an event OG preview. Returns a public URL.",
243
+ "parameters": {
244
+ "type": "object",
245
+ "properties": {
246
+ "image_base64": {"type": "string", "description": "Base64-encoded image data"},
247
+ "content_type": {"type": "string", "description": "MIME type (default image/png)"},
248
+ "event_code": {"type": "string", "description": "Event code"},
249
+ },
250
+ "required": ["image_base64", "event_code"],
251
+ },
252
+ }
253
+
240
254
  EVENT_CHECKIN_SCHEMA = {
241
255
  "name": "antenna_event_checkin",
242
256
  "description": "Check in at an event \u2014 marks you as present at the event location. Optionally updates GPS.",
@@ -480,6 +480,18 @@ def handle_event_end(params: dict) -> str:
480
480
  return _ok({"ended": False, "error": data.get("error", "η»“ζŸζ΄»εŠ¨ε€±θ΄₯")})
481
481
 
482
482
 
483
+ def handle_event_upload_image(params: dict) -> str:
484
+ import base64 as b64mod
485
+ sb = _sb()
486
+ content_type = params.get("content_type") or "image/png"
487
+ ext = content_type.split("/")[1] if "/" in content_type else "png"
488
+ path = f"{params['event_code']}.{ext}"
489
+ buf = b64mod.b64decode(params["image_base64"])
490
+ resp = sb.storage.from_("event-images").upload(path, buf, {"content-type": content_type, "upsert": "true"})
491
+ pub = sb.storage.from_("event-images").get_public_url(path)
492
+ return _ok({"url": pub})
493
+
494
+
483
495
  def handle_event_checkin(params: dict) -> str:
484
496
  sb = _sb()
485
497
  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
 
@@ -246,6 +247,24 @@ export async function startMcpServer() {
246
247
  }
247
248
  );
248
249
 
250
+ // ─── antenna_event_upload_image ──────────────────────────────
251
+
252
+ server.tool(
253
+ "antenna_event_upload_image",
254
+ "Upload an image for an event. Returns a public URL to use as og_image.",
255
+ {
256
+ image_base64: z.string().describe("Base64-encoded image data"),
257
+ content_type: z.string().optional().describe("MIME type (default image/png)"),
258
+ event_code: z.string().describe("Event code to associate the image with"),
259
+ },
260
+ async ({ image_base64, content_type, event_code }) => {
261
+ try {
262
+ const url = await uploadEventImage({ image_data: image_base64, content_type, event_code });
263
+ return jsonResult({ url });
264
+ } catch (e) { return jsonResult({ error: e.message }); }
265
+ }
266
+ );
267
+
249
268
  // ─── antenna_event_end ───────────────────────────────────────
250
269
 
251
270
  server.tool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
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
@@ -282,6 +282,8 @@ Create an event. Returns a shareable link (antenna.fyi/e/CODE).
282
282
  - `description`: optional event description
283
283
  - `og_image`: optional OG image URL for social sharing preview
284
284
 
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`. Don't use profile location β€” the user may create an event at a different place.
286
+
285
287
  ### `antenna_event_end`
286
288
  End an event. Only the creator can end it.
287
289
  - `code`: event code
@@ -303,3 +305,9 @@ Check in at an event β€” marks you as present at the event location. Optionally
303
305
  - `code`: event code
304
306
  - `sender_id`, `channel`: from context
305
307
  - `lat`, `lng`: optional GPS coordinates
308
+
309
+ ### `antenna_event_upload_image`
310
+ Upload an image for an event OG preview. Returns a public URL.
311
+ - `image_base64`: base64-encoded image data
312
+ - `content_type`: MIME type (default image/png)
313
+ - `event_code`: event code