antenna-fyi 1.2.24 → 1.2.25

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
@@ -501,42 +501,35 @@ export async function handleWatch(f) {
501
501
  console.log(` Press Ctrl+C to stop.\n`);
502
502
 
503
503
  // Push notification helper
504
- function pushNotify(message) {
504
+ async function pushNotify(message) {
505
505
  console.log(message); // always print to terminal
506
506
 
507
507
  if (pushMethod === "openclaw") {
508
508
  try {
509
+ // Try to get chat_id from DB for direct message send
510
+ const profile = await getProfile({ device_id: id });
511
+ const chatId = profile?.last_chat_id;
509
512
  const parts = id.split(":");
510
- const channel = parts[0];
511
- const userId = parts.slice(1).join(":");
512
- execSync(
513
- `openclaw agent` +
514
- ` --message ${JSON.stringify(message)}` +
515
- ` --deliver` +
516
- ` --reply-channel ${channel}` +
517
- ` --reply-to "${userId}"`,
518
- { timeout: 30_000, stdio: "pipe" }
519
- );
520
- } catch (err) {
521
- // silent — terminal output is the fallback
522
- }
513
+ const chan = parts[0];
514
+ if (chatId) {
515
+ execSync(
516
+ `openclaw message send --channel ${chan} --target ${chatId} -m ${JSON.stringify(message)}`,
517
+ { timeout: 30_000, stdio: "pipe" }
518
+ );
519
+ } else {
520
+ execSync(
521
+ `openclaw agent --message ${JSON.stringify(message)} --deliver --agent main --to ${id}`,
522
+ { timeout: 30_000, stdio: "pipe" }
523
+ );
524
+ }
525
+ } catch (err) { /* terminal output is the fallback */ }
523
526
  } else if (pushMethod === "hermes") {
524
527
  try {
525
- // Use hermes cron to create a one-shot notification
526
- const parts = id.split(":");
527
- const channel = parts[0];
528
528
  execSync(
529
- `hermes cron create` +
530
- ` --name "Antenna notification"` +
531
- ` --run-now` +
532
- ` --once` +
533
- ` --message ${JSON.stringify(message)}` +
534
- ` --deliver ${channel}`,
529
+ `hermes cron create --name "Antenna notification" --run-now --once --message ${JSON.stringify(message)}`,
535
530
  { timeout: 30_000, stdio: "pipe" }
536
531
  );
537
- } catch (err) {
538
- // silent — terminal output is the fallback
539
- }
532
+ } catch (err) { /* terminal output is the fallback */ }
540
533
  }
541
534
  }
542
535
 
@@ -620,6 +613,50 @@ export async function handleWatch(f) {
620
613
  }
621
614
  });
622
615
 
616
+ // Subscribe to event_participants changes (approval notifications)
617
+ sb
618
+ .channel("antenna-cli-watch-events")
619
+ .on("postgres_changes",
620
+ { event: "INSERT", schema: "public", table: "event_participants" },
621
+ async (payload) => {
622
+ try {
623
+ const row = payload.new;
624
+ if (!row || row.status !== "pending") return;
625
+ // Someone applied to my event — check if I'm the creator
626
+ const event = await sb.rpc("get_event_by_id", { p_event_id: row.event_id }).then(r => r.data);
627
+ if (!event?.found || event.created_by !== id) return;
628
+ const applicant = await getProfile({ device_id: row.device_id });
629
+ const name = applicant?.display_name || "Someone";
630
+ const emoji = applicant?.emoji || "👤";
631
+ pushNotify(`📩 ${emoji} ${name} applied to join \"${event.name}\"! Run: antenna event --scan --code ${event.code} --id ${id}`);
632
+ } catch {}
633
+ }
634
+ )
635
+ .on("postgres_changes",
636
+ { event: "UPDATE", schema: "public", table: "event_participants" },
637
+ async (payload) => {
638
+ try {
639
+ const row = payload.new;
640
+ const old = payload.old;
641
+ if (!row || !old) return;
642
+ // My application was approved/rejected
643
+ if (row.device_id !== id) return;
644
+ if (old.status === "pending" && row.status === "active") {
645
+ const event = await sb.rpc("get_event_by_id", { p_event_id: row.event_id }).then(r => r.data);
646
+ pushNotify(`✅ Your application to \"${event?.name || 'an event'}\" was approved! You're in.`);
647
+ } else if (old.status === "pending" && row.status === "rejected") {
648
+ const event = await sb.rpc("get_event_by_id", { p_event_id: row.event_id }).then(r => r.data);
649
+ pushNotify(`❌ Your application to \"${event?.name || 'an event'}\" was not approved.`);
650
+ }
651
+ } catch {}
652
+ }
653
+ )
654
+ .subscribe((status) => {
655
+ if (status === "SUBSCRIBED") {
656
+ console.log("✅ Connected — listening for event notifications.");
657
+ }
658
+ });
659
+
623
660
  // Keep alive — also poll every 2 minutes as fallback
