twindex-openclaw-plugin 0.5.6 → 0.6.0

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.0",
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
@@ -419,6 +419,113 @@ export default function register(api: any) {
419
419
  },
420
420
  });
421
421
 
422
+ api.registerTool({
423
+ name: "twindex_upload_photo",
424
+ description:
425
+ "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.",
426
+ parameters: {
427
+ type: "object",
428
+ properties: {
429
+ photo_url: {
430
+ type: "string",
431
+ description:
432
+ "URL of the user's photo (from the media attachment in their message)",
433
+ },
434
+ },
435
+ required: ["photo_url"],
436
+ },
437
+ async execute(_id: string, params: { photo_url: string }) {
438
+ const apiKey = cfg().apiKey;
439
+ if (!apiKey) {
440
+ return {
441
+ content: [
442
+ { type: "text", text: "Not set up yet. Use twindex_setup first." },
443
+ ],
444
+ };
445
+ }
446
+
447
+ try {
448
+ await twindex.uploadPhoto(apiKey, params.photo_url);
449
+ return {
450
+ content: [
451
+ {
452
+ type: "text",
453
+ text: "Photo saved! I can now show you how merch looks on you.",
454
+ },
455
+ ],
456
+ };
457
+ } catch (err: any) {
458
+ return {
459
+ content: [
460
+ {
461
+ type: "text",
462
+ text: `Failed to upload photo: ${err.message}`,
463
+ },
464
+ ],
465
+ };
466
+ }
467
+ },
468
+ });
469
+
470
+ api.registerTool({
471
+ name: "twindex_try_on",
472
+ description:
473
+ "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.",
474
+ parameters: {
475
+ type: "object",
476
+ properties: {
477
+ artist: {
478
+ type: "string",
479
+ description: "Artist slug (lowercase, hyphens). Example: metallica",
480
+ },
481
+ product_name: {
482
+ type: "string",
483
+ description: "Name of the merch product to try on",
484
+ },
485
+ },
486
+ required: ["artist", "product_name"],
487
+ },
488
+ async execute(
489
+ _id: string,
490
+ params: { artist: string; product_name: string },
491
+ ) {
492
+ const apiKey = cfg().apiKey;
493
+ if (!apiKey) {
494
+ return {
495
+ content: [
496
+ { type: "text", text: "Not set up yet. Use twindex_setup first." },
497
+ ],
498
+ };
499
+ }
500
+
501
+ try {
502
+ const result = await twindex.tryOn(
503
+ apiKey,
504
+ params.artist,
505
+ params.product_name,
506
+ );
507
+ return {
508
+ content: [
509
+ {
510
+ type: "text",
511
+ text: `Here's you in the ${result.product.title} ($${result.product.price})!\nBuy it: ${result.product.buy_url}`,
512
+ },
513
+ { type: "image", url: result.image_url },
514
+ ],
515
+ };
516
+ } catch (err: any) {
517
+ return {
518
+ content: [
519
+ {
520
+ type: "text",
521
+ text: `Failed to generate try-on: ${err.message}`,
522
+ },
523
+ ],
524
+ };
525
+ }
526
+ },
527
+ });
528
+
422
529
  api.registerTool({
423
530
  name: "twindex_artists",
424
531
  description: "List all artists currently indexed on Twindex.",
@@ -83,8 +83,10 @@ export function createNotificationService(api: any) {
83
83
  logger?.info?.(`Twindex: ${unread.length} unread notification(s)`);
84
84
 
85
85
  for (const notif of unread) {
86
- const message =
87
- `🎸 ${notif.brand} — ${notif.event_type}\n${notif.summary}`;
86
+ let message = `🎸 ${notif.brand} — ${notif.event_type}\n${notif.summary}`;
87
+ if (notif.media_url) {
88
+ message += `\n\nHere's how it looks on you: ${notif.media_url}`;
89
+ }
88
90
 
89
91
  const delivered = await sendMessage(message, channel, target);
90
92
  if (delivered) {