antenna-fyi 1.0.1 → 1.2.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/lib/core.js CHANGED
@@ -13,6 +13,28 @@ let _url = null;
13
13
  // ─── Embedding ───────────────────────────────────────────────────────
14
14
 
15
15
  const GEMINI_EMBEDDING_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent";
16
+ const GEMINI_FLASH_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent";
17
+
18
+ async function generateMatchReason(myLines, theirLines) {
19
+ const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
20
+ if (!apiKey) return null;
21
+
22
+ const prompt = `You are matching two people. Person A: "${myLines}". Person B: "${theirLines}". Write ONE short sentence (under 20 words) in the SAME LANGUAGE as the profiles explaining why they might click. Be specific, not generic. No fluff.`;
23
+
24
+ try {
25
+ const res = await fetch(`${GEMINI_FLASH_URL}?key=${apiKey}`, {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({
29
+ contents: [{ parts: [{ text: prompt }] }],
30
+ generationConfig: { maxOutputTokens: 60, temperature: 0.7 },
31
+ }),
32
+ });
33
+ if (!res.ok) return null;
34
+ const data = await res.json();
35
+ return data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || null;
36
+ } catch { return null; }
37
+ }
16
38
 
17
39
  async function generateEmbedding(text) {
18
40
  const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
@@ -382,20 +404,33 @@ export async function discover({ device_id, supabaseUrl, supabaseKey }) {
382
404
  };
383
405
  }
384
406
 
385
- // Build ref map + persist to DB
407
+ // Build ref map + generate match reasons
386
408
  const _refMap = {};
387
- const profiles = results.map((p, i) => {
409
+ const myProfile = await getProfile({ device_id, supabaseUrl, supabaseKey });
410
+ const myLines = myProfile ? [myProfile.line1, myProfile.line2, myProfile.line3].filter(Boolean).join(". ") : "";
411
+
412
+ const profiles = [];
413
+ for (let i = 0; i < results.length; i++) {
414
+ const p = results[i];
388
415
  const ref = String(i + 1);
389
416
  _refMap[ref] = p.device_id;
390
- return {
417
+
418
+ const theirLines = [p.line1, p.line2, p.line3].filter(Boolean).join(". ");
419
+ let reason = null;
420
+ if (myLines && theirLines) {
421
+ reason = await generateMatchReason(myLines, theirLines);
422
+ }
423
+
424
+ profiles.push({
391
425
  ref,
392
426
  name: p.display_name || "匿名",
393
427
  emoji: p.emoji || "👤",
394
428
  line1: p.line1,
395
429
  line2: p.line2,
396
430
  line3: p.line3,
397
- };
398
- });
431
+ match_reason: reason,
432
+ });
433
+ }
399
434
 
400
435
  // Log who was recommended (for dedup)
401
436
  for (const p of results) {
@@ -421,6 +456,88 @@ export async function discover({ device_id, supabaseUrl, supabaseKey }) {
421
456
  };
422
457
  }
423
458
 
