twindex-openclaw-plugin 0.8.2 → 0.8.4

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.
@@ -2,7 +2,7 @@
2
2
  "id": "twindex",
3
3
  "name": "New Lore",
4
4
  "description": "Music intelligence for AI agents. Get notified about tours, merch drops, releases, and presales for your favorite artists.",
5
- "version": "0.8.2",
5
+ "version": "0.8.4",
6
6
  "skills": ["./skills"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twindex-openclaw-plugin",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Music intelligence for AI agents. Tours, merch drops, releases, presales.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,7 +39,7 @@ Use `newlore_subscribe`. The existing delivery schedule picks them up automatica
39
39
  This is the killer feature. Users can see themselves wearing merch.
40
40
 
41
41
  **When a user sends a photo/selfie:**
42
- 1. Call `newlore_upload_photo` with NO parameters (it auto-finds the photo)
42
+ 1. Call `newlore_upload_photo` with NO parameters (it auto-finds the most recent photo from the current chat)
43
43
  2. Tell them they're all set for try-ons
44
44
 
45
45
  **When a user asks to try on a product:**
package/src/config.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+
5
+ type PluginConfig = Record<string, any>;
6
+
7
+ export function readDiskPluginConfig(): PluginConfig {
8
+ try {
9
+ const configPath = join(homedir(), ".openclaw", "openclaw.json");
10
+ if (!existsSync(configPath)) return {};
11
+ const raw = readFileSync(configPath, "utf-8");
12
+ const parsed = JSON.parse(raw);
13
+ return parsed?.plugins?.entries?.twindex?.config ?? {};
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+
19
+ export function mergedPluginConfig(api: any): PluginConfig {
20
+ const disk = readDiskPluginConfig();
21
+ const memory = api.config?.plugins?.entries?.twindex?.config ?? {};
22
+ return { ...disk, ...memory };
23
+ }
package/src/index.ts CHANGED
@@ -3,19 +3,24 @@ import { join } from "path";
3
3
  import { homedir } from "os";
4
4
  import * as twindex from "./client.js";
5
5
  import { createNotificationService } from "./poll-service.js";
6
+ import { mergedPluginConfig } from "./config.js";
6
7
 
7
8
  let bootstrapping = false;
8
9
 
9
10
  export default function register(api: any) {
10
- const cfg = () => api.config?.plugins?.entries?.twindex?.config ?? {};
11
+ const cfg = () => mergedPluginConfig(api);
11
12
 
12
13
  function persistConfig(updates: Record<string, any>) {
13
- if (!api.config?.plugins?.entries?.twindex) return;
14
- // Update in-memory config
15
- api.config.plugins.entries.twindex.config = {
14
+ const nextConfig = {
16
15
  ...cfg(),
17
16
  ...updates,
18
17
  };
18
+
19
+ // Update in-memory config when the plugin entry is available.
20
+ if (api.config?.plugins?.entries?.twindex) {
21
+ api.config.plugins.entries.twindex.config = nextConfig;
22
+ }
23
+
19
24
  // Write to disk — OpenClaw plugin API does not expose config.save()
20
25
  try {
21
26
  const configPath = join(homedir(), ".openclaw", "openclaw.json");
@@ -24,7 +29,7 @@ export default function register(api: any) {
24
29
  if (!disk.plugins) disk.plugins = {};
25
30
  if (!disk.plugins.entries) disk.plugins.entries = {};
26
31
  if (!disk.plugins.entries.twindex) disk.plugins.entries.twindex = {};
27
- disk.plugins.entries.twindex.config = api.config.plugins.entries.twindex.config;
32
+ disk.plugins.entries.twindex.config = nextConfig;
28
33
  writeFileSync(configPath, JSON.stringify(disk, null, 2) + "\n", "utf-8");
29
34
  api.logger?.info?.("NewLore: config persisted to disk");
30
35
  } catch (err: any) {
@@ -517,22 +522,31 @@ export default function register(api: any) {
517
522
  if (params.photo_url) {
518
523
  await twindex.uploadPhoto(apiKey, params.photo_url);
519
524
  } else {
520
- // Find the most recent inbound photo from OpenClaw media
521
- const inboundDir = join(homedir(), ".openclaw", "media", "inbound");
522
- let files: string[];
523
- try {
524
- files = readdirSync(inboundDir).filter((f) =>
525
- /\.(jpg|jpeg|png)$/i.test(f),
526
- );
527
- } catch {
528
- return {
529
- content: [
530
- {
531
- type: "text",
532
- text: "No photos found. Please send a selfie in chat first.",
533
- },
534
- ],
535
- };
525
+ // Find the most recent inbound photo scoped to the current chat.
526
+ // Try chat-scoped directory first, then fall back to global inbound.
527
+ const chatTarget = inferChatTarget();
528
+ const mediaBase = join(homedir(), ".openclaw", "media", "inbound");
529
+ const searchDirs: string[] = [];
530
+ if (chatTarget) {
531
+ searchDirs.push(join(mediaBase, chatTarget));
532
+ }
533
+ searchDirs.push(mediaBase);
534
+
535
+ let files: string[] = [];
536
+ let resolvedDir = mediaBase;
537
+ for (const dir of searchDirs) {
538
+ try {
539
+ const found = readdirSync(dir).filter((f) =>
540
+ /\.(jpg|jpeg|png)$/i.test(f),
541
+ );
542
+ if (found.length > 0) {
543
+ files = found;
544
+ resolvedDir = dir;
545
+ break;
546
+ }
547
+ } catch {
548
+ // Directory doesn't exist, try next
549
+ }
536
550
  }
537
551
 
538
552
  if (files.length === 0) {
@@ -548,12 +562,12 @@ export default function register(api: any) {
548
562
 
549
563
  // Sort by modification time, newest first
550
564
  files.sort((a, b) => {
551
- const aStat = statSync(join(inboundDir, a));
552
- const bStat = statSync(join(inboundDir, b));
565
+ const aStat = statSync(join(resolvedDir, a));
566
+ const bStat = statSync(join(resolvedDir, b));
553
567
  return bStat.mtimeMs - aStat.mtimeMs;
554
568
  });
555
569
 
556
- const latestFile = join(inboundDir, files[0]);
570
+ const latestFile = join(resolvedDir, files[0]);
557
571
  const fileData = readFileSync(latestFile);
558
572
  await twindex.uploadPhotoFile(
559
573
  apiKey,
@@ -687,4 +701,3 @@ export default function register(api: any) {
687
701
  },
688
702
  });
689
703
  }
690
-
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { execFile } from "child_process";
7
7
  import * as twindex from "./client.js";
8
+ import { mergedPluginConfig } from "./config.js";
8
9
 
9
10
  const FREQUENCY_MS: Record<string, number> = {
10
11
  realtime: 20 * 1000, // 20 sec (demo speed)
@@ -12,36 +13,53 @@ const FREQUENCY_MS: Record<string, number> = {
12
13
  daily: 24 * 60 * 60 * 1000, // 24 hours
13
14
  };
14
15
 
16
+ const INITIAL_POLL_DELAY_MS = 15 * 1000;
17
+
15
18
  function sendMessage(
16
19
  message: string,
17
20
  channel: string,
18
21
  target: string,
19
- ): Promise<boolean> {
22
+ ): Promise<{ ok: boolean; reason?: string }> {
20
23
  return new Promise((resolve) => {
21
24
  execFile(
22
25
  "openclaw",
23
26
  ["message", "send", "--channel", channel, "--target", target, "-m", message],
24
- { timeout: 30_000 },
25
- (err) => resolve(!err),
27
+ { timeout: 10_000 },
28
+ (err, stdout, stderr) => {
29
+ if (!err) {
30
+ resolve({ ok: true });
31
+ return;
32
+ }
33
+
34
+ const details = [err.message, stderr?.trim(), stdout?.trim()]
35
+ .filter(Boolean)
36
+ .join(" | ");
37
+ resolve({ ok: false, reason: details || "unknown delivery error" });
38
+ },
26
39
  );
27
40
  });
28
41
  }
29
42
 
30
43
  export function createNotificationService(api: any) {
31
44
  let timer: ReturnType<typeof setInterval> | null = null;
45
+ let startupPoll: ReturnType<typeof setTimeout> | null = null;
32
46
  let running = false;
33
47
  let polling = false; // guard against overlapping polls
34
48
 
49
+ function cfg(): Record<string, any> {
50
+ return mergedPluginConfig(api);
51
+ }
52
+
35
53
  function getApiKey(): string | undefined {
36
- return api.config?.plugins?.entries?.twindex?.config?.apiKey;
54
+ return cfg().apiKey;
37
55
  }
38
56
 
39
57
  function getFrequency(): string {
40
- return api.config?.plugins?.entries?.twindex?.config?.frequency ?? "periodic";
58
+ return cfg().frequency ?? "periodic";
41
59
  }
42
60
 
43
61
  function getChatTarget(): string | undefined {
44
- const raw = api.config?.plugins?.entries?.twindex?.config?.chatTarget;
62
+ const raw = cfg().chatTarget;
45
63
  if (raw) {
46
64
  // Strip channel prefix if agent included it (e.g. "telegram:123" → "123")
47
65
  const colonIdx = raw.indexOf(":");
@@ -58,12 +76,12 @@ export function createNotificationService(api: any) {
58
76
  }
59
77
 
60
78
  function getChatChannel(): string {
61
- const raw = api.config?.plugins?.entries?.twindex?.config?.chatTarget ?? "";
79
+ const raw = cfg().chatTarget ?? "";
62
80
  // Infer channel from prefix if present (e.g. "telegram:123" → "telegram")
63
81
  const colonIdx = raw.indexOf(":");
64
82
  if (colonIdx > 0) return raw.slice(0, colonIdx);
65
83
 
66
- const explicit = api.config?.plugins?.entries?.twindex?.config?.chatChannel;
84
+ const explicit = cfg().chatChannel;
67
85
  if (explicit) return explicit;
68
86
 
69
87
  // Fallback: infer from which channel is configured
@@ -138,15 +156,17 @@ export function createNotificationService(api: any) {
138
156
  message += `\n\nWant to see how it looks on you? Send me a selfie and I'll show you.`;
139
157
  }
140
158
 
141
- const delivered = await sendMessage(message, channel, target);
142
- if (delivered) {
159
+ const delivery = await sendMessage(message, channel, target);
160
+ if (delivery.ok) {
143
161
  try {
144
162
  await twindex.markRead(apiKey, [notif.id]);
145
163
  } catch {
146
164
  // Non-fatal — delivered but read status will catch up
147
165
  }
148
166
  } else {
149
- logger?.warn?.("Twindex: message delivery failed, will retry next poll");
167
+ logger?.warn?.(
168
+ `Twindex: message delivery failed, will retry next poll (${delivery.reason})`,
169
+ );
150
170
  break; // Don't pile up failed deliveries
151
171
  }
152
172
  }
@@ -166,6 +186,10 @@ export function createNotificationService(api: any) {
166
186
  clearInterval(timer);
167
187
  timer = null;
168
188
  }
189
+ if (startupPoll) {
190
+ clearTimeout(startupPoll);
191
+ startupPoll = null;
192
+ }
169
193
 
170
194
  const apiKey = getApiKey();
171
195
  if (!apiKey) {
@@ -177,11 +201,15 @@ export function createNotificationService(api: any) {
177
201
  const freq = getFrequency();
178
202
  const intervalMs = FREQUENCY_MS[freq] ?? FREQUENCY_MS.periodic;
179
203
 
180
- // Immediate first poll
181
- poll();
182
-
183
204
  timer = setInterval(poll, intervalMs);
184
- logger?.info?.(`Twindex: poll service started (${freq}, every ${intervalMs / 1000}s)`);
205
+ const initialDelay = Math.min(INITIAL_POLL_DELAY_MS, intervalMs);
206
+ startupPoll = setTimeout(() => {
207
+ startupPoll = null;
208
+ poll();
209
+ }, initialDelay);
210
+ logger?.info?.(
211
+ `Twindex: poll service started (${freq}, every ${intervalMs / 1000}s, initial delay ${initialDelay / 1000}s)`,
212
+ );
185
213
  },
186
214
 
187
215
  stop() {
@@ -190,6 +218,10 @@ export function createNotificationService(api: any) {
190
218
  clearInterval(timer);
191
219
  timer = null;
192
220
  }
221
+ if (startupPoll) {
222
+ clearTimeout(startupPoll);
223
+ startupPoll = null;
224
+ }
193
225
  logger?.info?.("Twindex: poll service stopped");
194
226
  },
195
227
  };