@eyeclaw/eyeclaw 2.0.0 β†’ 2.0.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/README.md CHANGED
@@ -212,6 +212,112 @@ client.disconnect()
212
212
  - **Issues**: [https://github.com/eyeclaw/eyeclaw/issues](https://github.com/eyeclaw/eyeclaw/issues)
213
213
  - **Discord**: [https://discord.gg/eyeclaw](https://discord.gg/eyeclaw)
214
214
 
215
+ ## πŸ’¬ Chat Functionality
216
+
217
+ The EyeClaw SDK includes built-in chat functionality that allows you to interact with your bot through the web dashboard.
218
+
219
+ ### Testing Chat
220
+
221
+ 1. Start your bot with the EyeClaw plugin installed
222
+ 2. Go to your bot's detail page on EyeClaw Dashboard
223
+ 3. Click **"Test Chat"** button
224
+ 4. Send messages - the bot will respond with smart replies
225
+ 5. Use quick test buttons:
226
+ - **Ping** - Test bot connectivity
227
+ - **Status** - Get bot status (online, sessions, uptime)
228
+ - **Echo** - Echo back a test message
229
+ - **Help** - Show available commands
230
+
231
+ ### Supported Commands
232
+
233
+ The SDK automatically handles these commands from the web dashboard:
234
+
235
+ - **`chat`** - Receive and respond to chat messages
236
+ - Params: `{ message: string }`
237
+ - The bot uses pattern matching to generate smart responses
238
+ - Supports greetings, questions, thanks, farewells in Chinese and English
239
+
240
+ - **`ping`** - Test bot connectivity
241
+ - Response: "πŸ“ Pong! Bot is responding."
242
+
243
+ - **`status`** - Get current bot status
244
+ - Returns: online status, active sessions, total sessions, uptime
245
+
246
+ - **`echo`** - Echo back a message
247
+ - Params: `{ message: string }`
248
+ - Response: Echoes the message back
249
+
250
+ - **`help`** - Show available commands
251
+ - Response: Lists all available commands and their usage
252
+
253
+ ### Customizing Bot Responses
254
+
255
+ You can customize the bot's chat responses by modifying `generateBotReply()` in `sdk/src/client.ts`:
256
+
257
+ ```typescript
258
+ private generateBotReply(userMessage: string): string {
259
+ const lowerMessage = userMessage.toLowerCase()
260
+
261
+ // Add your custom patterns
262
+ if (lowerMessage.includes('weather')) {
263
+ return '🌀️ The weather is great today!'
264
+ }
265
+
266
+ if (lowerMessage.includes('time')) {
267
+ return `πŸ• Current time: ${new Date().toLocaleTimeString()}`
268
+ }
269
+
270
+ // Default response
271
+ return `πŸ’¬ You said: "${userMessage}"`
272
+ }
273
+ ```
274
+
275
+ ### Integrating AI/LLM Services
276
+
277
+ For production use, integrate your preferred AI service:
278
+
279
+ ```typescript
280
+ import OpenAI from 'openai'
281
+
282
+ private async generateBotReply(userMessage: string): Promise<string> {
283
+ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
284
+
285
+ const completion = await openai.chat.completions.create({
286
+ model: 'gpt-4',
287
+ messages: [
288
+ { role: 'system', content: 'You are a helpful assistant.' },
289
+ { role: 'user', content: userMessage }
290
+ ]
291
+ })
292
+
293
+ return completion.choices[0].message.content || 'Sorry, I could not generate a response.'
294
+ }
295
+ ```
296
+
297
+ ### Message Flow
298
+
299
+ ```
300
+ User (Web Dashboard)
301
+ β”‚
302
+ β–Ό
303
+ DashboardChannel ──execute_command──► BotChannel
304
+ β”‚ β”‚
305
+ β”‚ β–Ό
306
+ β”‚ SDK receives command
307
+ β”‚ β”‚
308
+ β”‚ β–Ό
309
+ β”‚ handleExecuteCommand()
310
+ β”‚ β”‚
311
+ β”‚ β–Ό
312
+ β”‚ generateBotReply()
313
+ β”‚ β”‚
314
+ β”‚ β–Ό
315
+ │◄───────── sendLog() ──────────sendLog('info', reply)
316
+ β”‚
317
+ β–Ό
318
+ Chat Controller displays message
319
+ ```
320
+
215
321
  ## License
216
322
 
217
323
  MIT Β© EyeClaw Team
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eyeclaw/eyeclaw",
3
- "version": "2.0.0",
3
+ "version": "2.0.4",
4
4
  "description": "EyeClaw channel plugin for OpenClaw - Connect your local OpenClaw instance to EyeClaw platform",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
package/src/channel.ts CHANGED
@@ -178,6 +178,113 @@ export const eyeclawPlugin: ChannelPlugin<ResolvedEyeClawAccount> = {
178
178
  const client = new EyeClawClient(clientConfig, logger)
179
179
  clients.set(ctx.accountId, client)
180
180
 
181
+ // Register OpenClaw Agent callback for chat messages
182
+ client.setSendAgentCallback(async (message: string) => {
183
+ try {
184
+ ctx.log?.info(`πŸ€– Sending message to OpenClaw Agent: ${message}`)
185
+
186
+ // Call OpenClaw Agent via spawn for streaming output
187
+ const { spawn } = await import('child_process')
188
+
189
+ try {
190
+ // Spawn openclaw agent process for streaming output
191
+ const agentProcess = spawn('openclaw', [
192
+ 'agent',
193
+ '--session-id', 'eyeclaw-web-chat',
194
+ '--message', message,
195
+ '--json'
196
+ ])
197
+
198
+ let outputBuffer = ''
199
+ let streamId = Date.now().toString()
200
+
201
+ // Send stream_start event
202
+ client.sendStreamChunk('stream_start', streamId, '')
203
+
204
+ // Handle stdout (streaming response)
205
+ agentProcess.stdout?.on('data', (data: Buffer) => {
206
+ const text = data.toString()
207
+ outputBuffer += text
208
+
209
+ // For streaming text output, send each chunk immediately
210
+ // Try to parse as JSON first
211
+ try {
212
+ const parsed = JSON.parse(outputBuffer)
213
+ // If we can parse the complete JSON, extract the text
214
+ const response = parsed.result?.payloads?.[0]?.text
215
+ if (response) {
216
+ // Send the text in chunks
217
+ const chunkSize = 50 // Send ~50 chars at a time
218
+ for (let i = 0; i < response.length; i += chunkSize) {
219
+ const chunk = response.substring(i, i + chunkSize)
220
+ client.sendStreamChunk('stream_chunk', streamId, chunk)
221
+ }
222
+ }
223
+ outputBuffer = '' // Clear buffer after successful parse
224
+ } catch (e) {
225
+ // Not valid JSON yet, check if we have complete lines to send
226
+ const lines = outputBuffer.split('\n')
227
+ // Keep last incomplete line in buffer
228
+ if (lines.length > 1) {
229
+ for (let i = 0; i < lines.length - 1; i++) {
230
+ if (lines[i].trim()) {
231
+ client.sendStreamChunk('stream_chunk', streamId, lines[i] + '\n')
232
+ }
233
+ }
234
+ outputBuffer = lines[lines.length - 1]
235
+ }
236
+ }
237
+ })
238
+
239
+ // Handle stderr (errors)
240
+ agentProcess.stderr?.on('data', (data: Buffer) => {
241
+ const errorText = data.toString()
242
+ ctx.log?.error(`Agent stderr: ${errorText}`)
243
+ })
244
+
245
+ // Handle process completion
246
+ agentProcess.on('close', (code: number) => {
247
+ // Send any remaining buffered content
248
+ if (outputBuffer.trim()) {
249
+ try {
250
+ const parsed = JSON.parse(outputBuffer)
251
+ const response = parsed.result?.payloads?.[0]?.text || outputBuffer.trim()
252
+ client.sendStreamChunk('stream_chunk', streamId, response)
253
+ } catch (e) {
254
+ client.sendStreamChunk('stream_chunk', streamId, outputBuffer.trim())
255
+ }
256
+ }
257
+
258
+ // Send stream_end event
259
+ client.sendStreamChunk('stream_end', streamId, '')
260
+
261
+ if (code === 0) {
262
+ ctx.log?.info(`βœ… Agent completed successfully`)
263
+ } else {
264
+ ctx.log?.error(`Agent exited with code ${code}`)
265
+ client.sendLog('error', `❌ Agent error: process exited with code ${code}`)
266
+ }
267
+ })
268
+
269
+ // Handle process errors
270
+ agentProcess.on('error', (error: Error) => {
271
+ ctx.log?.error(`Failed to start agent process: ${error.message}`)
272
+ client.sendStreamChunk('stream_error', streamId, error.message)
273
+ client.sendLog('error', `❌ Failed to start agent: ${error.message}`)
274
+ })
275
+
276
+ } catch (error) {
277
+ const errorMsg = error instanceof Error ? error.message : String(error)
278
+ ctx.log?.error(`Failed to execute openclaw agent: ${errorMsg}`)
279
+ client.sendLog('error', `❌ Agent error: ${errorMsg}`)
280
+ }
281
+ } catch (error) {
282
+ const errorMsg = error instanceof Error ? error.message : String(error)
283
+ ctx.log?.error(`Failed to call OpenClaw Agent: ${errorMsg}`)
284
+ client.sendLog('error', `❌ Failed to call agent: ${errorMsg}`)
285
+ }
286
+ })
287
+
181
288
  try {
182
289
  await client.connect()
183
290
  ctx.log?.info('βœ… Successfully connected to EyeClaw platform')
package/src/client.ts CHANGED
@@ -105,15 +105,105 @@ export class EyeClawClient {
105
105
  this.logger.info(`Command received by server: ${message.command}`)
106
106
  break
107
107
 
108
+ case 'execute_command':
109
+ this.handleExecuteCommand(message)
110
+ break
111
+
112
+ case 'ping':
113
+ this.logger.debug('Received ping from dashboard')
114
+ // Send pong response
115
+ this.sendLog('info', 'πŸ“ Pong! Bot is alive.')
116
+ break
117
+
108
118
  case 'log':
109
119
  this.logger.info(`[Server Log] ${message.level}: ${message.message}`)
110
120
  break
111
121
 
122
+ case 'stream_chunk':
123
+ // Forward stream chunks to handler (will be handled by web UI)
124
+ const chunkPreview = typeof message.chunk === 'string' ? message.chunk.substring(0, 50) : String(message.chunk || '').substring(0, 50)
125
+ this.logger.debug(`Stream chunk: ${message.type} - ${chunkPreview}...`)
126
+ break
127
+
112
128
  default:
113
129
  this.logger.warn(`Unknown message type: ${type}`)
114
130
  }
115
131
  }
116
132
 
133
+ private handleExecuteCommand(message: Record<string, unknown>): void {
134
+ const command = message.command as string
135
+ const params = (message.params as Record<string, unknown>) || {}
136
+
137
+ this.logger.info(`πŸ“₯ Executing command: ${command}`)
138
+
139
+ switch (command) {
140
+ case 'chat': {
141
+ const userMessage = params.message as string
142
+ this.logger.info(`πŸ’¬ Chat message: ${userMessage}`)
143
+
144
+ // Send user message acknowledgment
145
+ this.sendLog('info', `ζ”Άεˆ°ζΆˆζ―: ${userMessage}`)
146
+
147
+ // Call OpenClaw Agent via callback
148
+ this.handleChatMessage(userMessage)
149
+ break
150
+ }
151
+
152
+ case 'ping': {
153
+ this.sendLog('info', 'πŸ“ Pong! Bot is responding.')
154
+ break
155
+ }
156
+
157
+ case 'status': {
158
+ this.requestStatus()
159
+ break
160
+ }
161
+
162
+ case 'echo': {
163
+ const echoMessage = params.message as string
164
+ this.sendLog('info', `Echo: ${echoMessage}`)
165
+ break
166
+ }
167
+
168
+ case 'help': {
169
+ const helpMessage = [
170
+ 'πŸ€– Available Commands:',
171
+ 'β€’ chat - Send a chat message',
172
+ 'β€’ ping - Test connection',
173
+ 'β€’ status - Get bot status',
174
+ 'β€’ echo - Echo a message',
175
+ 'β€’ help - Show this help',
176
+ ].join('\n')
177
+ this.sendLog('info', helpMessage)
178
+ break
179
+ }
180
+
181
+ default:
182
+ this.logger.warn(`Unknown command: ${command}`)
183
+ this.sendLog('error', `❌ Unknown command: ${command}`)
184
+ }
185
+ }
186
+
187
+ private handleChatMessage(userMessage: string): void {
188
+ // This will be called by OpenClaw channel plugin via sendAgent
189
+ if (this.sendAgentCallback) {
190
+ this.logger.info('πŸ€– Calling OpenClaw Agent...')
191
+ this.sendAgentCallback(userMessage)
192
+ } else {
193
+ // Fallback: simple echo if not running in OpenClaw context
194
+ this.logger.warn('No OpenClaw Agent available, using echo mode')
195
+ this.sendLog('info', `πŸ’¬ Echo: "${userMessage}" (OpenClaw Agent not connected)`)
196
+ }
197
+ }
198
+
199
+ // Callback to send message to OpenClaw Agent (injected by channel plugin)
200
+ private sendAgentCallback: ((message: string) => Promise<void>) | null = null
201
+
202
+ setSendAgentCallback(callback: (message: string) => Promise<void>): void {
203
+ this.sendAgentCallback = callback
204
+ this.logger.info('βœ… OpenClaw Agent callback registered')
205
+ }
206
+
117
207
  private handleStatusResponse(status: BotStatus): void {
118
208
  this.logger.info(`Bot status: online=${status.online}, status=${status.status}, sessions=${status.active_sessions}, uptime=${Math.floor(status.uptime / 60)}m`)
119
209
  }
@@ -190,6 +280,15 @@ export class EyeClawClient {
190
280
  })
191
281
  }
192
282
 
283
+ sendStreamChunk(type: string, streamId: string, chunk: string): void {
284
+ this.sendChannelMessage('stream_chunk', {
285
+ type,
286
+ stream_id: streamId,
287
+ chunk,
288
+ timestamp: new Date().toISOString(),
289
+ })
290
+ }
291
+
193
292
  sendCommandResult(command: string, result: unknown, error?: string): void {
194
293
  this.sendChannelMessage('command_result', {
195
294
  command,