twindex-openclaw-plugin 0.5.6 → 0.6.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twindex-openclaw-plugin",
3
- "version": "0.5.6",
3
+ "version": "0.6.1",
4
4
  "description": "Music intelligence for AI agents. Tours, merch drops, releases, presales.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,6 +10,8 @@ tools:
10
10
  - twindex_artist
11
11
  - twindex_search
12
12
  - twindex_artists
13
+ - twindex_upload_photo
14
+ - twindex_try_on
13
15
  ---
14
16
 
15
17
  # Twindex — Music Intelligence
@@ -44,6 +46,18 @@ merch, releases, bio).
44
46
 
45
47
  Use `twindex_subscriptions` to see what the user is currently following.
46
48
 
49
+ ## Merch Try-On
50
+
51
+ When a user sends you a photo/selfie:
52
+ - Call `twindex_upload_photo` with the photo URL from their message
53
+ - Confirm their photo is saved
54
+
55
+ When a user asks to "try on" merch, or you're showing merch and want to offer:
56
+ - Call `twindex_try_on` with the artist slug and product name
57
+ - Show the generated image and buy link
58
+
59
+ If try-on returns a 404 (no photo), ask the user to send a selfie first.
60
+
47
61
  ## Important
48
62
 
49
63
  - Do NOT create cron jobs, scheduled tasks, or shell scripts.
package/src/client.ts CHANGED
@@ -21,6 +21,7 @@ interface Notification {
21
21
  summary: string;
22
22
  twindex_url?: string;
23
23
  detail_url?: string;
24
+ media_url?: string;
24
25
  created_at: string;
25
26
  read_at?: string | null;
26
27
  }
@@ -166,3 +167,33 @@ export async function getArtist(slug: string): Promise<string> {
166
167
  }
167
168
  return res.text();
168
169
  }
