antenna-fyi 1.3.11 โ†’ 1.3.13

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
@@ -13,6 +13,7 @@ import {
13
13
  handlePass,
14
14
  handleWatch,
15
15
  handleSetup,
16
+ handleConfig,
16
17
  handleStatus,
17
18
  handleInstallSkill,
18
19
  handleInstallPlugin,
@@ -51,6 +52,8 @@ async function main() {
51
52
  }
52
53
  case "setup":
53
54
  return handleSetup(f);
55
+ case "config":
56
+ return handleConfig(f);
54
57
  case "status":
55
58
  return handleStatus(f);
56
59
  case "install-skill":
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, updateEvent, approveParticipant, rejectParticipant, addCohost, sendEventMessage, getMyEventMessages, getClient } from "./core.js";
3
+ import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover, createEvent, endEvent, eventCheckin, joinEvent, eventScan, pass as passUser, uploadEventImage, updateEvent, approveParticipant, rejectParticipant, addCohost, sendEventMessage, getMyEventMessages, getClient, verifyApiKey } from "./core.js";
4
4
  import { createInterface } from "readline";
5
5
  import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "fs";
6
6
  import path from "path";
@@ -31,12 +31,13 @@ export function parseFlags(args) {
31
31
  }
32
32
 
33
33
  export async function handleScan(f) {
34
- if (!f.lat && !f.lng && !f.id) return console.error("Usage: antenna scan --lat 39.99 --lng 116.48 [--radius 500] (max 1000) [--id <platform>:<user_id>]\n Or just: antenna scan --id <platform>:<user_id> (uses saved location from GPS bind)");
34
+ const _scanId = resolveId(f);
35
+ if (!f.lat && !f.lng && !_scanId) return console.error("Usage: antenna scan --lat 39.99 --lng 116.48 [--radius 500] (max 1000) [--id <platform>:<user_id>]\n Or just: antenna scan --id <platform>:<user_id> (uses saved location from GPS bind)");
35
36
  const result = await scan({
36
37
  lat: f.lat ? +f.lat : undefined,
37
38
  lng: f.lng ? +f.lng : undefined,
38
39
  radius_m: +(f.radius || 500),
39
- device_id: f.id || null,
40
+ device_id: _scanId || null,
40
41
  });
41
42
  if (result.count === 0) return console.log(result.message || "๐Ÿ“ก No one nearby");
42
43
  if (result.global) {
@@ -64,10 +65,11 @@ export async function handleScan(f) {
64
65
  }
65
66
 
66
67
  export async function handleProfile(f) {
67
- if (!f.id) return console.error("Usage: antenna profile --id <platform>:<user_id> [--name Yi --emoji ๐Ÿฆฆ --line1 '...' --line2 '...' --line3 '...']");
68
+ const id = resolveId(f);
69
+ if (!id) return console.error("Usage: antenna profile --id <platform>:<user_id> [--name Yi --emoji ๐Ÿฆฆ --line1 '...' --line2 '...' --line3 '...']");
68
70
  if (f.name || f.line1 || f.line2 || f.line3 || f.visible !== undefined || f.hide !== undefined) {
69
71
  const visible = f.hide ? false : (f.visible !== undefined ? f.visible === 'true' || f.visible === true : undefined);
70
- const payload = { device_id: f.id };
72
+ const payload = { device_id: id };
71
73
  if (f.name) payload.display_name = f.name;
72
74
  if (f.emoji) payload.emoji = f.emoji;
73
75
  if (f.line1 !== undefined) payload.line1 = f.line1;
@@ -78,7 +80,7 @@ export async function handleProfile(f) {
78
80
  console.log("โœ… Profile saved");
79
81
  console.log(JSON.stringify(data, null, 2));
80
82
  } else {
81
- const data = await getProfile({ device_id: f.id });
83
+ const data = await getProfile({ device_id: id });
82
84
  if (!data) return console.log("No profile yet. Create one with --name and --line1/2/3");
83
85
  console.log(`${data.emoji || "๐Ÿ‘ค"} ${data.display_name || "Anonymous"}`);
84
86
  if (data.line1) console.log(` ${data.line1}`);
@@ -88,9 +90,10 @@ export async function handleProfile(f) {
88
90
  }
89
91
 
90
92
  export async function handleAccept(f) {
91
- if (!f.id || (!f.target && !f.ref)) return console.error("Usage: antenna accept --id <platform>:<user_id> --ref 1 [--contact 'WeChat: yi']\n antenna accept --id <platform>:<user_id> --target <ref_or_device_id> [--contact 'WeChat: yi']");
93
+ const id = resolveId(f);
94
+ if (!id || (!f.target && !f.ref)) return console.error("Usage: antenna accept --id <platform>:<user_id> --ref 1 [--contact 'WeChat: yi']\n antenna accept --id <platform>:<user_id> --target <ref_or_device_id> [--contact 'WeChat: yi']");
92
95
  const result = await accept({
93
- device_id: f.id,
96
+ device_id: id,
94
97
  target_device_id: f.target || null,
95
98
  ref: f.ref || null,
96
99
  contact_info: f.contact,
@@ -100,18 +103,20 @@ export async function handleAccept(f) {
100
103
  }
101
104
 
102
105
  export async function handleCheckin(f) {
103
- if (!f.id || !f.lat || !f.lng) return console.error("Usage: antenna checkin --id <platform>:<user_id> --lat 39.99 --lng 116.48 [--place 'ไธ‰้‡Œๅฑฏ']");
106
+ const id = resolveId(f);
107
+ if (!id || !f.lat || !f.lng) return console.error("Usage: antenna checkin --id <platform>:<user_id> --lat 39.99 --lng 116.48 [--place 'ไธ‰้‡Œๅฑฏ']");
104
108
  const result = await checkin({
105
109
  lat: +f.lat,
106
110
  lng: +f.lng,
107
- device_id: f.id,
111
+ device_id: id,
108
112
  });
109
113
  console.log(result.checked_in ? "โœ… " + result.message : "โŒ " + result.message);
110
114
  }
111
115
 
112
116
  export async function handleMatches(f) {
113
- if (!f.id) return console.error("Usage: antenna matches --id <platform>:<user_id>");
114
- const result = await checkMatches({ device_id: f.id });
117
+ const id = resolveId(f);
118
+ if (!id) return console.error("Usage: antenna matches --id <platform>:<user_id>");
119
+ const result = await checkMatches({ device_id: id });
115
120
  if (!result.mutual_matches.length && !result.incoming_accepts.length) {
116
121
  return console.log(result.message);
117
122
  }
@@ -130,8 +135,9 @@ export async function handleMatches(f) {
130
135
  }
131
136
 
132
137
  export async function handleDiscover(f) {
133
- if (!f.id) return console.error("Usage: antenna discover --id <platform>:<user_id>");
134
- const result = await discover({ device_id: f.id });
138
+ const id = resolveId(f);
139
+ if (!id) return console.error("Usage: antenna discover --id <platform>:<user_id>");
140
+ const result = await discover({ device_id: id });
135
141
  if (result.count === 0) return console.log(result.message || "๐ŸŒ No global recommendation available right now.");
136
142
  console.log(`๐ŸŒ Global discover:\n`);
137
143
  result.profiles.forEach((p) => {
@@ -145,6 +151,7 @@ export async function handleDiscover(f) {
145
151
  }
146
152
 
147
153
  export async function handleEvent(f) {
154
+ f.id = f.id || resolveId(f);
148
155
  const sub = f._?.[0] || Object.keys(f).find(k => ["create", "join", "scan", "end", "checkin", "upload-image"].includes(k));
149
156
 
150
157
  if (f['upload-image']) {
@@ -180,7 +187,8 @@ export async function handleEvent(f) {
180
187
 
181
188
  if (f.create || (!f.join && !f.scan && !f.end && !f.update && !f.approve && !f.reject && !f['add-host'] && f.name)) {
182
189
  if (!f.name) return console.error("Usage: antenna event --create --name 'AI Meetup' --id <platform>:<user_id> --starts-at '2026-04-19T14:00' --ends-at '2026-04-19T18:00' [--lat 34.05 --lng -118.25] [--desc 'description'] [--og-image 'url'] [--requires-approval] [--screening-questions 'Q1|Q2']");
183
- if (!f.id) return console.error("โŒ --id is required (e.g. --id <platform>:<user_id>). Creator identity needed to manage the event.");
190
+ const id = resolveId(f);
191
+ if (!id) return console.error("โŒ --id is required (e.g. --id <platform>:<user_id>). Creator identity needed to manage the event.");
184
192
  if (!f['starts-at'] || !f['ends-at']) return console.error("โŒ --starts-at and --ends-at are required. Example: --starts-at '2026-04-19T14:00' --ends-at '2026-04-19T18:00'");
185
193
  const result = await createEvent({ name: f.name, device_id: f.id, lat: f.lat ? +f.lat : undefined, lng: f.lng ? +f.lng : undefined, starts_at: f['starts-at'], ends_at: f['ends-at'], description: f.desc || undefined, og_image: f['og-image'] || undefined, requires_approval: f['requires-approval'] === true || f['requires-approval'] === 'true' || undefined, screening_questions: f['screening-questions'] ? f['screening-questions'].split('|') : undefined });
186
194
  console.log(`\n๐ŸŽ‰ Event created!\n`);
@@ -294,8 +302,9 @@ export async function handleEvent(f) {
294
302
  }
295
303
 
296
304
  export async function handleBind(f) {
297
- if (!f.id) return console.error("Usage: antenna bind --id <platform>:<user_id>");
298
- const result = await createBindToken({ device_id: f.id });
305
+ const id = resolveId(f);
306
+ if (!id) return console.error("Usage: antenna bind --id <platform>:<user_id>");
307
+ const result = await createBindToken({ device_id: id });
299
308
  console.log("\n๐Ÿ”— GPS Binding Link:\n");
300
309
  console.log(` ${result.url}\n`);
301
310
  console.log("Send this to the user. Opening it on their phone will share GPS with their agent.");
@@ -303,10 +312,11 @@ export async function handleBind(f) {
303
312
  }
304
313
 
305
314
  export async function handlePass(f) {
306
- if (!f.id) return console.error("Usage: antenna pass --id <platform>:<user_id> --target <ref_or_device_id>");
315
+ const id = resolveId(f);
316
+ if (!id) return console.error("Usage: antenna pass --id <platform>:<user_id> --target <ref_or_device_id>");
307
317
  if (!f.target && !f.ref) return console.error("Usage: antenna pass --id <platform>:<user_id> --target <ref_or_device_id> (or --ref 1)");
308
318
  const result = await passUser({
309
- device_id: f.id,
319
+ device_id: id,
310
320
  target_device_id: f.target,
311
321
  ref: f.ref,
312
322
  });
@@ -347,6 +357,70 @@ export async function handleSetup(f) {
347
357
  console.log();
348
358
  }
349
359
 
360
+ // โ”€โ”€โ”€ Config file helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
361
+
362
+ const CONFIG_DIR = path.join(os.homedir(), '.antenna');
363
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
364
+
365
+ export function loadConfig() {
366
+ try {
367
+ if (existsSync(CONFIG_FILE)) {
368
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
369
+ }
370
+ } catch {}
371
+ return {};
372
+ }
373
+
374
+ function saveConfig(config) {
375
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
376
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
377
+ }
378
+
379
+ /** Resolve device_id: --id flag > config file > null */
380
+ function resolveId(f) {
381
+ if (f.id) return f.id;
382
+ const config = loadConfig();
383
+ if (config.device_id) return config.device_id;
384
+ return null;
385
+ }
386
+
387
+ export async function handleConfig(f) {
388
+ if (f.key) {
389
+ // Verify the key against Supabase
390
+ console.log('\n๐Ÿ“ก Verifying API key...');
391
+ try {
392
+ const result = await verifyApiKey({ key: f.key });
393
+ if (!result.valid) {
394
+ console.error(`\nโŒ ${result.error || 'Invalid API key'}`);
395
+ process.exit(1);
396
+ }
397
+ const config = loadConfig();
398
+ config.key = f.key;
399
+ config.device_id = result.device_id;
400
+ config.display_name = result.display_name;
401
+ saveConfig(config);
402
+ console.log(`\nโœ… Authenticated as ${result.display_name || 'Antenna user'}`);
403
+ console.log(` Device ID: ${result.device_id}`);
404
+ console.log(` Config saved to ~/.antenna/config.json\n`);
405
+ } catch (e) {
406
+ console.error(`\nโŒ Failed to verify key: ${e.message}`);
407
+ process.exit(1);
408
+ }
409
+ } else {
410
+ // Show current config
411
+ const config = loadConfig();
412
+ if (config.key) {
413
+ console.log('\n๐Ÿ“ก Antenna Config\n');
414
+ console.log(` Key: ${config.key.slice(0, 10)}...${config.key.slice(-4)}`);
415
+ console.log(` Device ID: ${config.device_id || '(unknown)'}`);
416
+ console.log(` Name: ${config.display_name || '(unknown)'}\n`);
417
+ } else {
418
+ console.log('\n๐Ÿ“ก No API key configured.');
419
+ console.log(' Run: antenna config --key <your-api-key>\n');
420
+ }
421
+ }
422
+ }
423
+
350
424
  export async function handleStatus(f) {
351
425
  const supabaseUrl = process.env.ANTENNA_SUPABASE_URL || process.env.ANTENNA_URL || "https://bcudjloikmpcqwcptuyd.supabase.co";
352
426
  console.log("\n๐Ÿ“ก Antenna Status\n");
@@ -907,6 +981,7 @@ Usage:
907
981
  antenna bind --id <platform>:<user_id>
908
982
  antenna serve Start MCP server (stdio transport)
909
983
  antenna setup Interactive profile setup [--id <platform>:<user_id>]
984
+ antenna config Configure API key [--key <ant_...>]
910
985
  antenna status Show config & status [--id <platform>:<user_id>]
911
986
  antenna install-skill Install SKILL.md (detects OpenClaw + Hermes)
912
987
  antenna install-plugin Copy OpenClaw plugin template to cwd
package/lib/core.js CHANGED
@@ -189,6 +189,10 @@ export const PROFILE_FIELDS = {
189
189
  line2: { label: "ๆƒณ่ฎค่ฏ†็š„ไบบ", description: "The kind of people you want to meet", maxLength: 140 },
190
190
  line3: { label: "ๆƒณ่ฆ็š„ไบคๆตๆ–นๅผ", description: "The type of conversations you want", maxLength: 160 },
191
191
  matching_context: { label: "ๅŒน้…ไธŠไธ‹ๆ–‡", description: "Agent-generated rich context for better matching (not shown to others)", maxLength: 1000 },
192
+ interest_tags: { label: "ๅ…ด่ถฃๆ ‡็ญพ", description: "Interest/topic tags shown on the card (up to 8)", maxItems: 8 },
193
+ city: { label: "ๅ›ฝๅฎถ/ๅœฐๅŒบ", description: "Country or region" },
194
+ links: { label: "็คพไบค้“พๆŽฅ", description: "Social links shown on the card footer (up to 3)", maxItems: 3 },
195
+ is_active: { label: "็Šถๆ€", description: "Whether the profile is active or quiet" },
192
196
  };
193
197
 
194
198
  // โ”€โ”€โ”€ getProfile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -197,7 +201,21 @@ export async function getProfile({ device_id, supabaseUrl, supabaseKey }) {
197
201
  const sb = getClient(supabaseUrl, supabaseKey);
198
202
  const { data, error } = await sb.rpc("get_profile", { p_device_id: device_id });
199
203
  if (error) throw new Error(error.message);
200
- return data || null;
204
+ if (!data) return null;
205
+
206
+ // Unpack matching_context JSON into top-level fields for easier agent consumption
207
+ if (data.matching_context) {
208
+ try {
209
+ const ctx = JSON.parse(data.matching_context);
210
+ data.interest_tags = ctx.interestTags || [];
211
+ data.city = ctx.city || null;
212
+ data.links = ctx.links || [];
213
+ data.is_active = typeof ctx.isActive === "boolean" ? ctx.isActive : true;
214
+ data.archetype_override = ctx.archetypeOverride || null;
215
+ data.context = ctx.context || null;
216
+ } catch {}
217
+ }
218
+ return data;
201
219
  }
202
220
 
203
221
  // โ”€โ”€โ”€ setProfile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -211,10 +229,42 @@ export async function setProfile({
211
229
  line3,
212
230
  matching_context,
213
231
  visible = true,
232
+ interest_tags,
233
+ city,
234
+ links,
235
+ profile_slug,
236
+ is_active,
237
+ archetype_override,
214
238
  supabaseUrl,
215
239
  supabaseKey,
216
240
  }) {
217
241
  const sb = getClient(supabaseUrl, supabaseKey);
242
+
243
+ // Pack structured fields into matching_context JSON
244
+ let contextJson = matching_context;
245
+ if (interest_tags || city || links || is_active !== undefined || archetype_override !== undefined) {
246
+ let existing = {};
247
+ // If matching_context is already JSON, parse and merge
248
+ if (matching_context) {
249
+ try { existing = JSON.parse(matching_context); } catch { existing = { context: matching_context }; }
250
+ } else {
251
+ // Read existing matching_context from DB to merge
252
+ try {
253
+ const current = await getProfile({ device_id, supabaseUrl, supabaseKey });
254
+ if (current?.matching_context) {
255
+ try { existing = JSON.parse(current.matching_context); } catch { existing = {}; }
256
+ }
257
+ } catch {}
258
+ }
259
+ if (interest_tags) existing.interestTags = interest_tags;
260
+ if (city) existing.city = city;
261
+ if (links) existing.links = links;
262
+ if (is_active !== undefined) existing.isActive = is_active;
263
+ if (archetype_override !== undefined) existing.archetypeOverride = archetype_override;
264
+ existing.version = existing.version || 1;
265
+ contextJson = JSON.stringify(existing);
266
+ }
267
+
218
268
  const { data, error } = await sb.rpc("upsert_profile", {
219
269
  p_device_id: device_id,
220
270
  p_display_name: display_name || null,
@@ -223,7 +273,7 @@ export async function setProfile({
223
273
  p_line2: line2 || null,
224
274
  p_line3: line3 || null,
225
275
  p_visible: visible,
226
- p_matching_context: matching_context || null,
276
+ p_matching_context: contextJson || null,
227
277
  });
228
278
  if (error) throw new Error(error.message);
229
279
 
@@ -735,3 +785,12 @@ export async function getMyEventMessages({ device_id, supabaseUrl, supabaseKey }
735
785
  count: (data || []).length,
736
786
  };
737
787
  }
788
+
789
+ // โ”€โ”€โ”€ verifyApiKey โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
790
+
791
+ export async function verifyApiKey({ key, supabaseUrl, supabaseKey }) {
792
+ const sb = getClient(supabaseUrl, supabaseKey);
793
+ const { data, error } = await sb.rpc("verify_api_key", { p_key: key });
794
+ if (error) throw new Error(error.message);
795
+ return data;
796
+ }
package/lib/mcp.js CHANGED
@@ -116,9 +116,13 @@ export async function startMcpServer() {
116
116
  line2: z.string().optional().describe("Looking for โ€” the kind of people you want to meet (max 140 chars)"),
117
117
  line3: z.string().optional().describe("Conversation style โ€” the type of conversations you want (max 160 chars)"),
118
118
  matching_context: z.string().optional().describe("Agent-generated rich context for embedding-based matching (not shown to others, max 1000 chars). Generate this FIRST, then derive the three descriptions from it."),
119
+ interest_tags: z.array(z.string()).optional().describe("Interest/topic tags shown on the card (up to 8)"),
120
+ city: z.string().optional().describe("Country or region (e.g. 'United States', 'Beijing')"),
121
+ links: z.array(z.string()).optional().describe("Social links shown on the card footer (up to 3)"),
122
+ is_active: z.boolean().optional().describe("Whether the profile is active or quiet"),
119
123
  visible: z.boolean().optional().default(true),
120
124
  },
121
- async ({ action, sender_id, channel, display_name, emoji, line1, line2, line3, matching_context, visible }) => {
125
+ async ({ action, sender_id, channel, display_name, emoji, line1, line2, line3, matching_context, interest_tags, city, links, is_active, visible }) => {
122
126
  const deviceId = deriveDeviceId(sender_id, channel);
123
127
  try {
124
128
  if (action === "get") {
@@ -128,7 +132,7 @@ export async function startMcpServer() {
128
132
  : { profile: null, message: "่ฟ˜ๆฒกๆœ‰ๅ็‰‡ใ€‚่ทŸ็”จๆˆท่Š่Šไป–ไปฌๆ˜ฏ่ฐใ€ๅšไป€ไนˆใ€ๆƒณ่ฎค่ฏ†ไป€ไนˆไบบ๏ผŒ็„ถๅŽๅธฎไป–ไปฌๅˆ›ๅปบใ€‚", fields: PROFILE_FIELDS };
129
133
  return jsonResult(await withMatchNotifications(deviceId, result));
130
134
  }
131
- const data = await setProfile({ device_id: deviceId, display_name, emoji, line1, line2, line3, matching_context, visible });
135
+ const data = await setProfile({ device_id: deviceId, display_name, emoji, line1, line2, line3, matching_context, interest_tags, city, links, is_active, visible });
132
136
  return jsonResult(await withMatchNotifications(deviceId, { saved: true, profile: data }));
133
137
  } catch (e) {
134
138
  return jsonResult({ error: e.message });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.3.11",
3
+ "version": "1.3.13",
4
4
  "description": "Antenna \u2014 nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {