@yeego/yeego-openclaw 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.
@@ -0,0 +1,31 @@
1
+ {
2
+ "id": "yeego-openclaw",
3
+ "channels": ["yeego"],
4
+ "configSchema": {
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "token": {
9
+ "type": "string",
10
+ "description": "Yeego authentication token"
11
+ },
12
+ "profileId": {
13
+ "type": "string",
14
+ "description": "Yeego profile ID"
15
+ },
16
+ "baseUrl": {
17
+ "type": "string",
18
+ "description": "Yeego base URL"
19
+ },
20
+ "sidecarUrl": {
21
+ "type": "string",
22
+ "description": "Yeego sidecar URL"
23
+ },
24
+ "connectUrl": {
25
+ "type": "string",
26
+ "description": "Yeego connect endpoint URL"
27
+ }
28
+ },
29
+ "required": ["token", "profileId", "baseUrl", "sidecarUrl", "connectUrl"]
30
+ }
31
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@yeego/yeego-openclaw",
3
+ "version": "1.0.0",
4
+ "description": "Yeego plugin for OpenClaw - Connect your AI personas",
5
+ "main": "index.ts",
6
+ "type": "module",
7
+ "bin": {
8
+ "yeego-poller": "./poller.ts"
9
+ },
10
+ "scripts": {
11
+ "setup": "bun run poller.ts setup",
12
+ "start": "bun run poller.ts start",
13
+ "build": "bun build poller.ts --compile --outfile yeego-poller"
14
+ },
15
+ "keywords": [
16
+ "yeego",
17
+ "openclaw",
18
+ "ai",
19
+ "chatbot",
20
+ "plugin"
21
+ ],
22
+ "author": "Yeego Team",
23
+ "license": "MIT",
24
+ "homepage": "https://yeego.app",
25
+ "openclaw": {
26
+ "extensions": ["./index.ts"]
27
+ },
28
+ "files": [
29
+ "index.ts",
30
+ "poller.ts",
31
+ "src/",
32
+ "openclaw.plugin.json",
33
+ "README.md",
34
+ "QUICKSTART.md"
35
+ ],
36
+ "dependencies": {
37
+ "@sinclair/typebox": "^0.34.18",
38
+ "eventsource": "^4.1.0",
39
+ "pocketbase": "^0.26.8",
40
+ "tsx": "^4.21.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.0.0"
44
+ }
45
+ }
package/poller.ts ADDED
@@ -0,0 +1,516 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Yeego OpenClaw Plugin - Message Poller
4
+ *
5
+ * Polls PocketBase for new user messages and processes them through OpenClaw agent.
6
+ */
7
+
8
+ import PocketBase from 'pocketbase';
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { homedir } from 'os';
12
+ import { spawn, ChildProcess } from 'child_process';
13
+ import { setSessionConversation } from './src/sessionMapping.js';
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ interface YeegoConfig {
20
+ token: string;
21
+ profile_id: string;
22
+ base_url: string;
23
+ sidecar_url?: string;
24
+ }
25
+
26
+ interface PocketBaseMessage {
27
+ id: string;
28
+ conversation: string;
29
+ message: string;
30
+ by: 'user' | 'persona';
31
+ is_ai_generated: boolean;
32
+ has_finished_generating?: boolean;
33
+ created: string;
34
+ }
35
+
36
+ interface PocketBaseConversation {
37
+ id: string;
38
+ profile: string;
39
+ }
40
+
41
+ interface OpenClawResponse {
42
+ result?: {
43
+ payloads?: Array<{ text?: string }>;
44
+ };
45
+ text?: string;
46
+ }
47
+
48
+ // ============================================================================
49
+ // Constants
50
+ // ============================================================================
51
+
52
+ const CONFIG_DIR = join(homedir(), '.yeego');
53
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
54
+ const PROCESSED_FILE = join(CONFIG_DIR, 'processed.json');
55
+ const SESSION_METADATA_DELAY_MS = 1000;
56
+ const POLL_INTERVAL_MS = 5000; // Poll every 5 seconds (avoid rate limiting)
57
+
58
+ // ============================================================================
59
+ // Main Poller Class
60
+ // ============================================================================
61
+
62
+ class YeegoPoller {
63
+ private readonly config: YeegoConfig;
64
+ private readonly pb: PocketBase;
65
+ private readonly processedMessages: Set<string>;
66
+ private isRunning = false;
67
+ private pollInterval?: NodeJS.Timeout;
68
+
69
+ constructor(config: YeegoConfig) {
70
+ this.config = config;
71
+ this.pb = new PocketBase(config.base_url);
72
+ this.pb.autoCancellation(false); // Disable auto-cancellation to allow updates during realtime subscription
73
+ this.pb.authStore.save(config.token);
74
+ this.processedMessages = this.loadProcessedMessages();
75
+ }
76
+
77
+ // --------------------------------------------------------------------------
78
+ // Persistence
79
+ // --------------------------------------------------------------------------
80
+
81
+ private loadProcessedMessages(): Set<string> {
82
+ try {
83
+ if (existsSync(PROCESSED_FILE)) {
84
+ const data = JSON.parse(readFileSync(PROCESSED_FILE, 'utf-8'));
85
+ return new Set(data);
86
+ }
87
+ } catch (error) {
88
+ console.error('[Yeego] Failed to load processed messages:', error);
89
+ }
90
+ return new Set();
91
+ }
92
+
93
+ private saveProcessedMessages(): void {
94
+ try {
95
+ writeFileSync(
96
+ PROCESSED_FILE,
97
+ JSON.stringify(Array.from(this.processedMessages)),
98
+ 'utf-8'
99
+ );
100
+ } catch (error) {
101
+ console.error('[Yeego] Failed to save processed messages:', error);
102
+ }
103
+ }
104
+
105
+ // --------------------------------------------------------------------------
106
+ // Connection
107
+ // --------------------------------------------------------------------------
108
+
109
+ async connect(): Promise<void> {
110
+ try {
111
+ console.log('[Yeego] Connecting to Yeego API...');
112
+
113
+ const sidecarUrl = this.config.sidecar_url || this.config.base_url;
114
+ await fetch(`${sidecarUrl}/public/openclaw/connect`, {
115
+ method: 'POST',
116
+ body: JSON.stringify({
117
+ profile_id: this.config.profile_id,
118
+ }),
119
+ headers: {
120
+ 'Content-Type': 'application/json',
121
+ 'Authorization': `Bearer ${this.config.token}`,
122
+ },
123
+ });
124
+
125
+ console.log('[Yeego] Connected successfully');
126
+ } catch (error) {
127
+ console.error('[Yeego] Connection failed:', error);
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ // --------------------------------------------------------------------------
133
+ // Message Processing
134
+ // --------------------------------------------------------------------------
135
+
136
+ async checkAndProcessLastMessage(conversationId: string): Promise<void> {
137
+ try {
138
+ const conversation = await this.pb
139
+ .collection('profile_chat_conversations')
140
+ .getOne<PocketBaseConversation>(conversationId);
141
+
142
+ if (conversation.profile !== this.config.profile_id) {
143
+ return;
144
+ }
145
+
146
+ const lastMessages = await this.pb
147
+ .collection('profile_chat_messages')
148
+ .getList<PocketBaseMessage>(1, 1, {
149
+ filter: `conversation="${conversationId}"`,
150
+ sort: '-created',
151
+ });
152
+
153
+ if (lastMessages.items.length === 0) {
154
+ return;
155
+ }
156
+
157
+ const lastMessage = lastMessages.items[0];
158
+
159
+ if (lastMessage.by === 'user' && !lastMessage.is_ai_generated) {
160
+ if (!this.processedMessages.has(lastMessage.id)) {
161
+ await this.processMessage(lastMessage, conversation);
162
+ this.processedMessages.add(lastMessage.id);
163
+ this.saveProcessedMessages();
164
+ }
165
+ }
166
+ } catch (error) {
167
+ console.error('[Yeego] Error checking last message:', error);
168
+ }
169
+ }
170
+
171
+ private async processMessage(
172
+ message: PocketBaseMessage,
173
+ conversation: PocketBaseConversation
174
+ ): Promise<void> {
175
+ let placeholderId: string | null = null;
176
+
177
+ try {
178
+ console.log(`[Yeego] Processing message: ${message.id}`);
179
+
180
+ // Create placeholder message to show loading state
181
+ const placeholder = await this.pb.collection('profile_chat_messages').create({
182
+ conversation: conversation.id,
183
+ message: '',
184
+ by: 'persona',
185
+ is_ai_generated: true,
186
+ has_finished_generating: false,
187
+ source: 'openclaw',
188
+ });
189
+ placeholderId = placeholder.id;
190
+ console.log(`[Yeego] Created placeholder: ${placeholderId}`);
191
+
192
+ this.saveSessionMapping(conversation.id);
193
+
194
+ const aiText = await this.callOpenClawAgent(message, conversation);
195
+ console.log(`[Yeego] Got AI response, updating placeholder...`);
196
+
197
+ // Update placeholder with AI response
198
+ await this.pb.collection('profile_chat_messages').update(placeholderId, {
199
+ conversation: conversation.id,
200
+ message: aiText,
201
+ has_finished_generating: true,
202
+ });
203
+ console.log(`[Yeego] Update completed!`);
204
+
205
+ // Workaround: Set messageChannel in session file after agent creates it
206
+ setTimeout(() => {
207
+ this.setSessionMessageChannel(conversation.id, 'yeego');
208
+ }, SESSION_METADATA_DELAY_MS);
209
+
210
+ console.log(`[Yeego] ✓ Processed message ${message.id}`);
211
+ } catch (error) {
212
+ console.error(`[Yeego] Error processing message:`, error);
213
+
214
+ // Clean up placeholder on error
215
+ if (placeholderId) {
216
+ try {
217
+ await this.pb.collection('profile_chat_messages').delete(placeholderId);
218
+ } catch (deleteError) {
219
+ console.error(`[Yeego] Failed to delete placeholder:`, deleteError);
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ private saveSessionMapping(conversationId: string): void {
226
+ console.log(`[Yeego] Saving session mappings for conversation: ${conversationId}`);
227
+ setSessionConversation(conversationId, conversationId);
228
+ setSessionConversation(`agent:main:${conversationId}`, conversationId);
229
+ setSessionConversation(`yeego:${conversationId}`, conversationId);
230
+ }
231
+
232
+ private async callOpenClawAgent(
233
+ message: PocketBaseMessage,
234
+ conversation: PocketBaseConversation
235
+ ): Promise<string> {
236
+ const proc = spawn('openclaw', [
237
+ 'agent',
238
+ '--session-id',
239
+ conversation.id,
240
+ '--reply-channel',
241
+ 'yeego',
242
+ '--reply-to',
243
+ conversation.id,
244
+ '--message',
245
+ message.message,
246
+ '--json',
247
+ ], {
248
+ stdio: ['ignore', 'pipe', 'pipe'],
249
+ });
250
+
251
+ const { stdout, stderr, exitCode } = await this.captureProcessOutput(proc);
252
+
253
+ if (exitCode !== 0) {
254
+ console.error(`[Yeego] OpenClaw error:`, stderr);
255
+ return 'Error processing message';
256
+ }
257
+
258
+ return this.parseOpenClawResponse(stdout);
259
+ }
260
+
261
+ private captureProcessOutput(proc: ChildProcess): Promise<{
262
+ stdout: string;
263
+ stderr: string;
264
+ exitCode: number;
265
+ }> {
266
+ return new Promise((resolve) => {
267
+ let stdout = '';
268
+ let stderr = '';
269
+
270
+ proc.stdout?.on('data', (data) => {
271
+ stdout += data.toString();
272
+ });
273
+
274
+ proc.stderr?.on('data', (data) => {
275
+ stderr += data.toString();
276
+ });
277
+
278
+ proc.on('close', (code) => {
279
+ resolve({ stdout, stderr, exitCode: code || 0 });
280
+ });
281
+ });
282
+ }
283
+
284
+ private parseOpenClawResponse(output: string): string {
285
+ try {
286
+ const jsonMatch = output.match(/\{[\s\S]*\}/);
287
+ if (jsonMatch) {
288
+ const result: any = JSON.parse(jsonMatch[0]);
289
+
290
+ // Try to find payloads in different locations
291
+ let payloads = result.result?.payloads || result.payloads;
292
+
293
+ if (payloads && payloads.length > 0) {
294
+ const texts = payloads
295
+ .map((p: any) => p.text)
296
+ .filter((t: string) => t && t.trim())
297
+ .join('\n\n');
298
+ return texts || 'No response';
299
+ }
300
+
301
+ return result.text || 'No response';
302
+ }
303
+ return output.trim() || 'No response';
304
+ } catch (error) {
305
+ console.error(`[Yeego] Failed to parse OpenClaw response:`, error);
306
+ return output.trim() || 'No response';
307
+ }
308
+ }
309
+
310
+ private async createAIResponse(conversationId: string, text: string): Promise<void> {
311
+ await this.pb.collection('profile_chat_messages').create({
312
+ conversation: conversationId,
313
+ message: text,
314
+ by: 'persona',
315
+ is_ai_generated: true,
316
+ has_finished_generating: true,
317
+ source: 'openclaw',
318
+ });
319
+ }
320
+
321
+ // --------------------------------------------------------------------------
322
+ // Session Metadata Workaround
323
+ // --------------------------------------------------------------------------
324
+
325
+ /**
326
+ * Sets messageChannel in OpenClaw session metadata file.
327
+ * This is a workaround because --reply-channel CLI flag doesn't set the session's messageChannel property.
328
+ */
329
+ private setSessionMessageChannel(sessionId: string, channel: string): boolean {
330
+ try {
331
+ const sessionDir = join(homedir(), '.openclaw', 'agents', 'main', 'sessions');
332
+ const sessionFile = join(sessionDir, `${sessionId}.jsonl`);
333
+
334
+ if (!existsSync(sessionFile)) {
335
+ console.log(`[Yeego] Session file not found yet: ${sessionFile}`);
336
+ return false;
337
+ }
338
+
339
+ const lines = readFileSync(sessionFile, 'utf-8')
340
+ .split('\n')
341
+ .filter(l => l.trim());
342
+
343
+ if (lines.length === 0) {
344
+ return false;
345
+ }
346
+
347
+ const firstLine = JSON.parse(lines[0]);
348
+ if (firstLine.type === 'session') {
349
+ firstLine.messageChannel = channel;
350
+ lines[0] = JSON.stringify(firstLine);
351
+ writeFileSync(sessionFile, lines.join('\n') + '\n', 'utf-8');
352
+ console.log(`[Yeego] ✓ Set messageChannel="${channel}" for session ${sessionId}`);
353
+ return true;
354
+ }
355
+
356
+ return false;
357
+ } catch (error) {
358
+ console.error(`[Yeego] Failed to set messageChannel:`, error);
359
+ return false;
360
+ }
361
+ }
362
+
363
+ // --------------------------------------------------------------------------
364
+ // Polling
365
+ // --------------------------------------------------------------------------
366
+
367
+ private async pollForNewMessages(): Promise<void> {
368
+ try {
369
+ // Get recent messages from the last 10 seconds
370
+ const tenSecondsAgo = new Date(Date.now() - 10000).toISOString().replace('T', ' ').substring(0, 19);
371
+ const messages = await this.pb.collection('profile_chat_messages').getList<PocketBaseMessage>(1, 50, {
372
+ filter: `created >= "${tenSecondsAgo}" && by = "user" && is_ai_generated = false`,
373
+ sort: '-created',
374
+ });
375
+
376
+ for (const message of messages.items) {
377
+ if (!this.processedMessages.has(message.id) && message.conversation) {
378
+ console.log(`[Yeego] New message detected: ${message.id}`);
379
+ await this.checkAndProcessLastMessage(message.conversation);
380
+ }
381
+ }
382
+ } catch (error) {
383
+ console.error('[Yeego] Error polling messages:', error);
384
+ }
385
+ }
386
+
387
+ // --------------------------------------------------------------------------
388
+ // Lifecycle
389
+ // --------------------------------------------------------------------------
390
+
391
+ async start(): Promise<void> {
392
+ if (this.isRunning) {
393
+ console.log('[Yeego] Already running');
394
+ return;
395
+ }
396
+
397
+ this.isRunning = true;
398
+ console.log('[Yeego] Starting poller...');
399
+
400
+ await this.connect();
401
+
402
+ console.log('[Yeego] Starting to poll for new messages...');
403
+
404
+ // Poll for new messages at regular intervals
405
+ this.pollInterval = setInterval(() => {
406
+ if (this.isRunning) {
407
+ this.pollForNewMessages();
408
+ }
409
+ }, POLL_INTERVAL_MS);
410
+
411
+ console.log('[Yeego] Polling for messages...');
412
+
413
+ return new Promise<void>((resolve) => {
414
+ const checkInterval = setInterval(() => {
415
+ if (!this.isRunning) {
416
+ clearInterval(checkInterval);
417
+ resolve();
418
+ }
419
+ }, 1000);
420
+ });
421
+ }
422
+
423
+ stop(): void {
424
+ this.isRunning = false;
425
+ if (this.pollInterval) {
426
+ clearInterval(this.pollInterval);
427
+ this.pollInterval = undefined;
428
+ }
429
+ console.log('[Yeego] Stopped');
430
+ }
431
+ }
432
+
433
+ // ============================================================================
434
+ // CLI Commands
435
+ // ============================================================================
436
+
437
+ async function setup(configText: string): Promise<void> {
438
+ try {
439
+ console.log('[Yeego] Setting up configuration...');
440
+
441
+ const config: Partial<YeegoConfig> = {};
442
+ configText.split('\n').forEach(line => {
443
+ line = line.trim();
444
+ if (line && !line.startsWith('#')) {
445
+ const match = line.match(/^(\w+)=(.+)$/);
446
+ if (match) {
447
+ const [, key, value] = match;
448
+ if (key === 'YEEGO_TOKEN') config.token = value;
449
+ else if (key === 'YEEGO_PROFILE_ID') config.profile_id = value;
450
+ else if (key === 'YEEGO_BASE_URL') config.base_url = value;
451
+ else if (key === 'YEEGO_SIDECAR_URL') config.sidecar_url = value;
452
+ }
453
+ }
454
+ });
455
+
456
+ if (!config.token || !config.profile_id || !config.base_url) {
457
+ throw new Error('Missing required configuration: token, profile_id, or base_url');
458
+ }
459
+
460
+ if (!existsSync(CONFIG_DIR)) {
461
+ mkdirSync(CONFIG_DIR, { recursive: true });
462
+ }
463
+
464
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
465
+ console.log('[Yeego] Configuration saved to', CONFIG_FILE);
466
+ } catch (error) {
467
+ console.error('[Yeego] Setup failed:', error);
468
+ process.exit(1);
469
+ }
470
+ }
471
+
472
+ async function start(): Promise<void> {
473
+ try {
474
+ if (!existsSync(CONFIG_FILE)) {
475
+ console.error('[Yeego] Configuration not found. Run setup first.');
476
+ process.exit(1);
477
+ }
478
+
479
+ const config: YeegoConfig = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
480
+ const poller = new YeegoPoller(config);
481
+
482
+ const shutdown = () => {
483
+ console.log('\n[Yeego] Shutting down...');
484
+ poller.stop();
485
+ process.exit(0);
486
+ };
487
+
488
+ process.on('SIGINT', shutdown);
489
+ process.on('SIGTERM', shutdown);
490
+
491
+ await poller.start();
492
+ } catch (error) {
493
+ console.error('[Yeego] Fatal error:', error);
494
+ process.exit(1);
495
+ }
496
+ }
497
+
498
+ // ============================================================================
499
+ // Main Entry Point
500
+ // ============================================================================
501
+
502
+ const command = process.argv[2];
503
+ const arg = process.argv[3];
504
+
505
+ if (command === 'setup') {
506
+ if (!arg) {
507
+ console.error('Usage: poller.ts setup <config-text>');
508
+ process.exit(1);
509
+ }
510
+ setup(arg);
511
+ } else if (command === 'start') {
512
+ start();
513
+ } else {
514
+ console.error('Usage: poller.ts {setup|start}');
515
+ process.exit(1);
516
+ }