170
+
171
+ interface TryOnResult {
172
+ image_url: string;
173
+ product: { title: string; price: string; buy_url: string };
174
+ }
175
+
176
+ /** Upload a selfie for personalized merch try-on. */
177
+ export async function uploadPhoto(
178
+ apiKey: string,
179
+ photoUrl: string,
180
+ ): Promise<void> {
181
+ await request("/api/v1/me/photo", {
182
+ method: "POST",
183
+ headers: authHeader(apiKey),
184
+ body: JSON.stringify({ photo_url: photoUrl }),
185
+ });
186
+ }
187
+
188
+ /** Generate an image of the user wearing merch. */
189
+ export async function tryOn(
190
+ apiKey: string,
191
+ artist: string,
192
+ productName: string,
193
+ ): Promise<TryOnResult> {
194
+ return request("/api/v1/try-on", {
195
+ method: "POST",
196
+ headers: authHeader(apiKey),
197
+ body: JSON.stringify({ artist, product_name: productName }),
198
+ });
199
+ }
package/src/index.ts CHANGED
@@ -36,6 +36,32 @@ export default function register(api: any) {
36
36
  const pollService = createNotificationService(api);
37
37
  api.registerService?.(pollService);
38
38
 
39
+ // ── Auto-detect chatTarget from Telegram channel config ─────────
40
+ function inferChatTarget(): string | undefined {
41
+ const config = cfg();
42
+ if (config.chatTarget) return config.chatTarget;
43
+
44
+ // Infer from Telegram allowFrom list (first allowed chat ID)
45
+ const telegramAllow = api.config?.channels?.telegram?.allowFrom;
46
+ if (Array.isArray(telegramAllow) && telegramAllow.length > 0) {
47
+ return String(telegramAllow[0]);
48
+ }
49
+
50
+ // Infer from Slack channel config
51
+ const slackChannel = api.config?.channels?.slack?.channel;
52
+ if (slackChannel) return slackChannel;
53
+
54
+ return undefined;
55
+ }
56
+
57
+ function inferChatChannel(): string {
58
+ const config = cfg();
59
+ if (config.chatChannel) return config.chatChannel;
60
+ if (api.config?.channels?.telegram?.allowFrom?.length > 0) return "telegram";
61
+ if (api.config?.channels?.slack?.channel) return "slack";
62
+ return "telegram";
63
+ }
64
+
39
65
  // ── Auto-bootstrap: register + subscribe ────────────────────────
40
66
 
41
67
  (async () => {
@@ -60,7 +86,16 @@ export default function register(api: any) {
60
86
  }
61
87
  }
62
88
 
63
- persistConfig({ apiKey });
89
+ // Auto-detect chatTarget from channel config if not explicitly set
90
+ const updates: Record<string, any> = { apiKey };
91
+ const chatTarget = inferChatTarget();
92
+ if (chatTarget) {
93
+ updates.chatTarget = chatTarget;
94
+ updates.chatChannel = inferChatChannel();
95
+ api.logger?.info?.(`Twindex: auto-detected chatTarget=${chatTarget}`);
96
+ }
97
+
98
+ persistConfig(updates);
64
99
  pollService.start();
65
100
  api.logger?.info?.(
66
101
  "Twindex: auto-bootstrap complete. Delivery via polling.",
@@ -419,6 +454,113 @@ export default function register(api: any) {
419
454
  },
420
455
  });
421
456
 
457
+ api.registerTool({
458
+ name: "twindex_upload_photo",
459
+ description:
460
+ "Upload the user's photo for personalized merch try-on. The user sends a selfie in chat, and you pass the photo URL here. One photo per user — uploading replaces the previous one.",
461
+ parameters: {
462
+ type: "object",
463
+ properties: {
464
+ photo_url: {
465
+ type: "string",
466
+ description:
467
+ "URL of the user's photo (from the media attachment in their message)",
468
+ },
469
+ },
470
+ required: ["photo_url"],
471
+ },
472
+ async execute(_id: string, params: { photo_url: string }) {
473
+ const apiKey = cfg().apiKey;
474
+ if (!apiKey) {
475
+ return {
476
+ content: [
477
+ { type: "text", text: "Not set up yet. Use twindex_setup first." },
478
+ ],
479
+ };
480
+ }
481
+
482
+ try {
483
+ await twindex.uploadPhoto(apiKey, params.photo_url);
484
+ return {
485
+ content: [
486
+ {
487
+ type: "text",
488
+ text: "Photo saved! I can now show you how merch looks on you.",
489
+ },
490
+ ],
491
+ };
492
+ } catch (err: any) {
493
+ return {
494
+ content: [
495
+ {
496
+ type: "text",
497
+ text: `Failed to upload photo: ${err.message}`,
498
+ },
499
+ ],
500
+ };
501
+ }
502
+ },
503
+ });
504
+
505
+ api.registerTool({
506
+ name: "twindex_try_on",
507
+ description:
508
+ "Generate an image of the user wearing a merch product. Requires a photo to be uploaded first via twindex_upload_photo. Use after the user asks to 'try on' a product or when showing merch recommendations.",
509
+ parameters: {
510
+ type: "object",
511
+ properties: {
512
+ artist: {
513
+ type: "string",
514
+ description: "Artist slug (lowercase, hyphens). Example: metallica",
515
+ },
516
+ product_name: {
517
+ type: "string",
518
+ description: "Name of the merch product to try on",
519
+ },
520
+ },
521
+ required: ["artist", "product_name"],
522
+ },
523
+ async execute(
524
+ _id: string,
525
+ params: { artist: string; product_name: string },
526
+ ) {
527
+ const apiKey = cfg().apiKey;
528
+ if (!apiKey) {
529
+ return {
530
+ content: [
531
+ { type: "text", text: "Not set up yet. Use twindex_setup first." },
532
+ ],
533
+ };
534
+ }
535
+
536
+ try {
537
+ const result = await twindex.tryOn(
538
+ apiKey,
539
+ params.artist,
540
+ params.product_name,
541
+ );
542
+ return {
543
+ content: [
544
+ {
545
+ type: "text",
546
+ text: `Here's you in the ${result.product.title} ($${result.product.price})!\nBuy it: ${result.product.buy_url}`,
547
+ },
548
+ { type: "image", url: result.image_url },
549
+ ],
550
+ };
551
+ } catch (err: any) {
552
+ return {
553
+ content: [
554
+ {
555
+ type: "text",
556
+ text: `Failed to generate try-on: ${err.message}`,
557
+ },
558
+ ],
559
+ };
560
+ }
561
+ },
562
+ });
563
+
422
564
  api.registerTool({
423
565
  name: "twindex_artists",
424
566
  description: "List all artists currently indexed on Twindex.",
@@ -42,10 +42,19 @@ export function createNotificationService(api: any) {
42
42
 
43
43
  function getChatTarget(): string | undefined {
44
44
  const raw = api.config?.plugins?.entries?.twindex?.config?.chatTarget;
45
- if (!raw) return undefined;
46
- // Strip channel prefix if agent included it (e.g. "telegram:123" → "123")
47
- const colonIdx = raw.indexOf(":");
48
- return colonIdx >= 0 ? raw.slice(colonIdx + 1) : raw;
45
+ if (raw) {
46
+ // Strip channel prefix if agent included it (e.g. "telegram:123" → "123")
47
+ const colonIdx = raw.indexOf(":");
48
+ return colonIdx >= 0 ? raw.slice(colonIdx + 1) : raw;
49
+ }
50
+
51
+ // Fallback: infer from Telegram allowFrom
52
+ const telegramAllow = api.config?.channels?.telegram?.allowFrom;
53
+ if (Array.isArray(telegramAllow) && telegramAllow.length > 0) {
54
+ return String(telegramAllow[0]);
55
+ }
56
+
57
+ return undefined;
49
58
  }
50
59
 
51
60
  function getChatChannel(): string {
@@ -53,7 +62,14 @@ export function createNotificationService(api: any) {
53
62
  // Infer channel from prefix if present (e.g. "telegram:123" → "telegram")
54
63
  const colonIdx = raw.indexOf(":");
55
64
  if (colonIdx > 0) return raw.slice(0, colonIdx);
56
- return api.config?.plugins?.entries?.twindex?.config?.chatChannel ?? "telegram";
65
+
66
+ const explicit = api.config?.plugins?.entries?.twindex?.config?.chatChannel;
67
+ if (explicit) return explicit;
68
+
69
+ // Fallback: infer from which channel is configured
70
+ if (api.config?.channels?.telegram?.allowFrom?.length > 0) return "telegram";
71
+ if (api.config?.channels?.slack?.channel) return "slack";
72
+ return "telegram";
57
73
  }
58
74
 
59
75
  const logger = api.logger;
@@ -83,8 +99,10 @@ export function createNotificationService(api: any) {
83
99
  logger?.info?.(`Twindex: ${unread.length} unread notification(s)`);
84
100
 
85
101
  for (const notif of unread) {
86
- const message =
87
- `🎸 ${notif.brand} — ${notif.event_type}\n${notif.summary}`;
102
+ let message = `🎸 ${notif.brand} — ${notif.event_type}\n${notif.summary}`;
103
+ if (notif.media_url) {
104
+ message += `\n\nHere's how it looks on you: ${notif.media_url}`;
105
+ }
88
106
 
89
107
  const delivered = await sendMessage(message, channel, target);
90
108
  if (delivered) {