antenna-fyi 1.2.28 → 1.2.30

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.
Files changed (3) hide show
  1. package/lib/cli.js +94 -30
  2. package/lib/core.js +32 -57
  3. package/package.json +1 -1
package/lib/cli.js CHANGED
@@ -2,7 +2,7 @@
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, writeFileSync, unlinkSync } from "fs";
5
+ import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "fs";
6
6
  import path from "path";
7
7
  import os from "os";
8
8
  import { join, dirname, extname } from "path";
@@ -125,7 +125,7 @@ export async function handleMatches(f) {
125
125
  for (const m of result.incoming_accepts) {
126
126
  console.log(`📩 WANTS TO MEET YOU: ${m.emoji} ${m.name}`);
127
127
  if (m.line1) console.log(` ${m.line1}`);
128
- console.log(` Accept: antenna accept --id ${f.id} --target ${m.device_id}`);
128
+ console.log(` Accept: antenna accept --id ${f.id} --ref ${m.ref}`);
129
129
  console.log();
130
130
  }
131
131
  }
@@ -483,6 +483,20 @@ export async function handleWatch(f) {
483
483
  // Write PID file for health check
484
484
  const pidDir = path.join(os.homedir(), '.antenna');
485
485
  const pidFile = path.join(pidDir, 'watch.pid');
486
+
487
+ // Check for existing watch process
488
+ if (existsSync(pidFile)) {
489
+ try {
490
+ const existingPid = parseInt(readFileSync(pidFile, 'utf8').trim());
491
+ process.kill(existingPid, 0); // throws if not running
492
+ console.error(`❌ Watch already running (PID ${existingPid}). Kill it first or remove ${pidFile}`);
493
+ process.exit(1);
494
+ } catch (e) {
495
+ if (e.code !== 'ESRCH') { /* process exists */ throw e; }
496
+ // Process not running, stale PID file — continue
497
+ }
498
+ }
499
+
486
500
  try {
487
501
  if (!existsSync(pidDir)) mkdirSync(pidDir, { recursive: true });
488
502
  writeFileSync(pidFile, String(process.pid));
@@ -492,25 +506,52 @@ export async function handleWatch(f) {
492
506
  process.on('SIGTERM', () => { try { unlinkSync(pidFile); } catch {} process.exit(0); });
493
507
 
494
508
  const sb = getClient();
495
- const notified = new Set();
496
509
 
497
- // Detect local agent framework for push notifications
498
- let pushMethod = "terminal"; // default: just print
499
- try {
500
- execSync("which openclaw", { stdio: "pipe" });
501
- // Verify gateway is running
510
+ // Persist notified set to disk
511
+ const notifiedFile = path.join(pidDir, 'notified.json');
512
+
513
+ function loadNotified() {
502
514
  try {
503
- execSync("openclaw gateway health", { stdio: "pipe", timeout: 5000 });
504
- pushMethod = "openclaw";
505
- } catch { /* gateway not running */ }
506
- } catch { /* openclaw not installed */ }
515
+ const data = JSON.parse(readFileSync(notifiedFile, 'utf8'));
516
+ const now = Date.now();
517
+ const TTL = 48 * 60 * 60 * 1000;
518
+ const set = new Set();
519
+ for (const [key, ts] of Object.entries(data)) {
520
+ if (now - ts < TTL) set.add(key);
521
+ }
522
+ return set;
523
+ } catch { return new Set(); }
524
+ }
507
525
 
508
- if (pushMethod === "terminal") {
526
+ function saveNotified(set) {
527
+ const now = Date.now();
528
+ const obj = {};
529
+ for (const key of set) obj[key] = now;
530
+ const tmp = notifiedFile + '.tmp';
531
+ writeFileSync(tmp, JSON.stringify(obj));
532
+ renameSync(tmp, notifiedFile);
533
+ }
534
+
535
+ const notified = loadNotified();
536
+
537
+ // Detect local agent frameworks for push notifications
538
+ // Push to ALL available frameworks, not just one
539
+ const pushMethods = new Set();
540
+ if (f.push) {
541
+ f.push.split(",").forEach(m => pushMethods.add(m.trim()));
542
+ } else {
543
+ try {
544
+ execSync("which openclaw", { stdio: "pipe" });
545
+ try {
546
+ execSync("openclaw gateway health", { stdio: "pipe", timeout: 5000 });
547
+ pushMethods.add("openclaw");
548
+ } catch { /* gateway not running */ }
549
+ } catch { /* openclaw not installed */ }
509
550
  try {
510
551
  execSync("which hermes", { stdio: "pipe" });
511
552
  try {
512
553
  execSync("hermes gateway status", { stdio: "pipe", timeout: 5000 });
513
- pushMethod = "hermes";
554
+ pushMethods.add("hermes");
514
555
  } catch { /* hermes gateway not running */ }
515
556
  } catch { /* hermes not installed */ }
516
557
  }
@@ -530,10 +571,8 @@ export async function handleWatch(f) {
530
571
  };
531
572
 
532
573
  _log(`📡 Watching for new matches for ${id}...`);
533
- if (pushMethod === "openclaw") {
534
- _log(` 🔗 Detected OpenClaw will push notifications to your channel.`);
535
- } else if (pushMethod === "hermes") {
536
- _log(` 🔗 Detected Hermes — will push notifications to your channel.`);
574
+ if (pushMethods.size > 0) {
575
+ _log(` 🔗 Push targets: ${[...pushMethods].join(", ")}`);
537
576
  } else {
538
577
  _log(` ℹ️ No agent framework detected — notifications will print here.`);
539
578
  }
@@ -546,9 +585,9 @@ export async function handleWatch(f) {
546
585
  async function pushNotify(message) {
547
586
  _log(message); // always print to terminal
548
587
 
549
- if (pushMethod === "openclaw") {
588
+ // Push to ALL available frameworks
589
+ if (pushMethods.has("openclaw")) {
550
590
  try {
551
- // Try to get chat_id from DB for direct message send
552
591
  const profile = await getProfile({ device_id: id });
553
592
  const chatId = profile?.last_chat_id;
554
593
  const parts = id.split(":");
@@ -565,7 +604,8 @@ export async function handleWatch(f) {
565
604
  );
566
605
  }
567
606
  } catch (err) { /* terminal output is the fallback */ }
568
- } else if (pushMethod === "hermes") {
607
+ }
608
+ if (pushMethods.has("hermes")) {
569
609
  try {
570
610
  execSync(
571
611
  `hermes cron create --name "Antenna notification" --run-now --once --message ${JSON.stringify(message)}`,
@@ -580,19 +620,21 @@ export async function handleWatch(f) {
580
620
  if (initial.mutual_matches?.length) {
581
621
  _log(`🎉 You have ${initial.mutual_matches.length} mutual match(es)!`);
582
622
  for (const m of initial.mutual_matches) {
583
- const key = `mutual:${m.device_id}`;
623
+ const key = `mutual:${m._device_id}`;
584
624
  notified.add(key);
585
625
  _log(` ${m.emoji || "👤"} ${m.name}${m.their_contact ? " — contact: " + m.their_contact : ""}`);
586
626
  }
627
+ saveNotified(notified);
587
628
  _log();
588
629
  }
589
630
  if (initial.incoming_accepts?.length) {
590
631
  _log(`📩 ${initial.incoming_accepts.length} person(s) want to meet you!`);
591
632
  for (const m of initial.incoming_accepts) {
592
- const key = `incoming:${m.device_id}`;
633
+ const key = `incoming:${m._device_id}`;
593
634
  notified.add(key);
594
635
  _log(` ${m.emoji || "👤"} ${m.name} — ${m.line1 || ""}`);
595
636
  }
637
+ saveNotified(notified);
596
638
  _log();
597
639
  }
598
640
 
@@ -618,26 +660,30 @@ export async function handleWatch(f) {
618
660
 
619
661
  // Check if mutual
620
662
  const matches = await checkMatches({ device_id: id });
621
- const isMutual = matches.mutual_matches?.some(m => m.device_id === row.device_id_a);
663
+ const isMutual = matches.mutual_matches?.some(m => m._device_id === row.device_id_a);
622
664
 
623
665
  if (isMutual) {
624
666
  const mutualKey = `mutual:${row.device_id_a}`;
625
667
  notified.add(mutualKey);
668
+ saveNotified(notified);
626
669
  const contact = row.contact_info_a;
627
670
  pushNotify(`🎉 MUTUAL MATCH! ${emoji} ${name} also accepted you!${contact ? " Contact: " + contact : ""}`);
628
671
  } else {
629
- pushNotify(`📩 ${emoji} ${name} wants to meet you! Run: antenna accept --id ${id} --target ${row.device_id_a}`);
672
+ notified.add(key);
673
+ saveNotified(notified);
674
+ pushNotify(`📩 ${emoji} ${name} wants to meet you! Use 'antenna matches --id ${id}' to respond.`);
630
675
  }
631
676
  }
632
677
 
633
678
  // I accepted someone and they also accepted me
634
679
  if (row.device_id_a === id) {
635
680
  const matches = await checkMatches({ device_id: id });
636
- const mutual = matches.mutual_matches?.find(m => m.device_id === row.device_id_b);
681
+ const mutual = matches.mutual_matches?.find(m => m._device_id === row.device_id_b);
637
682
  if (mutual) {
638
683
  const mutualKey = `mutual:${row.device_id_b}`;
639
684
  if (!notified.has(mutualKey)) {
640
685
  notified.add(mutualKey);
686
+ saveNotified(notified);
641
687
  pushNotify(`🎉 MUTUAL MATCH! ${mutual.emoji || "👤"} ${mutual.name}!${mutual.their_contact ? " Contact: " + mutual.their_contact : ""}`);
642
688
  }
643
689
  }
@@ -649,12 +695,28 @@ export async function handleWatch(f) {
649
695
  )
650
696
  .subscribe((status) => {
651
697
  if (status === "SUBSCRIBED") {
698
+ retryCount = 0;
652
699
  _log("✅ Connected — listening for matches in real-time.");
653
700
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
654
- _log(`⚠️ Connection issue (${status}), retrying...`);
701
+ retryCount++;
702
+ if (retryCount < MAX_FAST_RETRIES) {
703
+ const delay = Math.min(1000 * Math.pow(2, retryCount), 60000);
704
+ _log(`⚠️ Connection issue (${status}), retry #${retryCount} in ${Math.round(delay/1000)}s...`);
705
+ setTimeout(() => { try { channel.subscribe(); } catch {} }, delay);
706
+ } else if (retryCount === MAX_FAST_RETRIES) {
707
+ _log(`⚠️ Connection unstable after ${MAX_FAST_RETRIES} retries. Switching to ${COOLDOWN_MS/1000}s cooldown. Polling fallback still active.`);
708
+ setTimeout(() => { try { channel.subscribe(); } catch {} }, COOLDOWN_MS);
709
+ } else {
710
+ setTimeout(() => { try { channel.subscribe(); } catch {} }, COOLDOWN_MS);
711
+ }
655
712
  }
656
713
  });
657
714
 
715
+ // Reconnection state for matches channel
716
+ let retryCount = 0;
717
+ const MAX_FAST_RETRIES = 20;
718
+ const COOLDOWN_MS = 5 * 60 * 1000; // 5 min
719
+
658
720
  // Subscribe to event_participants changes (approval notifications)
659
721
  sb
660
722
  .channel("antenna-cli-watch-events")
@@ -704,16 +766,18 @@ export async function handleWatch(f) {
704
766
  try {
705
767
  const result = await checkMatches({ device_id: id });
706
768
  for (const m of (result.mutual_matches || [])) {
707
- const key = `mutual:${m.device_id}`;
769
+ const key = `mutual:${m._device_id}`;
708
770
  if (!notified.has(key)) {
709
771
  notified.add(key);
772
+ saveNotified(notified);
710
773
  pushNotify(`🎉 MUTUAL MATCH! ${m.emoji || "👤"} ${m.name}!${m.their_contact ? " Contact: " + m.their_contact : ""}`);
711
774
  }
712
775
  }
713
776
  for (const m of (result.incoming_accepts || [])) {
714
- const key = `incoming:${m.device_id}`;
777
+ const key = `incoming:${m._device_id}`;
715
778
  if (!notified.has(key)) {
716
779
  notified.add(key);
780
+ saveNotified(notified);
717
781
  pushNotify(`📩 ${m.emoji || "👤"} ${m.name} wants to meet you!`);
718
782
  }
719
783
  }
@@ -741,7 +805,7 @@ Usage:
741
805
  antenna matches --id telegram:123
742
806
  antenna discover --id telegram:123
743
807
  antenna event --create --name 'AI Meetup' [--desc '...'] [--og-image 'url'] [--requires-approval] [--screening-questions 'Q1|Q2'] | --join --code abc123 | --scan --code abc123 | --end --code abc123 --id telegram:123 | --upload-image --code abc123 --file /path/to/image.png | --update --code abc123 --name 'New Name' | --approve --code abc123 --ref 1 | --reject --code abc123 --ref 1 | --add-host --code abc123 --ref 1
744
- antenna watch --id telegram:123 Watch for new matches in real-time (Ctrl+C to stop)
808
+ antenna watch --id telegram:123 [--push hermes|openclaw|terminal] Watch for new matches in real-time (Ctrl+C to stop)
745
809
  antenna bind --id telegram:123
746
810
  antenna serve Start MCP server (stdio transport)
747
811
  antenna setup Interactive profile setup [--id telegram:123]
package/lib/core.js CHANGED
@@ -79,20 +79,12 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
79
79
  lat = loc.lat;
80
80
  lng = loc.lng;
81
81
  } else {
82
- return { count: 0, radius_m, profiles: [], message: "还没有位置信息。请先通过链接分享位置,或者发送位置消息。" };
82
+ return { count: 0, radius_m, profiles: [], message: "还没有位置信息。请先用 'antenna checkin' 分享位置,或通过链接分享位置。" };
83
83
  }
84
84
  }
85
85
 
86
86
  const fuzzy = fuzzyCoord(lat, lng);
87
87
 
88
- if (device_id) {
89
- await sb.rpc("upsert_profile_location", {
90
- p_device_id: device_id,
91
- p_lng: fuzzy.lng,
92
- p_lat: fuzzy.lat,
93
- });
94
- }
95
-
96
88
  const { data, error } = await sb.rpc("nearby_profiles", {
97
89
  p_lat: fuzzy.lat,
98
90
  p_lng: fuzzy.lng,
@@ -323,60 +315,43 @@ export async function checkin({ lat, lng, device_id, supabaseUrl, supabaseKey })
323
315
  export async function checkMatches({ device_id, supabaseUrl, supabaseKey }) {
324
316
  const sb = getClient(supabaseUrl, supabaseKey);
325
317
 
326
- const { data: allMatches, error } = await sb.rpc("get_my_matches", { p_device_id: device_id });
318
+ // Single RPC with JOINed profiles no N+1
319
+ const { data, error } = await sb.rpc("get_my_matches_with_profiles", { p_device_id: device_id });
327
320
  if (error) throw new Error(error.message);
328
321
 
329
- if (!allMatches?.length) {
330
- return {
331
- mutual_matches: [],
332
- incoming_accepts: [],
333
- message: "目前没有进行中的匹配。",
334
- };
335
- }
336
-
337
- const myMatches = allMatches.filter((m) => m.device_id_a === device_id);
338
- const incomingMatches = allMatches.filter((m) => m.device_id_b === device_id);
339
-
340
- // Mutual
341
- const mutualMatches = [];
342
- for (const match of myMatches) {
343
- const reverse = incomingMatches.find((m) => m.device_id_a === match.device_id_b);
344
- if (reverse) {
345
- const profile = await getProfile({ device_id: match.device_id_b, supabaseUrl, supabaseKey });
346
- mutualMatches.push({
347
- device_id: match.device_id_b,
348
- name: profile?.display_name || "匿名",
349
- emoji: profile?.emoji || "👤",
350
- line1: profile?.line1,
351
- line2: profile?.line2,
352
- line3: profile?.line3,
353
- their_contact: reverse.contact_info_a || null,
354
- you_shared: match.contact_info_a || null,
355
- });
356
- }
357
- }
358
-
359
- // Incoming only
360
- const incomingAccepts = [];
361
- for (const match of incomingMatches) {
362
- const iAccepted = myMatches.find((m) => m.device_id_b === match.device_id_a);
363
- if (!iAccepted) {
364
- const profile = await getProfile({ device_id: match.device_id_a, supabaseUrl, supabaseKey });
365
- incomingAccepts.push({
366
- device_id: match.device_id_a,
367
- name: profile?.display_name || "匿名",
368
- emoji: profile?.emoji || "👤",
369
- line1: profile?.line1,
370
- line2: profile?.line2,
371
- line3: profile?.line3,
372
- });
373
- }
374
- }
322
+ const raw = data || { mutual_matches: [], incoming_accepts: [] };
323
+
324
+ // Add refs and rename target_id to _device_id (internal only)
325
+ const mutualMatches = (raw.mutual_matches || []).map((m, i) => ({
326
+ ref: String(i + 1),
327
+ _device_id: m.target_id,
328
+ name: m.name || "匿名",
329
+ emoji: m.emoji || "👤",
330
+ line1: m.line1,
331
+ line2: m.line2,
332
+ line3: m.line3,
333
+ their_contact: m.their_contact || null,
334
+ you_shared: m.you_shared || null,
335
+ }));
336
+
337
+ const incomingAccepts = (raw.incoming_accepts || []).map((m, i) => ({
338
+ ref: String(i + 1),
339
+ _device_id: m.target_id,
340
+ name: m.name || "匿名",
341
+ emoji: m.emoji || "👤",
342
+ line1: m.line1,
343
+ line2: m.line2,
344
+ line3: m.line3,
345
+ }));
375
346
 
376
347
  const messages = [];
377
348
  if (mutualMatches.length > 0) messages.push(`${mutualMatches.length} 个双向匹配!可以交换联系方式了`);
378
349
  if (incomingAccepts.length > 0) messages.push(`${incomingAccepts.length} 个人想认识你,等你回应`);
379
- if (messages.length === 0) messages.push("你接受了一些匹配,但对方还没有回应。耐心等等 ⏳");
350
+ if (messages.length === 0 && mutualMatches.length === 0 && incomingAccepts.length === 0) {
351
+ messages.push("目前没有进行中的匹配。");
352
+ } else if (messages.length === 0) {
353
+ messages.push("你接受了一些匹配,但对方还没有回应。耐心等等 ⏳");
354
+ }
380
355
 
381
356
  return {
382
357
  mutual_matches: mutualMatches,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.2.28",
3
+ "version": "1.2.30",
4
4
  "description": "Antenna — nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {