crawd 0.8.7 → 0.9.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.
@@ -1,31 +1,28 @@
1
1
  import 'dotenv/config';
2
2
  import { randomUUID } from "crypto";
3
- import { writeFile, mkdir } from "fs/promises";
4
3
  import { watch } from "fs";
5
- import { join } from "path";
6
4
  import Fastify from "fastify";
7
- import fastifyStatic from "@fastify/static";
8
5
  import cors from "@fastify/cors";
9
6
  import { Server } from "socket.io";
10
- import OpenAI from "openai";
11
7
  import { pumpfun } from "../lib/pumpfun/v2";
12
8
  import { ChatManager } from "../lib/chat/manager";
13
9
  import { PumpFunChatClient } from "../lib/chat/pumpfun/client";
14
10
  import { YouTubeChatClient } from "../lib/chat/youtube/client";
15
11
  import { GatewayClient, Coordinator, type CoordinatorConfig, type CoordinatorEvent, type InvokeRequestPayload } from "./coordinator";
16
12
  import { generateShortId } from "../lib/chat/types";
17
- import { configureTikTokTTS, generateTikTokTTS } from "../lib/tts/tiktok";
18
13
  import type { ChatMessage } from "../lib/chat/types";
19
- import { loadEnv, loadConfig } from "../config/store.js";
14
+ import { loadEnv } from "../config/store.js";
20
15
  import { ENV_PATH, CONFIG_PATH } from "../utils/paths.js";
21
- import type { TtsProvider, ReplyTurnEvent, TalkEvent } from "../types";
22
16
 
23
17
  // Parse coordinator config from env vars
