antenna-openclaw-plugin 1.2.0 โ†’ 1.2.2

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/index.ts CHANGED
@@ -47,6 +47,7 @@ let _supabaseClient: SupabaseClient | null = null;
47
47
  let _supabaseUrl: string | null = null;
48
48
  const _lastScanTime = new Map<string, number>();
49
49
  const SCAN_DEBOUNCE_MS = 30_000;
50
+ const _knownDeviceIds = new Set<string>();
50
51
 
51
52
  function getConfig(api: any): AntennaConfig {
52
53
  const cfg = api.config?.plugins?.entries?.antenna?.config ?? {};
@@ -99,7 +100,9 @@ function extractWords(profile: Partial<Profile>): string[] {
99
100
  }
100
101
 
101
102
  function deriveDeviceId(senderId: string, channel: string): string {
102
- return `${channel}:${senderId}`;
103
+ const id = `${channel}:${senderId}`;
104
+ _knownDeviceIds.add(id);
105
+ return id;
103
106
  }
104
107
 
105
108
  /** Wrap result as MCP tool response */
@@ -288,7 +291,7 @@ export default function register(api: any) {
288
291
  return { ref, emoji: p.emoji || "๐Ÿ‘ค", name: p.display_name || "ๅŒฟๅ", line1: p.line1, line2: p.line2, line3: p.line3 };
289
292
  });
290
293
  (api as any)._antennaRefMap = gRefMap;
291
- try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: JSON.stringify(gRefMap) }); } catch {}
294
+ try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: gRefMap }); } catch {}
292
295
  for (const p of globalOthers) {
293
296
  try { await supabase.rpc("log_recommendation", { p_device_id: deviceId, p_recommended_id: p.device_id }); } catch {}
294
297
  }
@@ -318,7 +321,7 @@ export default function register(api: any) {
318
321
  // Store ref map for accept โ€” memory + DB
319
322
  (api as any)._antennaRefMap = _refMap;
320
323
  try {
321
- await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: JSON.stringify(_refMap) });
324
+ await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap });
322
325
  } catch { /* best effort */ }
323
326
 
