crawd 0.8.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.
Files changed (50) hide show
  1. package/README.md +176 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +975 -0
  4. package/dist/client.d.ts +53 -0
  5. package/dist/client.js +40 -0
  6. package/dist/types.d.ts +86 -0
  7. package/dist/types.js +0 -0
  8. package/openclaw.plugin.json +108 -0
  9. package/package.json +86 -0
  10. package/skills/crawd/SKILL.md +81 -0
  11. package/src/backend/coordinator.ts +883 -0
  12. package/src/backend/index.ts +581 -0
  13. package/src/backend/server.ts +589 -0
  14. package/src/cli.ts +130 -0
  15. package/src/client.ts +101 -0
  16. package/src/commands/auth.ts +145 -0
  17. package/src/commands/config.ts +43 -0
  18. package/src/commands/down.ts +15 -0
  19. package/src/commands/logs.ts +32 -0
  20. package/src/commands/skill.ts +189 -0
  21. package/src/commands/start.ts +120 -0
  22. package/src/commands/status.ts +73 -0
  23. package/src/commands/stop.ts +16 -0
  24. package/src/commands/stream-key.ts +45 -0
  25. package/src/commands/talk.ts +30 -0
  26. package/src/commands/up.ts +59 -0
  27. package/src/commands/update.ts +92 -0
  28. package/src/config/schema.ts +66 -0
  29. package/src/config/store.ts +185 -0
  30. package/src/daemon/manager.ts +280 -0
  31. package/src/daemon/pid.ts +102 -0
  32. package/src/lib/chat/base.ts +13 -0
  33. package/src/lib/chat/manager.ts +105 -0
  34. package/src/lib/chat/pumpfun/client.ts +56 -0
  35. package/src/lib/chat/types.ts +48 -0
  36. package/src/lib/chat/youtube/client.ts +131 -0
  37. package/src/lib/pumpfun/live/client.ts +69 -0
  38. package/src/lib/pumpfun/live/index.ts +3 -0
  39. package/src/lib/pumpfun/live/types.ts +38 -0
  40. package/src/lib/pumpfun/v2/client.ts +139 -0
  41. package/src/lib/pumpfun/v2/index.ts +5 -0
  42. package/src/lib/pumpfun/v2/socket/client.ts +60 -0
  43. package/src/lib/pumpfun/v2/socket/index.ts +6 -0
  44. package/src/lib/pumpfun/v2/socket/types.ts +7 -0
  45. package/src/lib/pumpfun/v2/types.ts +234 -0
  46. package/src/lib/tts/tiktok.ts +91 -0
  47. package/src/plugin.ts +280 -0
  48. package/src/types.ts +78 -0
  49. package/src/utils/logger.ts +43 -0
  50. package/src/utils/paths.ts +55 -0
