antenna-fyi 1.2.2 → 1.2.4

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/bin/antenna.js CHANGED
@@ -8,7 +8,9 @@ import {
8
8
  handleCheckin,
9
9
  handleMatches,
10
10
  handleDiscover,
11
+ handleEvent,
11
12
  handleBind,
13
+ handlePass,
12
14
  handleSetup,
13
15
  handleStatus,
14
16
  handleInstallSkill,
@@ -34,8 +36,12 @@ async function main() {
34
36
  return handleMatches(f);
35
37
  case "discover":
36
38
  return handleDiscover(f);
39
+ case "event":
40
+ return handleEvent(f);
37
41
  case "bind":
38
42
  return handleBind(f);
43
+ case "pass":
44
+ return handlePass(f);
39
45
  case "serve": {
40
46
  const { startMcpServer } = await import("../lib/mcp.js");
41
47
  return startMcpServer();
package/lib/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // antenna CLI command handlers
2
2
 
3
- import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover } from "./core.js";
3
+ import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover, createEvent, joinEvent, eventScan, pass as passUser } from "./core.js";
4
4
  import { createInterface } from "readline";
5
5
  import { existsSync, mkdirSync, copyFileSync } from "fs";
6
6
  import { join, dirname } from "path";
@@ -16,8 +16,13 @@ export function parseFlags(args) {
16
16
  for (let i = 0; i < args.length; i++) {
17
17
  if (args[i].startsWith("--")) {
18
18
  const key = args[i].slice(2);
19
- flags[key] = args[i + 1] || true;
20
- i++;
19
+ const next = args[i + 1];
20
+ if (next && !next.startsWith("--")) {
21
+ flags[key] = next;
22
+ i++;
23
+ } else {
24
+ flags[key] = true;
25
+ }
21
26
  }
22
27
  }
23
28
  return flags;
@@ -125,6 +130,50 @@ export async function handleDiscover(f) {
125
130
  });
126
131
  }
127
132
 
