twindex-openclaw-plugin 0.5.5 → 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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/twindex/SKILL.md +14 -0
- package/src/client.ts +31 -0
- package/src/index.ts +107 -0
- package/src/poll-service.ts +13 -3
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "twindex",
|
|
3
3
|
"name": "Twindex",
|
|
4
4
|
"description": "Music intelligence for AI agents. Get notified about tours, merch drops, releases, and presales for your favorite artists.",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.6",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
package/skills/twindex/SKILL.md
CHANGED
|
@@ -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.",
|
package/src/poll-service.ts
CHANGED
|
@@ -41,10 +41,18 @@ export function createNotificationService(api: any) {
|
|
|
41
41
|
}
|
|
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
49
|
}
|
|
46
50
|
|
|
47
51
|
function getChatChannel(): string {
|
|
52
|
+
const raw = api.config?.plugins?.entries?.twindex?.config?.chatTarget ?? "";
|
|
53
|
+
// Infer channel from prefix if present (e.g. "telegram:123" → "telegram")
|
|
54
|
+
const colonIdx = raw.indexOf(":");
|
|
55
|
+
if (colonIdx > 0) return raw.slice(0, colonIdx);
|
|
48
56
|
return api.config?.plugins?.entries?.twindex?.config?.chatChannel ?? "telegram";
|
|
49
57
|
}
|
|
50
58
|
|
|
@@ -75,8 +83,10 @@ export function createNotificationService(api: any) {
|
|
|
75
83
|
logger?.info?.(`Twindex: ${unread.length} unread notification(s)`);
|
|
76
84
|
|
|
77
85
|
for (const notif of unread) {
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
90
|
|
|
81
91
|
const delivered = await sendMessage(message, channel, target);
|
|
82
92
|
if (delivered) {
|