@@ -0,0 +1,589 @@
1
+ /**
2
+ * CrawdBackend — encapsulates the Fastify+Socket.IO server, TTS, coordinator,
3
+ * and chat system. Used by both standalone mode (backend/index.ts) and the
4
+ * OpenClaw plugin (plugin.ts).
5
+ */
6
+ import { randomUUID } from 'crypto'
7
+ import { writeFile, mkdir } from 'fs/promises'
8
+ import { join } from 'path'
9
+ import Fastify, { type FastifyInstance } from 'fastify'
10
+ import fastifyStatic from '@fastify/static'
11
+ import cors from '@fastify/cors'
12
+ import { Server } from 'socket.io'
13
+ import OpenAI from 'openai'
14
+ import { pumpfun } from '../lib/pumpfun/v2/index.js'
15
+ import { ChatManager } from '../lib/chat/manager.js'
16
+ import { PumpFunChatClient } from '../lib/chat/pumpfun/client.js'
17
+ import { YouTubeChatClient } from '../lib/chat/youtube/client.js'
18
+ import { Coordinator, OneShotGateway, type CoordinatorConfig, type CoordinatorEvent } from './coordinator.js'
19
+ import { generateShortId } from '../lib/chat/types.js'
20
+ import { configureTikTokTTS, generateTikTokTTS } from '../lib/tts/tiktok.js'
21
+ import type { ChatMessage } from '../lib/chat/types.js'
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Config types
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export type TtsVoiceEntry = {
28
+ provider: 'openai' | 'elevenlabs' | 'tiktok'
29
+ voice: string
30
+ }
31
+
32
+ export type CrawdConfig = {
33
+ enabled: boolean
34
+ port: number
35
+ bindHost: string
36
+ backendUrl?: string
37
+ tts: {
38
+ chat: TtsVoiceEntry[]
39
+ bot: TtsVoiceEntry[]
40
+ openaiApiKey?: string
41
+ elevenlabsApiKey?: string
42
+ tiktokSessionId?: string
43
+ }
44
+ vibe: {
45
+ enabled: boolean
46
+ intervalMs: number
47
+ idleAfterMs: number
48
+ sleepAfterIdleMs: number
49
+ prompt?: string
50
+ }
51
+ chat: {
52
+ youtube: {
53
+ enabled: boolean
54
+ videoId?: string
55
+ }
56
+ pumpfun: {
57
+ enabled: boolean
58
+ tokenMint?: string
59
+ authToken?: string
60
+ }
61
+ }
62
+ gatewayUrl?: string
63
+ gatewayToken?: string
64
+ }
65
+
66
+ export type CrawdLogger = {
67
+ info: (msg: string, ...args: unknown[]) => void
68
+ warn: (msg: string, ...args: unknown[]) => void
69
+ error: (msg: string, ...args: unknown[]) => void
70
+ }
71
+
72
+ const defaultLogger: CrawdLogger = {
73
+ info: (...args) => console.log('[Crawd]', ...args),
74
+ warn: (...args) => console.warn('[Crawd]', ...args),
75
+ error: (...args) => console.error('[Crawd]', ...args),
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // CrawdBackend
80
+ // ---------------------------------------------------------------------------
81
+
82
+ export class CrawdBackend {
83
+ private fastify: FastifyInstance
84
+ private io!: Server
85
+ private config: CrawdConfig
86
+ private logger: CrawdLogger
87
+
88
+ private openai: OpenAI | null = null
89
+ private elevenlabs: any = null
90
+ private ttsDir: string
91
+ private backendUrl: string
92
+ private buildVersion: string
93
+
94
+ private chatManager: ChatManager | null = null
95
+ coordinator: Coordinator | null = null
96
+ private latestMcap: number | null = null
97
+ private mcapInterval: NodeJS.Timeout | null = null
98
+
99
+ constructor(config: CrawdConfig, logger?: CrawdLogger) {
100
+ this.config = config
101
+ this.logger = logger ?? defaultLogger
102
+ this.fastify = Fastify({ logger: true })
103
+ this.ttsDir = join(process.cwd(), 'tmp', 'tts')
104
+ this.backendUrl = config.backendUrl ?? `http://localhost:${config.port}`
105
+ this.buildVersion = randomUUID()
106
+
107
+ // Initialize TTS providers based on config
108
+ if (config.tts.openaiApiKey) {
109
+ this.openai = new OpenAI({ apiKey: config.tts.openaiApiKey })
110
+ }
111
+ if (config.tts.tiktokSessionId) {
112
+ configureTikTokTTS(config.tts.tiktokSessionId)
113
+ }
114
+ }
115
+
116
+ // =========================================================================
117
+ // Lifecycle
118
+ // =========================================================================
119
+
120
+ async start(): Promise<void> {
121
+ // Lazy-init ElevenLabs (optional dep)
122
+ if (this.config.tts.elevenlabsApiKey && !this.elevenlabs) {
123
+ try {
124
+ const { ElevenLabsClient } = await import('@elevenlabs/elevenlabs-js')
125
+ this.elevenlabs = new ElevenLabsClient({ apiKey: this.config.tts.elevenlabsApiKey })
126
+ } catch {
127
+ this.logger.warn('ElevenLabs SDK not installed, ElevenLabs TTS disabled')
128
+ }
129
+ }
130
+
131
+ await this.fastify.register(cors, { origin: true })
132
+ await mkdir(this.ttsDir, { recursive: true })
133
+ await this.fastify.register(fastifyStatic, {
134
+ root: this.ttsDir,
135
+ prefix: '/tts/',
136
+ decorateReply: false,
137
+ })
138
+
139
+ this.io = new Server(this.fastify.server, {
140
+ cors: { origin: '*' },
141
+ })
142
+
143
+ this.registerSocketHandlers()
144
+ this.registerHttpRoutes()
145
+ await this.startChatSystem()
146
+
147
+ const host = this.config.bindHost
148
+ await this.fastify.listen({ port: this.config.port, host })
149
+
150
+ this.pollMarketCap()
151
+ this.mcapInterval = setInterval(() => this.pollMarketCap(), 10_000)
152
+
153
+ this.logger.info(`Backend started on ${host}:${this.config.port}`)
154
+ }
155
+
156
+ async stop(): Promise<void> {
157
+ if (this.mcapInterval) {
158
+ clearInterval(this.mcapInterval)
159
+ this.mcapInterval = null
160
+ }
161
+ this.coordinator?.stop()
162
+ this.chatManager?.disconnectAll()
163
+ this.io?.close()
164
+ await this.fastify.close()
165
+ this.logger.info('Backend stopped')
166
+ }
167
+
168
+ // =========================================================================
169
+ // Public API (used by plugin tool handlers)
170
+ // =========================================================================
171
+
172
+ /** Speak on the livestream — emits overlay event + TTS. */
173
+ async handleTalk(text: string): Promise<{ spoken: boolean }> {
174
+ if (!text || typeof text !== 'string') {
175
+ return { spoken: false }
176
+ }
177
+
178
+ this.coordinator?.notifySpeech()
179
+
180
+ const id = randomUUID()
181
+ try {
182
+ const ttsUrl = await this.generateTTSWithFallback(text, this.config.tts.bot)
183
+ this.logger.info(`TTS generated: ${ttsUrl}`)
184
+ this.io.emit('crawd:talk', { id, message: text, ttsUrl })
185
+ } catch (e) {
186
+ this.logger.error('Failed to generate TTS, emitting without audio', e)
187
+ this.io.emit('crawd:talk', { id, message: text, ttsUrl: '' })
188
+ }
189
+
190
+ return { spoken: true }
191
+ }
192
+
193
+ /**
194
+ * Reply to a chat message — reads original aloud (chat voice),
195
+ * then speaks bot reply (bot voice). Emits `crawd:reply-turn`.
196
+ */
197
+ async handleReply(
198
+ text: string,
199
+ chat: { username: string; message: string },
200
+ ): Promise<{ spoken: boolean }> {
201
+ if (!text || typeof text !== 'string') {
202
+ return { spoken: false }
203
+ }
204
+
205
+ this.coordinator?.notifySpeech()
206
+
207
+ try {
208
+ const [chatTtsUrl, botTtsUrl] = await Promise.all([
209
+ this.generateTTSWithFallback(`Chat says: ${chat.message}`, this.config.tts.chat),
210
+ this.generateTTSWithFallback(text, this.config.tts.bot),
211
+ ])
212
+ this.io.emit('crawd:reply-turn', {
213
+ chat: { username: chat.username, message: chat.message },
214
+ botMessage: text,
215
+ chatTtsUrl,
216
+ botTtsUrl,
217
+ })
218
+ } catch (e) {
219
+ this.logger.error('Failed to generate reply-turn TTS, falling back to talk', e)
220
+ const id = randomUUID()
221
+ this.generateTTSWithFallback(text, this.config.tts.bot)
222
+ .then((ttsUrl) => this.io.emit('crawd:talk', { id, message: text, ttsUrl }))
223
+ .catch(() => this.io.emit('crawd:talk', { id, message: text, ttsUrl: '' }))
224
+ }
225
+
226
+ return { spoken: true }
227
+ }
228
+
229
+ getIO(): Server {
230
+ return this.io
231
+ }
232
+
233
+ // =========================================================================
234
+ // TTS (with ordered fallback chain)
235
+ // =========================================================================
236
+
237
+ async generateTTSWithFallback(text: string, chain: TtsVoiceEntry[]): Promise<string> {
238
+ let lastError: Error | null = null
239
+
240
+ for (const entry of chain) {
241
+ try {
242
+ switch (entry.provider) {
243
+ case 'elevenlabs':
244
+ return await this.generateElevenLabsTTS(text, entry.voice)
245
+ case 'openai':
246
+ return await this.generateOpenAITTS(text, entry.voice)
247
+ case 'tiktok':
248
+ return await this.generateTikTokTTSFile(text, entry.voice)
249
+ }
250
+ } catch (e) {
251
+ lastError = e instanceof Error ? e : new Error(String(e))
252
+ this.logger.warn(`TTS ${entry.provider}/${entry.voice} failed: ${lastError.message}, trying next...`)
253
+ }
254
+ }
255
+
256
+ throw lastError ?? new Error('No TTS providers configured')
257
+ }
258
+
259
+ private async generateOpenAITTS(text: string, voice: string): Promise<string> {
260
+ if (!this.openai) throw new Error('OpenAI not configured (missing apiKey)')
261
+
262
+ const response = await this.openai.audio.speech.create({
263
+ model: 'gpt-4o-mini-tts',
264
+ voice: voice as 'onyx',
265
+ input: text,
266
+ })
267
+
268
+ const buffer = Buffer.from(await response.arrayBuffer())
269
+ return await this.saveTTSFile(buffer)
270
+ }
271
+
272
+ private async generateElevenLabsTTS(text: string, voiceId: string): Promise<string> {
273
+ if (!this.elevenlabs) throw new Error('ElevenLabs not configured (missing apiKey)')
274
+
275
+ const audio = await this.elevenlabs.textToSpeech.convert(voiceId, {
276
+ modelId: 'eleven_multilingual_v2',
277
+ text,
278
+ outputFormat: 'mp3_44100_128',
279
+ voiceSettings: {
280
+ stability: 0,
281
+ similarityBoost: 1.0,
282
+ useSpeakerBoost: true,
283
+ speed: 1.0,
284
+ },
285
+ })
286
+
287
+ const response = new Response(audio as any)
288
+ const arrayBuffer = await response.arrayBuffer()
289
+ const buffer = Buffer.from(arrayBuffer)
290
+
291
+ // Check if response is valid MP3
292
+ const isMP3 =
293
+ (buffer[0] === 0x49 && buffer[1] === 0x44 && buffer[2] === 0x33) ||
294
+ (buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0)
295
+
296
+ if (!isMP3) {
297
+ const preview = buffer.subarray(0, 200).toString('utf-8')
298
+ throw new Error(`ElevenLabs returned non-audio response: ${preview.slice(0, 100)}`)
299
+ }
300
+
301
+ return await this.saveTTSFile(buffer)
302
+ }
303
+
304
+ private async generateTikTokTTSFile(text: string, voice?: string): Promise<string> {
305
+ const buffer = await generateTikTokTTS(text, voice)
306
+ return await this.saveTTSFile(buffer)
307
+ }
308
+
309
+ private async saveTTSFile(buffer: Buffer): Promise<string> {
310
+ const filename = `${randomUUID()}.mp3`
311
+ await mkdir(this.ttsDir, { recursive: true })
312
+ await writeFile(join(this.ttsDir, filename), buffer)
313
+ this.logger.info(`TTS file written: ${filename}, size: ${buffer.length} bytes`)
314
+ return `${this.backendUrl}/tts/${filename}`
315
+ }
316
+
317
+ // =========================================================================
318
+ // Chat system + Coordinator
319
+ // =========================================================================
320
+
321
+ private async startChatSystem(): Promise<void> {
322
+ this.chatManager = new ChatManager()
323
+
324
+ const pf = this.config.chat.pumpfun
325
+ if (pf.enabled && pf.tokenMint) {
326
+ this.chatManager.registerClient(
327
+ 'pumpfun',
328
+ new PumpFunChatClient(pf.tokenMint, pf.authToken ?? null),
329
+ )
330
+ }
331
+
332
+ const yt = this.config.chat.youtube
333
+ if (yt.enabled && yt.videoId) {
334
+ this.chatManager.registerClient('youtube', new YouTubeChatClient(yt.videoId))
335
+ }
336
+
337
+ this.chatManager.onMessage((msg: ChatMessage) => {
338
+ this.fastify.log.info({ platform: msg.platform, user: msg.username }, 'chat message')
339
+ this.io.emit('crawd:chat', msg)
340
+ this.coordinator?.onMessage(msg)
341
+ })
342
+
343
+ if (this.config.gatewayUrl) {
344
+ const gateway = new OneShotGateway(
345
+ this.config.gatewayUrl,
346
+ this.config.gatewayToken ?? '',
347
+ )
348
+
349
+ const coordConfig: Partial<CoordinatorConfig> = {
350
+ vibeEnabled: this.config.vibe.enabled,
351
+ vibeIntervalMs: this.config.vibe.intervalMs,
352
+ idleAfterMs: this.config.vibe.idleAfterMs,
353
+ sleepAfterIdleMs: this.config.vibe.sleepAfterIdleMs,
354
+ }
355
+ if (this.config.vibe.prompt) {
356
+ coordConfig.vibePrompt = this.config.vibe.prompt
357
+ }
358
+
359
+ this.coordinator = new Coordinator(
360
+ gateway.triggerAgent.bind(gateway),
361
+ coordConfig,
362
+ )
363
+
364
+ this.coordinator.setOnEvent((event: CoordinatorEvent) => {
365
+ if (event.type === 'stateChange') {
366
+ this.io.emit('crawd:status', { status: event.to })
367
+ } else if (event.type === 'vibeExecuted' && !event.skipped) {
368
+ this.io.emit('crawd:status', { status: 'vibing' })
369
+ } else if (event.type === 'chatProcessed') {
370
+ this.io.emit('crawd:status', { status: 'chatting' })
371
+ }
372
+ })
373
+
374
+ this.coordinator.start()
375
+ this.fastify.log.info('Coordinator started (one-shot gateway)')
376
+ } else {
377
+ this.fastify.log.warn('Gateway not configured — coordinator disabled')
378
+ }
379
+
380
+ await this.chatManager.connectAll()
381
+ }
382
+
383
+ // =========================================================================
384
+ // Socket.IO handlers
385
+ // =========================================================================
386
+
387
+ private registerSocketHandlers(): void {
388
+ this.io.on('connection', (socket) => {
389
+ this.fastify.log.info(`socket connected: ${socket.id}`)
390
+
391
+ if (this.latestMcap !== null) {
392
+ socket.emit('crawd:mcap', { mcap: this.latestMcap })
393
+ }
394
+
395
+ socket.on('crawd:mock-chat', (data: { username?: string; message?: string }) => {
396
+ const { username, message } = data
397
+ if (!username || !message) return
398
+ const mockMsg: ChatMessage = {
399
+ id: randomUUID(),
400
+ shortId: generateShortId(),
401
+ username,
402
+ message,
403
+ platform: 'youtube',
404
+ timestamp: Date.now(),
405
+ }
406
+ this.fastify.log.info({ username, message }, 'mock chat (socket)')
407
+ this.io.emit('crawd:chat', mockMsg)
408
+ if (this.coordinator) {
409
+ this.coordinator.onMessage(mockMsg)
410
+ } else {
411
+ this.fastify.log.warn('mock chat: coordinator is null — gateway not configured or failed to connect')
412
+ }
413
+ })
414
+
415
+ socket.on('disconnect', () => {
416
+ this.fastify.log.info(`socket disconnected: ${socket.id}`)
417
+ })
418
+ })
419
+ }
420
+
421
+ // =========================================================================
422
+ // HTTP routes
423
+ // =========================================================================
424
+
425
+ private registerHttpRoutes(): void {
426
+ this.fastify.post<{ Body: { message: string } }>(
427
+ '/crawd/talk',
428
+ async (request, reply) => {
429
+ const { message } = request.body
430
+ if (!message || typeof message !== 'string') {
431
+ return reply.status(400).send({ error: 'message is required' })
432
+ }
433
+ await this.handleTalk(message)
434
+ return { ok: true }
435
+ },
436
+ )
437
+
438
+ this.fastify.get('/chat/status', async () => {
439
+ return { connected: this.chatManager?.getConnectedKeys() ?? [] }
440
+ })
441
+
442
+ this.fastify.get('/version', async () => {
443
+ return { version: this.buildVersion }
444
+ })
445
+
446
+ this.fastify.get('/coordinator/status', async () => {
447
+ if (!this.coordinator) return { enabled: false }
448
+ return { enabled: true, ...this.coordinator.getState() }
449
+ })
450
+
451
+ this.fastify.post<{ Body: Partial<CoordinatorConfig> }>(
452
+ '/coordinator/config',
453
+ async (request, reply) => {
454
+ if (!this.coordinator) {
455
+ return reply.status(400).send({ error: 'Coordinator not enabled' })
456
+ }
457
+ this.coordinator.updateConfig(request.body)
458
+ return { ok: true, ...this.coordinator.getState() }
459
+ },
460
+ )
461
+
462
+ this.fastify.post<{ Body: { username: string; message: string } }>(
463
+ '/mock/chat',
464
+ async (request, reply) => {
465
+ const { username, message } = request.body
466
+ if (!username || !message) {
467
+ return reply.status(400).send({ error: 'username and message are required' })
468
+ }
469
+ const id = randomUUID()
470
+ const mockMsg: ChatMessage = {
471
+ id,
472
+ shortId: generateShortId(),
473
+ username,
474
+ message,
475
+ platform: 'youtube',
476
+ timestamp: Date.now(),
477
+ }
478
+ this.fastify.log.info({ username, message }, 'mock chat message')
479
+ this.io.emit('crawd:chat', mockMsg)
480
+ this.coordinator?.onMessage(mockMsg)
481
+ return { ok: true, id }
482
+ },
483
+ )
484
+
485
+ this.fastify.post<{ Body: { username: string; message: string; response: string } }>(
486
+ '/mock/turn',
487
+ async (request, reply) => {
488
+ const { username, message, response } = request.body
489
+ if (!username || !message || !response) {
490
+ return reply.status(400).send({ error: 'username, message, and response are required' })
491
+ }
492
+
493
+ try {
494
+ const [chatTtsUrl, botTtsUrl] = await Promise.all([
495
+ this.generateTTSWithFallback(`Chat says: ${message}`, this.config.tts.chat),
496
+ this.generateTTSWithFallback(response, this.config.tts.bot),
497
+ ])
498
+ this.io.emit('crawd:reply-turn', {
499
+ chat: { username, message },
500
+ botMessage: response,
501
+ chatTtsUrl,
502
+ botTtsUrl,
503
+ })
504
+ return { ok: true }
505
+ } catch (e) {
506
+ this.fastify.log.error(e, 'failed to generate mock turn TTS')
507
+ return reply.status(500).send({ error: 'Failed to generate TTS' })
508
+ }
509
+ },
510
+ )
511
+ }
512
+
513
+ // =========================================================================
514
+ // Market cap polling
515
+ // =========================================================================
516
+
517
+ private async pollMarketCap(): Promise<void> {
518
+ const mint = this.config.chat.pumpfun.tokenMint
519
+ if (!mint) return
520
+
521
+ try {
522
+ const coin = await pumpfun.getCoin(mint)
523
+ this.latestMcap = coin.usd_market_cap
524
+ this.io.emit('crawd:mcap', { mcap: this.latestMcap })
525
+ } catch (e) {
526
+ this.fastify.log.error(e, 'failed to fetch market cap')
527
+ }
528
+ }
529
+ }
530
+
531
+ // ---------------------------------------------------------------------------
532
+ // Config builder: create CrawdConfig from environment variables
533
+ // (used by standalone mode in backend/index.ts)
534
+ // ---------------------------------------------------------------------------
535
+
536
+ export function configFromEnv(): CrawdConfig {
537
+ const port = Number(process.env.PORT || 4000)
538
+
539
+ const botChain: TtsVoiceEntry[] = []
540
+ const chatChain: TtsVoiceEntry[] = []
541
+
542
+ if (process.env.ELEVENLABS_API_KEY) {
543
+ botChain.push({ provider: 'elevenlabs', voice: process.env.TTS_BOT_VOICE || 'TX3LPaxmHKxFdv7VOQHJ' })
544
+ }
545
+ if (process.env.OPENAI_API_KEY) {
546
+ botChain.push({ provider: 'openai', voice: process.env.TTS_BOT_VOICE || 'onyx' })
547
+ }
548
+
549
+ if (process.env.TIKTOK_SESSION_ID) {
550
+ chatChain.push({ provider: 'tiktok', voice: process.env.TTS_CHAT_VOICE || 'en_us_002' })
551
+ }
552
+ if (process.env.OPENAI_API_KEY) {
553
+ chatChain.push({ provider: 'openai', voice: process.env.TTS_CHAT_VOICE || 'onyx' })
554
+ }
555
+
556
+ return {
557
+ enabled: true,
558
+ port,
559
+ bindHost: process.env.BIND_HOST || '0.0.0.0',
560
+ backendUrl: process.env.BACKEND_URL || `http://localhost:${port}`,
561
+ tts: {
562
+ chat: chatChain,
563
+ bot: botChain,
564
+ openaiApiKey: process.env.OPENAI_API_KEY,
565
+ elevenlabsApiKey: process.env.ELEVENLABS_API_KEY,
566
+ tiktokSessionId: process.env.TIKTOK_SESSION_ID,
567
+ },
568
+ vibe: {
569
+ enabled: process.env.VIBE_ENABLED !== 'false',
570
+ intervalMs: Number(process.env.VIBE_INTERVAL_MS || 30_000),
571
+ idleAfterMs: Number(process.env.IDLE_AFTER_MS || 180_000),
572
+ sleepAfterIdleMs: Number(process.env.SLEEP_AFTER_IDLE_MS || 180_000),
573
+ prompt: process.env.VIBE_PROMPT,
574
+ },
575
+ chat: {
576
+ youtube: {
577
+ enabled: process.env.YOUTUBE_ENABLED === 'true',
578
+ videoId: process.env.YOUTUBE_VIDEO_ID,
579
+ },
580
+ pumpfun: {
581
+ enabled: process.env.PUMPFUN_ENABLED !== 'false',
582
+ tokenMint: process.env.NEXT_PUBLIC_TOKEN_MINT,
583
+ authToken: process.env.PUMPFUN_AUTH_TOKEN,
584
+ },
585
+ },
586
+ gatewayUrl: process.env.OPENCLAW_GATEWAY_URL,
587
+ gatewayToken: process.env.OPENCLAW_GATEWAY_TOKEN,
588
+ }
589
+ }