twindex-openclaw-plugin 0.6.0 → 0.6.2

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.0",
3
+ "version": "0.6.2",
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";
@@ -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.",
@@ -422,19 +457,18 @@ export default function register(api: any) {
422
457
  api.registerTool({
423
458
  name: "twindex_upload_photo",
424
459
  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.",
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.",
426
461
  parameters: {
427
462
  type: "object",
428
463
  properties: {
429
464
  photo_url: {
430
465
  type: "string",
431
466
  description:
432
- "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.",
433
468
  },
434
469
  },
435
- required: ["photo_url"],
436
470
  },
437
- async execute(_id: string, params: { photo_url: string }) {
471
+ async execute(_id: string, params: { photo_url?: string }) {
438
472
  const apiKey = cfg().apiKey;
439
473
  if (!apiKey) {
440
474
  return {
@@ -445,7 +479,54 @@ export default function register(api: any) {
445
479
  }
446
480
 
447
481
  try {
448
- 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
+
449
530
  return {
450
531
  content: [
451
532
  {
@@ -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;