@ezshine/waifusmy 1.0.0

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/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # OpenClaw Channel Plugin - WaifusMy
2
+
3
+ [OpenClaw](https://github.com/openclaw/openclaw) Channel Plugin for connecting to WaifusMy desktop companion app.
4
+
5
+ ## Overview
6
+
7
+ This plugin enables OpenClaw to connect to the WaifusMy desktop application via a WebSocket relay server, allowing the AI agent to send and receive messages through the WaifusMy companion.
8
+
9
+ ```
10
+ OpenClaw Agent ◄──Gateway──► WaifusMy Plugin (this project)
11
+
12
+ WSS (outbound connection)
13
+
14
+ Relay Server (wss://api.waifus.my)
15
+
16
+ WSS
17
+
18
+ Tauri Desktop App (user's computer)
19
+ ```
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ cd ~/.openclaw/extensions/
25
+ git clone https://github.com/ezshine/clawplugin-waifusmy.git
26
+ cd clawplugin-waifusmy
27
+ npm install
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ Add the following to your OpenClaw configuration file (`~/.openclaw/config.yaml` or similar):
33
+
34
+ ```yaml
35
+ channels:
36
+ waifusmy:
37
+ enabled: true
38
+ api_key: "waifu_key_xxxxxx" # Your WaifusMy API key
39
+ relay_url: "wss://waifus-relay.ezshine.workers.dev/ws" # Optional, has default
40
+ ```
41
+
42
+ ## Features
43
+
44
+ - **WebSocket Connection**: Maintains persistent connection to WaifusMy relay server
45
+ - **Automatic Reconnection**: Exponential on backoff reconnection disconnect
46
+ - **Heartbeat**: Regular ping/pong to keep connection alive
47
+ - **Message Forwarding**:
48
+ - Receives messages from WaifusMy desktop app
49
+ - Sends AI responses back to the app
50
+ - **Simple DM**: Direct message communication (no group support yet)
51
+
52
+ ## Requirements
53
+
54
+ - OpenClaw instance
55
+ - WaifusMy desktop application with API key
56
+ - Network access to `wss://api.waifus.my`
57
+
58
+ ## Development
59
+
60
+ ### Project Structure
61
+
62
+ ```
63
+ openclaw-channel-waifusmy/
64
+ ├── package.json # NPM package configuration
65
+ ├── tsconfig.json # TypeScript configuration
66
+ ├── index.ts # Plugin entry point
67
+ ├── src/
68
+ │ ├── channel.ts # Main channel implementation
69
+ │ └── types.ts # TypeScript type definitions
70
+ └── README.md # This file
71
+ ```
72
+
73
+ ### Build
74
+
75
+ ```bash
76
+ npm run build
77
+ ```
78
+
79
+ ### Testing
80
+
81
+ The plugin can be tested using a WebSocket client to simulate the relay server:
82
+
83
+ ```bash
84
+ # Using wscat
85
+ npm install -g wscat
86
+ wscat -c wss://api.waifus.my/api/channel/ws
87
+
88
+ # Then send auth message:
89
+ {"type": "auth", "api_key": "your_key", "role": "plugin"}
90
+ ```
91
+
92
+ ## WebSocket Protocol
93
+
94
+ ### Outbound (Plugin → Server)
95
+
96
+ **Authentication:**
97
+ ```json
98
+ {
99
+ "type": "auth",
100
+ "api_key": "waifu_key_xxx",
101
+ "role": "app"
102
+ }
103
+ ```
104
+
105
+ **Send Message:**
106
+ ```json
107
+ {
108
+ "type": "message",
109
+ "id": "uuid",
110
+ "text": "AI response",
111
+ "ts": 1739577601000
112
+ }
113
+ ```
114
+
115
+ **Heartbeat:**
116
+ ```json
117
+ { "type": "ping" }
118
+ ```
119
+
120
+ ### Inbound (Server → Plugin)
121
+
122
+ **Auth Success:**
123
+ ```json
124
+ {
125
+ "type": "auth_ok",
126
+ "peer_online": true
127
+ }
128
+ ```
129
+
130
+ **Auth Error:**
131
+ ```json
132
+ {
133
+ "type": "auth_error",
134
+ "error": "Invalid API key"
135
+ }
136
+ ```
137
+
138
+ **User Message:**
139
+ ```json
140
+ {
141
+ "type": "message",
142
+ "id": "msg_uuid",
143
+ "text": "User's message",
144
+ "ts": 1739577600000
145
+ }
146
+ ```
147
+
148
+ **Peer Status:**
149
+ ```json
150
+ {
151
+ "type": "peer_status",
152
+ "online": true/false
153
+ }
154
+ ```
155
+
156
+ **Heartbeat Response:**
157
+ ```json
158
+ { "type": "pong" }
159
+ ```
160
+
161
+ ## License
162
+
163
+ MIT
package/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * WaifusMy Channel Plugin Entry Point
3
+ *
4
+ * Registers the WaifusMy channel plugin with OpenClaw.
5
+ */
6
+
7
+ import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";
8
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
9
+ import { waifusmyPlugin } from "./src/channel.js";
10
+
11
+ const plugin = {
12
+ id: "waifusmy",
13
+ name: "WaifusMy",
14
+ description: "OpenClaw channel plugin for WaifusMy desktop companion app",
15
+ configSchema: emptyPluginConfigSchema(),
16
+ register(api: OpenClawPluginApi) {
17
+ api.registerChannel({ plugin: waifusmyPlugin as ChannelPlugin });
18
+ },
19
+ };
20
+
21
+ export default plugin;
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "waifusmy",
3
+ "channels": ["waifusmy"],
4
+ "configSchema": {
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {}
8
+ }
9
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@ezshine/waifusmy",
3
+ "version": "1.0.0",
4
+ "description": "OpenClaw channel plugin for WaifusMy desktop companion app",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "types": "index.d.ts",
8
+ "files": [
9
+ "index.ts",
10
+ "src",
11
+ "openclaw.plugin.json"
12
+ ],
13
+ "devDependencies": {
14
+ "typescript": "^5.0.0"
15
+ },
16
+ "peerDependencies": {
17
+ "openclaw": ">=2026.0.0"
18
+ },
19
+ "openclaw": {
20
+ "extensions": [
21
+ "./index.ts"
22
+ ]
23
+ }
24
+ }
package/src/channel.ts ADDED
@@ -0,0 +1,469 @@
1
+ /**
2
+ * WaifusMy Channel Implementation
3
+ *
4
+ * Connects OpenClaw to WaifusMy desktop companion app via WebSocket relay server.
5
+ */
6
+
7
+ import type {
8
+ ChannelPlugin,
9
+ ChannelMeta,
10
+ ChannelCapabilities,
11
+ ChannelId,
12
+ OpenClawConfig,
13
+ } from "openclaw/plugin-sdk";
14
+ import { getChatChannelMeta, buildChannelConfigSchema, DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
15
+ import type {
16
+ WaifusMyConfig,
17
+ WaifusMyInboundMessage,
18
+ WaifusMyOutboundMessage,
19
+ WaifusMyMessage,
20
+ WaifusMyAuth,
21
+ WaifusMyAuthOk,
22
+ WaifusMyAuthError,
23
+ WaifusMyPeerStatus,
24
+ ResolvedWaifusMyAccount,
25
+ } from "./types.js";
26
+ import { WaifusMyConfigSchema } from "./types.js";
27
+
28
+ // Channel metadata
29
+ const meta: ChannelMeta = getChatChannelMeta("waifusmy");
30
+
31
+ // Global runtime reference
32
+ let waifusmyRuntime: WaifusMyRuntime | null = null;
33
+
34
+ // Runtime interface
35
+ interface WaifusMyRuntime {
36
+ account: ResolvedWaifusMyAccount | null;
37
+ ws: WebSocket | null;
38
+ reconnectAttempts: number;
39
+ maxReconnectAttempts: number;
40
+ reconnectDelay: number;
41
+ maxReconnectDelay: number;
42
+ connected: boolean;
43
+ authenticated: boolean;
44
+ peerOnline: boolean;
45
+ messageHandler: ((msg: WaifusMyMessage) => void) | null;
46
+ abortSignal: AbortSignal | null;
47
+ }
48
+
49
+ // Get runtime or create new one
50
+ function getRuntime(): WaifusMyRuntime {
51
+ if (!waifusmyRuntime) {
52
+ waifusmyRuntime = {
53
+ account: null,
54
+ ws: null,
55
+ reconnectAttempts: 0,
56
+ maxReconnectAttempts: 10,
57
+ reconnectDelay: 1000,
58
+ maxReconnectDelay: 30000,
59
+ connected: false,
60
+ authenticated: false,
61
+ peerOnline: false,
62
+ messageHandler: null,
63
+ abortSignal: null,
64
+ };
65
+ }
66
+ return waifusmyRuntime;
67
+ }
68
+
69
+ // Reset runtime
70
+ function resetRuntime(): void {
71
+ const runtime = getRuntime();
72
+ if (runtime.ws) {
73
+ runtime.ws.close();
74
+ runtime.ws = null;
75
+ }
76
+ runtime.connected = false;
77
+ runtime.authenticated = false;
78
+ runtime.reconnectAttempts = 0;
79
+ runtime.reconnectDelay = 1000;
80
+ }
81
+
82
+ // Set message handler (called from gateway)
83
+ export function setWaifusMyMessageHandler(
84
+ handler: (msg: WaifusMyMessage) => void
85
+ ): void {
86
+ const runtime = getRuntime();
87
+ runtime.messageHandler = handler;
88
+ }
89
+
90
+ // Set abort signal
91
+ export function setWaifusMyAbortSignal(signal: AbortSignal): void {
92
+ const runtime = getRuntime();
93
+ runtime.abortSignal = signal;
94
+ signal.addEventListener("abort", () => {
95
+ console.log("[waifusmy] Abort signal received, closing connection");
96
+ resetRuntime();
97
+ });
98
+ }
99
+
100
+ // WebSocket connection
101
+ function connectWebSocket(account: ResolvedWaifusMyAccount): Promise<void> {
102
+ return new Promise((resolve, reject) => {
103
+ const runtime = getRuntime();
104
+ runtime.account = account;
105
+
106
+ console.log(`[waifusmy] Connecting to ${account.config.relay_url}`);
107
+
108
+ try {
109
+ runtime.ws = new WebSocket(account.config.relay_url);
110
+
111
+ runtime.ws.onopen = () => {
112
+ console.log("[waifusmy] WebSocket connected");
113
+ runtime.connected = true;
114
+ runtime.reconnectAttempts = 0;
115
+ runtime.reconnectDelay = 1000;
116
+
117
+ // Send authentication
118
+ const authMsg: WaifusMyAuth = {
119
+ type: "auth",
120
+ api_key: account.config.api_key,
121
+ role: "app",
122
+ };
123
+ runtime.ws?.send(JSON.stringify(authMsg));
124
+ console.log("[waifusmy] Sent auth message");
125
+ };
126
+
127
+ runtime.ws.onmessage = (event) => {
128
+ try {
129
+ const data = JSON.parse(event.data) as WaifusMyInboundMessage;
130
+ handleInboundMessage(data);
131
+ } catch (err) {
132
+ console.error("[waifusmy] Failed to parse message:", err);
133
+ }
134
+ };
135
+
136
+ runtime.ws.onerror = (error) => {
137
+ console.error("[waifusmy] WebSocket error:", error);
138
+ };
139
+
140
+ runtime.ws.onclose = () => {
141
+ console.log("[waifusmy] WebSocket closed");
142
+ runtime.connected = false;
143
+ runtime.authenticated = false;
144
+
145
+ // Attempt reconnection if not aborted
146
+ if (!runtime.abortSignal?.aborted) {
147
+ attemptReconnect(account);
148
+ }
149
+ };
150
+
151
+ resolve();
152
+ } catch (err) {
153
+ reject(err);
154
+ }
155
+ });
156
+ }
157
+
158
+ // Handle inbound messages
159
+ function handleInboundMessage(msg: WaifusMyInboundMessage): void {
160
+ const runtime = getRuntime();
161
+
162
+ switch (msg.type) {
163
+ case "auth_ok":
164
+ console.log("[waifusmy] Authentication successful, peer online:", msg.peer_online);
165
+ runtime.authenticated = true;
166
+ runtime.peerOnline = msg.peer_online;
167
+ break;
168
+
169
+ case "auth_error":
170
+ console.error("[waifusmy] Authentication failed:", msg.error);
171
+ runtime.authenticated = false;
172
+ break;
173
+
174
+ case "message":
175
+ console.log("[waifusmy] Received message:", msg.id, msg.text.substring(0, 50));
176
+ // Forward to message handler for gateway processing
177
+ if (runtime.messageHandler) {
178
+ runtime.messageHandler(msg);
179
+ }
180
+ break;
181
+
182
+ case "peer_status":
183
+ console.log("[waifusmy] Peer status changed, online:", msg.online);
184
+ runtime.peerOnline = msg.online;
185
+ break;
186
+
187
+ case "pong":
188
+ // Heartbeat response received
189
+ break;
190
+
191
+ default:
192
+ console.log("[waifusmy] Unknown message type:", (msg as any).type);
193
+ }
194
+ }
195
+
196
+ // Exponential backoff reconnection
197
+ function attemptReconnect(account: ResolvedWaifusMyAccount): void {
198
+ const runtime = getRuntime();
199
+
200
+ if (runtime.reconnectAttempts >= runtime.maxReconnectAttempts) {
201
+ console.error("[waifusmy] Max reconnection attempts reached");
202
+ return;
203
+ }
204
+
205
+ runtime.reconnectAttempts++;
206
+ const delay = Math.min(
207
+ runtime.reconnectDelay * Math.pow(2, runtime.reconnectAttempts - 1),
208
+ runtime.maxReconnectDelay
209
+ );
210
+
211
+ console.log(
212
+ `[waifusmy] Reconnecting in ${delay}ms (attempt ${runtime.reconnectAttempts}/${runtime.maxReconnectAttempts})`
213
+ );
214
+
215
+ setTimeout(() => {
216
+ if (!runtime.abortSignal?.aborted) {
217
+ connectWebSocket(account).catch((err) => {
218
+ console.error("[waifusmy] Reconnection failed:", err);
219
+ });
220
+ }
221
+ }, delay);
222
+ }
223
+
224
+ // Send outbound message
225
+ export async function sendWaifusMyMessage(text: string): Promise<{ channel: string; messageId: string }> {
226
+ const runtime = getRuntime();
227
+
228
+ if (!runtime.ws || !runtime.connected || !runtime.authenticated) {
229
+ throw new Error("WaifusMy not connected or authenticated");
230
+ }
231
+
232
+ const message: WaifusMyMessage = {
233
+ type: "message",
234
+ id: crypto.randomUUID(),
235
+ text,
236
+ ts: Date.now(),
237
+ };
238
+
239
+ runtime.ws.send(JSON.stringify(message));
240
+ console.log("[waifusmy] Sent message:", message.id);
241
+
242
+ return { channel: "waifusmy", messageId: message.id };
243
+ }
244
+
245
+ // Start heartbeat ping
246
+ let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
247
+
248
+ function startHeartbeat(): void {
249
+ if (heartbeatInterval) {
250
+ clearInterval(heartbeatInterval);
251
+ }
252
+
253
+ heartbeatInterval = setInterval(() => {
254
+ const runtime = getRuntime();
255
+ if (runtime.ws && runtime.connected) {
256
+ runtime.ws.send(JSON.stringify({ type: "ping" }));
257
+ }
258
+ }, 30000); // Ping every 30 seconds
259
+ }
260
+
261
+ function stopHeartbeat(): void {
262
+ if (heartbeatInterval) {
263
+ clearInterval(heartbeatInterval);
264
+ heartbeatInterval = null;
265
+ }
266
+ }
267
+
268
+ // The channel plugin
269
+ export const waifusmyPlugin: ChannelPlugin<ResolvedWaifusMyAccount> = {
270
+ id: "waifusmy",
271
+ meta: {
272
+ ...meta,
273
+ id: "waifusmy" as ChannelId,
274
+ label: "WaifusMy",
275
+ selectionLabel: "WaifusMy Desktop Companion",
276
+ detailLabel: "WaifusMy",
277
+ docsPath: "/channels/waifusmy",
278
+ docsLabel: "waifusmy",
279
+ blurb: "Connect to WaifusMy desktop companion app",
280
+ systemImage: "desktopcomputer",
281
+ quickstartAllowFrom: false,
282
+ },
283
+ capabilities: {
284
+ chatTypes: ["direct"],
285
+ reactions: false,
286
+ threads: false,
287
+ media: false,
288
+ nativeCommands: false,
289
+ blockStreaming: true,
290
+ },
291
+ reload: { configPrefixes: ["channels.waifusmy"] },
292
+ configSchema: buildChannelConfigSchema(WaifusMyConfigSchema),
293
+ config: {
294
+ listAccountIds: (_cfg) => [DEFAULT_ACCOUNT_ID],
295
+ resolveAccount: (cfg, accountId) => {
296
+ const waifusmyConfig = cfg.channels?.waifusmy as WaifusMyConfig | undefined;
297
+ const resolvedId = accountId ?? DEFAULT_ACCOUNT_ID;
298
+
299
+ if (!waifusmyConfig) {
300
+ return {
301
+ accountId: resolvedId,
302
+ name: "WaifusMy",
303
+ enabled: false,
304
+ config: { enabled: false, api_key: "", relay_url: "wss://waifus-relay.ezshine.workers.dev/ws" },
305
+ apiKey: "",
306
+ relayUrl: "wss://waifus-relay.ezshine.workers.dev/ws",
307
+ tokenSource: "none" as const,
308
+ };
309
+ }
310
+
311
+ return {
312
+ accountId: resolvedId,
313
+ name: "WaifusMy",
314
+ enabled: waifusmyConfig.enabled ?? true,
315
+ config: waifusmyConfig,
316
+ apiKey: waifusmyConfig.api_key ?? "",
317
+ relayUrl: waifusmyConfig.relay_url ?? "wss://waifus-relay.ezshine.workers.dev/ws",
318
+ tokenSource: waifusmyConfig.api_key ? ("config" as const) : ("none" as const),
319
+ };
320
+ },
321
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID,
322
+ setAccountEnabled: ({ cfg, accountId, enabled }) => {
323
+ const current = (cfg.channels?.waifusmy ?? {}) as Partial<WaifusMyConfig>;
324
+ return {
325
+ ...cfg,
326
+ channels: {
327
+ ...cfg.channels,
328
+ waifusmy: {
329
+ ...current,
330
+ enabled,
331
+ },
332
+ },
333
+ };
334
+ },
335
+ deleteAccount: ({ cfg }) => {
336
+ const { waifusmy: _waifusmy, ...restChannels } = cfg.channels ?? {};
337
+ return {
338
+ ...cfg,
339
+ channels: restChannels,
340
+ };
341
+ },
342
+ isConfigured: (account) => {
343
+ return Boolean(account.config.api_key?.trim());
344
+ },
345
+ describeAccount: (account) => ({
346
+ accountId: account.accountId,
347
+ name: account.name,
348
+ enabled: account.enabled,
349
+ configured: Boolean(account.config.api_key?.trim()),
350
+ tokenSource: account.tokenSource,
351
+ }),
352
+ resolveAllowFrom: () => [],
353
+ formatAllowFrom: ({ allowFrom }) => allowFrom,
354
+ },
355
+ security: {
356
+ resolveDmPolicy: () => ({
357
+ policy: "open" as const,
358
+ allowFrom: [],
359
+ policyPath: "channels.waifusmy.dmPolicy",
360
+ allowFromPath: "channels.waifusmy.",
361
+ approveHint: undefined,
362
+ normalizeEntry: (raw) => raw,
363
+ }),
364
+ collectWarnings: () => [],
365
+ },
366
+ outbound: {
367
+ deliveryMode: "direct",
368
+ sendText: async ({ text }) => {
369
+ return sendWaifusMyMessage(text);
370
+ },
371
+ },
372
+ status: {
373
+ defaultRuntime: {
374
+ accountId: DEFAULT_ACCOUNT_ID,
375
+ running: false,
376
+ lastStartAt: null,
377
+ lastStopAt: null,
378
+ lastError: null,
379
+ },
380
+ buildChannelSummary: ({ snapshot }) => ({
381
+ configured: snapshot.configured ?? false,
382
+ running: snapshot.running ?? false,
383
+ connected: snapshot.runtime?.connected ?? false,
384
+ authenticated: snapshot.runtime?.authenticated ?? false,
385
+ peerOnline: snapshot.runtime?.peerOnline ?? false,
386
+ lastStartAt: snapshot.lastStartAt ?? null,
387
+ lastStopAt: snapshot.lastStopAt ?? null,
388
+ lastError: snapshot.lastError ?? null,
389
+ }),
390
+ buildAccountSnapshot: ({ account, runtime }) => ({
391
+ accountId: account.accountId,
392
+ name: account.name,
393
+ enabled: account.enabled,
394
+ configured: Boolean(account.config.api_key?.trim()),
395
+ tokenSource: account.tokenSource,
396
+ running: runtime?.running ?? false,
397
+ lastStartAt: runtime?.lastStartAt ?? null,
398
+ lastStopAt: runtime?.lastStopAt ?? null,
399
+ lastError: runtime?.lastError ?? null,
400
+ runtime: {
401
+ connected: waifusmyRuntime?.connected ?? false,
402
+ authenticated: waifusmyRuntime?.authenticated ?? false,
403
+ peerOnline: waifusmyRuntime?.peerOnline ?? false,
404
+ },
405
+ }),
406
+ },
407
+ gateway: {
408
+ startAccount: async (ctx) => {
409
+ const account = ctx.account;
410
+
411
+ console.log(`[waifusmy] Starting gateway for account: ${account.accountId}`);
412
+
413
+ if (!account.config.api_key?.trim()) {
414
+ throw new Error("WaifusMy API key not configured");
415
+ }
416
+
417
+ // Set abort signal
418
+ setWaifusMyAbortSignal(ctx.abortSignal);
419
+
420
+ // Set message handler to forward to gateway
421
+ setWaifusMyMessageHandler((msg) => {
422
+ // Create inbound message for gateway
423
+ ctx.log?.info(`[waifusmy] Processing inbound message: ${msg.id}`);
424
+
425
+ // The message handler should inject into gateway's message pipeline
426
+ // This is typically done via runtime.handleInboundMessage or similar
427
+ if (ctx.runtime?.onInboundMessage) {
428
+ ctx.runtime.onInboundMessage({
429
+ channel: "waifusmy",
430
+ messageId: msg.id,
431
+ text: msg.text,
432
+ senderId: "user",
433
+ timestamp: msg.ts,
434
+ chatType: "direct",
435
+ });
436
+ }
437
+ });
438
+
439
+ // Connect to WebSocket
440
+ await connectWebSocket(account);
441
+ startHeartbeat();
442
+
443
+ ctx.log?.info("[waifusmy] Gateway started successfully");
444
+
445
+ return {
446
+ running: true,
447
+ mode: "websocket",
448
+ };
449
+ },
450
+ logoutAccount: async ({ accountId, cfg }) => {
451
+ stopHeartbeat();
452
+ resetRuntime();
453
+
454
+ const { waifusmy: _waifusmy, ...restChannels } = cfg.channels ?? {};
455
+ const newCfg = {
456
+ ...cfg,
457
+ channels: restChannels,
458
+ };
459
+
460
+ return {
461
+ cleared: true,
462
+ loggedOut: true,
463
+ };
464
+ },
465
+ },
466
+ };
467
+
468
+ // Export types for external use
469
+ export type { WaifusMyConfig, WaifusMyMessage };
package/src/types.ts ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * WaifusMy Channel Types
3
+ */
4
+
5
+ import type { z } from "zod";
6
+ import { z } from "zod";
7
+
8
+ // WebSocket message types from the relay server
9
+ export const WaifusMyAuthSchema = z.object({
10
+ type: z.literal("auth"),
11
+ api_key: z.string(),
12
+ role: z.literal("plugin"),
13
+ });
14
+
15
+ export const WaifusMyAuthOkSchema = z.object({
16
+ type: z.literal("auth_ok"),
17
+ peer_online: z.boolean(),
18
+ });
19
+
20
+ export const WaifusMyAuthErrorSchema = z.object({
21
+ type: z.literal("auth_error"),
22
+ error: z.string(),
23
+ });
24
+
25
+ export const WaifusMyMessageSchema = z.object({
26
+ type: z.literal("message"),
27
+ id: z.string(),
28
+ text: z.string(),
29
+ ts: z.number(),
30
+ });
31
+
32
+ export const WaifusMyPeerStatusSchema = z.object({
33
+ type: z.literal("peer_status"),
34
+ online: z.boolean(),
35
+ });
36
+
37
+ export const WaifusMyPingSchema = z.object({
38
+ type: z.literal("ping"),
39
+ });
40
+
41
+ export const WaifusMyPongSchema = z.object({
42
+ type: z.literal("pong"),
43
+ });
44
+
45
+ export type WaifusMyAuth = z.infer<typeof WaifusMyAuthSchema>;
46
+ export type WaifusMyAuthOk = z.infer<typeof WaifusMyAuthOkSchema>;
47
+ export type WaifusMyAuthError = z.infer<typeof WaifusMyAuthErrorSchema>;
48
+ export type WaifusMyMessage = z.infer<typeof WaifusMyMessageSchema>;
49
+ export type WaifusMyPeerStatus = z.infer<typeof WaifusMyPeerStatusSchema>;
50
+ export type WaifusMyPing = z.infer<typeof WaifusMyPingSchema>;
51
+ export type WaifusMyPong = z.infer<typeof WaifusMyPongSchema>;
52
+
53
+ export type WaifusMyInboundMessage =
54
+ | WaifusMyAuthOk
55
+ | WaifusMyAuthError
56
+ | WaifusMyMessage
57
+ | WaifusMyPeerStatus
58
+ | WaifusMyPong;
59
+
60
+ export type WaifusMyOutboundMessage =
61
+ | WaifusMyAuth
62
+ | WaifusMyMessage
63
+ | WaifusMyPing;
64
+
65
+ // Configuration schema
66
+ export const WaifusMyConfigSchema = z.object({
67
+ enabled: z.boolean().default(true),
68
+ api_key: z.string().min(1, "API key is required"),
69
+ relay_url: z.string().url().default("wss://waifus-relay.ezshine.workers.dev/ws"),
70
+ });
71
+
72
+ export type WaifusMyConfig = z.infer<typeof WaifusMyConfigSchema>;
73
+
74
+ // Resolved account type
75
+ export interface ResolvedWaifusMyAccount {
76
+ accountId: string;
77
+ name: string;
78
+ enabled: boolean;
79
+ config: WaifusMyConfig;
80
+ apiKey: string;
81
+ relayUrl: string;
82
+ tokenSource: "config" | "none";
83
+ }
84
+
85
+ // Runtime state
86
+ export interface WaifusMyRuntime {
87
+ ws: WebSocket | null;
88
+ reconnectAttempts: number;
89
+ maxReconnectAttempts: number;
90
+ reconnectDelay: number;
91
+ maxReconnectDelay: number;
92
+ connected: boolean;
93
+ authenticated: boolean;
94
+ peerOnline: boolean;
95
+ }