twindex-openclaw-plugin 0.6.1 → 0.6.3

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.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Music intelligence for AI agents. Tours, merch drops, releases, presales.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/client.ts CHANGED
@@ -173,7 +173,7 @@ interface TryOnResult {
173
173
  product: { title: string; price: string; buy_url: string };
174
174
  }
175
175
 
176
- /** Upload a selfie for personalized merch try-on. */
176
+ /** Upload a selfie for personalized merch try-on (from URL). */
177
177
  export async function uploadPhoto(
178
178
  apiKey: string,
179
179
  photoUrl: string,
@@ -185,6 +185,29 @@ export async function uploadPhoto(
185
185
  });
186
186
  }
187
187
 
188
+ /** Upload a selfie from raw file bytes (multipart). */
189
+ export async function uploadPhotoFile(
190
+ apiKey: string,
191
+ fileData: Uint8Array,
192
+ filename: string,
193
+ ): Promise<void> {
194
+ const formData = new FormData();
195
+ formData.append("photo", new Blob([fileData]), filename);
196
+
197
+ const url = `${BASE_URL}/api/v1/me/photo/upload`;
198
+ const res = await fetch(url, {
199
+ method: "POST",
200
+ headers: { Authorization: `Bearer ${apiKey}` },
201
+ body: formData,
202
+ signal: AbortSignal.timeout(30_000),
203
+ });
204
+
205
+ if (!res.ok) {
206
+ const body = await res.text().catch(() => "");
207
+ throw new Error(`Twindex API ${res.status}: ${body}`);
208
+ }
209
+ }
210
+
188
211
  /** Generate an image of the user wearing merch. */
189
212
  export async function tryOn(
190
213
  apiKey: string,
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync } from "fs";
1
+ import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { homedir } from "os";
4
4
  import * as twindex from "./client.js";
@@ -457,19 +457,18 @@ export default function register(api: any) {
457
457
  api.registerTool({
458
458
  name: "twindex_upload_photo",
459
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.",
460
+ "Upload the user's selfie for personalized merch try-on. When the user sends a photo in chat, call this tool with NO parameters — it automatically finds the most recent photo from the conversation. One photo per user — uploading replaces the previous one.",
461
461
  parameters: {
462
462
  type: "object",
463
463
  properties: {
464
464
  photo_url: {
465
465
  type: "string",
466
466
  description:
467
- "URL of the user's photo (from the media attachment in their message)",
467
+ "Optional. URL of the user's photo. If omitted, automatically uses the most recent photo sent in chat.",
468
468
  },
469
469
  },
470
- required: ["photo_url"],
471
470
  },
472
- async execute(_id: string, params: { photo_url: string }) {
471
+ async execute(_id: string, params: { photo_url?: string }) {
473
472
  const apiKey = cfg().apiKey;
474
473
  if (!apiKey) {
475
474
  return {
@@ -480,7 +479,54 @@ export default function register(api: any) {
480
479
  }
481
480
 
482
481
  try {
483
- await twindex.uploadPhoto(apiKey, params.photo_url);
482
+ if (params.photo_url) {
483
+ await twindex.uploadPhoto(apiKey, params.photo_url);
484
+ } else {
485
+ // Find the most recent inbound photo from OpenClaw media
486
+ const inboundDir = join(homedir(), ".openclaw", "media", "inbound");
487
+ let files: string[];
488
+ try {
489
+ files = readdirSync(inboundDir).filter((f) =>
490
+ /\.(jpg|jpeg|png)$/i.test(f),
491
+ );
492
+ } catch {
493
+ return {
494
+ content: [
495
+ {
496
+ type: "text",
497
+ text: "No photos found. Please send a selfie in chat first.",
498
+ },
499
+ ],
500
+ };
501
+ }
502
+
503
+ if (files.length === 0) {
504
+ return {
505
+ content: [
506
+ {
507
+ type: "text",
508
+ text: "No photos found in chat. Please send a selfie first.",
509
+ },
510
+ ],
511
+ };
512
+ }
513
+
514
+ // Sort by modification time, newest first
515
+ files.sort((a, b) => {
516
+ const aStat = statSync(join(inboundDir, a));
517
+ const bStat = statSync(join(inboundDir, b));
518
+ return bStat.mtimeMs - aStat.mtimeMs;
519
+ });
520
+
521
+ const latestFile = join(inboundDir, files[0]);
522
+ const fileData = readFileSync(latestFile);
523
+ await twindex.uploadPhotoFile(
524
+ apiKey,
525
+ new Uint8Array(fileData),
526
+ files[0],
527
+ );
528
+ }
529
+
484
530
  return {
485
531
  content: [
486
532
  {
@@ -543,9 +589,8 @@ export default function register(api: any) {
543
589
  content: [
544
590
  {
545
591
  type: "text",
546
- text: `Here's you in the ${result.product.title} ($${result.product.price})!\nBuy it: ${result.product.buy_url}`,
592
+ text: `Here's you in the ${result.product.title} ($${result.product.price})!\n\nTry-on image: ${result.image_url}\n\nBuy it: ${result.product.buy_url}\n\nIMPORTANT: Send the try-on image URL to the user so they can see themselves wearing it. The image URL above is the generated photo of the user in this merch.`,
547
593
  },
548
- { type: "image", url: result.image_url },
549
594
  ],
550
595
  };
551
596
  } catch (err: any) {