antenna-fyi 1.2.9 โ†’ 1.2.11

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
@@ -11,6 +11,7 @@ import {
11
11
  handleEvent,
12
12
  handleBind,
13
13
  handlePass,
14
+ handleWatch,
14
15
  handleSetup,
15
16
  handleStatus,
16
17
  handleInstallSkill,
@@ -42,6 +43,8 @@ async function main() {
42
43
  return handleBind(f);
43
44
  case "pass":
44
45
  return handlePass(f);
46
+ case "watch":
47
+ return handleWatch(f);
45
48
  case "serve": {
46
49
  const { startMcpServer } = await import("../lib/mcp.js");
47
50
  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, createEvent, endEvent, eventCheckin, joinEvent, eventScan, pass as passUser, uploadEventImage } from "./core.js";
3
+ import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover, createEvent, endEvent, eventCheckin, joinEvent, eventScan, pass as passUser, uploadEventImage, getClient } from "./core.js";
4
4
  import { createInterface } from "readline";
5
5
  import { existsSync, mkdirSync, copyFileSync, readFileSync } from "fs";
6
6
  import { join, dirname, extname } from "path";
@@ -63,7 +63,8 @@ export async function handleScan(f) {
63
63
 
64
64
  export async function handleProfile(f) {
65
65
  if (!f.id) return console.error("Usage: antenna profile --id telegram:123 [--name Yi --emoji ๐Ÿฆฆ --line1 '...' --line2 '...' --line3 '...']");
66
- if (f.name || f.line1 || f.line2 || f.line3) {
66
+ if (f.name || f.line1 || f.line2 || f.line3 || f.visible !== undefined || f.hide !== undefined) {
67
+ const visible = f.hide ? false : (f.visible !== undefined ? f.visible === 'true' || f.visible === true : undefined);
67
68
  const data = await setProfile({
68
69
  device_id: f.id,
69
70
  display_name: f.name,
@@ -71,6 +72,7 @@ export async function handleProfile(f) {
71
72
  line1: f.line1,
72
73
  line2: f.line2,
73
74
  line3: f.line3,
75
+ ...(visible !== undefined && { visible }),
74
76
  });
75
77
  console.log("โœ… Profile saved");
76
78
  console.log(JSON.stringify(data, null, 2));
@@ -85,10 +87,11 @@ export async function handleProfile(f) {
85
87
  }
86
88
 
87
89
  export async function handleAccept(f) {
88
- if (!f.id || !f.target) return console.error("Usage: antenna accept --id telegram:123 --target telegram:789 [--contact 'WeChat: yi']");
90
+ if (!f.id || (!f.target && !f.ref)) return console.error("Usage: antenna accept --id telegram:123 --ref 1 [--contact 'WeChat: yi']\n antenna accept --id telegram:123 --target telegram:789 [--contact 'WeChat: yi']");
89
91
  const result = await accept({
90
92
  device_id: f.id,
91
- target_device_id: f.target,
93
+ target_device_id: f.target || null,
94
+ ref: f.ref || null,
92
95
  contact_info: f.contact,
93
96
  });
94
97
  console.log("โœ… " + result.message);
@@ -401,6 +404,197 @@ export function handleInstallHermesPlugin() {
401
404
  console.log();
402
405
  }
403
406
 
407
+ export async function handleWatch(f) {
408
+ const id = f.id;
409
+ if (!id) {
410
+ console.error("โŒ --id required (e.g. --id telegram:123)");
411
+ process.exit(1);
412
+ }
413
+
414
+ const sb = getClient();
415
+ const notified = new Set();
416
+
417
+ // Detect local agent framework for push notifications
418
+ let pushMethod = "terminal"; // default: just print
419
+ try {
420
+ execSync("which openclaw", { stdio: "pipe" });
421
+ // Verify gateway is running
422
+ try {
423
+ execSync("openclaw gateway health", { stdio: "pipe", timeout: 5000 });
424
+ pushMethod = "openclaw";
425
+ } catch { /* gateway not running */ }
426
+ } catch { /* openclaw not installed */ }
427
+
428
+ if (pushMethod === "terminal") {
429
+ try {
430
+ execSync("which hermes", { stdio: "pipe" });
431
+ try {
432
+ execSync("hermes gateway status", { stdio: "pipe", timeout: 5000 });
433
+ pushMethod = "hermes";
434
+ } catch { /* hermes gateway not running */ }
435
+ } catch { /* hermes not installed */ }
436
+ }
437
+
438
+ console.log(`๐Ÿ“ก Watching for new matches for ${id}...`);
439
+ if (pushMethod === "openclaw") {
440
+ console.log(` ๐Ÿ”— Detected OpenClaw โ€” will push notifications to your channel.`);
441
+ } else if (pushMethod === "hermes") {
442
+ console.log(` ๐Ÿ”— Detected Hermes โ€” will push notifications to your channel.`);
443
+ } else {
444
+ console.log(` โ„น๏ธ No agent framework detected โ€” notifications will print here.`);
445
+ }
446
+ console.log(` Press Ctrl+C to stop.\n`);
447
+
448
+ // Push notification helper
449
+ function pushNotify(message) {
450
+ console.log(message); // always print to terminal
451
+
452
+ if (pushMethod === "openclaw") {
453
+ try {
454
+ const parts = id.split(":");
455
+ const channel = parts[0];
456
+ const userId = parts.slice(1).join(":");
457
+ execSync(
458
+ `openclaw agent` +
459
+ ` --message ${JSON.stringify(message)}` +
460
+ ` --deliver` +
461
+ ` --reply-channel ${channel}` +
462
+ ` --reply-to "${userId}"`,
463
+ { timeout: 30_000, stdio: "pipe" }
464
+ );
465
+ } catch (err) {
466
+ // silent โ€” terminal output is the fallback
467
+ }
468
+ } else if (pushMethod === "hermes") {
469
+ try {
470
+ // Use hermes cron to create a one-shot notification
471
+ const parts = id.split(":");
472
+ const channel = parts[0];
473
+ execSync(
474
+ `hermes cron create` +
475
+ ` --name "Antenna notification"` +
476
+ ` --run-now` +
477
+ ` --once` +
478
+ ` --message ${JSON.stringify(message)}` +
479
+ ` --deliver ${channel}`,
480
+ { timeout: 30_000, stdio: "pipe" }
481
+ );
482
+ } catch (err) {
483
+ // silent โ€” terminal output is the fallback
484
+ }
485
+ }
486
+ }
487
+
488
+ // Initial check
489
+ const initial = await checkMatches({ device_id: id });
490
+ if (initial.mutual_matches?.length) {
491
+ console.log(`๐ŸŽ‰ You have ${initial.mutual_matches.length} mutual match(es)!`);
492
+ for (const m of initial.mutual_matches) {
493
+ const key = `mutual:${m.device_id}`;
494
+ notified.add(key);
495
+ console.log(` ${m.emoji || "๐Ÿ‘ค"} ${m.name}${m.their_contact ? " โ€” contact: " + m.their_contact : ""}`);
496
+ }
497
+ console.log();
498
+ }
499
+ if (initial.incoming_accepts?.length) {
500
+ console.log(`๐Ÿ“ฉ ${initial.incoming_accepts.length} person(s) want to meet you!`);
501
+ for (const m of initial.incoming_accepts) {
502
+ const key = `incoming:${m.device_id}`;
503
+ notified.add(key);
504
+ console.log(` ${m.emoji || "๐Ÿ‘ค"} ${m.name} โ€” ${m.line1 || ""}`);
505
+ }
506
+ console.log();
507
+ }
508
+
509
+ // Subscribe to realtime changes on matches table
510
+ const channel = sb
511
+ .channel("antenna-cli-watch")
512
+ .on("postgres_changes",
513
+ { event: "INSERT", schema: "public", table: "matches" },
514
+ async (payload) => {
515
+ try {
516
+ const row = payload.new;
517
+ if (!row) return;
518
+
519
+ // Someone accepted me
520
+ if (row.device_id_b === id) {
521
+ const key = `incoming:${row.device_id_a}`;
522
+ if (notified.has(key)) return;
523
+ notified.add(key);
524
+
525
+ const profile = await getProfile({ device_id: row.device_id_a });
526
+ const name = profile?.display_name || "Someone";
527
+ const emoji = profile?.emoji || "๐Ÿ‘ค";
528
+
529
+ // Check if mutual
530
+ const matches = await checkMatches({ device_id: id });
531
+ const isMutual = matches.mutual_matches?.some(m => m.device_id === row.device_id_a);
532
+
533
+ if (isMutual) {
534
+ const mutualKey = `mutual:${row.device_id_a}`;
535
+ notified.add(mutualKey);
536
+ const contact = row.contact_info_a;
537
+ pushNotify(`๐ŸŽ‰ MUTUAL MATCH! ${emoji} ${name} also accepted you!${contact ? " Contact: " + contact : ""}`);
538
+ } else {
539
+ pushNotify(`๐Ÿ“ฉ ${emoji} ${name} wants to meet you! Run: antenna accept --id ${id} --target ${row.device_id_a}`);
540
+ }
541
+ }
542
+
543
+ // I accepted someone and they also accepted me
544
+ if (row.device_id_a === id) {
545
+ const matches = await checkMatches({ device_id: id });
546
+ const mutual = matches.mutual_matches?.find(m => m.device_id === row.device_id_b);
547
+ if (mutual) {
548
+ const mutualKey = `mutual:${row.device_id_b}`;
549
+ if (!notified.has(mutualKey)) {
550
+ notified.add(mutualKey);
551
+ pushNotify(`๐ŸŽ‰ MUTUAL MATCH! ${mutual.emoji || "๐Ÿ‘ค"} ${mutual.name}!${mutual.their_contact ? " Contact: " + mutual.their_contact : ""}`);
552
+ }
553
+ }
554
+ }
555
+ } catch (err) {
556
+ // silent
557
+ }
558
+ }
559
+ )
560
+ .subscribe((status) => {
561
+ if (status === "SUBSCRIBED") {
562
+ console.log("โœ… Connected โ€” listening for matches in real-time.");
563
+ } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
564
+ console.log(`โš ๏ธ Connection issue (${status}), retrying...`);
565
+ }
566
+ });
567
+
568
+ // Keep alive โ€” also poll every 2 minutes as fallback
569
+ const pollInterval = setInterval(async () => {
570
+ try {
571
+ const result = await checkMatches({ device_id: id });
572
+ for (const m of (result.mutual_matches || [])) {
573
+ const key = `mutual:${m.device_id}`;
574
+ if (!notified.has(key)) {
575
+ notified.add(key);
576
+ pushNotify(`๐ŸŽ‰ MUTUAL MATCH! ${m.emoji || "๐Ÿ‘ค"} ${m.name}!${m.their_contact ? " Contact: " + m.their_contact : ""}`);
577
+ }
578
+ }
579
+ for (const m of (result.incoming_accepts || [])) {
580
+ const key = `incoming:${m.device_id}`;
581
+ if (!notified.has(key)) {
582
+ notified.add(key);
583
+ pushNotify(`๐Ÿ“ฉ ${m.emoji || "๐Ÿ‘ค"} ${m.name} wants to meet you!`);
584
+ }
585
+ }
586
+ } catch { /* silent */ }
587
+ }, 2 * 60 * 1000);
588
+
589
+ // Handle Ctrl+C
590
+ process.on("SIGINT", () => {
591
+ console.log("\n๐Ÿ‘‹ Stopped watching.");
592
+ clearInterval(pollInterval);
593
+ sb.removeChannel(channel);
594
+ process.exit(0);
595
+ });
596
+ }
597
+
404
598
  export function printHelp() {
405
599
  console.log(`๐Ÿ“ก Antenna โ€” nearby people discovery
406
600
 
@@ -413,6 +607,7 @@ Usage:
413
607
  antenna matches --id telegram:123
414
608
  antenna discover --id telegram:123
415
609
  antenna event --create --name 'AI Meetup' [--desc '...'] [--og-image 'url'] | --join --code abc123 | --scan --code abc123 | --end --code abc123 --id telegram:123 | --upload-image --code abc123 --file /path/to/image.png
610
+ antenna watch --id telegram:123 Watch for new matches in real-time (Ctrl+C to stop)
416
611
  antenna bind --id telegram:123
417
612
  antenna serve Start MCP server (stdio transport)
418
613
  antenna setup Interactive profile setup [--id telegram:123]
package/lib/mcp.js CHANGED
@@ -37,6 +37,39 @@ export async function startMcpServer() {
37
37
  // Store last scan ref map for resolving refs in accept
38
38
  let _lastRefMap = {};
39
39
 
40
+ // Track known device_ids and last notified matches for piggyback notifications
41
+ const _knownDeviceIds = new Set();
42
+ const _notifiedMatches = new Set();
43
+
44
+ // Piggyback match check: append pending notifications to any tool response
45
+ async function withMatchNotifications(deviceId, result) {
46
+ _knownDeviceIds.add(deviceId);
47
+ try {
48
+ const matches = await checkMatches({ device_id: deviceId });
49
+ const notifications = [];
50
+
51
+ for (const m of (matches.mutual_matches || [])) {
52
+ const key = `mutual:${m.device_id}`;
53
+ if (!_notifiedMatches.has(key)) {
54
+ _notifiedMatches.add(key);
55
+ notifications.push(`๐ŸŽ‰ ๅŒๅ‘ๅŒน้…๏ผ${m.emoji || "๐Ÿ‘ค"} ${m.name} ไนŸๆŽฅๅ—ไบ†ไฝ ๏ผ${m.their_contact ? "่”็ณปๆ–นๅผ๏ผš" + m.their_contact : ""}`);
56
+ }
57
+ }
58
+ for (const m of (matches.incoming_accepts || [])) {
59
+ const key = `incoming:${m.device_id}`;
60
+ if (!_notifiedMatches.has(key)) {
61
+ _notifiedMatches.add(key);
62
+ notifications.push(`๐Ÿ“ฉ ${m.emoji || "๐Ÿ‘ค"} ${m.name} ๆƒณ่ฎค่ฏ†ไฝ ๏ผ็”จ antenna_check_matches ๆŸฅ็œ‹่ฏฆๆƒ…ใ€‚`);
63
+ }
64
+ }
65
+
66
+ if (notifications.length > 0) {
67
+ result._pending_notifications = notifications;
68
+ }
69
+ } catch { /* silent */ }
70
+ return result;
71
+ }
72
+
40
73
  // โ”€โ”€โ”€ antenna_scan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
41
74
 
42
75
  server.tool(
@@ -51,10 +84,11 @@ export async function startMcpServer() {
51
84
  },
52
85
  async ({ lat, lng, radius_m, sender_id, channel }) => {
53
86
  try {
54
- const result = await scan({ lat, lng, radius_m, device_id: deriveDeviceId(sender_id, channel) });
87
+ const deviceId = deriveDeviceId(sender_id, channel);
88
+ const result = await scan({ lat, lng, radius_m, device_id: deviceId });
55
89
  _lastRefMap = result._ref_map || {};
56
90
  const { _ref_map, ...clean } = result;
57
- return jsonResult(clean);
91
+ return jsonResult(await withMatchNotifications(deviceId, clean));
58
92
  } catch (e) {
59
93
  return jsonResult({ error: e.message });
60
94
  }
@@ -83,10 +117,11 @@ export async function startMcpServer() {
83
117
  try {
84
118
  if (action === "get") {
85
119
  const data = await getProfile({ device_id: deviceId });
86
- return jsonResult(data ? { profile: data } : { profile: null, message: "่ฟ˜ๆฒกๆœ‰ๅ็‰‡๏ผŒๅธฎไฝ ๅˆ›ๅปบไธ€ไธช๏ผŸ" });
120
+ const result = data ? { profile: data } : { profile: null, message: "่ฟ˜ๆฒกๆœ‰ๅ็‰‡๏ผŒๅธฎไฝ ๅˆ›ๅปบไธ€ไธช๏ผŸ" };
121
+ return jsonResult(await withMatchNotifications(deviceId, result));
87
122
  }
88
123
  const data = await setProfile({ device_id: deviceId, display_name, emoji, line1, line2, line3, matching_context, visible });
89
- return jsonResult({ saved: true, profile: data });
124
+ return jsonResult(await withMatchNotifications(deviceId, { saved: true, profile: data }));
90
125
  } catch (e) {
91
126
  return jsonResult({ error: e.message });
92
127
  }
@@ -107,8 +142,9 @@ export async function startMcpServer() {
107
142
  },
108
143
  async ({ sender_id, channel, ref, target_device_id, contact_info }) => {
109
144
  try {
110
- const result = await accept({ device_id: deriveDeviceId(sender_id, channel), target_device_id, ref, contact_info });
111
- return jsonResult(result);
145
+ const deviceId = deriveDeviceId(sender_id, channel);
146
+ const result = await accept({ device_id: deviceId, target_device_id, ref, contact_info });
147
+ return jsonResult(await withMatchNotifications(deviceId, result));
112
148
  } catch (e) {
113
149
  return jsonResult({ error: e.message });
114
150
  }
@@ -129,11 +165,12 @@ export async function startMcpServer() {
129
165
  },
130
166
  async ({ lat, lng, sender_id, channel, place_name }) => {
131
167
  try {
132
- const result = await checkin({ lat, lng, device_id: deriveDeviceId(sender_id, channel) });
168
+ const deviceId = deriveDeviceId(sender_id, channel);
169
+ const result = await checkin({ lat, lng, device_id: deviceId });
133
170
  if (result.checked_in && place_name) {
134
171
  result.message = `ๅทฒ็ญพๅˆฐ (${place_name}) ๐Ÿ“ ็Žฐๅœจ้™„่ฟ‘็š„ไบบๆ‰ซๆๅฐฑ่ƒฝ็œ‹ๅˆฐไฝ ไบ†ใ€‚`;
135
172
  }
136
- return jsonResult(result);
173
+ return jsonResult(await withMatchNotifications(deviceId, result));
137
174
  } catch (e) {
138
175
  return jsonResult({ error: e.message });
139
176
  }
@@ -151,7 +188,11 @@ export async function startMcpServer() {
151
188
  },
152
189
  async ({ sender_id, channel }) => {
153
190
  try {
154
- const result = await checkMatches({ device_id: deriveDeviceId(sender_id, channel) });
191
+ const deviceId = deriveDeviceId(sender_id, channel);
192
+ const result = await checkMatches({ device_id: deviceId });
193
+ // Mark all as notified since user explicitly checked
194
+ for (const m of (result.mutual_matches || [])) _notifiedMatches.add(`mutual:${m.device_id}`);
195
+ for (const m of (result.incoming_accepts || [])) _notifiedMatches.add(`incoming:${m.device_id}`);
155
196
  return jsonResult(result);
156
197
  } catch (e) {
157
198
  return jsonResult({ error: e.message });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.2.9",
3
+ "version": "1.2.11",
4
4
  "description": "Antenna โ€” nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {