twindex-openclaw-plugin 0.8.1 → 0.8.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/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 +48 -25
- package/src/poll-service.ts +10 -5
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.3",
|
|
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) {
|
|
@@ -117,6 +122,16 @@ export default function register(api: any) {
|
|
|
117
122
|
} finally {
|
|
118
123
|
bootstrapping = false;
|
|
119
124
|
}
|
|
125
|
+
|
|
126
|
+
// Sync city on every reload (city often arrives after bootstrap via config.patch)
|
|
127
|
+
const postConfig = cfg();
|
|
128
|
+
if (postConfig.apiKey && postConfig.city) {
|
|
129
|
+
twindex.setCity(postConfig.apiKey, postConfig.city).then(() => {
|
|
130
|
+
api.logger?.info?.(`NewLore: city synced to ${postConfig.city}`);
|
|
131
|
+
}).catch((err: any) => {
|
|
132
|
+
api.logger?.warn?.(`NewLore: failed to sync city: ${err.message}`);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
120
135
|
})();
|
|
121
136
|
|
|
122
137
|
// ── Tools ──────────────────────────────────────────────────────────
|
|
@@ -507,22 +522,31 @@ export default function register(api: any) {
|
|
|
507
522
|
if (params.photo_url) {
|
|
508
523
|
await twindex.uploadPhoto(apiKey, params.photo_url);
|
|
509
524
|
} else {
|
|
510
|
-
// Find the most recent inbound photo
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
+
}
|
|
526
550
|
}
|
|
527
551
|
|
|
528
552
|
if (files.length === 0) {
|
|
@@ -538,12 +562,12 @@ export default function register(api: any) {
|
|
|
538
562
|
|
|
539
563
|
// Sort by modification time, newest first
|
|
540
564
|
files.sort((a, b) => {
|
|
541
|
-
const aStat = statSync(join(
|
|
542
|
-
const bStat = statSync(join(
|
|
565
|
+
const aStat = statSync(join(resolvedDir, a));
|
|
566
|
+
const bStat = statSync(join(resolvedDir, b));
|
|
543
567
|
return bStat.mtimeMs - aStat.mtimeMs;
|
|
544
568
|
});
|
|
545
569
|
|
|
546
|
-
const latestFile = join(
|
|
570
|
+
const latestFile = join(resolvedDir, files[0]);
|
|
547
571
|
const fileData = readFileSync(latestFile);
|
|
548
572
|
await twindex.uploadPhotoFile(
|
|
549
573
|
apiKey,
|
|
@@ -677,4 +701,3 @@ export default function register(api: any) {
|
|
|
677
701
|
},
|
|
678
702
|
});
|
|
679
703
|
}
|
|
680
|
-
|
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)
|
|
@@ -32,16 +33,20 @@ export function createNotificationService(api: any) {
|
|
|
32
33
|
let running = false;
|
|
33
34
|
let polling = false; // guard against overlapping polls
|
|
34
35
|
|
|
36
|
+
function cfg(): Record<string, any> {
|
|
37
|
+
return mergedPluginConfig(api);
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
function getApiKey(): string | undefined {
|
|
36
|
-
return
|
|
41
|
+
return cfg().apiKey;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
function getFrequency(): string {
|
|
40
|
-
return
|
|
45
|
+
return cfg().frequency ?? "periodic";
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
function getChatTarget(): string | undefined {
|
|
44
|
-
const raw =
|
|
49
|
+
const raw = cfg().chatTarget;
|
|
45
50
|
if (raw) {
|
|
46
51
|
// Strip channel prefix if agent included it (e.g. "telegram:123" → "123")
|
|
47
52
|
const colonIdx = raw.indexOf(":");
|
|
@@ -58,12 +63,12 @@ export function createNotificationService(api: any) {
|
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
function getChatChannel(): string {
|
|
61
|
-
const raw =
|
|
66
|
+
const raw = cfg().chatTarget ?? "";
|
|
62
67
|
// Infer channel from prefix if present (e.g. "telegram:123" → "telegram")
|
|
63
68
|
const colonIdx = raw.indexOf(":");
|
|
64
69
|
if (colonIdx > 0) return raw.slice(0, colonIdx);
|
|
65
70
|
|
|
66
|
-
const explicit =
|
|
71
|
+
const explicit = cfg().chatChannel;
|
|
67
72
|
if (explicit) return explicit;
|
|
68
73
|
|
|
69
74
|
// Fallback: infer from which channel is configured
|