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.
- package/dist/types.d.ts +5 -23
- package/openclaw.plugin.json +8 -40
- package/package.json +4 -2
- package/skills/crawd/SKILL.md +37 -0
- package/src/backend/coordinator.test.ts +393 -0
- package/src/backend/coordinator.ts +267 -16
- package/src/backend/index.ts +29 -208
- package/src/backend/server.ts +67 -219
- package/src/plugin.ts +122 -33
- package/src/types.ts +4 -23
- package/src/lib/tts/tiktok.ts +0 -91
package/src/backend/index.ts
CHANGED
|
@@ -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
|
|
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.
|
|
28
|
-
|
|
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
|
-
*
|
|
348
|
-
* If replyTo is provided,
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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 —
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
|