@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.
- package/LICENSE +21 -0
- package/QUICKSTART.md +82 -0
- package/README.md +271 -0
- package/index.ts +334 -0
- package/openclaw.plugin.json +31 -0
- package/package.json +45 -0
- package/poller.ts +516 -0
- package/src/channel.ts +500 -0
- package/src/sessionMapping.ts +80 -0
- package/src/tools/index.ts +25 -0
- package/src/tools/pocketbase.ts +56 -0
- package/src/tools/profileTools.ts +214 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yeego Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/QUICKSTART.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Quick Start Guide
|
|
2
|
+
|
|
3
|
+
## 🚀 Get Started in 3 Steps
|
|
4
|
+
|
|
5
|
+
### Step 1: Get Your Configuration
|
|
6
|
+
|
|
7
|
+
1. Open Yeego mobile app
|
|
8
|
+
2. Go to your profile → Settings
|
|
9
|
+
3. Tap "Connect to OpenClaw" 🦞
|
|
10
|
+
4. Tap "Copy Configuration"
|
|
11
|
+
|
|
12
|
+
You'll get something like:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
# Yeego OpenClaw Plugin Configuration
|
|
16
|
+
YEEGO_TOKEN=eyJhbG...
|
|
17
|
+
YEEGO_PROFILE_ID=abc123...
|
|
18
|
+
YEEGO_BASE_URL=http://localhost:8091
|
|
19
|
+
YEEGO_PLUGIN_URL=http://localhost:8092/openclaw-plugin
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Step 2: Save Configuration
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd openclaw-plugin
|
|
26
|
+
|
|
27
|
+
# Paste your config and save
|
|
28
|
+
bun run setup "YEEGO_TOKEN=eyJhbG...
|
|
29
|
+
YEEGO_PROFILE_ID=abc123...
|
|
30
|
+
YEEGO_BASE_URL=http://localhost:8091"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
✅ Configuration saved to `~/.yeego/config.json`
|
|
34
|
+
|
|
35
|
+
### Step 3: Start Polling
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bun run start
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it! The plugin will:
|
|
42
|
+
- ✅ Connect to your Yeego profile
|
|
43
|
+
- ✅ Poll for new messages every 5 seconds
|
|
44
|
+
- ✅ Process them through OpenClaw
|
|
45
|
+
- ✅ Send AI responses back to Yeego
|
|
46
|
+
|
|
47
|
+
## 📱 Test It
|
|
48
|
+
|
|
49
|
+
1. Open Yeego app
|
|
50
|
+
2. Send a message to your AI persona
|
|
51
|
+
3. Watch the poller process it
|
|
52
|
+
4. See the AI response appear in the app
|
|
53
|
+
|
|
54
|
+
## 🔧 Troubleshooting
|
|
55
|
+
|
|
56
|
+
**No messages being processed?**
|
|
57
|
+
- Make sure you sent a message in the Yeego app
|
|
58
|
+
- Check the profile_id matches your actual profile
|
|
59
|
+
|
|
60
|
+
**Connection errors?**
|
|
61
|
+
- Verify backend is running (`http://localhost:8091`)
|
|
62
|
+
- Check your token hasn't expired (30 days validity)
|
|
63
|
+
|
|
64
|
+
**Want to reset?**
|
|
65
|
+
```bash
|
|
66
|
+
rm -rf ~/.yeego
|
|
67
|
+
# Then run setup again
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 📦 Production Deployment
|
|
71
|
+
|
|
72
|
+
When ready to deploy:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Build standalone executable
|
|
76
|
+
bun run build
|
|
77
|
+
|
|
78
|
+
# Run it anywhere
|
|
79
|
+
./yeego-poller start
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
No dependencies needed - it's a single binary! 🎉
|
package/README.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Yeego OpenClaw Plugin
|
|
2
|
+
|
|
3
|
+
OpenClaw channel plugin for integrating Yeego with OpenClaw AI agent.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This plugin enables bidirectional communication between Yeego and OpenClaw:
|
|
8
|
+
|
|
9
|
+
- **Inbound**: Listens to Yeego user messages via PocketBase realtime subscriptions
|
|
10
|
+
- **Outbound**: Sends OpenClaw AI responses back to Yeego conversations
|
|
11
|
+
- **Proactive**: Allows OpenClaw to initiate messages through cron jobs and tools
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
### Components
|
|
16
|
+
|
|
17
|
+
1. **Channel Plugin** (`src/channel.ts`)
|
|
18
|
+
- Registers Yeego as an OpenClaw channel
|
|
19
|
+
- Handles outbound message sending via PocketBase
|
|
20
|
+
- Manages poller subprocess lifecycle
|
|
21
|
+
- Implements target resolution for conversation IDs
|
|
22
|
+
|
|
23
|
+
2. **Realtime Listener** (`poller.ts`)
|
|
24
|
+
- Subscribes to PocketBase message events
|
|
25
|
+
- Processes incoming user messages
|
|
26
|
+
- Calls OpenClaw agent CLI
|
|
27
|
+
- Stores AI responses back to PocketBase
|
|
28
|
+
- Sets messageChannel metadata (workaround for CLI limitation)
|
|
29
|
+
|
|
30
|
+
3. **Session Mapping** (`src/sessionMapping.ts`)
|
|
31
|
+
- Maintains persistent session ↔ conversation mapping
|
|
32
|
+
- File-based storage for cross-process sharing
|
|
33
|
+
- Enables proactive message targeting
|
|
34
|
+
|
|
35
|
+
4. **Tools** (`src/tools/`)
|
|
36
|
+
- Profile management tools (create, list, get profiles)
|
|
37
|
+
- Target ID lookup for Yeego conversations
|
|
38
|
+
|
|
39
|
+
## Key Implementation Details
|
|
40
|
+
|
|
41
|
+
### Message Flow
|
|
42
|
+
|
|
43
|
+
#### Inbound (User → OpenClaw)
|
|
44
|
+
```
|
|
45
|
+
User Message (Yeego App)
|
|
46
|
+
↓
|
|
47
|
+
PocketBase Realtime Event
|
|
48
|
+
↓
|
|
49
|
+
Poller (poller.ts)
|
|
50
|
+
↓
|
|
51
|
+
OpenClaw Agent CLI (--reply-channel yeego)
|
|
52
|
+
↓
|
|
53
|
+
AI Response → PocketBase
|
|
54
|
+
↓
|
|
55
|
+
Yeego App displays response
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Outbound (OpenClaw → User)
|
|
59
|
+
```
|
|
60
|
+
OpenClaw wants to send message
|
|
61
|
+
↓
|
|
62
|
+
Resolves target via messaging.targetResolver.looksLikeId
|
|
63
|
+
↓
|
|
64
|
+
Calls outbound.sendText
|
|
65
|
+
↓
|
|
66
|
+
Creates message in PocketBase
|
|
67
|
+
↓
|
|
68
|
+
Yeego App displays message
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Critical Workarounds
|
|
72
|
+
|
|
73
|
+
#### 1. messageChannel Metadata
|
|
74
|
+
**Problem**: OpenClaw CLI's `--reply-channel` flag doesn't set the session's `messageChannel` property.
|
|
75
|
+
|
|
76
|
+
**Solution**: `setSessionMessageChannel()` in `poller.ts` directly modifies the session JSONL file after creation to inject `messageChannel: "yeego"`.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Wait for session file creation, then patch it
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
this.setSessionMessageChannel(conversation.id, 'yeego');
|
|
82
|
+
}, 1000);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### 2. Target Recognition
|
|
86
|
+
**Problem**: OpenClaw's target resolver tries to look up conversation IDs in a directory, causing "Unknown target" errors.
|
|
87
|
+
|
|
88
|
+
**Solution**: Implement `messaging.targetResolver.looksLikeId` to tell OpenClaw that our alphanumeric conversation IDs are valid IDs:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
messaging: {
|
|
92
|
+
targetResolver: {
|
|
93
|
+
looksLikeId: (_raw, normalized) => /^[a-z0-9]+$/i.test(normalized),
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This bypasses directory lookup entirely.
|
|
99
|
+
|
|
100
|
+
#### 3. Cron Job Delivery
|
|
101
|
+
|
|
102
|
+
**Important**: When using cron jobs or other isolated sessions to send messages to Yeego, you must specify both `channel` and `to` parameters in the payload:
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
{
|
|
106
|
+
"deliver": true,
|
|
107
|
+
"channel": "yeego",
|
|
108
|
+
"to": "conversation_id_here"
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Unlike normal conversations where OpenClaw knows the origin context, cron jobs run in isolated sessions. The `channel` and `to` parameters tell OpenClaw where to deliver the message.
|
|
113
|
+
|
|
114
|
+
**Example cron job configuration**:
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"id": "daily_reminder",
|
|
118
|
+
"enabled": true,
|
|
119
|
+
"schedule": "0 9 * * *",
|
|
120
|
+
"payload": {
|
|
121
|
+
"deliver": true,
|
|
122
|
+
"channel": "yeego",
|
|
123
|
+
"to": "lq45hgra9wpado5",
|
|
124
|
+
"message": "Good morning! Don't forget your daily tasks."
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Without these parameters, you'll get an error: "Cron delivery requires a recipient (--to)".
|
|
130
|
+
|
|
131
|
+
## Installation
|
|
132
|
+
|
|
133
|
+
### Quick Start (Recommended)
|
|
134
|
+
|
|
135
|
+
1. Install the plugin:
|
|
136
|
+
```bash
|
|
137
|
+
openclaw plugins install @yeego/openclaw-plugin
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
2. Get your configuration from Yeego app:
|
|
141
|
+
- Open Yeego mobile app
|
|
142
|
+
- Go to Profile → Settings
|
|
143
|
+
- Tap "Connect to OpenClaw" 🦞
|
|
144
|
+
- Copy the configuration
|
|
145
|
+
|
|
146
|
+
3. Add configuration to `~/.openclaw/openclaw.json`:
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"channels": {
|
|
150
|
+
"yeego": {
|
|
151
|
+
"config": {
|
|
152
|
+
"token": "your-token-here",
|
|
153
|
+
"profileId": "your-profile-id",
|
|
154
|
+
"baseUrl": "https://yeego.app",
|
|
155
|
+
"sidecarUrl": "https://yeego.app/_sidecar",
|
|
156
|
+
"connectUrl": "https://yeego.app/_sidecar/public/openclaw/connect"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
4. Restart OpenClaw gateway:
|
|
164
|
+
```bash
|
|
165
|
+
openclaw gateway restart
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The plugin will automatically:
|
|
169
|
+
- ✅ Enable the Yeego channel
|
|
170
|
+
- ✅ Start the message poller process
|
|
171
|
+
- ✅ Connect to your Yeego profile
|
|
172
|
+
|
|
173
|
+
### Manual Installation (Development)
|
|
174
|
+
|
|
175
|
+
For local development, contact the Yeego team for access to the plugin source code.
|
|
176
|
+
|
|
177
|
+
### Configuration Files
|
|
178
|
+
|
|
179
|
+
The plugin creates and manages these files automatically:
|
|
180
|
+
- `~/.yeego/config.json` - Yeego credentials (auto-generated from OpenClaw config)
|
|
181
|
+
- `~/.openclaw/yeego-sessions/session-mappings.json` - Session mappings
|
|
182
|
+
- `~/.yeego/processed.json` - Processed message IDs (deduplication)
|
|
183
|
+
|
|
184
|
+
## Development
|
|
185
|
+
|
|
186
|
+
### File Structure
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
openclaw-plugin/
|
|
190
|
+
├── index.ts # Plugin entry point
|
|
191
|
+
├── poller.ts # Realtime listener (subprocess)
|
|
192
|
+
├── src/
|
|
193
|
+
│ ├── channel.ts # Channel plugin implementation
|
|
194
|
+
│ ├── sessionMapping.ts # Session storage
|
|
195
|
+
│ └── tools/
|
|
196
|
+
│ ├── index.ts # Tool factory
|
|
197
|
+
│ ├── profileTools.ts # Profile management
|
|
198
|
+
│ └── pocketbase.ts # PocketBase client wrapper
|
|
199
|
+
├── package.json
|
|
200
|
+
└── README.md
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Type Safety
|
|
204
|
+
|
|
205
|
+
All code uses TypeScript with strict typing:
|
|
206
|
+
- PocketBase record types
|
|
207
|
+
- OpenClaw plugin SDK types
|
|
208
|
+
- Explicit return types for all functions
|
|
209
|
+
- No `any` types (except for globalThis polyfill)
|
|
210
|
+
|
|
211
|
+
### Code Style
|
|
212
|
+
|
|
213
|
+
- Clear section separators with `// ===` comments
|
|
214
|
+
- Descriptive function and variable names
|
|
215
|
+
- JSDoc comments for public APIs
|
|
216
|
+
- Grouped related functions
|
|
217
|
+
- Constants at top of file
|
|
218
|
+
|
|
219
|
+
## Testing
|
|
220
|
+
|
|
221
|
+
Send a message in Yeego app to test the integration. Check logs:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
tail -f ~/.openclaw/logs/gateway.log | grep Yeego
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Expected flow:
|
|
228
|
+
1. `[Yeego] New message detected`
|
|
229
|
+
2. `[Yeego] Processing message`
|
|
230
|
+
3. `[Yeego] ✓ Set messageChannel="yeego"`
|
|
231
|
+
4. `[Yeego] Message delivered`
|
|
232
|
+
|
|
233
|
+
## Troubleshooting
|
|
234
|
+
|
|
235
|
+
### "Unknown target" errors
|
|
236
|
+
- Check that `messaging.targetResolver.looksLikeId` is implemented
|
|
237
|
+
- Verify conversation ID format is alphanumeric
|
|
238
|
+
|
|
239
|
+
### "Failed to connect to PocketBase"
|
|
240
|
+
- Use `127.0.0.1` instead of `localhost` in config
|
|
241
|
+
- Verify PocketBase is running: `curl http://127.0.0.1:8091/api/health`
|
|
242
|
+
|
|
243
|
+
### Messages go to wrong channel
|
|
244
|
+
- Check `~/.openclaw/agents/main/sessions/{sessionId}.jsonl`
|
|
245
|
+
- First line should contain `"messageChannel": "yeego"`
|
|
246
|
+
- If missing, poller's `setSessionMessageChannel()` may have failed
|
|
247
|
+
|
|
248
|
+
### Poller not starting
|
|
249
|
+
- Check gateway logs: `tail ~/.openclaw/logs/gateway.log`
|
|
250
|
+
- Verify tsx is available: `which tsx`
|
|
251
|
+
- Check poller config: `cat ~/.yeego/config.json`
|
|
252
|
+
|
|
253
|
+
### "Cron delivery requires a recipient (--to)"
|
|
254
|
+
This error occurs when a cron job or isolated session tries to send messages without specifying the delivery target.
|
|
255
|
+
|
|
256
|
+
**Solution**: Add `channel` and `to` parameters to your cron job payload:
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"payload": {
|
|
260
|
+
"deliver": true,
|
|
261
|
+
"channel": "yeego",
|
|
262
|
+
"to": "conversation_id"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
See the "Cron Job Delivery" section under "Key Implementation Details" for more information.
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
Part of the Yeego application.
|
package/index.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yeego OpenClaw Plugin Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This plugin integrates Yeego with OpenClaw by:
|
|
5
|
+
* - Registering the Yeego channel for message sending/receiving
|
|
6
|
+
* - Providing tools for Yeego-specific operations (profile management, target lookup)
|
|
7
|
+
* - Auto-starting the yeego-poller process for message polling
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
11
|
+
import { yeegoPlugin } from "./src/channel.js";
|
|
12
|
+
import { createYeegoTools } from "./src/tools/index.js";
|
|
13
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
|
|
19
|
+
let pollerProcess: ChildProcess | null = null;
|
|
20
|
+
let pollerRestartCount = 0;
|
|
21
|
+
const MAX_RESTART_ATTEMPTS = 5;
|
|
22
|
+
|
|
23
|
+
function ensureChannelConfig() {
|
|
24
|
+
try {
|
|
25
|
+
const configPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
26
|
+
if (!existsSync(configPath)) {
|
|
27
|
+
console.error(`[Yeego] Config file not found: ${configPath}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const fullConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
32
|
+
|
|
33
|
+
// Check if channels.yeego.config exists
|
|
34
|
+
if (!fullConfig?.channels?.yeego?.config) {
|
|
35
|
+
console.error(`[Yeego] No channel config found, skipping auto-enable`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let modified = false;
|
|
40
|
+
|
|
41
|
+
// Auto-enable the channel if not explicitly set
|
|
42
|
+
if (fullConfig.channels.yeego.enabled === undefined) {
|
|
43
|
+
fullConfig.channels.yeego.enabled = true;
|
|
44
|
+
modified = true;
|
|
45
|
+
console.error(`[Yeego] Auto-enabled channel`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Save if modified
|
|
49
|
+
if (modified) {
|
|
50
|
+
writeFileSync(configPath, JSON.stringify(fullConfig, null, 2), 'utf-8');
|
|
51
|
+
console.error(`[Yeego] Updated OpenClaw config with required settings`);
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`[Yeego] Error ensuring channel config:`, error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const plugin = {
|
|
59
|
+
id: "yeego",
|
|
60
|
+
name: "Yeego",
|
|
61
|
+
description: "Yeego chat channel integration for OpenClaw",
|
|
62
|
+
|
|
63
|
+
configSchema: {
|
|
64
|
+
type: "object" as const,
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
properties: {
|
|
67
|
+
token: {
|
|
68
|
+
type: "string" as const,
|
|
69
|
+
description: "Yeego authentication token",
|
|
70
|
+
},
|
|
71
|
+
profileId: {
|
|
72
|
+
type: "string" as const,
|
|
73
|
+
description: "Yeego profile ID",
|
|
74
|
+
},
|
|
75
|
+
baseUrl: {
|
|
76
|
+
type: "string" as const,
|
|
77
|
+
description: "Yeego base URL",
|
|
78
|
+
},
|
|
79
|
+
sidecarUrl: {
|
|
80
|
+
type: "string" as const,
|
|
81
|
+
description: "Yeego sidecar URL",
|
|
82
|
+
},
|
|
83
|
+
connectUrl: {
|
|
84
|
+
type: "string" as const,
|
|
85
|
+
description: "Yeego connect endpoint URL",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
required: ["token", "profileId", "baseUrl", "sidecarUrl", "connectUrl"],
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async register(api: OpenClawPluginApi) {
|
|
92
|
+
try {
|
|
93
|
+
// Ensure channel configuration is complete
|
|
94
|
+
ensureChannelConfig();
|
|
95
|
+
|
|
96
|
+
// Register channel plugin
|
|
97
|
+
api.registerChannel({ plugin: yeegoPlugin });
|
|
98
|
+
console.error(`[Yeego] Channel registered`);
|
|
99
|
+
|
|
100
|
+
// Register tool factory
|
|
101
|
+
api.registerTool((ctx) => {
|
|
102
|
+
console.error(`[Yeego] Creating tools for session: ${ctx.sessionKey}`);
|
|
103
|
+
const tools = createYeegoTools(ctx);
|
|
104
|
+
console.error(`[Yeego] Registered ${tools.length} tools: ${tools.map(t => t.name).join(", ")}`);
|
|
105
|
+
return tools;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
console.error(`[Yeego] Plugin registration complete`);
|
|
109
|
+
|
|
110
|
+
// Call connect endpoint on startup
|
|
111
|
+
await callConnectEndpoint(api);
|
|
112
|
+
|
|
113
|
+
// Start the poller process
|
|
114
|
+
startPoller();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(`[Yeego] Registration error:`, error);
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function getPluginDir(): string {
|
|
123
|
+
// Get the directory where this plugin is installed
|
|
124
|
+
// Try multiple methods to find the plugin directory
|
|
125
|
+
try {
|
|
126
|
+
// Method 1: Use import.meta.url (ESM)
|
|
127
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
128
|
+
return dirname(__filename);
|
|
129
|
+
} catch {
|
|
130
|
+
// Method 2: Check common installation paths
|
|
131
|
+
const possiblePaths = [
|
|
132
|
+
join(homedir(), '.openclaw', 'plugins', '@yeego', 'yeego'),
|
|
133
|
+
join(homedir(), '.openclaw', 'plugins', '@yeego', 'openclaw-plugin'),
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
for (const p of possiblePaths) {
|
|
137
|
+
if (existsSync(join(p, 'yeego-poller'))) {
|
|
138
|
+
return p;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Fallback
|
|
143
|
+
return join(homedir(), '.openclaw', 'plugins', '@yeego', 'yeego');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function startPoller() {
|
|
148
|
+
try {
|
|
149
|
+
const pluginDir = getPluginDir();
|
|
150
|
+
const pollerPath = join(pluginDir, 'yeego-poller');
|
|
151
|
+
|
|
152
|
+
if (!existsSync(pollerPath)) {
|
|
153
|
+
console.error(`[Yeego] Poller binary not found at: ${pollerPath}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check if poller is already running
|
|
158
|
+
if (pollerProcess && !pollerProcess.killed) {
|
|
159
|
+
console.error(`[Yeego] Poller is already running (pid: ${pollerProcess.pid})`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.error(`[Yeego] Starting poller from: ${pollerPath}`);
|
|
164
|
+
|
|
165
|
+
pollerProcess = spawn(pollerPath, ['start'], {
|
|
166
|
+
detached: false,
|
|
167
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
pollerProcess.stdout?.on('data', (data) => {
|
|
171
|
+
console.error(`[Yeego Poller] ${data.toString().trim()}`);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
pollerProcess.stderr?.on('data', (data) => {
|
|
175
|
+
console.error(`[Yeego Poller] ${data.toString().trim()}`);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
pollerProcess.on('error', (err) => {
|
|
179
|
+
console.error(`[Yeego] Failed to start poller:`, err);
|
|
180
|
+
pollerProcess = null;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
pollerProcess.on('exit', (code, signal) => {
|
|
184
|
+
console.error(`[Yeego] Poller exited with code ${code}, signal ${signal}`);
|
|
185
|
+
pollerProcess = null;
|
|
186
|
+
|
|
187
|
+
// Auto-restart after 5 seconds if it crashed (with limit)
|
|
188
|
+
if (code !== 0 && code !== null) {
|
|
189
|
+
pollerRestartCount++;
|
|
190
|
+
if (pollerRestartCount <= MAX_RESTART_ATTEMPTS) {
|
|
191
|
+
const delay = Math.min(5000 * pollerRestartCount, 30000); // Exponential backoff, max 30s
|
|
192
|
+
console.error(`[Yeego] Poller crashed, restarting in ${delay}ms (attempt ${pollerRestartCount}/${MAX_RESTART_ATTEMPTS})...`);
|
|
193
|
+
setTimeout(() => startPoller(), delay);
|
|
194
|
+
} else {
|
|
195
|
+
console.error(`[Yeego] Poller crashed ${pollerRestartCount} times, giving up. Please check configuration.`);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// Normal exit, reset counter
|
|
199
|
+
pollerRestartCount = 0;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
console.error(`[Yeego] Poller started (pid: ${pollerProcess.pid})`);
|
|
204
|
+
|
|
205
|
+
// Reset restart counter on successful start
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
if (pollerProcess && !pollerProcess.killed) {
|
|
208
|
+
pollerRestartCount = 0;
|
|
209
|
+
console.error(`[Yeego] Poller running stable, reset restart counter`);
|
|
210
|
+
}
|
|
211
|
+
}, 10000); // Consider stable if running for 10 seconds
|
|
212
|
+
|
|
213
|
+
// Cleanup on process exit
|
|
214
|
+
process.on('exit', () => {
|
|
215
|
+
if (pollerProcess && !pollerProcess.killed) {
|
|
216
|
+
console.error(`[Yeego] Stopping poller...`);
|
|
217
|
+
pollerProcess.kill('SIGTERM');
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
process.on('SIGINT', () => {
|
|
222
|
+
if (pollerProcess && !pollerProcess.killed) {
|
|
223
|
+
pollerProcess.kill('SIGTERM');
|
|
224
|
+
}
|
|
225
|
+
process.exit(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
process.on('SIGTERM', () => {
|
|
229
|
+
if (pollerProcess && !pollerProcess.killed) {
|
|
230
|
+
pollerProcess.kill('SIGTERM');
|
|
231
|
+
}
|
|
232
|
+
process.exit(0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error(`[Yeego] Error starting poller:`, error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function validateYeegoUrl(url: string, baseUrl: string): boolean {
|
|
241
|
+
try {
|
|
242
|
+
const parsed = new URL(url);
|
|
243
|
+
const baseParsed = new URL(baseUrl);
|
|
244
|
+
|
|
245
|
+
// Ensure URL and baseUrl have the same hostname
|
|
246
|
+
if (parsed.hostname !== baseParsed.hostname) {
|
|
247
|
+
console.error(`[Yeego] URL hostname mismatch: ${parsed.hostname} !== ${baseParsed.hostname}`);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Ensure using https (except localhost)
|
|
252
|
+
if (parsed.protocol !== 'https:' && parsed.hostname !== 'localhost' && !parsed.hostname.endsWith('.localhost')) {
|
|
253
|
+
console.error(`[Yeego] Invalid protocol: ${parsed.protocol}, must use https`);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return true;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error(`[Yeego] Invalid URL:`, error);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function callConnectEndpoint(api: OpenClawPluginApi) {
|
|
265
|
+
try {
|
|
266
|
+
// Read config directly from file instead of using api.getChannelConfig
|
|
267
|
+
// This is more reliable during the registration phase
|
|
268
|
+
const configPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
269
|
+
if (!existsSync(configPath)) {
|
|
270
|
+
console.error(`[Yeego] Config file not found: ${configPath}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const fullConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
275
|
+
const config = fullConfig?.channels?.yeego?.config;
|
|
276
|
+
|
|
277
|
+
if (!config || !config.connectUrl || !config.token || !config.profileId || !config.baseUrl) {
|
|
278
|
+
console.error(`[Yeego] Missing config for connect endpoint:`, {
|
|
279
|
+
hasConnectUrl: !!config?.connectUrl,
|
|
280
|
+
hasToken: !!config?.token,
|
|
281
|
+
hasProfileId: !!config?.profileId,
|
|
282
|
+
hasBaseUrl: !!config?.baseUrl,
|
|
283
|
+
configPath,
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Validate connectUrl to prevent SSRF
|
|
289
|
+
if (!validateYeegoUrl(config.connectUrl, config.baseUrl)) {
|
|
290
|
+
console.error(`[Yeego] Invalid connectUrl: must match baseUrl domain`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.error(`[Yeego] Calling connect endpoint`);
|
|
295
|
+
|
|
296
|
+
const response = await fetch(config.connectUrl, {
|
|
297
|
+
method: "POST",
|
|
298
|
+
headers: {
|
|
299
|
+
"Authorization": `Bearer ${config.token}`,
|
|
300
|
+
"Content-Type": "application/json",
|
|
301
|
+
},
|
|
302
|
+
body: JSON.stringify({ profile_id: config.profileId }),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
const errorText = await response.text();
|
|
307
|
+
console.error(`[Yeego] Connect endpoint failed: ${response.status} ${errorText}`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const result = await response.json();
|
|
312
|
+
console.error(`[Yeego] Successfully connected:`, result);
|
|
313
|
+
|
|
314
|
+
// Write config to ~/.yeego/config.json for poller (using underscore format)
|
|
315
|
+
const yeegoConfigDir = join(homedir(), '.yeego');
|
|
316
|
+
const yeegoConfigFile = join(yeegoConfigDir, 'config.json');
|
|
317
|
+
|
|
318
|
+
mkdirSync(yeegoConfigDir, { recursive: true });
|
|
319
|
+
|
|
320
|
+
const pollerConfig = {
|
|
321
|
+
token: config.token,
|
|
322
|
+
profile_id: config.profileId,
|
|
323
|
+
base_url: config.baseUrl,
|
|
324
|
+
sidecar_url: config.sidecarUrl,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
writeFileSync(yeegoConfigFile, JSON.stringify(pollerConfig, null, 2), 'utf-8');
|
|
328
|
+
console.error(`[Yeego] Saved poller config to ${yeegoConfigFile}`);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error(`[Yeego] Error calling connect endpoint:`, error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export default plugin;
|