antenna-fyi 1.1.0 → 1.2.1

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
@@ -135,7 +135,7 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
135
135
  const saveRefs = async (refMap) => {
136
136
  if (device_id && Object.keys(refMap).length > 0) {
137
137
  try {
138
- await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: JSON.stringify(refMap) });
138
+ await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: refMap });
139
139
  } catch { /* best effort */ }
140
140
  }
141
141
  };
@@ -197,6 +197,7 @@ export async function setProfile({
197
197
  line1,
198
198
  line2,
199
199
  line3,
200
+ matching_context,
200
201
  visible = true,
201
202
  supabaseUrl,
202
203
  supabaseKey,
@@ -210,12 +211,14 @@ export async function setProfile({
210
211
  p_line2: line2 || null,
211
212
  p_line3: line3 || null,
212
213
  p_visible: visible,
214
+ p_matching_context: matching_context || null,
213
215
  });
214
216
  if (error) throw new Error(error.message);
215
217
 
216
- // Generate embedding for global discover matching
218
+ // Generate embedding using lines + matching_context for better quality
217
219
  try {
218
- const text = [line1, line2, line3].filter(Boolean).join(". ");
220
+ const textParts = [line1, line2, line3, matching_context].filter(Boolean);
221
+ const text = textParts.join(". ");
219
222
  if (text) {
220
223
  const embedding = await generateEmbedding(text);
221
224
  if (embedding) {
@@ -443,7 +446,7 @@ export async function discover({ device_id, supabaseUrl, supabaseKey }) {
443
446
  // Persist ref map to DB
444
447
  if (device_id && Object.keys(_refMap).length > 0) {
445
448
  try {
446
- await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: JSON.stringify(_refMap) });
449
+ await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: _refMap });
447
450
  } catch { /* best effort */ }
448
451
  }
449
452
 
@@ -474,6 +477,70 @@ export async function pass({ device_id, target_device_id, ref, supabaseUrl, supa
474
477
  return { passed: true, message: "已跳过,下次不会再推荐这个人。" };
475
478
  }
476
479
 
480
+ // ─── events ─────────────────────────────────────────────────────────
481
+
482
+ export async function createEvent({ name, lat, lng, device_id, starts_at, ends_at, supabaseUrl, supabaseKey }) {
483
+ const sb = getClient(supabaseUrl, supabaseKey);
484
+ const { data, error } = await sb.rpc("create_event", {
485
+ p_name: name,
486
+ p_lat: lat || null,
487
+ p_lng: lng || null,
488
+ p_created_by: device_id || null,
489
+ p_starts_at: starts_at || new Date().toISOString(),
490
+ p_ends_at: ends_at || new Date(Date.now() + 12 * 60 * 60 * 1000).toISOString(),
491
+ });
492
+ if (error) throw new Error(error.message);
493
+ return data;
494
+ }
495
+
496
+ export async function joinEvent({ code, device_id, supabaseUrl, supabaseKey }) {
497
+ const sb = getClient(supabaseUrl, supabaseKey);
498
+ const { data, error } = await sb.rpc("join_event", { p_code: code, p_device_id: device_id });
499
+ if (error) throw new Error(error.message);
500
+ return data;
501
+ }
502
+
503
+ export async function eventScan({ code, device_id, supabaseUrl, supabaseKey }) {
504
+ const sb = getClient(supabaseUrl, supabaseKey);
505
+ const { data, error } = await sb.rpc("event_participants_list", { p_code: code, p_device_id: device_id });
506
+ if (error) throw new Error(error.message);
507
+
508
+ const others = data || [];
509
+ const _refMap = {};
510
+ const profiles = others.map((p, i) => {
511
+ const ref = String(i + 1);
512
+ _refMap[ref] = p.device_id;
513
+ return {
514
+ ref,
515
+ name: p.display_name || "匿名",
516
+ emoji: p.emoji || "👤",
517
+ line1: p.line1,
518
+ line2: p.line2,
519
+ line3: p.line3,
520
+ source: "event",
521
+ };
522
+ });
523
+
524
+ // Persist refs
525
+ if (device_id && Object.keys(_refMap).length > 0) {
526
+ try { await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: _refMap }); } catch {}
527
+ }
528
+
529
+ return {
530
+ count: profiles.length,
531
+ profiles,
532
+ _ref_map: _refMap,
533
+ event: true,
534
+ };
535
+ }
536
+
537
+ export async function getEvent({ code, supabaseUrl, supabaseKey }) {
538
+ const sb = getClient(supabaseUrl, supabaseKey);
539
+ const { data, error } = await sb.rpc("get_event", { p_code: code });
540
+ if (error) throw new Error(error.message);
541
+ return data;
542
+ }
543
+
477
544
  export async function createBindToken({ device_id, supabaseUrl, supabaseKey }) {
478
545
  const sb = getClient(supabaseUrl, supabaseKey);
479
546
  const { data, error } = await sb.rpc("create_bind_token", { p_device_id: device_id });
package/lib/mcp.js CHANGED
@@ -13,6 +13,9 @@ import {
13
13
  checkin,
14
14
  createBindToken,
15
15
  discover,
16
+ createEvent,
17
+ joinEvent,
18
+ eventScan,
16
19
  deriveDeviceId,
17
20
  } from "./core.js";
18
21
 
@@ -69,16 +72,17 @@ export async function startMcpServer() {
69
72
  line1: z.string().optional(),
70
73
  line2: z.string().optional(),
71
74
  line3: z.string().optional(),
75
+ matching_context: z.string().optional().describe("Agent-generated rich context for better matching (not shown to others)"),
72
76
  visible: z.boolean().optional().default(true),
73
77
  },
74
- async ({ action, sender_id, channel, display_name, emoji, line1, line2, line3, visible }) => {
78
+ async ({ action, sender_id, channel, display_name, emoji, line1, line2, line3, matching_context, visible }) => {
75
79
  const deviceId = deriveDeviceId(sender_id, channel);
76
80
  try {
77
81
  if (action === "get") {
78
82
  const data = await getProfile({ device_id: deviceId });
79
83
  return jsonResult(data ? { profile: data } : { profile: null, message: "还没有名片,帮你创建一个?" });
80
84
  }
81
- const data = await setProfile({ device_id: deviceId, display_name, emoji, line1, line2, line3, visible });
85
+ const data = await setProfile({ device_id: deviceId, display_name, emoji, line1, line2, line3, matching_context, visible });
82
86
  return jsonResult({ saved: true, profile: data });
83
87
  } catch (e) {
84
88
  return jsonResult({ error: e.message });
@@ -216,6 +220,69 @@ export async function startMcpServer() {
216
220
  }
217
221
  );
218
222
 
223
+ // ─── antenna_event_create ──────────────────────────────────
224
+
225
+ server.tool(
226
+ "antenna_event_create",
227
+ "Create an event. Returns a shareable link (antenna.fyi/e/CODE) for participants to join.",
228
+ {
229
+ name: z.string().describe("Event name"),
230
+ sender_id: z.string().describe("Creator's user ID"),
231
+ channel: z.string().describe("Channel name"),
232
+ lat: z.number().optional().describe("Event latitude"),
233
+ lng: z.number().optional().describe("Event longitude"),
234
+ starts_at: z.string().optional().describe("Start time ISO string"),
235
+ ends_at: z.string().optional().describe("End time ISO string"),
236
+ },
237
+ async ({ name, sender_id, channel, lat, lng, starts_at, ends_at }) => {
238
+ try {
239
+ const result = await createEvent({ name, lat, lng, device_id: deriveDeviceId(sender_id, channel), starts_at, ends_at });
240
+ return jsonResult(result);
241
+ } catch (e) { return jsonResult({ error: e.message }); }
242
+ }
243
+ );
244
+
245
+ // ─── antenna_event_join ────────────────────────────────────
246
+
247
+ server.tool(
248
+ "antenna_event_join",
249
+ "Join an event by its code. The code is from the event URL (antenna.fyi/e/CODE).",
250
+ {
251
+ code: z.string().describe("Event code"),
252
+ sender_id: z.string().describe("The sender's user ID"),
253
+ channel: z.string().describe("Channel name"),
254
+ },
255
+ async ({ code, sender_id, channel }) => {
256
+ try {
257
+ const result = await joinEvent({ code, device_id: deriveDeviceId(sender_id, channel) });
258
+ return jsonResult(result);
259
+ } catch (e) { return jsonResult({ error: e.message }); }
260
+ }
261
+ );
262
+
263
+ // ─── antenna_event_scan ────────────────────────────────────
264
+
265
+ server.tool(
266
+ "antenna_event_scan",
267
+ "Scan people in an event. No distance limit — returns all event participants.",
268
+ {
269
+ code: z.string().describe("Event code"),
270
+ sender_id: z.string().describe("The sender's user ID"),
271
+ channel: z.string().describe("Channel name"),
272
+ },
273
+ async ({ code, sender_id, channel }) => {
274
+ try {
275
+ const result = await eventScan({ code, device_id: deriveDeviceId(sender_id, channel) });
276
+ if (result._ref_map) {
277
+ _lastRefMap = { ..._lastRefMap, ...result._ref_map };
278
+ const { _ref_map, ...clean } = result;
279
+ return jsonResult(clean);
280
+ }
281
+ return jsonResult(result);
282
+ } catch (e) { return jsonResult({ error: e.message }); }
283
+ }
284
+ );
285
+
219
286
  const transport = new StdioServerTransport();
220
287
  await server.connect(transport);
221
288
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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
@@ -83,7 +83,7 @@ After receiving the nearby profiles, **you decide** who to recommend:
83
83
  View or update the user's name card.
84
84
  - `action`: "get" or "set"
85
85
  - `sender_id`, `channel`: from context
86
- - For "set": `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`
86
+ - For "set": `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`, `matching_context`
87
87
 
88
88
  The name card has:
89
89
  - **emoji**: a single emoji that represents them
@@ -91,6 +91,12 @@ The name card has:
91
91
  - **line1**: who they are / what they do
92
92
  - **line2**: what they're into
93
93
  - **line3**: what they're looking for right now
94
+ - **matching_context** (optional, not shown to others): A richer description generated by the agent based on what it knows about the user — career background, tech stack, interests, projects, personality traits. ~200 words. Only used for embedding-based matching, never displayed to other users.
95
+
96
+ **After saving the profile, generate `matching_context` automatically** based on your knowledge of the user (memory, conversations, context). Don't ask the user to write it — you write it. Example:
97
+ > "Product designer at a tech company in Beijing, focusing on AI search experience. Interested in music (Sakamoto), swimming, cooking, language learning. Recently exploring AI agent ecosystems and social discovery. Looking to connect with AI builders, indie hackers, and creative technologists."
98
+ - **line2**: what they're into
99
+ - **line3**: what they're looking for right now
94
100
 
95
101
  ### `antenna_accept`
96
102
  Accept a match after the user sees results. Can optionally include contact info to share.
@@ -251,3 +257,21 @@ Plugin 自带后台服务,每 10 分钟轮询一次 Supabase 查新的 mutual
251
257
  3. 如果对方分享了联系方式,一并展示
252
258
 
253
259
  用户不需要主动问,agent 会自动收到通知。
260
+
261
+ ### `antenna_event_create`
262
+ Create an event. Returns a shareable link (antenna.fyi/e/CODE).
263
+ - `name`: event name
264
+ - `sender_id`, `channel`: from context
265
+ - `lat`, `lng`: optional event location
266
+ - `starts_at`, `ends_at`: optional time range (default: now to +12h)
267
+
268
+ ### `antenna_event_join`
269
+ Join an event by code.
270
+ - `code`: from the event URL (antenna.fyi/e/CODE)
271
+ - `sender_id`, `channel`: from context
272
+
273
+ ### `antenna_event_scan`
274
+ Scan people in an event. No distance limit — returns all participants.
275
+ - `code`: event code
276
+ - `sender_id`, `channel`: from context
277
+ - Returns profiles with `source: "event"` tag