324
327
  return ok({
@@ -587,23 +590,19 @@ export default function register(api: any) {
587
590
  const theirLines = [p.line1, p.line2, p.line3].filter(Boolean).join(". ");
588
591
  let match_reason: string | null = null;
589
592
 
590
- // Generate match reason with Gemini Flash
593
+ // Generate match reason via Edge Function (no client-side API key needed)
591
594
  if (myLines && theirLines) {
592
595
  try {
593
- const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
594
- if (apiKey) {
595
- const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, {
596
- method: "POST",
597
- headers: { "Content-Type": "application/json" },
598
- body: JSON.stringify({
599
- contents: [{ parts: [{ text: `You are matching two people. Person A: "${myLines}". Person B: "${theirLines}". Write ONE short sentence (under 20 words) in the SAME LANGUAGE as the profiles explaining why they might click. Be specific, not generic. No fluff.` }] }],
600
- generationConfig: { maxOutputTokens: 60, temperature: 0.7 },
601
- }),
602
- });
603
- if (res.ok) {
604
- const data = await res.json();
605
- match_reason = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || null;
606
- }
596
+ const supabaseUrl = cfg.supabaseUrl || BUILTIN_SUPABASE_URL;
597
+ const supabaseKey = cfg.supabaseKey || BUILTIN_SUPABASE_ANON_KEY;
598
+ const res = await fetch(`${supabaseUrl}/functions/v1/generate-match-reason`, {
599
+ method: "POST",
600
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${supabaseKey}` },
601
+ body: JSON.stringify({ my_lines: myLines, their_lines: theirLines }),
602
+ });
603
+ if (res.ok) {
604
+ const data = await res.json();
605
+ match_reason = data?.reason || null;
607
606
  }
608
607
  } catch { /* best effort */ }
609
608
  }
@@ -614,7 +613,7 @@ export default function register(api: any) {
614
613
  // Persist refs + log recommendation
615
614
  (api as any)._antennaRefMap = { ...(api as any)._antennaRefMap, ..._refMap };
616
615
  try {
617
- await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: JSON.stringify(_refMap) });
616
+ await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap });
618
617
  } catch { /* best effort */ }
619
618
  for (const p of results) {
620
619
  await supabase.rpc("log_recommendation", { p_device_id: deviceId, p_recommended_id: p.device_id });
@@ -720,7 +719,7 @@ export default function register(api: any) {
720
719
  });
721
720
 
722
721
  (api as any)._antennaRefMap = { ...(api as any)._antennaRefMap, ..._refMap };
723
- try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: JSON.stringify(_refMap) }); } catch {}
722
+ try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap }); } catch {}
724
723
 
725
724
  return ok({ count: profiles.length, profiles, event: true });
726
725
  },
@@ -849,11 +848,72 @@ export default function register(api: any) {
849
848
  const _notifiedMatches = new Set<string>(); // "deviceAโ†’deviceB" already notified
850
849
 
851
850
  let _pollTimer: ReturnType<typeof setInterval> | null = null;
851
+ let _realtimeChannel: any = null;
852
852
 
853
853
  api.registerService({
854
854
  id: "antenna-match-poller",
855
855
  start: () => {
856
- logger.info("Antenna: match poller started (10 min interval, real-time notify)");
856
+ logger.info("Antenna: match poller started (10 min interval + Supabase Realtime)");
857
+
858
+ // โ”€โ”€ Supabase Realtime: instant match notifications โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
859
+ try {
860
+ const rtCfg = getConfig(api);
861
+ const rtSupabase = getSupabase(rtCfg);
862
+ _realtimeChannel = rtSupabase
863
+ .channel('antenna-match-notify')
864
+ .on('postgres_changes',
865
+ { event: 'INSERT', schema: 'public', table: 'matches' },
866
+ async (payload: any) => {
867
+ try {
868
+ const targetDeviceId = payload.new?.device_id_b;
869
+ if (!targetDeviceId || !_knownDeviceIds.has(targetDeviceId)) return;
870
+
871
+ const key = `${payload.new.device_id_a}โ†’${targetDeviceId}`;
872
+ if (_notifiedMatches.has(key)) return;
873
+ _notifiedMatches.add(key);
874
+
875
+ const parts = targetDeviceId.split(":");
876
+ if (parts.length < 2) return;
877
+ const channel = parts[0];
878
+ const userId = parts.slice(1).join(":");
879
+
880
+ const innerCfg = getConfig(api);
881
+ const innerSb = getSupabase(innerCfg);
882
+
883
+ const { data: theirProfile } = await innerSb.rpc("get_profile", { p_device_id: payload.new.device_id_a });
884
+ const name = theirProfile?.display_name || "ๆœ‰ไบบ";
885
+ const emoji = theirProfile?.emoji || "๐Ÿ‘ค";
886
+
887
+ // Check if mutual
888
+ const { data: matches } = await innerSb.rpc("get_my_matches", { p_device_id: targetDeviceId });
889
+ const myAccept = (matches || []).find(
890
+ (m: any) => m.device_id_a === targetDeviceId && m.device_id_b === payload.new.device_id_a
891
+ );
892
+
893
+ if (myAccept) {
894
+ const contact = payload.new.contact_info_a ? `\nๅฏนๆ–น็š„่”็ณปๆ–นๅผ๏ผš${payload.new.contact_info_a}` : "";
895
+ notifyUser(channel, userId,
896
+ `[Antenna] ๐ŸŽ‰ ๅŒๅ‘ๅŒน้…๏ผ${emoji} ${name} ไนŸๆŽฅๅ—ไบ†ไฝ ๏ผ${contact}\n\n็”จ antenna_check_matches ๆŸฅ็œ‹่ฏฆๆƒ…ใ€‚`,
897
+ logger);
898
+ stopFollowUpCron(targetDeviceId, payload.new.device_id_a, logger);
899
+ } else {
900
+ notifyUser(channel, userId,
901
+ `[Antenna] ๐Ÿ“ฉ ${emoji} ${name} ๆƒณ่ฎค่ฏ†ไฝ ๏ผ็œ‹็œ‹ TA ็š„ๅ็‰‡๏ผŒๅ†ณๅฎš่ฆไธ่ฆๆŽฅๅ—๏ผŸ\n\n็”จ antenna_check_matches ๆŸฅ็œ‹่ฏฆๆƒ…ใ€‚`,
902
+ logger);
903
+ }
904
+ } catch (err: any) {
905
+ logger.warn("Antenna: realtime match handler error:", err.message);
906
+ }
907
+ }
908
+ )
909
+ .subscribe((status: string) => {
910
+ logger.info(`Antenna: realtime subscription status: ${status}`);
911
+ });
912
+ } catch (err: any) {
913
+ logger.warn("Antenna: failed to start realtime subscription, falling back to poll only:", err.message);
914
+ }
915
+
916
+ // โ”€โ”€ Poll fallback: catch anything Realtime missed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
857
917
  _pollTimer = setInterval(async () => {
858
918
  try {
859
919
  const cfg = getConfig(api);
@@ -948,7 +1008,15 @@ export default function register(api: any) {
948
1008
  },
949
1009
  stop: () => {
950
1010
  if (_pollTimer) clearInterval(_pollTimer);
951
- logger.info("Antenna: match poller stopped");
1011
+ if (_realtimeChannel) {
1012
+ try {
1013
+ const rtCfg = getConfig(api);
1014
+ const rtSupabase = getSupabase(rtCfg);
1015
+ rtSupabase.removeChannel(_realtimeChannel);
1016
+ } catch { /* best effort */ }
1017
+ _realtimeChannel = null;
1018
+ }
1019
+ logger.info("Antenna: match poller + realtime stopped");
952
1020
  },
953
1021
  });
954
1022
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-openclaw-plugin",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Antenna โ€” agent-mediated nearby people discovery for OpenClaw",
5
5
  "openclaw": {
6
6
  "extensions": ["./index.ts"]
@@ -83,7 +83,7 @@ After receiving the nearby profiles, **you decide** who to recommend:
83
83
  View or update the user's name card.
84
84
  - `action`: "get" or "set"
85
85
  - `sender_id`, `channel`: from context
86
- - For "set": `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`
86
+ - For "set": `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`, `matching_context`
87
87
 
88
88
  The name card has:
89
89
  - **emoji**: a single emoji that represents them
@@ -91,6 +91,12 @@ The name card has:
91
91
  - **line1**: who they are / what they do
92
92
  - **line2**: what they're into
93
93
  - **line3**: what they're looking for right now
94
+ - **matching_context** (optional, not shown to others): A richer description generated by the agent based on what it knows about the user โ€” career background, tech stack, interests, projects, personality traits. ~200 words. Only used for embedding-based matching, never displayed to other users.
95
+
96
+ **After saving the profile, generate `matching_context` automatically** based on your knowledge of the user (memory, conversations, context). Don't ask the user to write it โ€” you write it. Example:
97
+ > "Product designer at a tech company in Beijing, focusing on AI search experience. Interested in music (Sakamoto), swimming, cooking, language learning. Recently exploring AI agent ecosystems and social discovery. Looking to connect with AI builders, indie hackers, and creative technologists."
98
+ - **line2**: what they're into
99
+ - **line3**: what they're looking for right now
94
100
 
95
101
  ### `antenna_accept`
96
102
  Accept a match after the user sees results. Can optionally include contact info to share.