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 +3 -0
- package/lib/cli.js +199 -4
- package/lib/mcp.js +50 -9
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
111
|
-
|
|
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
|
|
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
|
|
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 });
|