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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/twindex/SKILL.md +1 -1
- package/src/config.ts +23 -0
- package/src/index.ts +38 -25
- package/src/poll-service.ts +47 -15
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.8.4",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
package/skills/twindex/SKILL.md
CHANGED
|
@@ -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
|
|
11
|
+
const cfg = () => mergedPluginConfig(api);
|
|
11
12
|
|
|
12
13
|
function persistConfig(updates: Record<string, any>) {
|
|
13
|
-
|
|
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 =
|
|
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
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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(
|
|
552
|
-
const bStat = statSync(join(
|
|
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(
|
|
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
|
-
|
package/src/poll-service.ts
CHANGED
|
@@ -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:
|
|
25
|
-
(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
|
|
54
|
+
return cfg().apiKey;
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
function getFrequency(): string {
|
|
40
|
-
return
|
|
58
|
+
return cfg().frequency ?? "periodic";
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
function getChatTarget(): string | undefined {
|
|
44
|
-
const raw =
|
|
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 =
|
|
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 =
|
|
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
|
|
142
|
-
if (
|
|
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?.(
|
|
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
|
-
|
|
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
|
};
|