antenna-fyi 1.2.28 → 1.2.29
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 +94 -27
- package/lib/core.js +32 -57
- 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} --
|
|
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,27 +506,56 @@ 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
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
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 framework for push notifications
|
|
538
|
+
let pushMethod = f.push || null;
|
|
539
|
+
if (!pushMethod) {
|
|
540
|
+
pushMethod = "terminal"; // default: just print
|
|
509
541
|
try {
|
|
510
|
-
execSync("which
|
|
542
|
+
execSync("which openclaw", { stdio: "pipe" });
|
|
543
|
+
// Verify gateway is running
|
|
544
|
+
try {
|
|
545
|
+
execSync("openclaw gateway health", { stdio: "pipe", timeout: 5000 });
|
|
546
|
+
pushMethod = "openclaw";
|
|
547
|
+
} catch { /* gateway not running */ }
|
|
548
|
+
} catch { /* openclaw not installed */ }
|
|
549
|
+
|
|
550
|
+
if (pushMethod === "terminal") {
|
|
511
551
|
try {
|
|
512
|
-
execSync("hermes
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
552
|
+
execSync("which hermes", { stdio: "pipe" });
|
|
553
|
+
try {
|
|
554
|
+
execSync("hermes gateway status", { stdio: "pipe", timeout: 5000 });
|
|
555
|
+
pushMethod = "hermes";
|
|
556
|
+
} catch { /* hermes gateway not running */ }
|
|
557
|
+
} catch { /* hermes not installed */ }
|
|
558
|
+
}
|
|
516
559
|
}
|
|
517
560
|
|
|
518
561
|
// Force stdout blocking mode for non-TTY environments (Hermes exec)
|
|
@@ -580,19 +623,21 @@ export async function handleWatch(f) {
|
|
|
580
623
|
if (initial.mutual_matches?.length) {
|
|
581
624
|
_log(`🎉 You have ${initial.mutual_matches.length} mutual match(es)!`);
|
|
582
625
|
for (const m of initial.mutual_matches) {
|
|
583
|
-
const key = `mutual:${m.
|
|
626
|
+
const key = `mutual:${m._device_id}`;
|
|
584
627
|
notified.add(key);
|
|
585
628
|
_log(` ${m.emoji || "👤"} ${m.name}${m.their_contact ? " — contact: " + m.their_contact : ""}`);
|
|
586
629
|
}
|
|
630
|
+
saveNotified(notified);
|
|
587
631
|
_log();
|
|
588
632
|
}
|
|
589
633
|
if (initial.incoming_accepts?.length) {
|
|
590
634
|
_log(`📩 ${initial.incoming_accepts.length} person(s) want to meet you!`);
|
|
591
635
|
for (const m of initial.incoming_accepts) {
|
|
592
|
-
const key = `incoming:${m.
|
|
636
|
+
const key = `incoming:${m._device_id}`;
|
|
593
637
|
notified.add(key);
|
|
594
638
|
_log(` ${m.emoji || "👤"} ${m.name} — ${m.line1 || ""}`);
|
|
595
639
|
}
|
|
640
|
+
saveNotified(notified);
|
|
596
641
|
_log();
|
|
597
642
|
}
|
|
598
643
|
|
|
@@ -618,26 +663,30 @@ export async function handleWatch(f) {
|
|
|
618
663
|
|
|
619
664
|
// Check if mutual
|
|
620
665
|
const matches = await checkMatches({ device_id: id });
|
|
621
|
-
const isMutual = matches.mutual_matches?.some(m => m.
|
|
666
|
+
const isMutual = matches.mutual_matches?.some(m => m._device_id === row.device_id_a);
|
|
622
667
|
|
|
623
668
|
if (isMutual) {
|
|
624
669
|
const mutualKey = `mutual:${row.device_id_a}`;
|
|
625
670
|
notified.add(mutualKey);
|
|
671
|
+
saveNotified(notified);
|
|
626
672
|
const contact = row.contact_info_a;
|
|
627
673
|
pushNotify(`🎉 MUTUAL MATCH! ${emoji} ${name} also accepted you!${contact ? " Contact: " + contact : ""}`);
|
|
628
674
|
} else {
|
|
629
|
-
|
|
675
|
+
notified.add(key);
|
|
676
|
+
saveNotified(notified);
|
|
677
|
+
pushNotify(`📩 ${emoji} ${name} wants to meet you! Use 'antenna matches --id ${id}' to respond.`);
|
|
630
678
|
}
|
|
631
679
|
}
|
|
632
680
|
|
|
633
681
|
// I accepted someone and they also accepted me
|
|
634
682
|
if (row.device_id_a === id) {
|
|
635
683
|
const matches = await checkMatches({ device_id: id });
|
|
636
|
-
const mutual = matches.mutual_matches?.find(m => m.
|
|
684
|
+
const mutual = matches.mutual_matches?.find(m => m._device_id === row.device_id_b);
|
|
637
685
|
if (mutual) {
|
|
638
686
|
const mutualKey = `mutual:${row.device_id_b}`;
|
|
639
687
|
if (!notified.has(mutualKey)) {
|
|
640
688
|
notified.add(mutualKey);
|
|
689
|
+
saveNotified(notified);
|
|
641
690
|
pushNotify(`🎉 MUTUAL MATCH! ${mutual.emoji || "👤"} ${mutual.name}!${mutual.their_contact ? " Contact: " + mutual.their_contact : ""}`);
|
|
642
691
|
}
|
|
643
692
|
}
|
|
@@ -649,12 +698,28 @@ export async function handleWatch(f) {
|
|
|
649
698
|
)
|
|
650
699
|
.subscribe((status) => {
|
|
651
700
|
if (status === "SUBSCRIBED") {
|
|
701
|
+
retryCount = 0;
|
|
652
702
|
_log("✅ Connected — listening for matches in real-time.");
|
|
653
703
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
|
|
654
|
-
|
|
704
|
+
retryCount++;
|
|
705
|
+
if (retryCount < MAX_FAST_RETRIES) {
|
|
706
|
+
const delay = Math.min(1000 * Math.pow(2, retryCount), 60000);
|
|
707
|
+
_log(`⚠️ Connection issue (${status}), retry #${retryCount} in ${Math.round(delay/1000)}s...`);
|
|
708
|
+
setTimeout(() => { try { channel.subscribe(); } catch {} }, delay);
|
|
709
|
+
} else if (retryCount === MAX_FAST_RETRIES) {
|
|
710
|
+
_log(`⚠️ Connection unstable after ${MAX_FAST_RETRIES} retries. Switching to ${COOLDOWN_MS/1000}s cooldown. Polling fallback still active.`);
|
|
711
|
+
setTimeout(() => { try { channel.subscribe(); } catch {} }, COOLDOWN_MS);
|
|
712
|
+
} else {
|
|
713
|
+
setTimeout(() => { try { channel.subscribe(); } catch {} }, COOLDOWN_MS);
|
|
714
|
+
}
|
|
655
715
|
}
|
|
656
716
|
});
|
|
657
717
|
|
|
718
|
+
// Reconnection state for matches channel
|
|
719
|
+
let retryCount = 0;
|
|
720
|
+
const MAX_FAST_RETRIES = 20;
|
|
721
|
+
const COOLDOWN_MS = 5 * 60 * 1000; // 5 min
|
|
722
|
+
|
|
658
723
|
// Subscribe to event_participants changes (approval notifications)
|
|
659
724
|
sb
|
|
660
725
|
.channel("antenna-cli-watch-events")
|
|
@@ -704,16 +769,18 @@ export async function handleWatch(f) {
|
|
|
704
769
|
try {
|
|
705
770
|
const result = await checkMatches({ device_id: id });
|
|
706
771
|
for (const m of (result.mutual_matches || [])) {
|
|
707
|
-
const key = `mutual:${m.
|
|
772
|
+
const key = `mutual:${m._device_id}`;
|
|
708
773
|
if (!notified.has(key)) {
|
|
709
774
|
notified.add(key);
|
|
775
|
+
saveNotified(notified);
|
|
710
776
|
pushNotify(`🎉 MUTUAL MATCH! ${m.emoji || "👤"} ${m.name}!${m.their_contact ? " Contact: " + m.their_contact : ""}`);
|
|
711
777
|
}
|
|
712
778
|
}
|
|
713
779
|
for (const m of (result.incoming_accepts || [])) {
|
|
714
|
-
const key = `incoming:${m.
|
|
780
|
+
const key = `incoming:${m._device_id}`;
|
|
715
781
|
if (!notified.has(key)) {
|
|
716
782
|
notified.add(key);
|
|
783
|
+
saveNotified(notified);
|
|
717
784
|
pushNotify(`📩 ${m.emoji || "👤"} ${m.name} wants to meet you!`);
|
|
718
785
|
}
|
|
719
786
|
}
|
|
@@ -741,7 +808,7 @@ Usage:
|
|
|
741
808
|
antenna matches --id telegram:123
|
|
742
809
|
antenna discover --id telegram:123
|
|
743
810
|
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)
|
|
811
|
+
antenna watch --id telegram:123 [--push hermes|openclaw|terminal] Watch for new matches in real-time (Ctrl+C to stop)
|
|
745
812
|
antenna bind --id telegram:123
|
|
746
813
|
antenna serve Start MCP server (stdio transport)
|
|
747
814
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
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,
|