antenna-fyi 1.3.41 → 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
@@ -26,7 +26,10 @@ antenna checkin --id <platform>:<user_id> --lat 39.99 --lng 116.48
26
26
 
27
27
  # View/edit your profile
28
28
  antenna profile --id <platform>:<user_id>
29
- antenna profile --id <platform>:<user_id> --name Yi --emoji 🦦 --line1 'Product Designer'
29
+ antenna config --key <ant_xxx>
30
+ antenna profile --id <platform>:<user_id> --name Yi --line1 'Product Designer'
31
+
32
+ Profile writes require the user's Antenna API key from antenna.fyi/me. The CLI verifies the key and writes to the dashboard-linked `user:<uuid>` profile; agents should not create profiles from `channel:sender_id`.
30
33
 
31
34
  # Accept a match
32
35
  antenna accept --id <platform>:<user_id> --target <ref_or_id> --contact 'WeChat: yi'
package/lib/cli.js CHANGED
@@ -68,10 +68,14 @@ export async function handleScan(f) {
68
68
 
69
69
  export async function handleProfile(f) {
70
70
  const id = resolveId(f);
71
- if (!id) return console.error("Usage: antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...' --looking-for '...' --conversation-style '...' --more-information '...' --tags 'AI,design,music' --city 'Beijing' --contact 'WeChat: yi' --slug mira --hide --visible true]");
71
+ if (!id) return console.error("Usage: antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...' --looking-for '...' --conversation-style '...' --more-information '...' --tags 'AI,design,music' --city 'Beijing' --contact 'WeChat: yi' --slug mira --hide --visible true]\n Writes require: antenna config --key <your-api-key>");
72
72
  if (f.name || f["personal-description"] !== undefined || f.line1 !== undefined || f["looking-for"] !== undefined || f.line2 !== undefined || f["conversation-style"] !== undefined || f.line3 !== undefined || f["more-information"] !== undefined || f.contact !== undefined || f.slug !== undefined || f.tags !== undefined || f.city !== undefined || f.visible !== undefined || f.hide !== undefined) {
73
+ const config = loadConfig();
74
+ if (!config.key) {
75
+ return console.error("❌ Profile writes require a dashboard API key. Run: antenna config --key <your-api-key>");
76
+ }
73
77
  const visible = f.hide ? false : (f.visible !== undefined ? f.visible === 'true' || f.visible === true : undefined);
74
- const payload = { device_id: id };
78
+ const payload = { device_id: config.device_id || id, api_key: config.key };
75
79
  if (f.name) payload.display_name = f.name;
76
80
  if (f["personal-description"] !== undefined) payload.line1 = f["personal-description"];
77
81
  else if (f.line1 !== undefined) payload.line1 = f.line1;
@@ -85,8 +89,6 @@ export async function handleProfile(f) {
85
89
  if (f.tags !== undefined) payload.interest_tags = typeof f.tags === 'string' ? f.tags.split(',').map(t => t.trim()).filter(Boolean) : [f.tags];
86
90
  if (f.city !== undefined) payload.city = f.city;
87
91
  if (visible !== undefined) payload.visible = visible;
88
- const config = loadConfig();
89
- if (config.key) payload.api_key = config.key;
90
92
  const data = await setProfile(payload);
91
93
  if (data?.error) {
92
94
  console.error("❌ " + data.error);
@@ -445,7 +447,7 @@ export async function handleConfig(f) {
445
447
  }
446
448
  const config = loadConfig();
447
449
  config.key = f.key;
448
- config.device_id = result.device_id;
450
+ config.device_id = result.user_id ? `user:${result.user_id}` : result.device_id;
449
451
  config.user_id = result.user_id;
450
452
  config.display_name = result.display_name;
451
453
  saveConfig(config);
@@ -1107,7 +1109,7 @@ Agent shortcuts:
1107
1109
  Usage:
1108
1110
  antenna scan --lat 39.99 --lng 116.48 [--radius 500] (max 1000) [--id <platform>:<user_id>]
1109
1111
  antenna checkin --id <platform>:<user_id> --lat 39.99 --lng 116.48
1110
- antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...' --looking-for '...' --conversation-style '...' --more-information '...' --tags 'AI,design,music' --city 'Beijing' --contact 'WeChat: yi' --slug mira --hide --visible true]
1112
+ antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...' --looking-for '...' --conversation-style '...' --more-information '...' --tags 'AI,design,music' --city 'Beijing' --contact 'WeChat: yi' --slug mira --hide --visible true] (writes require antenna config --key)
1111
1113
  antenna accept --id <platform>:<user_id> --target <ref_or_device_id> [--contact 'WeChat: yi']
1112
1114
  antenna pass --id <platform>:<user_id> --target <ref_or_device_id> (or --ref 1)
1113
1115
  antenna matches --id <platform>:<user_id>
package/lib/core.js CHANGED
@@ -301,6 +301,18 @@ export async function setProfile({
301
301
  supabaseKey,
302
302
  }) {
303
303
  const sb = getClient(supabaseUrl, supabaseKey);
304
+ if (!api_key) {
305
+ throw new 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.");
306
+ }
307
+ const auth = await verifyApiKey({ key: api_key, supabaseUrl, supabaseKey });
308
+ if (!auth?.valid) {
309
+ throw new Error(auth?.error || "Invalid Antenna API key");
310
+ }
311
+ const dashboardDeviceId = auth.user_id ? `user:${auth.user_id}` : auth.device_id;
312
+ if (!dashboardDeviceId) {
313
+ throw new Error("API key verified but did not return a dashboard device_id");
314
+ }
315
+ device_id = dashboardDeviceId;
304
316
 
305
317
  // Pack structured fields into matching_context JSON
306
318
  let contextJson = matching_context;
@@ -411,6 +423,7 @@ export async function setProfile({
411
423
  p_device_id: device_id,
412
424
  p_matching_context: JSON.stringify(ctx),
413
425
  p_visible: profile?.visible ?? true,
426
+ p_api_key: api_key,
414
427
  });
415
428
  } catch {}
416
429
  }
@@ -432,6 +445,8 @@ export async function setProfile({
432
445
  public_url: publicUrl,
433
446
  gps_bind_url: bindUrl,
434
447
  archetype: archetypeResult || null,
448
+ api_key_verified: true,
449
+ dashboard_device_id: device_id,
435
450
  next_step: "Send the public_url and gps_bind_url to the user. The GPS link should be opened on their phone to share location.",
436
451
  };
437
452
  }
@@ -56,6 +56,7 @@ PROFILE_SCHEMA = {
56
56
  "line3": {"type": "string", "description": "What you're looking for"},
57
57
  "visible": {"type": "boolean", "description": "Visible to others"},
58
58
  "matching_context": {"type": "string", "description": "Free-form context for AI matching (interests, goals, etc.)"},
59
+ "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."},
59
60
  },
60
61
  "required": ["action", "sender_id", "channel", "chat_id"],
61
62
  },
@@ -43,6 +43,19 @@ def _profile_slug_candidate(display_name: str | None, device_id: str) -> str:
43
43
  return f"user-{digest}"
44
44
 
45
45
 
46
+ def _dashboard_device_id(sb, api_key: str | None) -> tuple[str | None, str | None]:
47
+ if not api_key:
48
+ return None, "Profile writes require the user's Antenna API key from antenna.fyi/me. Do not create an agent-only profile from sender_id/channel."
49
+ resp = sb.rpc("verify_api_key", {"p_key": api_key}).execute()
50
+ data = resp.data or {}
51
+ if not data.get("valid"):
52
+ return None, data.get("error") or "Invalid Antenna API key"
53
+ device_id = f"user:{data.get('user_id')}" if data.get("user_id") else data.get("device_id")
54
+ if not device_id:
55
+ return None, "API key verified but did not return a dashboard device_id"
56
+ return device_id, None
57
+
58
+
46
59
  def _get_url():
47
60
  return os.environ.get("ANTENNA_SUPABASE_URL") or os.environ.get("ANTENNA_URL") or BUILTIN_URL
48
61
 
@@ -191,6 +204,9 @@ def handle_profile(params: dict) -> str:
191
204
  return _ok({"exists": True, "profile": resp.data})
192
205
 
193
206
  # set
207
+ did, auth_error = _dashboard_device_id(sb, params.get("api_key"))
208
+ if auth_error:
209
+ return _ok({"error": auth_error})
194
210
  rpc_params = {
195
211
  "p_device_id": did,
196
212
  "p_display_name": params.get("display_name"),
@@ -224,6 +240,8 @@ def handle_profile(params: dict) -> str:
224
240
  "updated": True,
225
241
  "profile": resp.data,
226
242
  "public_url": public_url,
243
+ "api_key_verified": True,
244
+ "dashboard_device_id": did,
227
245
  "next_step": "IMPORTANT: 1) Send public_url to the user — this is their shareable profile link. 2) Call antenna_bind to generate a GPS link. Do not skip either step.",
228
246
  })
229
247
  return _ok({"error": "upsert_profile failed"})
package/lib/mcp.js CHANGED
@@ -128,8 +128,9 @@ export async function startMcpServer() {
128
128
  links: z.array(z.string()).optional().describe("Social links shown on the card footer (up to 3)"),
129
129
  is_active: z.boolean().optional().describe("Whether the profile is active or quiet"),
130
130
  visible: z.boolean().optional().default(true),
131
+ api_key: z.string().optional().describe("Required for action='set': the user's Antenna API key from antenna.fyi/me. Profile writes use this key and the dashboard-linked user:<uuid> device_id."),
131
132
  },
132
- async ({ action, sender_id, channel, display_name, personal_description, looking_for, conversation_style, more_information, interest_tags, city, links, is_active, visible }) => {
133
+ async ({ action, sender_id, channel, display_name, personal_description, looking_for, conversation_style, more_information, interest_tags, city, links, is_active, visible, api_key }) => {
133
134
  const deviceId = deriveDeviceId(sender_id, channel);
134
135
  try {
135
136
  if (action === "get") {
@@ -139,8 +140,8 @@ export async function startMcpServer() {
139
140
  : { profile: null, message: "还没有名片。跟用户聊聊他们是谁、做什么、想认识什么人,然后帮他们创建。", fields: PROFILE_FIELDS };
140
141
  return jsonResult(await withMatchNotifications(deviceId, result));
141
142
  }
142
- const data = await setProfile({ device_id: deviceId, display_name, line1: personal_description, line2: looking_for, line3: conversation_style, matching_context: more_information, interest_tags, city, links, is_active, visible });
143
- return jsonResult(await withMatchNotifications(deviceId, { saved: true, profile: data }));
143
+ const data = await setProfile({ device_id: deviceId, display_name, line1: personal_description, line2: looking_for, line3: conversation_style, matching_context: more_information, interest_tags, city, links, is_active, visible, api_key });
144
+ return jsonResult(await withMatchNotifications(data.dashboard_device_id || deviceId, { saved: true, profile: data }));
144
145
  } catch (e) {
145
146
  return jsonResult({ error: e.message });
146
147
  }
@@ -77,6 +77,18 @@ function profileSlugCandidate(displayName: string | null | undefined, deviceId:
77
77
  return `user-${createHash("sha1").update(deviceId).digest("hex").slice(0, 10)}`;
78
78
  }
79
79
 
80
+ async function resolveDashboardDeviceId(supabase: SupabaseClient, apiKey: string | null | undefined) {
81
+ if (!apiKey) {
82
+ 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." };
83
+ }
84
+ const { data, error } = await supabase.rpc("verify_api_key", { p_key: apiKey });
85
+ if (error) return { error: error.message };
86
+ if (!data?.valid) return { error: data?.error || "Invalid Antenna API key" };
87
+ const deviceId = data.user_id ? `user:${data.user_id}` : data.device_id;
88
+ if (!deviceId) return { error: "API key verified but did not return a dashboard device_id" };
89
+ return { deviceId, userId: data.user_id, displayName: data.display_name };
90
+ }
91
+
80
92
  function isRateLimited(deviceId: string): boolean {
81
93
  const now = Date.now();
82
94
  const last = _lastScanTime.get(deviceId);
@@ -411,13 +423,14 @@ export default function register(api: any) {
411
423
  line2: { type: "string", description: "Second line (what you're into)" },
412
424
  line3: { type: "string", description: "Third line (what you're looking for)" },
413
425
  visible: { type: "boolean", description: "Whether to be visible to others" },
426
+ 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." },
414
427
  },
415
428
  required: ["action", "sender_id", "channel"],
416
429
  },
417
430
  async execute(_id: string, params: any) {
418
431
  const cfg = getConfig(api);
419
432
  const supabase = getSupabase(cfg);
420
- const deviceId = deriveDeviceId(params.sender_id, params.channel);
433
+ let deviceId = deriveDeviceId(params.sender_id, params.channel);
421
434
 
422
435
  if (params.action === "get") {
423
436
  const { data, error } = await supabase.rpc("get_profile", { p_device_id: deviceId });
@@ -431,12 +444,16 @@ export default function register(api: any) {
431
444
  });
432
445
  }
433
446
 
447
+ const resolved = await resolveDashboardDeviceId(supabase, params.api_key);
448
+ if (resolved.error) return ok({ error: resolved.error });
449
+ deviceId = resolved.deviceId!;
450
+
434
451
  const { data, error } = await supabase.rpc("upsert_profile", {
435
452
  p_device_id: deviceId,
436
453
  p_display_name: params.display_name ?? null, p_emoji: null,
437
454
  p_line1: params.line1 ?? null, p_line2: params.line2 ?? null,
438
455
  p_line3: params.line3 ?? null, p_visible: params.visible ?? true,
439
- p_api_key: null,
456
+ p_api_key: params.api_key,
440
457
  });
441
458
 
442
459
  if (error) return ok({ error: error.message });
@@ -447,7 +464,7 @@ export default function register(api: any) {
447
464
  let profileSlug = profile?.profile_slug || null;
448
465
  if (!profileSlug) {
449
466
  const targetSlug = profileSlugCandidate(params.display_name, deviceId);
450
- const { data: slugResult } = await supabase.rpc("set_profile_slug", { p_device_id: deviceId, p_slug: targetSlug, p_api_key: null });
467
+ const { data: slugResult } = await supabase.rpc("set_profile_slug", { p_device_id: deviceId, p_slug: targetSlug, p_api_key: params.api_key });
451
468
  if (slugResult?.set) profileSlug = targetSlug;
452
469
  }
453
470
  if (profileSlug) publicUrl = `https://www.antenna.fyi/p/${profileSlug}`;
@@ -458,6 +475,8 @@ export default function register(api: any) {
458
475
  profile: { display_name: data.display_name,
459
476
  line1: data.line1, line2: data.line2, line3: data.line3, visible: data.visible },
460
477
  public_url: publicUrl,
478
+ api_key_verified: true,
479
+ dashboard_device_id: deviceId,
461
480
  next_step: "IMPORTANT: Send the public_url to the user — this is their shareable profile link. Then call antenna_bind to generate a GPS link. Do not skip either step.",
462
481
  });
463
482
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.3.41",
3
+ "version": "1.3.42",
4
4
  "description": "Antenna — nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -38,9 +38,9 @@ Plugin 安装后,agent **主动**开始引导,不要等用户问。
38
38
  **第一步:拿到 API key → 配置**
39
39
  > "嘿,你装了 Antenna--它能帮你发现有意思的人。先确认一下,你在 antenna.fyi 注册过了吗?拿到 API key 了吗?"
40
40
 
41
- 用户给了 API key 后,调 `antenna config --key <key>` 验证。这会返回 `user_id` 和 `device_id`。
41
+ 用户给了 API key 后,调 `antenna config --key <key>` 验证。这会返回 `user_id` 和 dashboard 绑定的 `device_id`。
42
42
 
43
- **⚠️ 之后所有操作必须用 API key 验证返回的 device_id(格式 `user:xxx`)。不要自己拼 `channel:sender_id`。** 这样 agent 创建的 profile 才能在 dashboard 上显示。
43
+ **⚠️ Profile 写入必须通过用户的 Antenna API key。** `antenna_profile(action="set")` 时传 `api_key`,tool 会验证 API key 并使用返回的 dashboard `device_id`(格式 `user:xxx`) 写入。不要自己拼 `channel:sender_id`,不要在用户拿到 API key 前凭空创建 profile。这样 agent 填的内容才会显示在 dashboard
44
44
 
45
45
  **第二步:聊天收集 → 生成名片 → 确认**
46
46
 
@@ -58,9 +58,9 @@ Plugin 安装后,agent **主动**开始引导,不要等用户问。
58
58
  >
59
59
  > 这样可以吗?要改哪里告诉我。
60
60
 
61
- 确认后用 config 里的 device_id 调 `antenna_profile(action="set")` 保存。**不要跳过确认。**
61
+ 确认后调 `antenna_profile(action="set", api_key="ant_xxx")` 保存。**不要跳过确认。**
62
62
 
63
- **⚠️ sender_id config 里的 device_id,不要用 channel:sender_id。**
63
+ **⚠️ 保存 profile 时以 API key 为准;sender_id/channel 只用于上下文,不作为 profile 归属来源。**
64
64
 
65
65
  **第三步:立刻推荐 2-3 个人**
66
66
 
@@ -96,12 +96,12 @@ openclaw cron add --every 1h --message "Check antenna matches: call antenna_chec
96
96
 
97
97
  ### Linking to antenna.fyi account
98
98
 
99
- 如果用户之前通过 agent 创建过 profile(没有网站账号),现在注册了 antenna.fyi:
99
+ Legacy only: 如果用户之前通过旧版 agent 创建过 profile(没有网站账号),现在注册了 antenna.fyi:
100
100
  1. 让用户从 antenna.fyi/me 复制 API key
101
101
  2. 调 `antenna_link_account(api_key = "ant_xxx")`
102
102
  3. 确认:"关联成功!你现在可以在 dashboard 上看到完整的 profile 和匹配记录了。"
103
103
 
104
- 这把 agent 创建的 profile(带全部历史)关联到网站账号。
104
+ 这只用于迁移旧数据。新流程禁止先创建 agent-only profile 再绑定;必须先拿 API key,再通过 API 写入 dashboard profile
105
105
 
106
106
  ## When to use
107
107
 
@@ -164,6 +164,7 @@ openclaw cron add --every 1h --message "Check antenna matches: call antenna_chec
164
164
  - `action`:"get" 或 "set"
165
165
  - `sender_id`, `channel`, `chat_id`
166
166
  - "set" 时传:`display_name`, `personal_description`, `looking_for`, `conversation_style`, `visible`, `matching_context`
167
+ - "set" 必须传 `api_key`。tool 会验证 key,并写入 API key 绑定的 dashboard profile;不要用 `sender_id/channel` 创建独立 profile。
167
168
 
168
169
  名片内容:
169
170
  - **display_name**:显示名称