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.
- package/README.md +176 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +975 -0
- package/dist/client.d.ts +53 -0
- package/dist/client.js +40 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +0 -0
- package/openclaw.plugin.json +108 -0
- package/package.json +86 -0
- package/skills/crawd/SKILL.md +81 -0
- package/src/backend/coordinator.ts +883 -0
- package/src/backend/index.ts +581 -0
- package/src/backend/server.ts +589 -0
- package/src/cli.ts +130 -0
- package/src/client.ts +101 -0
- package/src/commands/auth.ts +145 -0
- package/src/commands/config.ts +43 -0
- package/src/commands/down.ts +15 -0
- package/src/commands/logs.ts +32 -0
- package/src/commands/skill.ts +189 -0
- package/src/commands/start.ts +120 -0
- package/src/commands/status.ts +73 -0
- package/src/commands/stop.ts +16 -0
- package/src/commands/stream-key.ts +45 -0
- package/src/commands/talk.ts +30 -0
- package/src/commands/up.ts +59 -0
- package/src/commands/update.ts +92 -0
- package/src/config/schema.ts +66 -0
- package/src/config/store.ts +185 -0
- package/src/daemon/manager.ts +280 -0
- package/src/daemon/pid.ts +102 -0
- package/src/lib/chat/base.ts +13 -0
- package/src/lib/chat/manager.ts +105 -0
- package/src/lib/chat/pumpfun/client.ts +56 -0
- package/src/lib/chat/types.ts +48 -0
- package/src/lib/chat/youtube/client.ts +131 -0
- package/src/lib/pumpfun/live/client.ts +69 -0
- package/src/lib/pumpfun/live/index.ts +3 -0
- package/src/lib/pumpfun/live/types.ts +38 -0
- package/src/lib/pumpfun/v2/client.ts +139 -0
- package/src/lib/pumpfun/v2/index.ts +5 -0
- package/src/lib/pumpfun/v2/socket/client.ts +60 -0
- package/src/lib/pumpfun/v2/socket/index.ts +6 -0
- package/src/lib/pumpfun/v2/socket/types.ts +7 -0
- package/src/lib/pumpfun/v2/types.ts +234 -0
- package/src/lib/tts/tiktok.ts +91 -0
- package/src/plugin.ts +280 -0
- package/src/types.ts +78 -0
- package/src/utils/logger.ts +43 -0
- package/src/utils/paths.ts +55 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
4
|
+
import { watch } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import Fastify from "fastify";
|
|
7
|
+
import fastifyStatic from "@fastify/static";
|
|
8
|
+
import cors from "@fastify/cors";
|
|
9
|
+
import { Server } from "socket.io";
|
|
10
|
+
import OpenAI from "openai";
|
|
11
|
+
import { pumpfun } from "../lib/pumpfun/v2";
|
|
12
|
+
import { ChatManager } from "../lib/chat/manager";
|
|
13
|
+
import { PumpFunChatClient } from "../lib/chat/pumpfun/client";
|
|
14
|
+
import { YouTubeChatClient } from "../lib/chat/youtube/client";
|
|
15
|
+
import { GatewayClient, Coordinator, type CoordinatorConfig, type CoordinatorEvent, type InvokeRequestPayload } from "./coordinator";
|
|
16
|
+
import { generateShortId } from "../lib/chat/types";
|
|
17
|
+
import { configureTikTokTTS, generateTikTokTTS } from "../lib/tts/tiktok";
|
|
18
|
+
import type { ChatMessage } from "../lib/chat/types";
|
|
19
|
+
import { loadEnv, loadConfig } from "../config/store.js";
|
|
20
|
+
import { ENV_PATH, CONFIG_PATH } from "../utils/paths.js";
|
|
21
|
+
import type { TtsProvider, ReplyTurnEvent, TalkEvent } from "../types";
|
|
22
|
+
|
|
23
|
+
// Parse coordinator config from env vars
|
|
24
|
+
function parseCoordinatorConfig(): Partial<CoordinatorConfig> {
|
|
25
|
+
const config: Partial<CoordinatorConfig> = {};
|
|
26
|
+
|
|
27
|
+
if (process.env.VIBE_ENABLED !== undefined) {
|
|
28
|
+
config.vibeEnabled = process.env.VIBE_ENABLED === 'true';
|
|
29
|
+
}
|
|
30
|
+
if (process.env.VIBE_INTERVAL_MS) {
|
|
31
|
+
config.vibeIntervalMs = Number(process.env.VIBE_INTERVAL_MS);
|
|
32
|
+
}
|
|
33
|
+
if (process.env.IDLE_AFTER_MS) {
|
|
34
|
+
config.idleAfterMs = Number(process.env.IDLE_AFTER_MS);
|
|
35
|
+
}
|
|
36
|
+
if (process.env.SLEEP_AFTER_IDLE_MS) {
|
|
37
|
+
config.sleepAfterIdleMs = Number(process.env.SLEEP_AFTER_IDLE_MS);
|
|
38
|
+
}
|
|
39
|
+
if (process.env.VIBE_PROMPT) {
|
|
40
|
+
config.vibePrompt = process.env.VIBE_PROMPT;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const port = Number(process.env.PORT || 4000);
|
|
47
|
+
const BACKEND_URL = process.env.BACKEND_URL || `http://localhost:${port}`;
|
|
48
|
+
const TOKEN_MINT = process.env.NEXT_PUBLIC_TOKEN_MINT;
|
|
49
|
+
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
|
+
|
|
58
|
+
// Unique version ID generated at startup - changes on each deploy/restart
|
|
59
|
+
const BUILD_VERSION = randomUUID();
|
|
60
|
+
|
|
61
|
+
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
|
+
|
|
80
|
+
// --- Auto-reload ~/.crawd/.env and config.json on change ---
|
|
81
|
+
|
|
82
|
+
async function reloadConfig() {
|
|
83
|
+
const env = loadEnv();
|
|
84
|
+
const config = loadConfig();
|
|
85
|
+
const changes: string[] = [];
|
|
86
|
+
|
|
87
|
+
// Update secrets in process.env
|
|
88
|
+
for (const [key, value] of Object.entries(env)) {
|
|
89
|
+
if (value && process.env[key] !== value) {
|
|
90
|
+
changes.push(key);
|
|
91
|
+
process.env[key] = value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
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
|
+
if (changes.length > 0) {
|
|
120
|
+
fastify.log.info({ changes }, 'Config reloaded');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let reloadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
125
|
+
function scheduleReload() {
|
|
126
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
127
|
+
reloadTimer = setTimeout(() => reloadConfig(), 100);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const file of [ENV_PATH, CONFIG_PATH]) {
|
|
131
|
+
try {
|
|
132
|
+
watch(file, () => scheduleReload());
|
|
133
|
+
} catch { /* file may not exist yet */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
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
|
+
// --- Non-TTS helpers ---
|
|
227
|
+
|
|
228
|
+
async function fetchMarketCap(): Promise<number | null> {
|
|
229
|
+
if (!TOKEN_MINT) return null;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const coin = await pumpfun.getCoin(TOKEN_MINT);
|
|
233
|
+
return coin.usd_market_cap;
|
|
234
|
+
} catch (e) {
|
|
235
|
+
fastify.log.error(e, "failed to fetch market cap");
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function main() {
|
|
241
|
+
fastify.log.info({ chatProvider: CHAT_PROVIDER, botProvider: BOT_PROVIDER }, 'TTS providers configured');
|
|
242
|
+
|
|
243
|
+
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
|
+
|
|
251
|
+
const io = new Server(fastify.server, {
|
|
252
|
+
cors: { origin: "*" },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
let latestMcap: number | null = null;
|
|
256
|
+
|
|
257
|
+
async function pollMarketCap() {
|
|
258
|
+
fastify.log.info("polling market cap");
|
|
259
|
+
const mcap = await fetchMarketCap();
|
|
260
|
+
fastify.log.info({ mcap }, `fetched market cap: ${mcap}`);
|
|
261
|
+
|
|
262
|
+
if (mcap === null) return;
|
|
263
|
+
|
|
264
|
+
latestMcap = mcap;
|
|
265
|
+
io.emit("crawd:mcap", { mcap });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// --- Pending talk ack tracking (for synchronous talk tool calls) ---
|
|
269
|
+
const pendingTalkAcks = new Map<string, { resolve: () => void; timer: ReturnType<typeof setTimeout> }>();
|
|
270
|
+
const TALK_ACK_TIMEOUT_MS = 60_000;
|
|
271
|
+
|
|
272
|
+
function waitForTalkAck(talkId: string): Promise<void> {
|
|
273
|
+
return new Promise((resolve) => {
|
|
274
|
+
const timer = setTimeout(() => {
|
|
275
|
+
pendingTalkAcks.delete(talkId);
|
|
276
|
+
fastify.log.warn({ talkId }, 'Talk ack timed out, resolving anyway');
|
|
277
|
+
resolve();
|
|
278
|
+
}, TALK_ACK_TIMEOUT_MS);
|
|
279
|
+
pendingTalkAcks.set(talkId, { resolve, timer });
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function resolveTalkAck(talkId: string) {
|
|
284
|
+
const pending = pendingTalkAcks.get(talkId);
|
|
285
|
+
if (pending) {
|
|
286
|
+
clearTimeout(pending.timer);
|
|
287
|
+
pendingTalkAcks.delete(talkId);
|
|
288
|
+
pending.resolve();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Chat manager and coordinator instances
|
|
293
|
+
let chatManager: ChatManager | null = null;
|
|
294
|
+
let coordinator: Coordinator | null = null;
|
|
295
|
+
|
|
296
|
+
async function startChatSystem() {
|
|
297
|
+
chatManager = new ChatManager();
|
|
298
|
+
|
|
299
|
+
// Register Pump Fun client if enabled
|
|
300
|
+
if (process.env.PUMPFUN_ENABLED !== 'false' && TOKEN_MINT) {
|
|
301
|
+
chatManager.registerClient('pumpfun', new PumpFunChatClient(
|
|
302
|
+
TOKEN_MINT,
|
|
303
|
+
process.env.PUMPFUN_AUTH_TOKEN ?? null
|
|
304
|
+
));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Register YouTube client if enabled
|
|
308
|
+
if (process.env.YOUTUBE_ENABLED === 'true' && process.env.YOUTUBE_VIDEO_ID) {
|
|
309
|
+
chatManager.registerClient('youtube', new YouTubeChatClient(
|
|
310
|
+
process.env.YOUTUBE_VIDEO_ID
|
|
311
|
+
));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Unified message handler
|
|
315
|
+
chatManager.onMessage((msg: ChatMessage) => {
|
|
316
|
+
fastify.log.info({ platform: msg.platform, user: msg.username }, 'chat message');
|
|
317
|
+
|
|
318
|
+
// Emit to frontend overlay
|
|
319
|
+
io.emit('crawd:chat', msg);
|
|
320
|
+
|
|
321
|
+
// Send to coordinator for batching (if connected)
|
|
322
|
+
coordinator?.onMessage(msg);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Start coordinator if gateway is configured
|
|
326
|
+
const gatewayUrl = process.env.OPENCLAW_GATEWAY_URL;
|
|
327
|
+
const gatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
328
|
+
|
|
329
|
+
if (gatewayUrl && gatewayToken) {
|
|
330
|
+
const gateway = new GatewayClient(gatewayUrl, gatewayToken);
|
|
331
|
+
const coordConfig = parseCoordinatorConfig();
|
|
332
|
+
coordinator = new Coordinator(gateway.triggerAgent.bind(gateway), coordConfig);
|
|
333
|
+
|
|
334
|
+
// Emit coordinator status changes to frontend
|
|
335
|
+
coordinator.setOnEvent((event: CoordinatorEvent) => {
|
|
336
|
+
if (event.type === 'stateChange') {
|
|
337
|
+
const status = event.to;
|
|
338
|
+
io.emit('crawd:status', { status });
|
|
339
|
+
} else if (event.type === 'vibeExecuted' && !event.skipped) {
|
|
340
|
+
io.emit('crawd:status', { status: 'vibing' });
|
|
341
|
+
}
|
|
342
|
+
// Note: chatProcessed no longer emits status — we only wake/emit
|
|
343
|
+
// when the agent actually replies (via talk tool or text fallback).
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
/**
|
|
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.
|
|
349
|
+
*/
|
|
350
|
+
async function handleTalkInvoke(text: string, replyTo?: ChatMessage): Promise<void> {
|
|
351
|
+
const talkId = randomUUID();
|
|
352
|
+
fastify.log.info({ talkId, text: text.slice(0, 80), replyTo: replyTo?.shortId }, 'Handling talk invoke');
|
|
353
|
+
|
|
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
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
io.emit('crawd:talk', event);
|
|
370
|
+
fastify.log.info({ talkId, hasChat: !!event.chat }, 'Emitted crawd:talk, waiting for ack');
|
|
371
|
+
|
|
372
|
+
await waitForTalkAck(talkId);
|
|
373
|
+
fastify.log.info({ talkId }, 'Talk complete');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
await gateway.connect();
|
|
378
|
+
coordinator.start();
|
|
379
|
+
fastify.log.info('Coordinator started, gateway connected');
|
|
380
|
+
|
|
381
|
+
// Register invoke handler on gateway client for the talk command
|
|
382
|
+
gateway.onInvokeRequest = async (payload: InvokeRequestPayload) => {
|
|
383
|
+
fastify.log.info({ command: payload.command, id: payload.id }, 'Invoke request received');
|
|
384
|
+
|
|
385
|
+
if (payload.command === 'talk') {
|
|
386
|
+
try {
|
|
387
|
+
const params = payload.paramsJSON ? JSON.parse(payload.paramsJSON) : {};
|
|
388
|
+
const text = params.text;
|
|
389
|
+
if (!text || typeof text !== 'string') {
|
|
390
|
+
await gateway.sendInvokeResult(payload.id, payload.nodeId, {
|
|
391
|
+
ok: false,
|
|
392
|
+
error: { code: 'INVALID_PARAMS', message: 'text parameter is required' },
|
|
393
|
+
});
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Agent is actively speaking — wake the coordinator
|
|
398
|
+
if (coordinator && coordinator.state !== 'active') {
|
|
399
|
+
coordinator.wake();
|
|
400
|
+
} else {
|
|
401
|
+
coordinator?.resetActivity();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Look up replyTo chat message if provided
|
|
405
|
+
const replyTo = params.replyTo && coordinator
|
|
406
|
+
? coordinator.getRecentMessage(params.replyTo)
|
|
407
|
+
: undefined;
|
|
408
|
+
|
|
409
|
+
await handleTalkInvoke(text, replyTo);
|
|
410
|
+
await gateway.sendInvokeResult(payload.id, payload.nodeId, {
|
|
411
|
+
ok: true,
|
|
412
|
+
payload: { spoken: true },
|
|
413
|
+
});
|
|
414
|
+
} catch (err) {
|
|
415
|
+
fastify.log.error(err, 'Talk invoke failed');
|
|
416
|
+
await gateway.sendInvokeResult(payload.id, payload.nodeId, {
|
|
417
|
+
ok: false,
|
|
418
|
+
error: { code: 'TTS_FAILED', message: String(err) },
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
fastify.log.warn({ command: payload.command }, 'Unknown invoke command');
|
|
423
|
+
await gateway.sendInvokeResult(payload.id, payload.nodeId, {
|
|
424
|
+
ok: false,
|
|
425
|
+
error: { code: 'UNKNOWN_COMMAND', message: `Unknown command: ${payload.command}` },
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
} catch (err) {
|
|
430
|
+
fastify.log.error(err, 'Failed to connect to gateway');
|
|
431
|
+
gateway.disconnect();
|
|
432
|
+
coordinator = null;
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
fastify.log.warn('Gateway not configured - coordinator disabled');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Connect all chat clients
|
|
439
|
+
await chatManager.connectAll();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
io.on("connection", (socket) => {
|
|
443
|
+
fastify.log.info(`socket connected: ${socket.id}`);
|
|
444
|
+
|
|
445
|
+
if (latestMcap !== null) {
|
|
446
|
+
socket.emit("crawd:mcap", { mcap: latestMcap });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Listen for talk:done acks from overlay
|
|
450
|
+
socket.on("crawd:talk:done", (data: { id: string }) => {
|
|
451
|
+
if (data?.id) {
|
|
452
|
+
fastify.log.info({ talkId: data.id }, 'Talk ack received from overlay');
|
|
453
|
+
resolveTalkAck(data.id);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
socket.on("disconnect", () => {
|
|
458
|
+
fastify.log.info(`socket disconnected: ${socket.id}`);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
fastify.post<{ Body: { message: string } }>(
|
|
463
|
+
"/crawd/talk",
|
|
464
|
+
async (request, reply) => {
|
|
465
|
+
const { message } = request.body;
|
|
466
|
+
if (!message || typeof message !== "string") {
|
|
467
|
+
return reply.status(400).send({ error: "message is required" });
|
|
468
|
+
}
|
|
469
|
+
|
|
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
|
+
}
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
fastify.get("/chat/status", async () => {
|
|
484
|
+
return { connected: chatManager?.getConnectedKeys() ?? [] };
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
fastify.get("/version", async () => {
|
|
488
|
+
return { version: BUILD_VERSION };
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
fastify.get("/coordinator/status", async () => {
|
|
492
|
+
if (!coordinator) {
|
|
493
|
+
return { enabled: false };
|
|
494
|
+
}
|
|
495
|
+
return { enabled: true, ...coordinator.getState() };
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
fastify.post<{ Body: Partial<CoordinatorConfig> }>(
|
|
499
|
+
"/coordinator/config",
|
|
500
|
+
async (request, reply) => {
|
|
501
|
+
if (!coordinator) {
|
|
502
|
+
return reply.status(400).send({ error: "Coordinator not enabled" });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const config = request.body;
|
|
506
|
+
coordinator.updateConfig(config);
|
|
507
|
+
return { ok: true, ...coordinator.getState() };
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
fastify.post<{ Body: { username: string; message: string } }>(
|
|
512
|
+
"/mock/chat",
|
|
513
|
+
async (request, reply) => {
|
|
514
|
+
const { username, message } = request.body;
|
|
515
|
+
if (!username || !message) {
|
|
516
|
+
return reply.status(400).send({ error: "username and message are required" });
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const id = randomUUID();
|
|
520
|
+
const mockMsg: ChatMessage = {
|
|
521
|
+
id,
|
|
522
|
+
shortId: generateShortId(),
|
|
523
|
+
username,
|
|
524
|
+
message,
|
|
525
|
+
platform: 'pumpfun',
|
|
526
|
+
timestamp: Date.now(),
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
fastify.log.info({ username, message }, "mock chat message");
|
|
530
|
+
io.emit("crawd:chat", mockMsg);
|
|
531
|
+
coordinator?.onMessage(mockMsg);
|
|
532
|
+
return { ok: true, id };
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
// Mock turn endpoint for debug UI — generates real TTS
|
|
537
|
+
fastify.post<{ Body: { username: string; message: string; response: string } }>(
|
|
538
|
+
"/mock/turn",
|
|
539
|
+
async (request, reply) => {
|
|
540
|
+
const { username, message, response } = request.body;
|
|
541
|
+
if (!username || !message || !response) {
|
|
542
|
+
return reply.status(400).send({ error: "username, message, and response are required" });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
fastify.log.info({ username, message, response }, "mock turn");
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
const [chatTtsUrl, botTtsUrl] = await Promise.all([
|
|
549
|
+
chatTTS(`Chat says: ${message}`),
|
|
550
|
+
botTTS(response),
|
|
551
|
+
]);
|
|
552
|
+
|
|
553
|
+
const event: ReplyTurnEvent = {
|
|
554
|
+
chat: { username, message },
|
|
555
|
+
botMessage: response,
|
|
556
|
+
chatTtsUrl,
|
|
557
|
+
botTtsUrl,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
io.emit('crawd:reply-turn', event);
|
|
561
|
+
|
|
562
|
+
return { ok: true };
|
|
563
|
+
} catch (e) {
|
|
564
|
+
fastify.log.error(e, "failed to generate mock turn TTS");
|
|
565
|
+
return reply.status(500).send({ error: "Failed to generate TTS" });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
await startChatSystem();
|
|
571
|
+
|
|
572
|
+
const host = process.env.BIND_HOST || "0.0.0.0";
|
|
573
|
+
await fastify.listen({ port, host });
|
|
574
|
+
|
|
575
|
+
pollMarketCap();
|
|
576
|
+
setInterval(pollMarketCap, MCAP_POLL_MS);
|
|
577
|
+
|
|
578
|
+
fastify.log.info("Chat system started");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
main();
|