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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TikTok Text-to-Speech
|
|
3
|
+
* Vendored from https://github.com/Steve0929/tiktok-tts
|
|
4
|
+
* Converted to TypeScript/ESM with native fetch
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DEFAULT_BASE_URL = 'https://api16-normal-v6.tiktokv.com/media/api/text/speech/invoke';
|
|
8
|
+
const DEFAULT_VOICE = 'en_us_002'; // Jessie
|
|
9
|
+
|
|
10
|
+
type TikTokTTSConfig = {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let config: TikTokTTSConfig | null = null;
|
|
16
|
+
|
|
17
|
+
export function configureTikTokTTS(sessionId: string, baseUrl?: string): void {
|
|
18
|
+
config = { sessionId, baseUrl };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function prepareText(text: string): string {
|
|
22
|
+
return text
|
|
23
|
+
.replace(/\+/g, 'plus')
|
|
24
|
+
.replace(/\s/g, '+')
|
|
25
|
+
.replace(/&/g, 'and');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function handleStatusError(statusCode: number): never {
|
|
29
|
+
switch (statusCode) {
|
|
30
|
+
case 1:
|
|
31
|
+
throw new Error(`TikTok session id invalid or expired. status_code: ${statusCode}`);
|
|
32
|
+
case 2:
|
|
33
|
+
throw new Error(`Text is too long. status_code: ${statusCode}`);
|
|
34
|
+
case 4:
|
|
35
|
+
throw new Error(`Invalid speaker voice. status_code: ${statusCode}`);
|
|
36
|
+
case 5:
|
|
37
|
+
throw new Error(`No session id found. status_code: ${statusCode}`);
|
|
38
|
+
default:
|
|
39
|
+
throw new Error(`TikTok TTS error. status_code: ${statusCode}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type TikTokResponse = {
|
|
44
|
+
status_code: number;
|
|
45
|
+
data?: {
|
|
46
|
+
v_str?: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate TTS audio using TikTok's API
|
|
52
|
+
* @returns Base64-encoded MP3 audio data
|
|
53
|
+
*/
|
|
54
|
+
export async function generateTikTokTTS(
|
|
55
|
+
text: string,
|
|
56
|
+
voice: string = DEFAULT_VOICE
|
|
57
|
+
): Promise<Buffer> {
|
|
58
|
+
if (!config) {
|
|
59
|
+
throw new Error('TikTok TTS not configured. Call configureTikTokTTS() first.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
63
|
+
const reqText = prepareText(text);
|
|
64
|
+
const url = `${baseUrl}/?text_speaker=${voice}&req_text=${reqText}&speaker_map_type=0&aid=1233`;
|
|
65
|
+
|
|
66
|
+
const response = await fetch(url, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'User-Agent': 'com.zhiliaoapp.musically/2022600030 (Linux; U; Android 7.1.2; es_ES; SM-G988N; Build/NRD90M;tt-ok/3.12.13.1)',
|
|
70
|
+
'Cookie': `sessionid=${config.sessionId}`,
|
|
71
|
+
'Accept-Encoding': 'gzip,deflate,compress',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`TikTok TTS request failed: ${response.status} ${response.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = (await response.json()) as TikTokResponse;
|
|
80
|
+
|
|
81
|
+
if (result.status_code !== 0) {
|
|
82
|
+
handleStatusError(result.status_code);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const encodedVoice = result.data?.v_str;
|
|
86
|
+
if (!encodedVoice) {
|
|
87
|
+
throw new Error('TikTok TTS returned no audio data');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Buffer.from(encodedVoice, 'base64');
|
|
91
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw plugin entry point for crawd.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - `livestream_talk` tool (unprompted speech on stream)
|
|
6
|
+
* - `livestream_reply` tool (reply to a chat message)
|
|
7
|
+
* - `crawd` service (Fastify + Socket.IO backend)
|
|
8
|
+
*/
|
|
9
|
+
import { Type } from '@sinclair/typebox'
|
|
10
|
+
import { CrawdBackend, type CrawdConfig, type TtsVoiceEntry } from './backend/server.js'
|
|
11
|
+
|
|
12
|
+
// Minimal plugin types — the real types come from openclaw/plugin-sdk at runtime.
|
|
13
|
+
// Defined inline so this package builds without the openclaw peerDep installed.
|
|
14
|
+
type PluginLogger = {
|
|
15
|
+
info: (message: string) => void
|
|
16
|
+
warn: (message: string) => void
|
|
17
|
+
error: (message: string) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type PluginApi = {
|
|
21
|
+
pluginConfig?: Record<string, unknown>
|
|
22
|
+
logger: PluginLogger
|
|
23
|
+
registerTool: (tool: Record<string, unknown>, opts?: { name?: string }) => void
|
|
24
|
+
registerService: (service: { id: string; start: () => Promise<void>; stop?: () => Promise<void> }) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type PluginDefinition = {
|
|
28
|
+
id: string
|
|
29
|
+
name: string
|
|
30
|
+
description: string
|
|
31
|
+
configSchema?: Record<string, unknown>
|
|
32
|
+
register?: (api: PluginApi) => void | Promise<void>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Config parsing — transform pluginConfig → CrawdConfig
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function parseTtsChain(raw: unknown): TtsVoiceEntry[] {
|
|
40
|
+
if (!Array.isArray(raw)) return []
|
|
41
|
+
return raw
|
|
42
|
+
.filter((e): e is { provider: string; voice: string } =>
|
|
43
|
+
e && typeof e === 'object' && typeof e.provider === 'string' && typeof e.voice === 'string',
|
|
44
|
+
)
|
|
45
|
+
.map((e) => ({
|
|
46
|
+
provider: e.provider as TtsVoiceEntry['provider'],
|
|
47
|
+
voice: e.voice,
|
|
48
|
+
}))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Resolve gateway WebSocket URL from env/defaults (same logic as OpenClaw's callGateway) */
|
|
52
|
+
function resolveGatewayUrl(port?: number): string {
|
|
53
|
+
if (port) return `ws://127.0.0.1:${port}`
|
|
54
|
+
const portRaw = process.env.OPENCLAW_GATEWAY_PORT?.trim()
|
|
55
|
+
const resolved = portRaw ? parseInt(portRaw, 10) || 18789 : 18789
|
|
56
|
+
return `ws://127.0.0.1:${resolved}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Read gateway auth + port from the OpenClaw host config.
|
|
61
|
+
* The real PluginApi has a `config: OpenClawConfig` property — our minimal
|
|
62
|
+
* inline type omits it so we access it via casting.
|
|
63
|
+
*/
|
|
64
|
+
function resolveGatewayFromHost(api: PluginApi): { token?: string; port?: number } {
|
|
65
|
+
const oclConfig = (api as Record<string, unknown>).config as Record<string, unknown> | undefined
|
|
66
|
+
if (!oclConfig) return {}
|
|
67
|
+
const gw = (oclConfig.gateway ?? {}) as Record<string, unknown>
|
|
68
|
+
const auth = (gw.auth ?? {}) as Record<string, unknown>
|
|
69
|
+
return {
|
|
70
|
+
token: typeof auth.token === 'string' ? auth.token : undefined,
|
|
71
|
+
port: typeof gw.port === 'number' ? gw.port : undefined,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parsePluginConfig(raw: Record<string, unknown> | undefined): CrawdConfig {
|
|
76
|
+
const cfg = raw ?? {}
|
|
77
|
+
const tts = (cfg.tts ?? {}) as Record<string, unknown>
|
|
78
|
+
const vibe = (cfg.vibe ?? {}) as Record<string, unknown>
|
|
79
|
+
const chat = (cfg.chat ?? {}) as Record<string, unknown>
|
|
80
|
+
const youtube = (chat.youtube ?? {}) as Record<string, unknown>
|
|
81
|
+
const pumpfun = (chat.pumpfun ?? {}) as Record<string, unknown>
|
|
82
|
+
|
|
83
|
+
const port = typeof cfg.port === 'number' ? cfg.port : 4000
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
enabled: cfg.enabled !== false,
|
|
87
|
+
port,
|
|
88
|
+
bindHost: typeof cfg.bindHost === 'string' ? cfg.bindHost : '0.0.0.0',
|
|
89
|
+
backendUrl: typeof cfg.backendUrl === 'string' ? cfg.backendUrl : `http://localhost:${port}`,
|
|
90
|
+
tts: {
|
|
91
|
+
chat: parseTtsChain(tts.chat),
|
|
92
|
+
bot: parseTtsChain(tts.bot),
|
|
93
|
+
openaiApiKey: typeof tts.openaiApiKey === 'string' ? tts.openaiApiKey : undefined,
|
|
94
|
+
elevenlabsApiKey: typeof tts.elevenlabsApiKey === 'string' ? tts.elevenlabsApiKey : undefined,
|
|
95
|
+
tiktokSessionId: typeof tts.tiktokSessionId === 'string' ? tts.tiktokSessionId : undefined,
|
|
96
|
+
},
|
|
97
|
+
vibe: {
|
|
98
|
+
enabled: vibe.enabled !== false,
|
|
99
|
+
intervalMs: typeof vibe.intervalMs === 'number' ? vibe.intervalMs : 10_000,
|
|
100
|
+
idleAfterMs: typeof vibe.idleAfterMs === 'number' ? vibe.idleAfterMs : 30_000,
|
|
101
|
+
sleepAfterIdleMs: typeof vibe.sleepAfterIdleMs === 'number' ? vibe.sleepAfterIdleMs : 60_000,
|
|
102
|
+
prompt: typeof vibe.prompt === 'string' ? vibe.prompt : undefined,
|
|
103
|
+
},
|
|
104
|
+
chat: {
|
|
105
|
+
youtube: {
|
|
106
|
+
enabled: youtube.enabled === true,
|
|
107
|
+
videoId: typeof youtube.videoId === 'string' ? youtube.videoId : undefined,
|
|
108
|
+
},
|
|
109
|
+
pumpfun: {
|
|
110
|
+
enabled: pumpfun.enabled !== false,
|
|
111
|
+
tokenMint: typeof pumpfun.tokenMint === 'string' ? pumpfun.tokenMint : undefined,
|
|
112
|
+
authToken: typeof pumpfun.authToken === 'string' ? pumpfun.authToken : undefined,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
// Gateway: plugin config overrides, then env vars, then OpenClaw defaults
|
|
116
|
+
gatewayUrl: typeof cfg.gatewayUrl === 'string' ? cfg.gatewayUrl
|
|
117
|
+
: process.env.OPENCLAW_GATEWAY_URL ?? resolveGatewayUrl(),
|
|
118
|
+
gatewayToken: typeof cfg.gatewayToken === 'string' ? cfg.gatewayToken
|
|
119
|
+
: process.env.OPENCLAW_GATEWAY_TOKEN,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Plugin definition
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
const crawdConfigSchema = {
|
|
128
|
+
parse(value: unknown) {
|
|
129
|
+
const raw = value && typeof value === 'object' && !Array.isArray(value)
|
|
130
|
+
? (value as Record<string, unknown>)
|
|
131
|
+
: {}
|
|
132
|
+
return parsePluginConfig(raw)
|
|
133
|
+
},
|
|
134
|
+
uiHints: {
|
|
135
|
+
enabled: { label: 'Enabled' },
|
|
136
|
+
port: { label: 'Backend Port', placeholder: '4000' },
|
|
137
|
+
bindHost: { label: 'Bind Host', placeholder: '0.0.0.0', advanced: true },
|
|
138
|
+
backendUrl: { label: 'Backend URL', advanced: true, help: 'Public URL for TTS file serving' },
|
|
139
|
+
'tts.chat': { label: 'Chat TTS Voices', help: 'Ordered fallback chain [{provider, voice}]' },
|
|
140
|
+
'tts.bot': { label: 'Bot TTS Voices', help: 'Ordered fallback chain [{provider, voice}]' },
|
|
141
|
+
'tts.openaiApiKey': { label: 'OpenAI API Key', sensitive: true },
|
|
142
|
+
'tts.elevenlabsApiKey': { label: 'ElevenLabs API Key', sensitive: true },
|
|
143
|
+
'tts.tiktokSessionId': { label: 'TikTok Session ID', sensitive: true },
|
|
144
|
+
'vibe.enabled': { label: 'Vibe Mode' },
|
|
145
|
+
'vibe.intervalMs': { label: 'Vibe Interval (ms)', advanced: true },
|
|
146
|
+
'vibe.idleAfterMs': { label: 'Idle After (ms)', advanced: true },
|
|
147
|
+
'vibe.sleepAfterIdleMs': { label: 'Sleep After Idle (ms)', advanced: true },
|
|
148
|
+
'vibe.prompt': { label: 'Vibe Prompt', advanced: true },
|
|
149
|
+
'chat.youtube.enabled': { label: 'YouTube Chat' },
|
|
150
|
+
'chat.youtube.videoId': { label: 'YouTube Video ID' },
|
|
151
|
+
'chat.pumpfun.enabled': { label: 'PumpFun Chat' },
|
|
152
|
+
'chat.pumpfun.tokenMint': { label: 'PumpFun Token Mint' },
|
|
153
|
+
'chat.pumpfun.authToken': { label: 'PumpFun Auth Token', sensitive: true },
|
|
154
|
+
gatewayUrl: { label: 'Gateway URL', advanced: true, help: 'Override auto-detected gateway URL (usually not needed)' },
|
|
155
|
+
gatewayToken: { label: 'Gateway Token', advanced: true, sensitive: true, help: 'Override OPENCLAW_GATEWAY_TOKEN env var' },
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const plugin: PluginDefinition = {
|
|
160
|
+
id: 'crawd',
|
|
161
|
+
name: 'Crawd Livestream',
|
|
162
|
+
description: 'crawd.bot plugin — AI agent livestreaming with TTS, chat integration, and OBS overlay',
|
|
163
|
+
configSchema: crawdConfigSchema,
|
|
164
|
+
|
|
165
|
+
register(api: PluginApi) {
|
|
166
|
+
const config = parsePluginConfig(api.pluginConfig)
|
|
167
|
+
if (!config.enabled) {
|
|
168
|
+
api.logger.info('crawd: disabled')
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Resolve gateway auth from OpenClaw host config (token lives there, not in env)
|
|
173
|
+
const hostGw = resolveGatewayFromHost(api)
|
|
174
|
+
if (!config.gatewayToken && hostGw.token) {
|
|
175
|
+
config.gatewayToken = hostGw.token
|
|
176
|
+
api.logger.info('crawd: resolved gateway token from host config')
|
|
177
|
+
}
|
|
178
|
+
if (hostGw.port && !process.env.OPENCLAW_GATEWAY_PORT) {
|
|
179
|
+
config.gatewayUrl = resolveGatewayUrl(hostGw.port)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
api.logger.info(`crawd: gateway url=${config.gatewayUrl}, token=${config.gatewayToken ? 'set' : '(none — gateway may not require auth in local mode)'}`)
|
|
183
|
+
|
|
184
|
+
let backend: CrawdBackend | null = null
|
|
185
|
+
let backendPromise: Promise<CrawdBackend> | null = null
|
|
186
|
+
|
|
187
|
+
const ensureBackend = async (): Promise<CrawdBackend> => {
|
|
188
|
+
if (backend) return backend
|
|
189
|
+
if (!backendPromise) {
|
|
190
|
+
backendPromise = (async () => {
|
|
191
|
+
const b = new CrawdBackend(config, {
|
|
192
|
+
info: (msg) => api.logger.info(`[crawd] ${msg}`),
|
|
193
|
+
warn: (msg) => api.logger.warn(`[crawd] ${msg}`),
|
|
194
|
+
error: (msg) => api.logger.error(`[crawd] ${msg}`),
|
|
195
|
+
})
|
|
196
|
+
await b.start()
|
|
197
|
+
return b
|
|
198
|
+
})()
|
|
199
|
+
}
|
|
200
|
+
backend = await backendPromise
|
|
201
|
+
return backend
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// livestream_talk — unprompted speech on stream
|
|
205
|
+
api.registerTool(
|
|
206
|
+
{
|
|
207
|
+
name: 'livestream_talk',
|
|
208
|
+
label: 'Livestream Talk',
|
|
209
|
+
description:
|
|
210
|
+
'Speak on the livestream unprompted. Shows a speech bubble on the overlay and generates TTS audio. Use for narration, vibes, and commentary — NOT for replying to chat (use livestream_reply for that).',
|
|
211
|
+
parameters: Type.Object({
|
|
212
|
+
text: Type.String({ description: 'Message to speak on stream' }),
|
|
213
|
+
}),
|
|
214
|
+
async execute(_toolCallId: string, params: unknown) {
|
|
215
|
+
const b = await ensureBackend()
|
|
216
|
+
const { text } = params as { text: string }
|
|
217
|
+
const result = await b.handleTalk(text)
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: 'text', text: result.spoken ? `Spoke on stream: "${text}"` : 'Failed to speak' }],
|
|
220
|
+
details: result,
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{ name: 'livestream_talk' },
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
// livestream_reply — reply to a chat message
|
|
228
|
+
api.registerTool(
|
|
229
|
+
{
|
|
230
|
+
name: 'livestream_reply',
|
|
231
|
+
label: 'Livestream Reply',
|
|
232
|
+
description:
|
|
233
|
+
'Reply to a chat message on the livestream. Reads the original message aloud with the chat voice, then speaks your reply with the bot voice. Use this ONLY when responding to a specific viewer message.',
|
|
234
|
+
parameters: Type.Object({
|
|
235
|
+
text: Type.String({ description: 'Your reply to the chat message' }),
|
|
236
|
+
username: Type.String({ description: 'Username of the person you are replying to' }),
|
|
237
|
+
message: Type.String({ description: 'The original chat message you are replying to' }),
|
|
238
|
+
}),
|
|
239
|
+
async execute(_toolCallId: string, params: unknown) {
|
|
240
|
+
const b = await ensureBackend()
|
|
241
|
+
const { text, username, message } = params as { text: string; username: string; message: string }
|
|
242
|
+
const result = await b.handleReply(text, { username, message })
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: 'text', text: result.spoken ? `Replied to @${username}: "${text}"` : 'Failed to reply' }],
|
|
245
|
+
details: result,
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
{ name: 'livestream_reply' },
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
// Service lifecycle
|
|
253
|
+
api.registerService({
|
|
254
|
+
id: 'crawd',
|
|
255
|
+
start: async () => {
|
|
256
|
+
try {
|
|
257
|
+
await ensureBackend()
|
|
258
|
+
api.logger.info('crawd: backend started')
|
|
259
|
+
} catch (err) {
|
|
260
|
+
api.logger.error(
|
|
261
|
+
`crawd: failed to start — ${err instanceof Error ? err.message : String(err)}`,
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
stop: async () => {
|
|
266
|
+
if (backendPromise) {
|
|
267
|
+
try {
|
|
268
|
+
const b = await backendPromise
|
|
269
|
+
await b.stop()
|
|
270
|
+
} finally {
|
|
271
|
+
backendPromise = null
|
|
272
|
+
backend = null
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export default plugin
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the CrawdBot backend ↔ frontend (overlay) API.
|
|
3
|
+
*
|
|
4
|
+
* Install `@crawd/cli` and import:
|
|
5
|
+
* import type { CrawdEvents, ReplyTurnEvent } from '@crawd/cli'
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export chat types used in events
|
|
9
|
+
export type {
|
|
10
|
+
ChatMessage,
|
|
11
|
+
ChatPlatform,
|
|
12
|
+
ChatMessageMetadata,
|
|
13
|
+
SuperChatInfo,
|
|
14
|
+
} from './lib/chat/types'
|
|
15
|
+
|
|
16
|
+
/** TTS provider identifier */
|
|
17
|
+
export type TtsProvider = 'openai' | 'elevenlabs' | 'tiktok'
|
|
18
|
+
|
|
19
|
+
// --- Socket.IO event payloads ---
|
|
20
|
+
|
|
21
|
+
/** Turn-based reply: chat message + bot response, each with TTS audio */
|
|
22
|
+
export type ReplyTurnEvent = {
|
|
23
|
+
chat: { username: string; message: string }
|
|
24
|
+
botMessage: string
|
|
25
|
+
chatTtsUrl: string
|
|
26
|
+
botTtsUrl: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Bot speech bubble with pre-generated TTS (atomic event) */
|
|
30
|
+
export type TalkEvent = {
|
|
31
|
+
/** Correlation ID — overlay sends talk:done with this ID when audio finishes */
|
|
32
|
+
id: string
|
|
33
|
+
/** Bot reply text */
|
|
34
|
+
message: string
|
|
35
|
+
/** Bot TTS audio URL */
|
|
36
|
+
ttsUrl: string
|
|
37
|
+
/** Optional: chat message being replied to (overlay plays this first) */
|
|
38
|
+
chat?: {
|
|
39
|
+
message: string
|
|
40
|
+
username: string
|
|
41
|
+
ttsUrl: string
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Overlay → backend acknowledgement that a talk finished playing */
|
|
46
|
+
export type TalkDoneEvent = {
|
|
47
|
+
id: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Overlay → backend mock chat message (for testing) */
|
|
51
|
+
export type MockChatEvent = {
|
|
52
|
+
username: string
|
|
53
|
+
message: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Incoming chat message from a platform */
|
|
57
|
+
export type { ChatMessage as ChatEvent } from './lib/chat/types'
|
|
58
|
+
|
|
59
|
+
/** Market cap update */
|
|
60
|
+
export type McapEvent = {
|
|
61
|
+
mcap: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Coordinator status change */
|
|
65
|
+
export type StatusEvent = {
|
|
66
|
+
status: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Map of all socket event names to their payload types */
|
|
70
|
+
export type CrawdEvents = {
|
|
71
|
+
'crawd:reply-turn': ReplyTurnEvent
|
|
72
|
+
'crawd:talk': TalkEvent
|
|
73
|
+
'crawd:talk:done': TalkDoneEvent
|
|
74
|
+
'crawd:chat': import('./lib/chat/types').ChatMessage
|
|
75
|
+
'crawd:mock-chat': MockChatEvent
|
|
76
|
+
'crawd:mcap': McapEvent
|
|
77
|
+
'crawd:status': StatusEvent
|
|
78
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
export const log = {
|
|
4
|
+
info: (msg: string) => console.log(chalk.blue('ℹ'), msg),
|
|
5
|
+
success: (msg: string) => console.log(chalk.green('✓'), msg),
|
|
6
|
+
warn: (msg: string) => console.log(chalk.yellow('⚠'), msg),
|
|
7
|
+
error: (msg: string) => console.log(chalk.red('✗'), msg),
|
|
8
|
+
dim: (msg: string) => console.log(chalk.dim(msg)),
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const fmt = {
|
|
12
|
+
url: (url: string) => chalk.cyan.underline(url),
|
|
13
|
+
path: (path: string) => chalk.yellow(path),
|
|
14
|
+
cmd: (cmd: string) => chalk.green(cmd),
|
|
15
|
+
bold: (s: string) => chalk.bold(s),
|
|
16
|
+
dim: (s: string) => chalk.dim(s),
|
|
17
|
+
success: (s: string) => chalk.green(s),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Print a labeled key-value pair */
|
|
21
|
+
export function printKv(label: string, value: string, indent = 2) {
|
|
22
|
+
const padding = ' '.repeat(indent)
|
|
23
|
+
console.log(`${padding}${chalk.dim(label + ':')} ${value}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Print a section header */
|
|
27
|
+
export function printHeader(title: string) {
|
|
28
|
+
console.log()
|
|
29
|
+
console.log(chalk.bold(title))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Print a status line with icon */
|
|
33
|
+
export function printStatus(
|
|
34
|
+
label: string,
|
|
35
|
+
ok: boolean,
|
|
36
|
+
detail?: string,
|
|
37
|
+
indent = 2
|
|
38
|
+
) {
|
|
39
|
+
const padding = ' '.repeat(indent)
|
|
40
|
+
const icon = ok ? chalk.green('✓') : chalk.red('✗')
|
|
41
|
+
const status = detail ? ` ${chalk.dim(`(${detail})`)}` : ''
|
|
42
|
+
console.log(`${padding}${icon} ${label}${status}`)
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { homedir } from 'os'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import { dirname } from 'path'
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
7
|
+
const __dirname = dirname(__filename)
|
|
8
|
+
|
|
9
|
+
/** User data directory: ~/.crawd */
|
|
10
|
+
export const CRAWD_HOME = join(homedir(), '.crawd')
|
|
11
|
+
|
|
12
|
+
/** Config file path */
|
|
13
|
+
export const CONFIG_PATH = join(CRAWD_HOME, 'config.json')
|
|
14
|
+
|
|
15
|
+
/** Secrets file path */
|
|
16
|
+
export const ENV_PATH = join(CRAWD_HOME, '.env')
|
|
17
|
+
|
|
18
|
+
/** PID files directory */
|
|
19
|
+
export const PIDS_DIR = join(CRAWD_HOME, 'pids')
|
|
20
|
+
|
|
21
|
+
/** Logs directory */
|
|
22
|
+
export const LOGS_DIR = join(CRAWD_HOME, 'logs')
|
|
23
|
+
|
|
24
|
+
/** User-editable overlay source */
|
|
25
|
+
export const OVERLAY_DIR = join(CRAWD_HOME, 'overlay')
|
|
26
|
+
|
|
27
|
+
/** TTS audio cache */
|
|
28
|
+
export const TTS_CACHE_DIR = join(CRAWD_HOME, 'tts')
|
|
29
|
+
|
|
30
|
+
/** Backend source (in package) */
|
|
31
|
+
export const BACKEND_TEMPLATE_DIR = join(__dirname, '../backend')
|
|
32
|
+
|
|
33
|
+
/** User backend directory */
|
|
34
|
+
export const BACKEND_DIR = join(CRAWD_HOME, 'backend')
|
|
35
|
+
|
|
36
|
+
/** PID file paths */
|
|
37
|
+
export const PID_FILES: Record<string, string> = {
|
|
38
|
+
backend: join(PIDS_DIR, 'backend.pid'),
|
|
39
|
+
overlay: join(PIDS_DIR, 'overlay.pid'),
|
|
40
|
+
crawdbot: join(PIDS_DIR, 'crawdbot.pid'),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Log file paths */
|
|
44
|
+
export const LOG_FILES: Record<string, string> = {
|
|
45
|
+
backend: join(LOGS_DIR, 'backend.log'),
|
|
46
|
+
overlay: join(LOGS_DIR, 'overlay.log'),
|
|
47
|
+
crawdbot: join(LOGS_DIR, 'crawdbot.log'),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Default ports */
|
|
51
|
+
export const DEFAULT_PORTS = {
|
|
52
|
+
backend: 4000,
|
|
53
|
+
overlay: 3000,
|
|
54
|
+
gateway: 18789,
|
|
55
|
+
}
|