459
+ // ─── pass ───────────────────────────────────────────────────────────
460
+
461
+ export async function pass({ device_id, target_device_id, ref, supabaseUrl, supabaseKey }) {
462
+ const sb = getClient(supabaseUrl, supabaseKey);
463
+
464
+ let targetId = target_device_id;
465
+ if (!targetId && ref && device_id) {
466
+ const { data } = await sb.rpc("resolve_ref", { p_owner: device_id, p_ref: ref });
467
+ targetId = data;
468
+ }
469
+ if (!targetId) {
470
+ return { passed: false, error: "No target. Ref may have expired — try scanning again." };
471
+ }
472
+
473
+ await sb.rpc("pass_user", { p_device_id: device_id, p_passed_device_id: targetId });
474
+ return { passed: true, message: "已跳过,下次不会再推荐这个人。" };
475
+ }
476
+
477
+ // ─── events ─────────────────────────────────────────────────────────
478
+
479
+ export async function createEvent({ name, lat, lng, device_id, starts_at, ends_at, supabaseUrl, supabaseKey }) {
480
+ const sb = getClient(supabaseUrl, supabaseKey);
481
+ const { data, error } = await sb.rpc("create_event", {
482
+ p_name: name,
483
+ p_lat: lat || null,
484
+ p_lng: lng || null,
485
+ p_created_by: device_id || null,
486
+ p_starts_at: starts_at || new Date().toISOString(),
487
+ p_ends_at: ends_at || new Date(Date.now() + 12 * 60 * 60 * 1000).toISOString(),
488
+ });
489
+ if (error) throw new Error(error.message);
490
+ return data;
491
+ }
492
+
493
+ export async function joinEvent({ code, device_id, supabaseUrl, supabaseKey }) {
494
+ const sb = getClient(supabaseUrl, supabaseKey);
495
+ const { data, error } = await sb.rpc("join_event", { p_code: code, p_device_id: device_id });
496
+ if (error) throw new Error(error.message);
497
+ return data;
498
+ }
499
+
500
+ export async function eventScan({ code, device_id, supabaseUrl, supabaseKey }) {
501
+ const sb = getClient(supabaseUrl, supabaseKey);
502
+ const { data, error } = await sb.rpc("event_participants_list", { p_code: code, p_device_id: device_id });
503
+ if (error) throw new Error(error.message);
504
+
505
+ const others = data || [];
506
+ const _refMap = {};
507
+ const profiles = others.map((p, i) => {
508
+ const ref = String(i + 1);
509
+ _refMap[ref] = p.device_id;
510
+ return {
511
+ ref,
512
+ name: p.display_name || "匿名",
513
+ emoji: p.emoji || "👤",
514
+ line1: p.line1,
515
+ line2: p.line2,
516
+ line3: p.line3,
517
+ source: "event",
518
+ };
519
+ });
520
+
521
+ // Persist refs
522
+ if (device_id && Object.keys(_refMap).length > 0) {
523
+ try { await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: JSON.stringify(_refMap) }); } catch {}
524
+ }
525
+
526
+ return {
527
+ count: profiles.length,
528
+ profiles,
529
+ _ref_map: _refMap,
530
+ event: true,
531
+ };
532
+ }
533
+
534
+ export async function getEvent({ code, supabaseUrl, supabaseKey }) {
535
+ const sb = getClient(supabaseUrl, supabaseKey);
536
+ const { data, error } = await sb.rpc("get_event", { p_code: code });
537
+ if (error) throw new Error(error.message);
538
+ return data;
539
+ }
540
+
424
541
  export async function createBindToken({ device_id, supabaseUrl, supabaseKey }) {
425
542
  const sb = getClient(supabaseUrl, supabaseKey);
426
543
  const { data, error } = await sb.rpc("create_bind_token", { p_device_id: device_id });
package/lib/mcp.js CHANGED
@@ -8,10 +8,14 @@ import {
8
8
  getProfile,
9
9
  setProfile,
10
10
  accept,
11
+ pass,
11
12
  checkMatches,
12
13
  checkin,
13
14
  createBindToken,
14
15
  discover,
16
+ createEvent,
17
+ joinEvent,
18
+ eventScan,
15
19
  deriveDeviceId,
16
20
  } from "./core.js";
17
21
 
@@ -194,6 +198,90 @@ export async function startMcpServer() {
194
198
  }
195
199
  );
196
200
 
201
+ // ─── antenna_pass ────────────────────────────────────────────
202
+
203
+ server.tool(
204
+ "antenna_pass",
205
+ "Pass/skip a person. They won't be recommended again.",
206
+ {
207
+ sender_id: z.string().describe("The sender's user ID"),
208
+ channel: z.string().describe("Channel name"),
209
+ ref: z.string().optional().describe("Ref number from scan/discover results"),
210
+ target_device_id: z.string().optional().describe("Device ID (use ref instead)"),
211
+ },
212
+ async ({ sender_id, channel, ref, target_device_id }) => {
213
+ try {
214
+ const result = await pass({ device_id: deriveDeviceId(sender_id, channel), target_device_id, ref });
215
+ return jsonResult(result);
216
+ } catch (e) {
217
+ return jsonResult({ error: e.message });
218
+ }
219
+ }
220
+ );
221
+
222
+ // ─── antenna_event_create ──────────────────────────────────
223
+
224
+ server.tool(
225
+ "antenna_event_create",
226
+ "Create an event. Returns a shareable link (antenna.fyi/e/CODE) for participants to join.",
227
+ {
228
+ name: z.string().describe("Event name"),
229
+ sender_id: z.string().describe("Creator's user ID"),
230
+ channel: z.string().describe("Channel name"),
231
+ lat: z.number().optional().describe("Event latitude"),
232
+ lng: z.number().optional().describe("Event longitude"),
233
+ starts_at: z.string().optional().describe("Start time ISO string"),
234
+ ends_at: z.string().optional().describe("End time ISO string"),
235
+ },
236
+ async ({ name, sender_id, channel, lat, lng, starts_at, ends_at }) => {
237
+ try {
238
+ const result = await createEvent({ name, lat, lng, device_id: deriveDeviceId(sender_id, channel), starts_at, ends_at });
239
+ return jsonResult(result);
240
+ } catch (e) { return jsonResult({ error: e.message }); }
241
+ }
242
+ );
243
+
244
+ // ─── antenna_event_join ────────────────────────────────────
245
+
246
+ server.tool(
247
+ "antenna_event_join",
248
+ "Join an event by its code. The code is from the event URL (antenna.fyi/e/CODE).",
249
+ {
250
+ code: z.string().describe("Event code"),
251
+ sender_id: z.string().describe("The sender's user ID"),
252
+ channel: z.string().describe("Channel name"),
253
+ },
254
+ async ({ code, sender_id, channel }) => {
255
+ try {
256
+ const result = await joinEvent({ code, device_id: deriveDeviceId(sender_id, channel) });
257
+ return jsonResult(result);
258
+ } catch (e) { return jsonResult({ error: e.message }); }
259
+ }
260
+ );
261
+
262
+ // ─── antenna_event_scan ────────────────────────────────────
263
+
264
+ server.tool(
265
+ "antenna_event_scan",
266
+ "Scan people in an event. No distance limit — returns all event participants.",
267
+ {
268
+ code: z.string().describe("Event code"),
269
+ sender_id: z.string().describe("The sender's user ID"),
270
+ channel: z.string().describe("Channel name"),
271
+ },
272
+ async ({ code, sender_id, channel }) => {
273
+ try {
274
+ const result = await eventScan({ code, device_id: deriveDeviceId(sender_id, channel) });
275
+ if (result._ref_map) {
276
+ _lastRefMap = { ..._lastRefMap, ...result._ref_map };
277
+ const { _ref_map, ...clean } = result;
278
+ return jsonResult(clean);
279
+ }
280
+ return jsonResult(result);
281
+ } catch (e) { return jsonResult({ error: e.message }); }
282
+ }
283
+ );
284
+
197
285
  const transport = new StdioServerTransport();
198
286
  await server.connect(transport);
199
287
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
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
@@ -251,3 +251,21 @@ Plugin 自带后台服务,每 10 分钟轮询一次 Supabase 查新的 mutual
251
251
  3. 如果对方分享了联系方式,一并展示
252
252
 
253
253
  用户不需要主动问,agent 会自动收到通知。
254
+
255
+ ### `antenna_event_create`
256
+ Create an event. Returns a shareable link (antenna.fyi/e/CODE).
257
+ - `name`: event name
258
+ - `sender_id`, `channel`: from context
259
+ - `lat`, `lng`: optional event location
260
+ - `starts_at`, `ends_at`: optional time range (default: now to +12h)
261
+
262
+ ### `antenna_event_join`
263
+ Join an event by code.
264
+ - `code`: from the event URL (antenna.fyi/e/CODE)
265
+ - `sender_id`, `channel`: from context
266
+
267
+ ### `antenna_event_scan`
268
+ Scan people in an event. No distance limit — returns all participants.
269
+ - `code`: event code
270
+ - `sender_id`, `channel`: from context
271
+ - Returns profiles with `source: "event"` tag