antenna-openclaw-plugin 1.3.40 → 1.3.42

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/README.md CHANGED
@@ -83,6 +83,8 @@ Supabase
83
83
  └── pg_cron cleanup — hourly expired match cleanup
84
84
  ```
85
85
 
86
+ Profile writes require the user's Antenna API key from antenna.fyi/me. Agents must pass `api_key` to `antenna_profile(action="set")`; the plugin verifies it and writes to the dashboard-linked `user:<uuid>` profile instead of creating a profile from `channel:sender_id`.
87
+
86
88
  ## Supported platforms
87
89
 
88
90
  Location auto-detection (OpenClaw parses the coordinates):
package/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createClient, SupabaseClient } from "@supabase/supabase-js";
2
2
  import { execSync } from "child_process";
3
+ import { createHash } from "crypto";
3
4
 
4
5
  // ─── Built-in Supabase config (shared backend, zero config) ─────────
5
6
 
@@ -72,6 +73,28 @@ function getSupabase(cfg: AntennaConfig): SupabaseClient {
72
73
  return _supabaseClient;
73
74
  }
74
75
 
76
+ function profileSlugCandidate(displayName: string | null | undefined, deviceId: string) {
77
+ const fromName = String(displayName || "")
78
+ .toLowerCase()
79
+ .replace(/[^a-z0-9]+/g, "-")
80
+ .replace(/^-|-$/g, "")
81
+ .substring(0, 30);
82
+ if (fromName) return fromName;
83
+ return `user-${createHash("sha1").update(deviceId).digest("hex").slice(0, 10)}`;
84
+ }
85
+
86
+ async function resolveDashboardDeviceId(supabase: SupabaseClient, apiKey: string | null | undefined) {
87
+ if (!apiKey) {
88
+ return { error: "Profile writes require the user's Antenna API key from antenna.fyi/me. Do not create an agent-only profile from sender_id/channel." };
89
+ }
90
+ const { data, error } = await supabase.rpc("verify_api_key", { p_key: apiKey });
91
+ if (error) return { error: error.message };
92
+ if (!data?.valid) return { error: data?.error || "Invalid Antenna API key" };
93
+ const deviceId = data.user_id ? `user:${data.user_id}` : data.device_id;
94
+ if (!deviceId) return { error: "API key verified but did not return a dashboard device_id" };
95
+ return { deviceId, userId: data.user_id, displayName: data.display_name };
96
+ }
97
+
75
98
  function isRateLimited(deviceId: string): boolean {
76
99
  const now = Date.now();
77
100
  const last = _lastScanTime.get(deviceId);
@@ -420,13 +443,14 @@ export default function register(api: any) {
420
443
  line3: { type: "string", description: "Third line (what you're looking for)" },
421
444
  visible: { type: "boolean", description: "Whether to be visible to others" },
422
445
  matching_context: { type: "string", description: "More information / free-form context for AI matching (interests, goals, background, etc.)" },
446
+ api_key: { type: "string", description: "Required for action='set': user's Antenna API key from antenna.fyi/me. Profile writes use the dashboard-linked user:<uuid> profile." },
423
447
  },
424
448
  required: ["action", "sender_id", "channel", "chat_id"],
425
449
  },
426
450
  async execute(_id: string, params: any) {
427
451
  const cfg = getConfig(api);
428
452
  const supabase = getSupabase(cfg);
429
- const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
453
+ let deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
430
454
 
431
455
  if (params.action === "get") {
432
456
  const { data, error } = await supabase.rpc("get_profile", { p_device_id: deviceId });
@@ -440,12 +464,17 @@ export default function register(api: any) {
440
464
  });
441
465
  }
442
466
 
467
+ const resolved = await resolveDashboardDeviceId(supabase, params.api_key);
468
+ if (resolved.error) return ok({ error: resolved.error });
469
+ deviceId = resolved.deviceId!;
470
+
443
471
  const { data, error } = await supabase.rpc("upsert_profile", {
444
472
  p_device_id: deviceId,
445
473
  p_display_name: params.display_name ?? null, p_emoji: params.emoji ?? null,
446
474
  p_line1: params.line1 ?? null, p_line2: params.line2 ?? null,
447
475
  p_line3: params.line3 ?? null, p_visible: params.visible ?? true,
448
476
  ...(params.matching_context != null ? { p_matching_context: params.matching_context } : {}),
477
+ p_api_key: params.api_key,
449
478
  });
450
479
 
451
480
  if (error) return ok({ error: error.message });
@@ -455,8 +484,14 @@ export default function register(api: any) {
455
484
  let archetypeResult = null;
456
485
  try {
457
486
  const { data: profile } = await supabase.rpc("get_profile", { p_device_id: deviceId });
458
- if (profile?.profile_slug) {
459
- publicUrl = `https://www.antenna.fyi/p/${profile.profile_slug}`;
487
+ let profileSlug = profile?.profile_slug || null;
488
+ if (!profileSlug) {
489
+ const targetSlug = profileSlugCandidate(params.display_name, deviceId);
490
+ const { data: slugResult } = await supabase.rpc("set_profile_slug", { p_device_id: deviceId, p_slug: targetSlug, p_api_key: params.api_key });
491
+ if (slugResult?.set) profileSlug = targetSlug;
492
+ }
493
+ if (profileSlug) {
494
+ publicUrl = `https://www.antenna.fyi/p/${profileSlug}`;
460
495
  }