624
661
  const pollInterval = setInterval(async () => {
625
662
  try {
package/lib/core.js CHANGED
@@ -502,7 +502,7 @@ export async function createEvent({ name, lat, lng, device_id, starts_at, ends_a
502
502
  p_description: description || null,
503
503
  p_og_image: og_image || null,
504
504
  p_requires_approval: requires_approval || false,
505
- p_screening_questions: screening_questions || null,
505
+ p_screening_questions: screening_questions ? screening_questions.flatMap(q => q.includes('|') || q.includes('|') ? q.split(/[|\uff5c]/).map(s => s.trim()).filter(Boolean) : [q]) : null,
506
506
  });
507
507
  if (error) throw new Error(error.message);
508
508
  return data;
@@ -44,6 +44,7 @@ PROFILE_SCHEMA = {
44
44
  },
45
45
  "sender_id": {"type": "string"},
46
46
  "channel": {"type": "string"},
47
+ "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
47
48
  "display_name": {"type": "string", "description": "Display name"},
48
49
  "emoji": {"type": "string", "description": "Profile emoji"},
49
50
  "line1": {"type": "string", "description": "Who you are / what you do"},
@@ -66,6 +67,7 @@ ACCEPT_SCHEMA = {
66
67
  "properties": {
67
68
  "sender_id": {"type": "string"},
68
69
  "channel": {"type": "string"},
70
+ "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
69
71
  "ref": {
70
72
  "type": "string",
71
73
  "description": "Ref number from scan results (e.g. '1')",
@@ -95,6 +97,7 @@ CHECKIN_SCHEMA = {
95
97
  "lng": {"type": "number", "description": "Longitude"},
96
98
  "sender_id": {"type": "string"},
97
99
  "channel": {"type": "string"},
100
+ "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
98
101
  "place_name": {
99
102
  "type": "string",
100
103
  "description": "Name of the place (optional)",
@@ -114,6 +117,7 @@ CHECK_MATCHES_SCHEMA = {
114
117
  "properties": {
115
118
  "sender_id": {"type": "string"},
116
119
  "channel": {"type": "string"},
120
+ "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
117
121
  },
118
122
  "required": ["sender_id", "channel"],
119
123
  },
@@ -129,6 +133,7 @@ BIND_SCHEMA = {
129
133
  "properties": {
130
134
  "sender_id": {"type": "string"},
131
135
  "channel": {"type": "string"},
136
+ "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
132
137
  "purpose": {"type": "string", "description": "'profile' (default) or 'event'"},
133
138
  "event_code": {"type": "string", "description": "Event code (when purpose=event)"},
134
139
  },
@@ -282,6 +287,7 @@ EVENT_UPDATE_SCHEMA = {
282
287
  "code": {"type": "string"},
283
288
  "sender_id": {"type": "string"},
284
289
  "channel": {"type": "string"},
290
+ "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
285
291
  "name": {"type": "string"},
286
292
  "description": {"type": "string"},
287
293
  "og_image": {"type": "string"},
@@ -303,6 +309,7 @@ EVENT_APPROVE_SCHEMA = {
303
309
  "code": {"type": "string"},
304
310
  "sender_id": {"type": "string"},
305
311
  "channel": {"type": "string"},
312
+ "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
306
313
  "ref": {"type": "string"},
307
314
  },
308
315
  "required": ["code", "sender_id", "channel", "ref"],
@@ -318,6 +325,7 @@ EVENT_REJECT_SCHEMA = {
318
325
  "code": {"type": "string"},
319
326
  "sender_id": {"type": "string"},
320
327
  "channel": {"type": "string"},
328
+ "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
321
329
  "ref": {"type": "string"},
322
330
  },
323
331
  "required": ["code", "sender_id", "channel", "ref"],
@@ -333,6 +341,7 @@ EVENT_ADD_HOST_SCHEMA = {
333
341
  "code": {"type": "string"},
334
342
  "sender_id": {"type": "string"},
335
343
  "channel": {"type": "string"},
344
+ "chat_id": {"type": "string", "description": "Chat/channel ID for notifications"},
336
345
  "ref": {"type": "string"},
337
346
  },
338
347
  "required": ["code", "sender_id", "channel", "ref"],
@@ -54,9 +54,16 @@ def _sb():
54
54
  return _client
55
55
 
56
56
 
57
- def _device_id(sender_id: str, channel: str) -> str:
57
+ def _device_id(sender_id: str, channel: str, chat_id: str = None) -> str:
58
58
  did = f"{channel}:{sender_id}"
59
59
  _my_device_ids.add(did)
60
+ # Persist chat_id for notifications
61
+ if chat_id:
62
+ try:
63
+ sb = _sb()
64
+ sb.rpc("upsert_profile", {"p_device_id": did, "p_last_chat_id": chat_id}).execute()
65
+ except Exception:
66
+ pass
60
67
  return did
61
68
 
62
69
 
@@ -72,7 +79,7 @@ def _ok(data) -> str:
72
79
 
73
80
  def handle_scan(params: dict) -> str:
74
81
  sb = _sb()
75
- did = _device_id(params["sender_id"], params["channel"])
82
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
76
83
  radius = params.get("radius_m", 500)
77
84
 
78
85
  # Rate limit
@@ -137,7 +144,7 @@ def handle_scan(params: dict) -> str:
137
144
 
138
145
  def handle_profile(params: dict) -> str:
139
146
  sb = _sb()
140
- did = _device_id(params["sender_id"], params["channel"])
147
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
141
148
 
142
149
  if params["action"] == "get":
143
150
  resp = sb.rpc("get_profile", {"p_device_id": did}).execute()
@@ -163,7 +170,7 @@ def handle_profile(params: dict) -> str:
163
170
 
164
171
  def handle_accept(params: dict) -> str:
165
172
  sb = _sb()
166
- did = _device_id(params["sender_id"], params["channel"])
173
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
167
174
 
168
175
  # Resolve ref to device_id
169
176
  ref = params.get("ref")
@@ -202,7 +209,7 @@ def handle_accept(params: dict) -> str:
202
209
 
203
210
  def handle_checkin(params: dict) -> str:
204
211
  sb = _sb()
205
- did = _device_id(params["sender_id"], params["channel"])
212
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
206
213
  flat, flng = _fuzzy(params["lat"], params["lng"])
207
214
 
208
215
  # Check profile exists
@@ -220,7 +227,7 @@ def handle_checkin(params: dict) -> str:
220
227
 
221
228
  def handle_check_matches(params: dict) -> str:
222
229
  sb = _sb()
223
- did = _device_id(params["sender_id"], params["channel"])
230
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
224
231
 
225
232
  resp = sb.rpc("get_my_matches", {"p_device_id": did}).execute()
226
233
  all_matches = resp.data or []
@@ -280,7 +287,7 @@ BASE_URL = "https://www.antenna.fyi"
280
287
 
281
288
  def handle_pass(params: dict) -> str:
282
289
  sb = _sb()
283
- did = _device_id(params["sender_id"], params["channel"])
290
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
284
291
 
285
292
  ref = params.get("ref")
286
293
  target = params.get("target_device_id")
@@ -307,7 +314,7 @@ def handle_pass(params: dict) -> str:
307
314
 
308
315
  def handle_discover(params: dict) -> str:
309
316
  sb = _sb()
310
- did = _device_id(params["sender_id"], params["channel"])
317
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
311
318
 
312
319
  resp = sb.rpc("global_discover", {"p_device_id": did}).execute()
313
320
  results = resp.data or []
@@ -365,7 +372,7 @@ def handle_discover(params: dict) -> str:
365
372
 
366
373
  def handle_event_create(params: dict) -> str:
367
374
  sb = _sb()
368
- did = _device_id(params["sender_id"], params["channel"])
375
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
369
376
 
370
377
  rpc_params = {
371
378
  "p_created_by": did,
@@ -403,7 +410,7 @@ def handle_event_create(params: dict) -> str:
403
410
 
404
411
  def handle_event_join(params: dict) -> str:
405
412
  sb = _sb()
406
- did = _device_id(params["sender_id"], params["channel"])
413
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
407
414
 
408
415
  # Profile gate
409
416
  prof = sb.rpc("get_profile", {"p_device_id": did}).execute()
@@ -484,10 +491,10 @@ def handle_event_join(params: dict) -> str:
484
491
 
485
492
  def handle_event_scan(params: dict) -> str:
486
493
  sb = _sb()
487
- did = _device_id(params["sender_id"], params["channel"])
494
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
488
495
 
489
496
  resp = sb.rpc("event_participants_list", {
490
- "p_code": params["code"],
497
+ "p_code": params["code"], "p_device_id": did,
491
498
  }).execute()
492
499
  results = resp.data or []
493
500
 
@@ -496,32 +503,40 @@ def handle_event_scan(params: dict) -> str:
496
503
  if not others:
497
504
  return _ok({"count": 0, "profiles": [], "message": "活动里还没有其他人。"})
498
505
 
506
+ global _last_ref_map
499
507
  global _last_ref_map
500
508
  _last_ref_map = {}
509
+ checked_in_count = 0
501
510
  profiles = []
502
511
  for i, p in enumerate(others):
503
512
  ref = str(i + 1)
504
513
  _last_ref_map[ref] = p.get("device_id")
514
+ if p.get("checked_in"):
515
+ checked_in_count += 1
505
516
  profiles.append({
506
517
  "ref": ref,
507
- "emoji": p.get("emoji") or "\ud83d\udc64",
518
+ "emoji": p.get("emoji") or "👤",
508
519
  "name": p.get("display_name") or "匿名",
509
520
  "line1": p.get("line1"),
510
521
  "line2": p.get("line2"),
511
522
  "line3": p.get("line3"),
523
+ "checked_in": bool(p.get("checked_in")),
524
+ "role": p.get("role") or "participant",
525
+ "status": p.get("status") or "active",
526
+ "application_context": p.get("application_context"),
512
527
  "source": "event",
513
528
  })
514
529
 
515
530
  return _ok({
516
531
  "count": len(profiles),
532
+ "checked_in_count": checked_in_count,
517
533
  "profiles": profiles,
518
534
  "instruction": "这些是活动参加者。根据你对用户的了解,推荐值得认识的人。使用 ref 编号引用。",
519
535
  })
520
536
 
521
-
522
537
  def handle_bind(params: dict) -> str:
523
538
  sb = _sb()
524
- did = _device_id(params["sender_id"], params["channel"])
539
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
525
540
  purpose = params.get("purpose", "profile")
526
541
  event_code = params.get("event_code")
527
542
 
@@ -549,7 +564,7 @@ def handle_bind(params: dict) -> str:
549
564
 
550
565
  def handle_event_end(params: dict) -> str:
551
566
  sb = _sb()
552
- did = _device_id(params["sender_id"], params["channel"])
567
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
553
568
 
554
569
  resp = sb.rpc("end_event", {
555
570
  "p_code": params["code"],
@@ -576,7 +591,7 @@ def handle_event_upload_image(params: dict) -> str:
576
591
 
577
592
  def handle_event_checkin(params: dict) -> str:
578
593
  sb = _sb()
579
- did = _device_id(params["sender_id"], params["channel"])
594
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
580
595
 
581
596
  lat = params.get("lat")
582
597
  lng = params.get("lng")
@@ -596,7 +611,7 @@ def handle_event_checkin(params: dict) -> str:
596
611
 
597
612
  def handle_event_update(params: dict) -> str:
598
613
  sb = _sb()
599
- did = _device_id(params["sender_id"], params["channel"])
614
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
600
615
  resp = sb.rpc("update_event", {
601
616
  "p_code": params["code"], "p_device_id": did,
602
617
  "p_name": params.get("name"), "p_description": params.get("description"),
@@ -609,7 +624,7 @@ def handle_event_update(params: dict) -> str:
609
624
 
610
625
  def handle_event_approve(params: dict) -> str:
611
626
  sb = _sb()
612
- did = _device_id(params["sender_id"], params["channel"])
627
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
613
628
  resp = sb.rpc("approve_participant", {
614
629
  "p_code": params["code"], "p_device_id": did, "p_target_ref": params["ref"],
615
630
  }).execute()
@@ -618,7 +633,7 @@ def handle_event_approve(params: dict) -> str:
618
633
 
619
634
  def handle_event_reject(params: dict) -> str:
620
635
  sb = _sb()
621
- did = _device_id(params["sender_id"], params["channel"])
636
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
622
637
  resp = sb.rpc("reject_participant", {
623
638
  "p_code": params["code"], "p_device_id": did, "p_target_ref": params["ref"],
624
639
  }).execute()
@@ -627,7 +642,7 @@ def handle_event_reject(params: dict) -> str:
627
642
 
628
643
  def handle_event_add_host(params: dict) -> str:
629
644
  sb = _sb()
630
- did = _device_id(params["sender_id"], params["channel"])
645
+ did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
631
646
  resp = sb.rpc("add_cohost", {
632
647
  "p_code": params["code"], "p_device_id": did, "p_target_ref": params["ref"],
633
648
  }).execute()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.2.24",
3
+ "version": "1.2.25",
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
@@ -60,6 +60,16 @@ cron 设完后,问用户一句:
60
60
  用户说要 → cron 每天调 `antenna_discover` 推送。
61
61
  用户说不要 → 只在 scan 没人时 fallback。
62
62
 
63
+ **第六步:启动实时通知**
64
+
65
+ onboarding 完成后,启动 `antenna watch` 后台进程,实时接收匹配通知和活动审批通知:
66
+ ```
67
+ antenna watch --id <device_id> &
68
+ ```
69
+ 这会保持一个 WebSocket 连接,有新匹配或活动审批变更时立即通知。比 cron 更实时。
70
+
71
+ 如果 exec 不支持后台进程,cron 是 fallback。
72
+
63
73
  ## When to use
64
74
 
65
75
  - **首次安装后**: 主动开始 onboarding(名片 → 位置)