24
18
  function parseCoordinatorConfig(): Partial<CoordinatorConfig> {
25
19
  const config: Partial<CoordinatorConfig> = {};
26
20
 
27
- if (process.env.VIBE_ENABLED !== undefined) {
28
- config.vibeEnabled = process.env.VIBE_ENABLED === 'true';
21
+ if (process.env.AUTONOMY_MODE) {
22
+ const mode = process.env.AUTONOMY_MODE
23
+ if (mode === 'vibe' || mode === 'plan' || mode === 'none') {
24
+ config.autonomyMode = mode
25
+ }
29
26
  }
30
27
  if (process.env.VIBE_INTERVAL_MS) {
31
28
  config.vibeIntervalMs = Number(process.env.VIBE_INTERVAL_MS);
@@ -44,44 +41,18 @@ function parseCoordinatorConfig(): Partial<CoordinatorConfig> {
44
41
  }
45
42
 
46
43
  const port = Number(process.env.PORT || 4000);
47
- const BACKEND_URL = process.env.BACKEND_URL || `http://localhost:${port}`;
48
44
  const TOKEN_MINT = process.env.NEXT_PUBLIC_TOKEN_MINT;
49
45
  const MCAP_POLL_MS = 10_000;
50
- const TTS_DIR = join(process.cwd(), "tmp", "tts");
51
-
52
- // TTS provider selection — mutable, updated by file watcher
53
- let CHAT_PROVIDER = (process.env.TTS_CHAT_PROVIDER || 'tiktok') as TtsProvider;
54
- let CHAT_VOICE = process.env.TTS_CHAT_VOICE;
55
- let BOT_PROVIDER = (process.env.TTS_BOT_PROVIDER || 'elevenlabs') as TtsProvider;
56
- let BOT_VOICE = process.env.TTS_BOT_VOICE;
57
46
 
58
47
  // Unique version ID generated at startup - changes on each deploy/restart
59
48
  const BUILD_VERSION = randomUUID();
60
49
 
61
50
  const fastify = Fastify({ logger: true });
62
- const openai = new OpenAI();
63
-
64
- // Dynamic import for optional ElevenLabs dependency
65
- let elevenlabs: any = null;
66
- if (process.env.ELEVENLABS_API_KEY) {
67
- try {
68
- const { ElevenLabsClient } = await import("@elevenlabs/elevenlabs-js");
69
- elevenlabs = new ElevenLabsClient({ apiKey: process.env.ELEVENLABS_API_KEY });
70
- } catch {
71
- fastify.log.warn("@elevenlabs/elevenlabs-js not installed, ElevenLabs TTS disabled");
72
- }
73
- }
74
-
75
- // Configure TikTok TTS if session ID is available
76
- if (process.env.TIKTOK_SESSION_ID) {
77
- configureTikTokTTS(process.env.TIKTOK_SESSION_ID);
78
- }
79
51
 
80
52
  // --- Auto-reload ~/.crawd/.env and config.json on change ---
81
53
 
82
54
  async function reloadConfig() {
83
55
  const env = loadEnv();
84
- const config = loadConfig();
85
56
  const changes: string[] = [];
86
57
 
87
58
  // Update secrets in process.env
@@ -92,30 +63,6 @@ async function reloadConfig() {
92
63
  }
93
64
  }
94
65
 
95
- // Update TTS provider/voice from config
96
- const newChatProvider = (config.tts.chatProvider || 'tiktok') as TtsProvider;
97
- const newChatVoice = config.tts.chatVoice;
98
- const newBotProvider = (config.tts.botProvider || 'elevenlabs') as TtsProvider;
99
- const newBotVoice = config.tts.botVoice;
100
-
101
- if (newChatProvider !== CHAT_PROVIDER) { changes.push('tts.chatProvider'); CHAT_PROVIDER = newChatProvider; }
102
- if (newChatVoice !== CHAT_VOICE) { changes.push('tts.chatVoice'); CHAT_VOICE = newChatVoice; }
103
- if (newBotProvider !== BOT_PROVIDER) { changes.push('tts.botProvider'); BOT_PROVIDER = newBotProvider; }
104
- if (newBotVoice !== BOT_VOICE) { changes.push('tts.botVoice'); BOT_VOICE = newBotVoice; }
105
-
106
- // Reinitialize ElevenLabs client if key changed
107
- if (changes.includes('ELEVENLABS_API_KEY') && process.env.ELEVENLABS_API_KEY) {
108
- try {
109
- const { ElevenLabsClient } = await import("@elevenlabs/elevenlabs-js");
110
- elevenlabs = new ElevenLabsClient({ apiKey: process.env.ELEVENLABS_API_KEY });
111
- } catch { /* already warned at startup */ }
112
- }
113
-
114
- // Reconfigure TikTok TTS if session ID changed
115
- if (changes.includes('TIKTOK_SESSION_ID') && process.env.TIKTOK_SESSION_ID) {
116
- configureTikTokTTS(process.env.TIKTOK_SESSION_ID);
117
- }
118
-
119
66
  if (changes.length > 0) {
120
67
  fastify.log.info({ changes }, 'Config reloaded');
121
68
  }
@@ -133,96 +80,6 @@ for (const file of [ENV_PATH, CONFIG_PATH]) {
133
80
  } catch { /* file may not exist yet */ }
134
81
  }
135
82
 
136
- // --- TTS provider functions ---
137
-
138
- async function generateOpenAITTS(text: string, voice?: string): Promise<string> {
139
- const response = await openai.audio.speech.create({
140
- model: "tts-1-hd",
141
- voice: (voice || "onyx") as "onyx",
142
- input: text,
143
- });
144
-
145
- const buffer = Buffer.from(await response.arrayBuffer());
146
- const filename = `${randomUUID()}.mp3`;
147
- await mkdir(TTS_DIR, { recursive: true });
148
- await writeFile(join(TTS_DIR, filename), buffer);
149
-
150
- return `${BACKEND_URL}/tts/${filename}`;
151
- }
152
-
153
- async function generateElevenLabsTTS(text: string, voice?: string): Promise<string> {
154
- if (!elevenlabs) throw new Error("ELEVENLABS_API_KEY not configured");
155
-
156
- const audio = await elevenlabs.textToSpeech.convert(voice || "TX3LPaxmHKxFdv7VOQHJ", {
157
- modelId: "eleven_multilingual_v2",
158
- text,
159
- outputFormat: "mp3_44100_128",
160
- voiceSettings: {
161
- stability: 0,
162
- similarityBoost: 1.0,
163
- useSpeakerBoost: true,
164
- speed: 1.0,
165
- },
166
- });
167
-
168
- // Convert stream to buffer - works with Bun and Node.js
169
- const response = new Response(audio as any);
170
- const arrayBuffer = await response.arrayBuffer();
171
- const buffer = Buffer.from(arrayBuffer);
172
-
173
- // Check if response is valid MP3 (starts with ID3 or FF FB/FA/F3/F2)
174
- const isMP3 = (buffer[0] === 0x49 && buffer[1] === 0x44 && buffer[2] === 0x33) || // ID3
175
- (buffer[0] === 0xFF && (buffer[1] & 0xE0) === 0xE0); // MP3 frame sync
176
-
177
- if (!isMP3) {
178
- const preview = buffer.subarray(0, 200).toString("utf-8");
179
- console.error(`ElevenLabs returned non-audio response: ${preview}`);
180
- throw new Error("ElevenLabs returned invalid audio (possibly error page)");
181
- }
182
-
183
- const filename = `${randomUUID()}.mp3`;
184
- await mkdir(TTS_DIR, { recursive: true });
185
- await writeFile(join(TTS_DIR, filename), buffer);
186
-
187
- console.log(`TTS file written: ${filename}, size: ${buffer.length} bytes`);
188
-
189
- return `${BACKEND_URL}/tts/${filename}`;
190
- }
191
-
192
- async function generateTikTokTTSFile(text: string, voice?: string): Promise<string> {
193
- const buffer = await generateTikTokTTS(text, voice);
194
- const filename = `${randomUUID()}.mp3`;
195
- await mkdir(TTS_DIR, { recursive: true });
196
- await writeFile(join(TTS_DIR, filename), buffer);
197
- console.log(`TikTok TTS file written: ${filename}, size: ${buffer.length} bytes`);
198
- return `${BACKEND_URL}/tts/${filename}`;
199
- }
200
-
201
- /** Generate TTS using the specified provider and voice, falling back to OpenAI on failure */
202
- async function tts(text: string, provider: TtsProvider, voice?: string): Promise<string> {
203
- const providers: Record<TtsProvider, () => Promise<string>> = {
204
- openai: () => generateOpenAITTS(text, voice),
205
- elevenlabs: () => generateElevenLabsTTS(text, voice),
206
- tiktok: () => generateTikTokTTSFile(text, voice),
207
- };
208
-
209
- try {
210
- return await providers[provider]();
211
- } catch (e) {
212
- if (provider !== 'openai') {
213
- fastify.log.warn(e, `${provider} TTS failed, falling back to OpenAI`);
214
- return await generateOpenAITTS(text);
215
- }
216
- throw e;
217
- }
218
- }
219
-
220
- /** Generate TTS for a chat message (uses CHAT_PROVIDER) */
221
- function chatTTS(text: string) { return tts(text, CHAT_PROVIDER, CHAT_VOICE); }
222
-
223
- /** Generate TTS for a bot message (uses BOT_PROVIDER) */
224
- function botTTS(text: string) { return tts(text, BOT_PROVIDER, BOT_VOICE); }
225
-
226
83
  // --- Non-TTS helpers ---
227
84
 
228
85
  async function fetchMarketCap(): Promise<number | null> {
@@ -238,15 +95,7 @@ async function fetchMarketCap(): Promise<number | null> {
238
95
  }
239
96
 
240
97
  async function main() {
241
- fastify.log.info({ chatProvider: CHAT_PROVIDER, botProvider: BOT_PROVIDER }, 'TTS providers configured');
242
-
243
98
  await fastify.register(cors, { origin: true });
244
- await mkdir(TTS_DIR, { recursive: true });
245
- await fastify.register(fastifyStatic, {
246
- root: TTS_DIR,
247
- prefix: "/tts/",
248
- decorateReply: false,
249
- });
250
99
 
251
100
  const io = new Server(fastify.server, {
252
101
  cors: { origin: "*" },
@@ -339,35 +188,27 @@ async function main() {
339
188
  } else if (event.type === 'vibeExecuted' && !event.skipped) {
340
189
  io.emit('crawd:status', { status: 'vibing' });
341
190
  }
342
- // Note: chatProcessed no longer emits status — we only wake/emit
343
- // when the agent actually replies (via talk tool or text fallback).
344
191
  });
345
192
 
346
193
  /**
347
- * Generate TTS and emit atomic talk event, wait for overlay ack.
348
- * If replyTo is provided, also generates chat TTS — overlay plays chat first, then bot.
194
+ * Emit text-only talk event, wait for overlay ack.
195
+ * If replyTo is provided, emits reply-turn instead of talk.
349
196
  */
350
197
  async function handleTalkInvoke(text: string, replyTo?: ChatMessage): Promise<void> {
351
198
  const talkId = randomUUID();
352
199
  fastify.log.info({ talkId, text: text.slice(0, 80), replyTo: replyTo?.shortId }, 'Handling talk invoke');
353
200
 
354
- // Generate TTS in parallel when there's a chat message to reply to
355
- const [ttsUrl, chatTtsUrl] = await Promise.all([
356
- botTTS(text),
357
- replyTo ? chatTTS(`Chat says: ${replyTo.message}`) : Promise.resolve(undefined),
358
- ]);
359
-
360
- const event: TalkEvent = { id: talkId, message: text, ttsUrl };
361
- if (replyTo && chatTtsUrl) {
362
- event.chat = {
363
- message: replyTo.message,
364
- username: replyTo.username,
365
- ttsUrl: chatTtsUrl,
366
- };
201
+ if (replyTo) {
202
+ io.emit('crawd:reply-turn', {
203
+ id: talkId,
204
+ chat: { username: replyTo.username, message: replyTo.message },
205
+ botMessage: text,
206
+ });
207
+ } else {
208
+ io.emit('crawd:talk', { id: talkId, message: text });
367
209
  }
368
210
 
369
- io.emit('crawd:talk', event);
370
- fastify.log.info({ talkId, hasChat: !!event.chat }, 'Emitted crawd:talk, waiting for ack');
211
+ fastify.log.info({ talkId, hasChat: !!replyTo }, 'Emitted event, waiting for ack');
371
212
 
372
213
  await waitForTalkAck(talkId);
373
214
  fastify.log.info({ talkId }, 'Talk complete');
@@ -415,7 +256,7 @@ async function main() {
415
256
  fastify.log.error(err, 'Talk invoke failed');
416
257
  await gateway.sendInvokeResult(payload.id, payload.nodeId, {
417
258
  ok: false,
418
- error: { code: 'TTS_FAILED', message: String(err) },
259
+ error: { code: 'INVOKE_FAILED', message: String(err) },
419
260
  });
420
261
  }
421
262
  } else {
@@ -467,16 +308,9 @@ async function main() {
467
308
  return reply.status(400).send({ error: "message is required" });
468
309
  }
469
310
 
470
- try {
471
- const talkId = randomUUID();
472
- const ttsUrl = await botTTS(message);
473
- const event: TalkEvent = { id: talkId, message, ttsUrl };
474
- io.emit("crawd:talk", event);
475
- return { ok: true, id: talkId };
476
- } catch (e) {
477
- fastify.log.error(e, "failed to generate TTS");
478
- return reply.status(500).send({ error: "Failed to generate TTS" });
479
- }
311
+ const talkId = randomUUID();
312
+ io.emit("crawd:talk", { id: talkId, message });
313
+ return { ok: true, id: talkId };
480
314
  }
481
315
  );
482
316
 
@@ -533,7 +367,7 @@ async function main() {
533
367
  }
534
368
  );
535
369
 
536
- // Mock turn endpoint for debug UI — generates real TTS
370
+ // Mock turn endpoint for debug UI — text-only
537
371
  fastify.post<{ Body: { username: string; message: string; response: string } }>(
538
372
  "/mock/turn",
539
373
  async (request, reply) => {
@@ -544,27 +378,14 @@ async function main() {
544
378
 
545
379
  fastify.log.info({ username, message, response }, "mock turn");
546
380
 
547
- try {
548
- const [chatTtsUrl, botTtsUrl] = await Promise.all([
549
- chatTTS(`Chat says: ${message}`),
550
- botTTS(response),
551
- ]);
552
-
553
- const event: ReplyTurnEvent = {
554
- id: randomUUID(),
555
- chat: { username, message },
556
- botMessage: response,
557
- chatTtsUrl,
558
- botTtsUrl,
559
- };
560
-
561
- io.emit('crawd:reply-turn', event);
381
+ const id = randomUUID();
382
+ io.emit('crawd:reply-turn', {
383
+ id,
384
+ chat: { username, message },
385
+ botMessage: response,
386
+ });
562
387
 
563
- return { ok: true };
564
- } catch (e) {
565
- fastify.log.error(e, "failed to generate mock turn TTS");
566
- return reply.status(500).send({ error: "Failed to generate TTS" });
567
- }
388
+ return { ok: true, id };
568
389
  }
569
390
  );
570
391