461
496
  } catch {}
462
497
 
@@ -502,8 +537,10 @@ export default function register(api: any) {
502
537
  profile: { display_name: data.display_name,
503
538
  line1: data.line1, line2: data.line2, line3: data.line3, visible: data.visible },
504
539
  public_url: publicUrl,
540
+ api_key_verified: true,
541
+ dashboard_device_id: deviceId,
505
542
  archetype: archetypeResult || null,
506
- next_step: "IMPORTANT: 1) Send the public_url to the user — this is their shareable profile link. 2) Tell the user their archetype and the personalized reason. 3) Call antenna_bind to generate a GPS link. Do not skip any step.",
543
+ next_step: "IMPORTANT: 1) Send the public_url to the user — this is their shareable profile link. If public_url is null, say profile link generation failed and retry profile save. 2) Tell the user their archetype and the personalized reason. 3) Call antenna_bind to generate a GPS link. Do not skip any step.",
507
544
  });
508
545
  },
509
546
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-openclaw-plugin",
3
- "version": "1.3.40",
3
+ "version": "1.3.42",
4
4
  "description": "Antenna — agent-mediated nearby people discovery for OpenClaw",
5
5
  "openclaw": {
6
6
  "extensions": [
@@ -25,6 +25,8 @@ Plugin 安装后,agent 应该**主动**开始引导,不要等用户问"怎么用
25
25
  **第一步:打招呼 + 解释**
26
26
  > "嘿,你装了 Antenna--它能帮你发现附近有意思的人。先确认一下,你在 antenna.fyi 注册过了吗?拿到 API key 了吗?有了的话我帮你做张名片,然后看看附近有谁。"
27
27
 
28
+ **硬约束:** Profile 写入必须通过用户从 antenna.fyi/me 拿到的 API key。`antenna_profile(action="set")` 必须传 `api_key`,tool 会验证 key 并写入 dashboard 绑定的 `user:<uuid>` profile。不要自己拼 `channel:sender_id`,不要在用户拿到 API key 前凭空创建 profile。
29
+
28
30
  **第二步:聊天收集 → 生成名片 → 确认**
29
31
 
30
32
  Agent 跟用户聊几句,了解他们是谁、做什么、对什么感兴趣、想认识什么人。然后 agent 自己完成以下工作(不需要用户参与):
@@ -40,7 +42,7 @@ Agent 跟用户聊几句,了解他们是谁、做什么、对什么感兴趣、
40
42
  >
41
43
  > 这样可以吗?要改哪里告诉我。
42
44
 
43
- 用户确认后才调 `antenna_profile(action="set")` 保存(matching_context + line1/2/3 + emoji + name 一起存)。
45
+ 用户确认后才调 `antenna_profile(action="set", api_key="ant_xxx")` 保存(matching_context + line1/2/3 + emoji + name 一起存)。
44
46
  用户要改 → 改完重新预览 → 再确认。
45
47
 
46
48
  **不要跳过确认。名片是展示给别人看的,必须让用户看过才存。**
@@ -159,7 +161,8 @@ After receiving the nearby profiles, **you decide** who to recommend:
159
161
  View or update the user's name card.
160
162
  - `action`: "get" or "set"
161
163
  - `sender_id`, `channel`: from context
162
- - For "set": `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`, `matching_context`
164
+ - For "set": `api_key`, `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`, `matching_context`
165
+ - `api_key` is mandatory for writes. It must be the user's Antenna API key from antenna.fyi/me. The tool writes to the dashboard-linked `user:<uuid>` profile; do not create profiles from `sender_id/channel`.
163
166
 
164
167
  The name card has:
165
168
  - **emoji**: a single emoji that represents them
@@ -172,6 +175,10 @@ The name card has:
172
175
  **During onboarding, generate `matching_context` FIRST** based on your conversation with the user (+ memory, SOUL.md, etc.). Then derive line1/2/3 from it. Don't ask the user to write matching_context - you write it. Example:
173
176
  > "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."
174
177
 
178
+ After setting a profile, the tool returns `public_url`. **You must immediately send that link to the user** as their shareable public profile. If `public_url` is empty, retry profile save or report that link generation failed.
179
+
180
+ **i18n:** Save and show user-written profile content in the user's original language. Do not machine-translate their personal description, looking-for text, or conversation style. Antenna UI labels can switch language; the user's own text stays as written.
181
+
175
182
  ### `antenna_accept`
176
183
  Accept a match after the user sees results. Can optionally include contact info to share.
177
184
  - `sender_id`, `channel`, `target_device_id`