antenna-fyi 1.2.24 → 1.2.26

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
@@ -2,7 +2,9 @@
2
2
 
3
3
  import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover, createEvent, endEvent, eventCheckin, joinEvent, eventScan, pass as passUser, uploadEventImage, updateEvent, approveParticipant, rejectParticipant, addCohost, getClient } from "./core.js";
4
4
  import { createInterface } from "readline";
5
- import { existsSync, mkdirSync, copyFileSync, readFileSync } from "fs";
5
+ import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, unlinkSync } from "fs";
6
+ import path from "path";
7
+ import os from "os";
6
8
  import { join, dirname, extname } from "path";
7
9
  import { fileURLToPath } from "url";
8
10
  import { homedir } from "os";
@@ -334,9 +336,21 @@ export async function handleSetup(f) {
334
336
 
335
337
  export async function handleStatus(f) {
336
338
  const supabaseUrl = process.env.ANTENNA_SUPABASE_URL || process.env.ANTENNA_URL || "https://bcudjloikmpcqwcptuyd.supabase.co";
337
- console.log("📡 Antenna Status\n");
339
+ console.log("\n📡 Antenna Status\n");
338
340
  console.log(` Supabase URL: ${supabaseUrl}`);
339
341
 
342
+ // Check watch process
343
+ const pidFile = path.join(os.homedir(), '.antenna', 'watch.pid');
344
+ try {
345
+ if (existsSync(pidFile)) {
346
+ const pid = parseInt(readFileSync(pidFile, 'utf8').trim());
347
+ try { process.kill(pid, 0); console.log(` Watch: ✅ running (PID ${pid})`); }
348
+ catch { console.log(' Watch: ❌ stale PID file (process dead)'); }
349
+ } else {
350
+ console.log(' Watch: ❌ not running');
351
+ }
352
+ } catch { console.log(' Watch: ❌ not running'); }
353
+
340
354
  if (f.id) {
341
355
  const profile = await getProfile({ device_id: f.id });
342
356
  if (profile) {
@@ -466,6 +480,17 @@ export async function handleWatch(f) {
466
480
  process.exit(1);
467
481
  }
468
482
 
483
+ // Write PID file for health check
484
+ const pidDir = path.join(os.homedir(), '.antenna');
485
+ const pidFile = path.join(pidDir, 'watch.pid');
486
+ try {
487
+ if (!existsSync(pidDir)) mkdirSync(pidDir, { recursive: true });
488
+ writeFileSync(pidFile, String(process.pid));
489
+ } catch {}
490
+ process.on('exit', () => { try { unlinkSync(pidFile); } catch {} });
491
+ process.on('SIGINT', () => { try { unlinkSync(pidFile); } catch {} process.exit(0); });
492
+ process.on('SIGTERM', () => { try { unlinkSync(pidFile); } catch {} process.exit(0); });
493
+
469
494
  const sb = getClient();
470
495
  const notified = new Set();
471
496
 
@@ -501,42 +526,35 @@ export async function handleWatch(f) {
501
526
  console.log(` Press Ctrl+C to stop.\n`);
502
527
 
503
528
  // Push notification helper
504
- function pushNotify(message) {
529
+ async function pushNotify(message) {
505
530
  console.log(message); // always print to terminal
506
531
 
507
532
  if (pushMethod === "openclaw") {
508
533
  try {
534
+ // Try to get chat_id from DB for direct message send
535
+ const profile = await getProfile({ device_id: id });
536
+ const chatId = profile?.last_chat_id;
509
537
  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
- }
538
+ const chan = parts[0];
539
+ if (chatId) {
540
+ execSync(
541
+ `openclaw message send --channel ${chan} --target ${chatId} -m ${JSON.stringify(message)}`,
542
+ { timeout: 30_000, stdio: "pipe" }
543
+ );
544
+ } else {
545
+ execSync(
546
+ `openclaw agent --message ${JSON.stringify(message)} --deliver --agent main --to ${id}`,
547
+ { timeout: 30_000, stdio: "pipe" }
548
+ );
549
+ }
550
+ } catch (err) { /* terminal output is the fallback */ }
523
551
  } else if (pushMethod === "hermes") {
524
552
  try {
525
- // Use hermes cron to create a one-shot notification
526
- const parts = id.split(":");
527
- const channel = parts[0];
528
553
  execSync(
529
- `hermes cron create` +
530
- ` --name "Antenna notification"` +
531
- ` --run-now` +
532
- ` --once` +
533
- ` --message ${JSON.stringify(message)}` +
534
- ` --deliver ${channel}`,
554
+ `hermes cron create --name "Antenna notification" --run-now --once --message ${JSON.stringify(message)}`,
535
555
  { timeout: 30_000, stdio: "pipe" }
536
556
  );
537
- } catch (err) {
538
- // silent — terminal output is the fallback
539
- }
557
+ } catch (err) { /* terminal output is the fallback */ }
540
558
  }
541
559
  }
542
560
 
@@ -620,6 +638,50 @@ export async function handleWatch(f) {
620
638
  }
621
639
  });
622
640
 
641
+ // Subscribe to event_participants changes (approval notifications)
642
+ sb
643
+ .channel("antenna-cli-watch-events")
644
+ .on("postgres_changes",
645
+ { event: "INSERT", schema: "public", table: "event_participants" },
646
+ async (payload) => {
647
+ try {
648
+ const row = payload.new;
649
+ if (!row || row.status !== "pending") return;
650
+ // Someone applied to my event — check if I'm the creator
651
+ const event = await sb.rpc("get_event_by_id", { p_event_id: row.event_id }).then(r => r.data);
652
+ if (!event?.found || event.created_by !== id) return;
653
+ const applicant = await getProfile({ device_id: row.device_id });
654
+ const name = applicant?.display_name || "Someone";
655
+ const emoji = applicant?.emoji || "👤";
656
+ pushNotify(`📩 ${emoji} ${name} applied to join \"${event.name}\"! Run: antenna event --scan --code ${event.code} --id ${id}`);
657
+ } catch {}
658
+ }
659
+ )
660
+ .on("postgres_changes",
661
+ { event: "UPDATE", schema: "public", table: "event_participants" },
662
+ async (payload) => {
663
+ try {
664
+ const row = payload.new;
665
+ const old = payload.old;
666
+ if (!row || !old) return;
667
+ // My application was approved/rejected
668
+ if (row.device_id !== id) return;
669
+ if (old.status === "pending" && row.status === "active") {
670
+ const event = await sb.rpc("get_event_by_id", { p_event_id: row.event_id }).then(r => r.data);
671
+ pushNotify(`✅ Your application to \"${event?.name || 'an event'}\" was approved! You're in.`);
672
+ } else if (old.status === "pending" && row.status === "rejected") {
673
+ const event = await sb.rpc("get_event_by_id", { p_event_id: row.event_id }).then(r => r.data);
674
+ pushNotify(`❌ Your application to \"${event?.name || 'an event'}\" was not approved.`);
675
+ }
676
+ } catch {}
677
+ }
678
+ )
679
+ .subscribe((status) => {
680
+ if (status === "SUBSCRIBED") {
681
+ console.log("✅ Connected — listening for event notifications.");
682
+ }
683
+ });
684
+
623
685
  // Keep alive — also poll every 2 minutes as fallback
624
686
  const pollInterval = setInterval(async () => {
625
687
  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.26",
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,18 @@ 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
+ **Health check:** 每次对话开始时,跑 `antenna status --id <device_id>` 检查 watch 是否在跑。如果显示 "Watch: ❌ not running",重新启动 `antenna watch --id <device_id> &`。
72
+
73
+ 如果 exec 不支持后台进程,cron 是 fallback。
74
+
63
75
  ## When to use
64
76
 
65
77
  - **首次安装后**: 主动开始 onboarding(名片 → 位置)