133
+ export async function handleEvent(f) {
134
+ const sub = f._?.[0] || Object.keys(f).find(k => ["create", "join", "scan"].includes(k));
135
+
136
+ if (f.create || (!f.join && !f.scan && f.name)) {
137
+ if (!f.name) return console.error("Usage: antenna event --create --name 'AI Meetup'");
138
+ const result = await createEvent({ name: f.name, device_id: f.id || null, lat: f.lat ? +f.lat : undefined, lng: f.lng ? +f.lng : undefined });
139
+ console.log(`\n🎉 Event created!\n`);
140
+ console.log(` Name: ${result.name}`);
141
+ console.log(` Code: ${result.code}`);
142
+ console.log(` URL: ${result.url}`);
143
+ console.log(` Ends: ${result.ends_at}\n`);
144
+ return;
145
+ }
146
+
147
+ if (f.join) {
148
+ if (!f.code || !f.id) return console.error("Usage: antenna event --join --code abc123 --id telegram:123");
149
+ const result = await joinEvent({ code: f.code, device_id: f.id });
150
+ if (result.joined) {
151
+ console.log(`\n✅ Joined "${result.name}" (${result.code})\n`);
152
+ } else {
153
+ console.log(`\n❌ ${result.error}\n`);
154
+ }
155
+ return;
156
+ }
157
+
158
+ if (f.scan) {
159
+ if (!f.code) return console.error("Usage: antenna event --scan --code abc123 [--id telegram:123]");
160
+ const result = await eventScan({ code: f.code, device_id: f.id || null });
161
+ if (result.count === 0) return console.log("\n🏟️ No participants yet.\n");
162
+ console.log(`\n🏟️ ${result.count} people in this event:\n`);
163
+ result.profiles.forEach((p) => {
164
+ console.log(` ${p.emoji} ${p.name}`);
165
+ if (p.line1) console.log(` ${p.line1}`);
166
+ console.log(` ref: ${p.ref}\n`);
167
+ });
168
+ return;
169
+ }
170
+
171
+ console.log(`Usage:
172
+ antenna event --create --name 'AI Meetup' [--id telegram:123]
173
+ antenna event --join --code abc123 --id telegram:123
174
+ antenna event --scan --code abc123 [--id telegram:123]`);
175
+ }
176
+
128
177
  export async function handleBind(f) {
129
178
  if (!f.id) return console.error("Usage: antenna bind --id telegram:123");
130
179
  const result = await createBindToken({ device_id: f.id });
@@ -134,6 +183,17 @@ export async function handleBind(f) {
134
183
  console.log();
135
184
  }
136
185
 
186
+ export async function handlePass(f) {
187
+ if (!f.id) return console.error("Usage: antenna pass --id telegram:123 --target telegram:789");
188
+ if (!f.target && !f.ref) return console.error("Usage: antenna pass --id telegram:123 --target telegram:789 (or --ref 1)");
189
+ const result = await passUser({
190
+ device_id: f.id,
191
+ target_device_id: f.target,
192
+ ref: f.ref,
193
+ });
194
+ console.log("✅ " + (result.message || "Passed."));
195
+ }
196
+
137
197
  export async function handleSetup(f) {
138
198
  const rl = createInterface({ input: process.stdin, output: process.stdout });
139
199
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
@@ -303,7 +363,10 @@ Usage:
303
363
  antenna checkin --id telegram:123 --lat 39.99 --lng 116.48
304
364
  antenna profile --id telegram:123 [--name Yi --emoji 🦦 --line1 '...']
305
365
  antenna accept --id telegram:123 --target telegram:789 [--contact 'WeChat: yi']
366
+ antenna pass --id telegram:123 --target telegram:789 (or --ref 1)
306
367
  antenna matches --id telegram:123
368
+ antenna discover --id telegram:123
369
+ antenna event --create --name 'AI Meetup' | --join --code abc123 | --scan --code abc123
307
370
  antenna bind --id telegram:123
308
371
  antenna serve Start MCP server (stdio transport)
309
372
  antenna setup Interactive profile setup [--id telegram:123]
@@ -13,15 +13,27 @@ from .tools import (
13
13
  handle_checkin,
14
14
  handle_check_matches,
15
15
  handle_bind,
16
+ handle_pass,
17
+ handle_discover,
18
+ handle_event_create,
19
+ handle_event_join,
20
+ handle_event_scan,
16
21
  _sb,
17
22
  _device_id,
18
23
  _my_device_ids,
24
+ )
25
+ from .schemas import (
19
26
  SCAN_SCHEMA,
20
27
  PROFILE_SCHEMA,
21
28
  ACCEPT_SCHEMA,
22
29
  CHECKIN_SCHEMA,
23
30
  CHECK_MATCHES_SCHEMA,
24
31
  BIND_SCHEMA,
32
+ PASS_SCHEMA,
33
+ DISCOVER_SCHEMA,
34
+ EVENT_CREATE_SCHEMA,
35
+ EVENT_JOIN_SCHEMA,
36
+ EVENT_SCAN_SCHEMA,
25
37
  )
26
38
  import re
27
39
  import time
@@ -44,6 +56,11 @@ def register(ctx):
44
56
  ctx.register_tool("antenna_checkin", CHECKIN_SCHEMA, handle_checkin)
45
57
  ctx.register_tool("antenna_check_matches", CHECK_MATCHES_SCHEMA, handle_check_matches)
46
58
  ctx.register_tool("antenna_bind", BIND_SCHEMA, handle_bind)
59
+ ctx.register_tool("antenna_pass", PASS_SCHEMA, handle_pass)
60
+ ctx.register_tool("antenna_discover", DISCOVER_SCHEMA, handle_discover)
61
+ ctx.register_tool("antenna_event_create", EVENT_CREATE_SCHEMA, handle_event_create)
62
+ ctx.register_tool("antenna_event_join", EVENT_JOIN_SCHEMA, handle_event_join)
63
+ ctx.register_tool("antenna_event_scan", EVENT_SCAN_SCHEMA, handle_event_scan)
47
64
 
48
65
  # ── Hook: auto-detect location + check web GPS events ─────────
49
66
  def on_pre_llm(messages, **kwargs):
@@ -134,3 +134,89 @@ BIND_SCHEMA = {
134
134
  "required": ["sender_id", "channel"],
135
135
  },
136
136
  }
137
+
138
+ PASS_SCHEMA = {
139
+ "name": "antenna_pass",
140
+ "description": "Pass/skip a person. They won't be recommended again.",
141
+ "parameters": {
142
+ "type": "object",
143
+ "properties": {
144
+ "sender_id": {"type": "string", "description": "The sender's user ID"},
145
+ "channel": {"type": "string", "description": "Platform name"},
146
+ "ref": {
147
+ "type": "string",
148
+ "description": "Ref number from scan/discover results (e.g. '1')",
149
+ },
150
+ "target_device_id": {
151
+ "type": "string",
152
+ "description": "Device ID (use ref instead when possible)",
153
+ },
154
+ },
155
+ "required": ["sender_id", "channel"],
156
+ },
157
+ }
158
+
159
+ DISCOVER_SCHEMA = {
160
+ "name": "antenna_discover",
161
+ "description": (
162
+ "Get today's global recommendation — the person most similar to you "
163
+ "worldwide. 1 per day, no repeats."
164
+ ),
165
+ "parameters": {
166
+ "type": "object",
167
+ "properties": {
168
+ "sender_id": {"type": "string", "description": "The sender's user ID"},
169
+ "channel": {"type": "string", "description": "Platform name"},
170
+ },
171
+ "required": ["sender_id", "channel"],
172
+ },
173
+ }
174
+
175
+ EVENT_CREATE_SCHEMA = {
176
+ "name": "antenna_event_create",
177
+ "description": (
178
+ "Create an event. Returns a shareable link (antenna.fyi/e/CODE) "
179
+ "for participants to join."
180
+ ),
181
+ "parameters": {
182
+ "type": "object",
183
+ "properties": {
184
+ "name": {"type": "string", "description": "Event name"},
185
+ "sender_id": {"type": "string", "description": "The sender's user ID"},
186
+ "channel": {"type": "string", "description": "Platform name"},
187
+ "lat": {"type": "number", "description": "Event latitude"},
188
+ "lng": {"type": "number", "description": "Event longitude"},
189
+ "starts_at": {"type": "string", "description": "Start time ISO"},
190
+ "ends_at": {"type": "string", "description": "End time ISO"},
191
+ },
192
+ "required": ["name", "sender_id", "channel"],
193
+ },
194
+ }
195
+
196
+ EVENT_JOIN_SCHEMA = {
197
+ "name": "antenna_event_join",
198
+ "description": "Join an event by its code from the event URL.",
199
+ "parameters": {
200
+ "type": "object",
201
+ "properties": {
202
+ "code": {"type": "string", "description": "Event code"},
203
+ "sender_id": {"type": "string", "description": "The sender's user ID"},
204
+ "channel": {"type": "string", "description": "Platform name"},
205
+ },
206
+ "required": ["code", "sender_id", "channel"],
207
+ },
208
+ }
209
+
210
+ EVENT_SCAN_SCHEMA = {
211
+ "name": "antenna_event_scan",
212
+ "description": "Scan people in an event. No distance limit.",
213
+ "parameters": {
214
+ "type": "object",
215
+ "properties": {
216
+ "code": {"type": "string", "description": "Event code"},
217
+ "sender_id": {"type": "string", "description": "The sender's user ID"},
218
+ "channel": {"type": "string", "description": "Platform name"},
219
+ },
220
+ "required": ["code", "sender_id", "channel"],
221
+ },
222
+ }
@@ -8,6 +8,7 @@ import json
8
8
  import math
9
9
  import os
10
10
  import time
11
+ import urllib.request
11
12
 
12
13
  try:
13
14
  from supabase import create_client
@@ -277,6 +278,173 @@ def handle_check_matches(params: dict) -> str:
277
278
  BASE_URL = "https://www.antenna.fyi"
278
279
 
279
280
 
281
+ def handle_pass(params: dict) -> str:
282
+ sb = _sb()
283
+ did = _device_id(params["sender_id"], params["channel"])
284
+
285
+ ref = params.get("ref")
286
+ target = params.get("target_device_id")
287
+ if ref and ref in _last_ref_map:
288
+ target = _last_ref_map[ref]
289
+ if not target and ref:
290
+ # Try resolve via RPC
291
+ try:
292
+ resp = sb.rpc("resolve_ref", {"p_device_id": did, "p_ref": ref}).execute()
293
+ if resp.data:
294
+ target = resp.data.get("target_device_id")
295
+ except Exception:
296
+ pass
297
+ if not target:
298
+ return _ok({"error": "No target. Use 'ref' from scan/discover results or 'target_device_id'."})
299
+
300
+ sb.rpc("pass_user", {
301
+ "p_device_id": did,
302
+ "p_target_device_id": target,
303
+ }).execute()
304
+
305
+ return _ok({"passed": True, "message": "已跳过,不会再推荐这个人了。"})
306
+
307
+
308
+ def handle_discover(params: dict) -> str:
309
+ sb = _sb()
310
+ did = _device_id(params["sender_id"], params["channel"])
311
+
312
+ resp = sb.rpc("global_discover", {"p_device_id": did}).execute()
313
+ results = resp.data or []
314
+
315
+ if not results:
316
+ return _ok({"count": 0, "message": "今天没有新的全球推荐了,明天再来看看。"})
317
+
318
+ global _last_ref_map
319
+ _last_ref_map = {}
320
+ profiles = []
321
+
322
+ # Get my profile for match reason
323
+ my_prof = sb.rpc("get_profile", {"p_device_id": did}).execute()
324
+ my_data = my_prof.data or {}
325
+ my_lines = [my_data.get("line1", ""), my_data.get("line2", ""), my_data.get("line3", "")]
326
+
327
+ for i, p in enumerate(results):
328
+ ref = str(i + 1)
329
+ _last_ref_map[ref] = p.get("device_id")
330
+
331
+ their_lines = [p.get("line1", ""), p.get("line2", ""), p.get("line3", "")]
332
+
333
+ # Generate match reason via Edge Function
334
+ match_reason = None
335
+ try:
336
+ req = urllib.request.Request(
337
+ f"{BUILTIN_URL}/functions/v1/generate-match-reason",
338
+ data=json.dumps({"my_lines": my_lines, "their_lines": their_lines}).encode(),
339
+ headers={"Content-Type": "application/json", "Authorization": f"Bearer {BUILTIN_KEY}"},
340
+ )
341
+ res = urllib.request.urlopen(req, timeout=10)
342
+ body = json.loads(res.read().decode())
343
+ match_reason = body.get("reason")
344
+ except Exception:
345
+ pass
346
+
347
+ profile = {
348
+ "ref": ref,
349
+ "emoji": p.get("emoji") or "\ud83d\udc64",
350
+ "name": p.get("display_name") or "匿名",
351
+ "line1": p.get("line1"),
352
+ "line2": p.get("line2"),
353
+ "line3": p.get("line3"),
354
+ }
355
+ if match_reason:
356
+ profile["match_reason"] = match_reason
357
+ profiles.append(profile)
358
+
359
+ return _ok({
360
+ "count": len(profiles),
361
+ "profiles": profiles,
362
+ "instruction": "这是全球推荐。根据你对用户的了解,判断是否值得推荐,写一句个性化的匹配理由。使用 ref 编号引用。",
363
+ })
364
+
365
+
366
+ def handle_event_create(params: dict) -> str:
367
+ sb = _sb()
368
+ did = _device_id(params["sender_id"], params["channel"])
369
+
370
+ rpc_params = {
371
+ "p_device_id": did,
372
+ "p_name": params["name"],
373
+ }
374
+ if params.get("lat") is not None:
375
+ rpc_params["p_lat"] = params["lat"]
376
+ if params.get("lng") is not None:
377
+ rpc_params["p_lng"] = params["lng"]
378
+ if params.get("starts_at"):
379
+ rpc_params["p_starts_at"] = params["starts_at"]
380
+ if params.get("ends_at"):
381
+ rpc_params["p_ends_at"] = params["ends_at"]
382
+
383
+ resp = sb.rpc("create_event", rpc_params).execute()
384
+ data = resp.data or {}
385
+
386
+ code = data.get("code", "")
387
+ return _ok({
388
+ "created": True,
389
+ "name": params["name"],
390
+ "code": code,
391
+ "url": f"{BASE_URL}/e/{code}",
392
+ "message": f"活动已创建!分享链接给参加的人:{BASE_URL}/e/{code}",
393
+ })
394
+
395
+
396
+ def handle_event_join(params: dict) -> str:
397
+ sb = _sb()
398
+ did = _device_id(params["sender_id"], params["channel"])
399
+
400
+ resp = sb.rpc("join_event", {
401
+ "p_device_id": did,
402
+ "p_code": params["code"],
403
+ }).execute()
404
+ data = resp.data or {}
405
+
406
+ if data.get("joined"):
407
+ return _ok({"joined": True, "name": data.get("name", ""), "message": f"已加入活动 \"{data.get('name', '')}\"!"})
408
+ return _ok({"joined": False, "error": data.get("error", "加入失败")})
409
+
410
+
411
+ def handle_event_scan(params: dict) -> str:
412
+ sb = _sb()
413
+ did = _device_id(params["sender_id"], params["channel"])
414
+
415
+ resp = sb.rpc("event_participants_list", {
416
+ "p_code": params["code"],
417
+ }).execute()
418
+ results = resp.data or []
419
+
420
+ others = [p for p in results if p.get("device_id") != did]
421
+
422
+ if not others:
423
+ return _ok({"count": 0, "profiles": [], "message": "活动里还没有其他人。"})
424
+
425
+ global _last_ref_map
426
+ _last_ref_map = {}
427
+ profiles = []
428
+ for i, p in enumerate(others):
429
+ ref = str(i + 1)
430
+ _last_ref_map[ref] = p.get("device_id")
431
+ profiles.append({
432
+ "ref": ref,
433
+ "emoji": p.get("emoji") or "\ud83d\udc64",
434
+ "name": p.get("display_name") or "匿名",
435
+ "line1": p.get("line1"),
436
+ "line2": p.get("line2"),
437
+ "line3": p.get("line3"),
438
+ "source": "event",
439
+ })
440
+
441
+ return _ok({
442
+ "count": len(profiles),
443
+ "profiles": profiles,
444
+ "instruction": "这些是活动参加者。根据你对用户的了解,推荐值得认识的人。使用 ref 编号引用。",
445
+ })
446
+
447
+
280
448
  def handle_bind(params: dict) -> str:
281
449
  sb = _sb()
282
450
  did = _device_id(params["sender_id"], params["channel"])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
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
@@ -122,6 +122,20 @@ Get today's global recommendation — the person most similar to you worldwide.
122
122
  - If all users have been recommended, returns a message saying "wait for new people"
123
123
  - Use this in the daily cron job, or when user asks "find someone interesting globally"
124
124
 
125
+ ### `antenna_pass`
126
+ Pass/skip a person. They won't be recommended again.
127
+ - `sender_id`, `channel`: from context
128
+ - `ref`: ref number from scan/discover results (e.g. '1')
129
+ - `target_device_id`: device ID (use ref instead when possible)
130
+ - Use when the user says "skip", "pass", "not interested", etc.
131
+
132
+ ### `antenna_checkin`
133
+ Check in at a location — update your position so others can find you when they scan.
134
+ - `lat`, `lng`: coordinates (required)
135
+ - `sender_id`, `channel`: from context
136
+ - `place_name`: optional name of the place
137
+ - Use when the user says "I'm at XX" or wants to be discoverable without scanning others
138
+
125
139
  ## Data Transparency — what Antenna sends
126
140
 
127
141
  Antenna only communicates with Supabase (bcudjloikmpcqwcptuyd.supabase.co) via HTTPS.