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
package/dist/cli.js ADDED
@@ -0,0 +1,975 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/auth.ts
7
+ import { createServer } from "http";
8
+ import open from "open";
9
+
10
+ // src/config/store.ts
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
+ import { dirname as dirname2 } from "path";
13
+
14
+ // src/utils/paths.ts
15
+ import { homedir } from "os";
16
+ import { join } from "path";
17
+ import { fileURLToPath } from "url";
18
+ import { dirname } from "path";
19
+ var __filename = fileURLToPath(import.meta.url);
20
+ var __dirname = dirname(__filename);
21
+ var CRAWD_HOME = join(homedir(), ".crawd");
22
+ var CONFIG_PATH = join(CRAWD_HOME, "config.json");
23
+ var ENV_PATH = join(CRAWD_HOME, ".env");
24
+ var PIDS_DIR = join(CRAWD_HOME, "pids");
25
+ var LOGS_DIR = join(CRAWD_HOME, "logs");
26
+ var OVERLAY_DIR = join(CRAWD_HOME, "overlay");
27
+ var TTS_CACHE_DIR = join(CRAWD_HOME, "tts");
28
+ var BACKEND_TEMPLATE_DIR = join(__dirname, "../backend");
29
+ var BACKEND_DIR = join(CRAWD_HOME, "backend");
30
+ var PID_FILES = {
31
+ backend: join(PIDS_DIR, "backend.pid"),
32
+ overlay: join(PIDS_DIR, "overlay.pid"),
33
+ crawdbot: join(PIDS_DIR, "crawdbot.pid")
34
+ };
35
+ var LOG_FILES = {
36
+ backend: join(LOGS_DIR, "backend.log"),
37
+ overlay: join(LOGS_DIR, "overlay.log"),
38
+ crawdbot: join(LOGS_DIR, "crawdbot.log")
39
+ };
40
+
41
+ // src/config/schema.ts
42
+ import { z } from "zod";
43
+ var ttsProviderEnum = z.enum(["openai", "elevenlabs", "tiktok"]);
44
+ var ConfigSchema = z.object({
45
+ /** Gateway configuration */
46
+ gateway: z.object({
47
+ url: z.string().default("ws://localhost:18789"),
48
+ /** Channel ID for the agent session */
49
+ channelId: z.string().default("agent:main:crawd:live")
50
+ }).default({}),
51
+ /** Server ports */
52
+ ports: z.object({
53
+ backend: z.number().default(4e3),
54
+ overlay: z.number().default(3e3)
55
+ }).default({}),
56
+ /** TTS configuration */
57
+ tts: z.object({
58
+ /** Provider for reading chat messages aloud */
59
+ chatProvider: ttsProviderEnum.default("tiktok"),
60
+ /** Voice ID for chat TTS (must match chatProvider) */
61
+ chatVoice: z.string().default("en_us_002"),
62
+ /** Provider for bot speech */
63
+ botProvider: ttsProviderEnum.default("elevenlabs"),
64
+ /** Voice ID for bot TTS (must match botProvider) */
65
+ botVoice: z.string().default("TX3LPaxmHKxFdv7VOQHJ")
66
+ }).default({}),
67
+ /** Chat platform configuration */
68
+ chat: z.object({
69
+ pumpfun: z.object({
70
+ enabled: z.boolean().default(false),
71
+ tokenMint: z.string().optional()
72
+ }).optional(),
73
+ youtube: z.object({
74
+ enabled: z.boolean().default(false),
75
+ videoId: z.string().optional()
76
+ }).default({})
77
+ }).default({}),
78
+ /** Autonomous vibing state machine */
79
+ vibe: z.object({
80
+ /** Enable autonomous vibing (agent acts on its own between chat messages) */
81
+ enabled: z.boolean().default(true),
82
+ /** Seconds between vibe pings while active */
83
+ interval: z.number().default(30),
84
+ /** Seconds of inactivity before going idle */
85
+ idleAfter: z.number().default(180),
86
+ /** Seconds of inactivity before going to sleep (must be > idleAfter) */
87
+ sleepAfter: z.number().default(360)
88
+ }).default({}),
89
+ /** Stream configuration */
90
+ stream: z.object({
91
+ /** RTMP stream key for pump.fun */
92
+ key: z.string().optional()
93
+ }).default({})
94
+ });
95
+ var DEFAULT_CONFIG = ConfigSchema.parse({});
96
+
97
+ // src/config/store.ts
98
+ function ensureHome() {
99
+ if (!existsSync(CRAWD_HOME)) {
100
+ mkdirSync(CRAWD_HOME, { recursive: true });
101
+ }
102
+ }
103
+ function loadConfig() {
104
+ ensureHome();
105
+ if (!existsSync(CONFIG_PATH)) {
106
+ return DEFAULT_CONFIG;
107
+ }
108
+ try {
109
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
110
+ const parsed = JSON.parse(raw);
111
+ return ConfigSchema.parse(parsed);
112
+ } catch (e) {
113
+ console.warn("Failed to parse config, using defaults:", e);
114
+ return DEFAULT_CONFIG;
115
+ }
116
+ }
117
+ function getConfigValue(path) {
118
+ const config = loadConfig();
119
+ return getByPath(config, path);
120
+ }
121
+ function setConfigValue(path, value) {
122
+ let raw = {};
123
+ if (existsSync(CONFIG_PATH)) {
124
+ try {
125
+ raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
126
+ } catch {
127
+ }
128
+ }
129
+ setByPath(raw, path, value);
130
+ const validated = ConfigSchema.parse(raw);
131
+ ensureHome();
132
+ const dir = dirname2(CONFIG_PATH);
133
+ if (!existsSync(dir)) {
134
+ mkdirSync(dir, { recursive: true });
135
+ }
136
+ writeFileSync(CONFIG_PATH, JSON.stringify(raw, null, 2));
137
+ return validated;
138
+ }
139
+ function loadEnv() {
140
+ if (!existsSync(ENV_PATH)) return {};
141
+ const content = readFileSync(ENV_PATH, "utf-8");
142
+ const env = {};
143
+ for (const line of content.split("\n")) {
144
+ const trimmed = line.trim();
145
+ if (!trimmed || trimmed.startsWith("#")) continue;
146
+ const eqIndex = trimmed.indexOf("=");
147
+ if (eqIndex === -1) continue;
148
+ const key = trimmed.slice(0, eqIndex).trim();
149
+ let value = trimmed.slice(eqIndex + 1).trim();
150
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
151
+ value = value.slice(1, -1);
152
+ }
153
+ env[key] = value;
154
+ }
155
+ return env;
156
+ }
157
+ function saveEnv(env) {
158
+ ensureHome();
159
+ const lines = Object.entries(env).map(([k, v]) => `${k}=${v}`);
160
+ writeFileSync(ENV_PATH, lines.join("\n") + "\n");
161
+ }
162
+ function loadApiKey() {
163
+ const env = loadEnv();
164
+ return env.CRAWD_API_KEY ?? null;
165
+ }
166
+ var ENV_TEMPLATE_KEYS = [
167
+ "CRAWD_API_KEY",
168
+ "OPENAI_API_KEY",
169
+ "ELEVENLABS_API_KEY",
170
+ "TIKTOK_SESSION_ID",
171
+ "OPENCLAW_GATEWAY_TOKEN"
172
+ ];
173
+ function saveApiKey(apiKey) {
174
+ const env = loadEnv();
175
+ for (const key of ENV_TEMPLATE_KEYS) {
176
+ if (!(key in env)) env[key] = "";
177
+ }
178
+ env.CRAWD_API_KEY = apiKey;
179
+ saveEnv(env);
180
+ }
181
+ function getByPath(obj, path) {
182
+ const parts = path.split(".");
183
+ let current = obj;
184
+ for (const part of parts) {
185
+ if (current === null || current === void 0) return void 0;
186
+ current = current[part];
187
+ }
188
+ return current;
189
+ }
190
+ function setByPath(obj, path, value) {
191
+ const parts = path.split(".");
192
+ let current = obj;
193
+ for (let i = 0; i < parts.length - 1; i++) {
194
+ const part = parts[i];
195
+ if (!(part in current) || typeof current[part] !== "object") {
196
+ current[part] = {};
197
+ }
198
+ current = current[part];
199
+ }
200
+ current[parts[parts.length - 1]] = value;
201
+ }
202
+
203
+ // src/utils/logger.ts
204
+ import chalk from "chalk";
205
+ var log = {
206
+ info: (msg) => console.log(chalk.blue("\u2139"), msg),
207
+ success: (msg) => console.log(chalk.green("\u2713"), msg),
208
+ warn: (msg) => console.log(chalk.yellow("\u26A0"), msg),
209
+ error: (msg) => console.log(chalk.red("\u2717"), msg),
210
+ dim: (msg) => console.log(chalk.dim(msg))
211
+ };
212
+ var fmt = {
213
+ url: (url) => chalk.cyan.underline(url),
214
+ path: (path) => chalk.yellow(path),
215
+ cmd: (cmd) => chalk.green(cmd),
216
+ bold: (s) => chalk.bold(s),
217
+ dim: (s) => chalk.dim(s),
218
+ success: (s) => chalk.green(s)
219
+ };
220
+ function printKv(label, value, indent = 2) {
221
+ const padding = " ".repeat(indent);
222
+ console.log(`${padding}${chalk.dim(label + ":")} ${value}`);
223
+ }
224
+ function printHeader(title) {
225
+ console.log();
226
+ console.log(chalk.bold(title));
227
+ }
228
+
229
+ // src/commands/auth.ts
230
+ var PLATFORM_URL = "https://platform.crawd.bot";
231
+ var CALLBACK_PORT = 9876;
232
+ async function fetchMe(apiKey) {
233
+ try {
234
+ const response = await fetch(`${PLATFORM_URL}/api/me`, {
235
+ headers: { "Authorization": `Bearer ${apiKey}` }
236
+ });
237
+ if (!response.ok) return null;
238
+ return await response.json();
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+ async function authCommand() {
244
+ const apiKey = loadApiKey();
245
+ if (apiKey) {
246
+ const me = await fetchMe(apiKey);
247
+ if (me) {
248
+ printHeader("Authenticated");
249
+ console.log();
250
+ printKv("Account", me.email);
251
+ if (me.displayName) printKv("Name", me.displayName);
252
+ printKv("Credentials", fmt.path(ENV_PATH));
253
+ console.log();
254
+ log.dim("To re-authenticate, run: crawd auth --force");
255
+ console.log();
256
+ return;
257
+ }
258
+ log.warn("Existing credential is invalid or expired");
259
+ console.log();
260
+ }
261
+ startAuthFlow();
262
+ }
263
+ async function authForceCommand() {
264
+ startAuthFlow();
265
+ }
266
+ function startAuthFlow() {
267
+ log.info("Starting authentication...");
268
+ const server = createServer((req, res) => {
269
+ const url = new URL(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
270
+ if (url.pathname === "/callback") {
271
+ const token = url.searchParams.get("token");
272
+ if (token) {
273
+ saveApiKey(token);
274
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
275
+ res.end(`
276
+ <!DOCTYPE html>
277
+ <html>
278
+ <head>
279
+ <meta charset="utf-8">
280
+ <title>crawd.bot - Authenticated</title>
281
+ <style>
282
+ body {
283
+ font-family: system-ui, -apple-system, sans-serif;
284
+ display: flex;
285
+ justify-content: center;
286
+ align-items: center;
287
+ height: 100vh;
288
+ margin: 0;
289
+ background: #000;
290
+ color: #fff;
291
+ }
292
+ .container {
293
+ text-align: center;
294
+ padding: 2rem;
295
+ }
296
+ h1 { color: #FBA875; }
297
+ p { color: #888; }
298
+ </style>
299
+ </head>
300
+ <body>
301
+ <div class="container">
302
+ <h1>\u2713 Authenticated!</h1>
303
+ <p>You can close this window and return to the terminal.</p>
304
+ </div>
305
+ </body>
306
+ </html>
307
+ `);
308
+ log.success("Authentication successful!");
309
+ log.dim(`API key saved to ${ENV_PATH}`);
310
+ setTimeout(() => {
311
+ server.close();
312
+ process.exit(0);
313
+ }, 1e3);
314
+ } else {
315
+ res.writeHead(400, { "Content-Type": "text/plain" });
316
+ res.end("Missing token");
317
+ log.error("Authentication failed - no token received");
318
+ }
319
+ } else {
320
+ res.writeHead(404);
321
+ res.end("Not found");
322
+ }
323
+ });
324
+ server.listen(CALLBACK_PORT, () => {
325
+ const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`;
326
+ const authUrl = `${PLATFORM_URL}/auth/cli?callback=${encodeURIComponent(callbackUrl)}`;
327
+ log.info("Opening browser for authentication...");
328
+ console.log();
329
+ log.dim("If browser does not open, visit:");
330
+ console.log(` ${fmt.url(authUrl)}`);
331
+ console.log();
332
+ open(authUrl).catch(() => {
333
+ log.warn("Could not open browser automatically");
334
+ });
335
+ });
336
+ server.on("error", (err) => {
337
+ log.error(`Failed to start callback server: ${err.message}`);
338
+ log.dim("Make sure port 9876 is available");
339
+ process.exit(1);
340
+ });
341
+ setTimeout(() => {
342
+ log.error("Authentication timed out");
343
+ server.close();
344
+ process.exit(1);
345
+ }, 5 * 60 * 1e3);
346
+ }
347
+
348
+ // src/commands/status.ts
349
+ var PLATFORM_URL2 = "https://platform.crawd.bot";
350
+ async function statusCommand() {
351
+ const apiKey = loadApiKey();
352
+ console.log();
353
+ console.log(fmt.bold("crawd.bot CLI"));
354
+ console.log();
355
+ if (!apiKey) {
356
+ log.warn("Not authenticated");
357
+ log.dim("Run: crawd auth");
358
+ console.log();
359
+ return;
360
+ }
361
+ log.info("Fetching stream status...");
362
+ try {
363
+ const response = await fetch(`${PLATFORM_URL2}/api/stream`, {
364
+ headers: {
365
+ "Authorization": `Bearer ${apiKey}`
366
+ }
367
+ });
368
+ if (response.status === 401) {
369
+ log.error("Authentication expired");
370
+ log.dim("Run: crawd auth");
371
+ return;
372
+ }
373
+ const data = await response.json();
374
+ if (data.error) {
375
+ log.error(data.error);
376
+ return;
377
+ }
378
+ const { stream } = data;
379
+ console.log();
380
+ console.log(fmt.bold("Stream Status"));
381
+ console.log();
382
+ if (stream.isLive) {
383
+ console.log(` Status: ${fmt.success("\u25CF LIVE")}`);
384
+ } else {
385
+ console.log(` Status: ${fmt.dim("\u25CB Offline")}`);
386
+ }
387
+ console.log(` Name: ${stream.name}`);
388
+ console.log(` Viewers: ${stream.viewerCount}`);
389
+ console.log();
390
+ console.log(fmt.bold("OBS Settings"));
391
+ console.log();
392
+ console.log(` Server: ${fmt.dim(stream.rtmpUrl)}`);
393
+ console.log(` Stream Key: ${fmt.dim(stream.streamKey.slice(0, 20) + "...")}`);
394
+ if (stream.playbackId) {
395
+ console.log();
396
+ console.log(fmt.bold("Preview"));
397
+ console.log();
398
+ console.log(` ${fmt.url(`${PLATFORM_URL2}/preview/${stream.playbackId}`)}`);
399
+ }
400
+ console.log();
401
+ } catch (err) {
402
+ log.error(`Failed to fetch status: ${err instanceof Error ? err.message : err}`);
403
+ }
404
+ }
405
+
406
+ // src/commands/skill.ts
407
+ var VERSION = "0.5.0";
408
+ var SKILL_TEXT = `# crawd.bot - AI Agent Livestreaming
409
+
410
+ Backend daemon for AI agent livestreams with:
411
+ - TTS audio generation (ElevenLabs, OpenAI, TikTok)
412
+ - Chat-to-speech pipeline with per-message-type provider config
413
+ - WebSocket API for real-time overlay events
414
+ - Gateway integration for AI agent coordination
415
+
416
+ ## Installation
417
+
418
+ \`\`\`bash
419
+ npm install -g @crawd/cli
420
+ \`\`\`
421
+
422
+ ## Setup
423
+
424
+ 1. Start the backend daemon:
425
+ \`\`\`bash
426
+ crawd start
427
+ \`\`\`
428
+
429
+ 2. Start streaming in OBS (RTMP endpoint is always accessible while the daemon is running).
430
+
431
+ ## Commands
432
+
433
+ | Command | Description |
434
+ |---------|-------------|
435
+ | \`crawd start\` | Start the backend daemon |
436
+ | \`crawd stop\` | Stop the backend daemon |
437
+ | \`crawd update\` | Update CLI and restart daemon |
438
+ | \`crawd talk <message>\` | Send a message to the overlay with TTS |
439
+ | \`crawd stream-key\` | Show RTMP URL and stream key for OBS |
440
+ | \`crawd status\` | Show daemon status |
441
+ | \`crawd logs\` | Tail backend daemon logs |
442
+ | \`crawd auth\` | Login to crawd.bot |
443
+ | \`crawd config show\` | Show all configuration |
444
+ | \`crawd config get <path>\` | Get a config value |
445
+ | \`crawd config set <path> <value>\` | Set a config value |
446
+ | \`crawd skill show\` | Show this skill reference |
447
+ | \`crawd skill install\` | Install the livestream skill |
448
+ | \`crawd version\` | Show CLI version |
449
+ | \`crawd help\` | Show help |
450
+
451
+ ### Talk
452
+
453
+ Send a message to connected overlays with TTS:
454
+
455
+ \`\`\`bash
456
+ crawd talk "Hello everyone!"
457
+ \`\`\`
458
+
459
+ ## Configuration
460
+
461
+ Config (\`~/.crawd/config.json\`):
462
+
463
+ \`\`\`bash
464
+ # TTS providers and voices (per role)
465
+ crawd config set tts.chatProvider tiktok
466
+ crawd config set tts.chatVoice en_us_002
467
+ crawd config set tts.botProvider elevenlabs
468
+ crawd config set tts.botVoice TX3LPaxmHKxFdv7VOQHJ
469
+
470
+ # Gateway
471
+ crawd config set gateway.url ws://localhost:18789
472
+
473
+ # Backend port
474
+ crawd config set ports.backend 4000
475
+ \`\`\`
476
+
477
+ Available providers: \`tiktok\`, \`openai\`, \`elevenlabs\`. Each role (chat/bot) has its own provider and voice.
478
+
479
+ Voice ID references:
480
+ - OpenAI TTS voices: https://platform.openai.com/docs/guides/text-to-speech
481
+ - ElevenLabs voice library: https://elevenlabs.io/voice-library
482
+ - TikTok voices: use voice codes like \`en_us_002\`, \`en_us_006\`, \`en_us_010\`
483
+
484
+ Secrets (\`~/.crawd/.env\`):
485
+
486
+ \`\`\`env
487
+ OPENCLAW_GATEWAY_TOKEN=your-token
488
+ OPENAI_API_KEY=sk-...
489
+ ELEVENLABS_API_KEY=your-key
490
+ TIKTOK_SESSION_ID=your-session-id
491
+ \`\`\`
492
+
493
+ ### Vibing (Autonomous Behavior)
494
+
495
+ The agent uses a state machine to stay active on stream:
496
+
497
+ \`\`\`
498
+ sleep \u2192 [chat message] \u2192 active \u2192 [no activity] \u2192 idle \u2192 [no activity] \u2192 sleep
499
+ \`\`\`
500
+
501
+ While **active** or **idle**, the agent receives periodic \`[VIBE]\` pings that prompt it to do something: browse the internet, tweet, check pump.fun, play music, or talk to chat. Pings are skipped when the agent is already busy.
502
+
503
+ A chat message does NOT automatically wake the agent. The agent only wakes when it actually produces a reply (talks or performs an action). If the agent decides not to respond, the bot stays asleep.
504
+
505
+ \`\`\`bash
506
+ # Vibe ping interval in seconds (default: 30)
507
+ crawd config set vibe.interval 30
508
+
509
+ # Seconds of inactivity before going idle (default: 180)
510
+ crawd config set vibe.idleAfter 180
511
+
512
+ # Seconds of inactivity before going to sleep (default: 360)
513
+ crawd config set vibe.sleepAfter 360
514
+
515
+ # Disable vibing entirely
516
+ crawd config set vibe.enabled false
517
+ \`\`\`
518
+
519
+ ## Speaking
520
+
521
+ To speak on stream, use the nodes tool:
522
+
523
+ \`\`\`
524
+ nodes action=invoke invokeCommand=talk invokeParamsJson='{"text":"your message here"}'
525
+ \`\`\`
526
+
527
+ This generates TTS audio and displays a speech bubble on the overlay. The tool blocks until the overlay finishes playing the audio \u2014 do NOT send another talk while one is in progress.
528
+
529
+ You MUST use this tool to speak. Do not just return text \u2014 it will not be voiced or shown. Every message you want spoken must go through the talk tool.
530
+
531
+ ## Streaming Behavior
532
+
533
+ When live on stream, follow these rules:
534
+
535
+ - Keep messages SHORT (1-2 sentences max). Long messages look bad on stream.
536
+ - NEVER describe the obvious. Viewers can SEE. Share quick THOUGHTS only.
537
+ - Scroll to elements before clicking.
538
+ - Reject cookie banners immediately.
539
+ - Be FAST. No hesitation.
540
+
541
+ On \`[VIBE]\` prompts, do ONE thing: browse the internet, tweet, check pump.fun, play music, or ask the chat a question. Use the talk tool to tell chat what you're doing or thinking.
542
+
543
+ ## Forbidden Tools
544
+
545
+ Do NOT use \`web_fetch\` or \`web_search\` directly in your main session. Viewers cannot see these actions on stream \u2014 they just see the bot sitting there doing nothing. Always use browser tools (via subagents) so viewers can watch you browse.
546
+
547
+ ## Browser & Token Optimization
548
+
549
+ Browser snapshots (DOM/ARIA trees) are the #1 source of context bloat. A single Twitter page snapshot can be thousands of tokens, and they accumulate with every turn.
550
+
551
+ **Always use subagents for browser tasks.** Use \`sessions_spawn\` to delegate browsing to a subagent instead of using browser tools directly in your chat session. This keeps your main session lean and responsive.
552
+
553
+ Why this matters:
554
+ - Subagents get their own isolated context \u2014 snapshots stay there and get discarded after the task
555
+ - Your chat session stays small, making every message cheaper and faster
556
+ - If a vision model is configured for subagents, it will be used automatically for browser tasks
557
+
558
+ How to do it:
559
+ - Give the subagent a specific task: "check twitter trending", "find a song on youtube", "look at pump.fun top movers"
560
+ - The subagent browses, summarizes, and returns a compact text result
561
+ - React to the result in your own voice \u2014 do not just repeat what the subagent said
562
+
563
+ Do NOT use browser tools directly in your main/chat session.`;
564
+ function skillInfoCommand() {
565
+ console.log();
566
+ console.log(fmt.bold("crawd skill"));
567
+ console.log();
568
+ log.info("crawd skill show \u2014 Print the full skill reference (for AI agents)");
569
+ log.info("crawd skill install \u2014 Install the livestream skill to your account");
570
+ console.log();
571
+ log.dim(`v${VERSION} \u2014 Run \`crawd skill show\` to see the full reference.`);
572
+ console.log();
573
+ }
574
+ function skillShowCommand() {
575
+ console.log(SKILL_TEXT);
576
+ }
577
+ async function skillInstallCommand() {
578
+ if (!loadApiKey()) {
579
+ log.error("Not authenticated. Run: crawd auth");
580
+ process.exit(1);
581
+ }
582
+ log.success("Livestream skill installed!");
583
+ console.log();
584
+ log.info("Start the daemon and go live in OBS:");
585
+ log.dim(" crawd start - Start the backend daemon");
586
+ log.dim(" crawd status - Check daemon status");
587
+ }
588
+
589
+ // src/commands/start.ts
590
+ import { spawn } from "child_process";
591
+ import { existsSync as existsSync3, openSync, mkdirSync as mkdirSync3 } from "fs";
592
+ import { join as join2, dirname as dirname3 } from "path";
593
+ import { fileURLToPath as fileURLToPath2 } from "url";
594
+
595
+ // src/daemon/pid.ts
596
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
597
+ function writePid(name, pid) {
598
+ if (!existsSync2(PIDS_DIR)) {
599
+ mkdirSync2(PIDS_DIR, { recursive: true });
600
+ }
601
+ writeFileSync2(PID_FILES[name], String(pid));
602
+ }
603
+ function readPid(name) {
604
+ const path = PID_FILES[name];
605
+ if (!existsSync2(path)) {
606
+ return null;
607
+ }
608
+ try {
609
+ const content = readFileSync2(path, "utf-8").trim();
610
+ const pid = parseInt(content, 10);
611
+ return isNaN(pid) ? null : pid;
612
+ } catch {
613
+ return null;
614
+ }
615
+ }
616
+ function removePid(name) {
617
+ const path = PID_FILES[name];
618
+ if (existsSync2(path)) {
619
+ unlinkSync(path);
620
+ }
621
+ }
622
+ function isProcessRunning(pid) {
623
+ try {
624
+ process.kill(pid, 0);
625
+ return true;
626
+ } catch {
627
+ return false;
628
+ }
629
+ }
630
+ function isRunning(name) {
631
+ const pid = readPid(name);
632
+ if (pid === null) return false;
633
+ return isProcessRunning(pid);
634
+ }
635
+ function killProcess(name) {
636
+ const pid = readPid(name);
637
+ if (pid === null) return false;
638
+ if (!isProcessRunning(pid)) {
639
+ removePid(name);
640
+ return false;
641
+ }
642
+ try {
643
+ process.kill(pid, "SIGTERM");
644
+ setTimeout(() => {
645
+ if (isProcessRunning(pid)) {
646
+ try {
647
+ process.kill(pid, "SIGKILL");
648
+ } catch {
649
+ }
650
+ }
651
+ }, 2e3);
652
+ removePid(name);
653
+ return true;
654
+ } catch {
655
+ removePid(name);
656
+ return false;
657
+ }
658
+ }
659
+
660
+ // src/commands/start.ts
661
+ var __filename2 = fileURLToPath2(import.meta.url);
662
+ var __dirname2 = dirname3(__filename2);
663
+ function getBackendEntry() {
664
+ const fromSrc = join2(__dirname2, "..", "backend", "index.ts");
665
+ if (existsSync3(fromSrc)) return fromSrc;
666
+ const fromDist = join2(__dirname2, "..", "src", "backend", "index.ts");
667
+ if (existsSync3(fromDist)) return fromDist;
668
+ throw new Error("Backend entry point not found");
669
+ }
670
+ function buildEnv(config) {
671
+ const secrets = loadEnv();
672
+ const env = { ...process.env };
673
+ for (const [key, value] of Object.entries(secrets)) {
674
+ env[key] = value;
675
+ }
676
+ env.PORT = String(config.ports.backend);
677
+ env.BACKEND_URL = `http://localhost:${config.ports.backend}`;
678
+ env.OPENCLAW_GATEWAY_URL = config.gateway.url;
679
+ env.CRAWD_CHANNEL_ID = config.gateway.channelId;
680
+ env.TTS_CHAT_PROVIDER = config.tts.chatProvider;
681
+ env.TTS_CHAT_VOICE = config.tts.chatVoice;
682
+ env.TTS_BOT_PROVIDER = config.tts.botProvider;
683
+ env.TTS_BOT_VOICE = config.tts.botVoice;
684
+ if (config.chat.pumpfun) {
685
+ env.PUMPFUN_ENABLED = String(config.chat.pumpfun.enabled);
686
+ if (config.chat.pumpfun.tokenMint) {
687
+ env.NEXT_PUBLIC_TOKEN_MINT = config.chat.pumpfun.tokenMint;
688
+ }
689
+ }
690
+ env.VIBE_ENABLED = String(config.vibe.enabled);
691
+ env.VIBE_INTERVAL_MS = String(config.vibe.interval * 1e3);
692
+ env.IDLE_AFTER_MS = String(config.vibe.idleAfter * 1e3);
693
+ env.SLEEP_AFTER_IDLE_MS = String((config.vibe.sleepAfter - config.vibe.idleAfter) * 1e3);
694
+ env.YOUTUBE_ENABLED = String(config.chat.youtube.enabled);
695
+ if (config.chat.youtube.videoId) {
696
+ env.YOUTUBE_VIDEO_ID = config.chat.youtube.videoId;
697
+ }
698
+ return env;
699
+ }
700
+ async function startCommand() {
701
+ if (isRunning("crawdbot")) {
702
+ const config2 = loadConfig();
703
+ log.warn("Backend is already running");
704
+ printKv("Backend", fmt.url(`http://localhost:${config2.ports.backend}`));
705
+ log.dim("Use `crawd stop` to stop it first");
706
+ return;
707
+ }
708
+ const backendEntry = getBackendEntry();
709
+ for (const dir of [LOGS_DIR, PIDS_DIR]) {
710
+ if (!existsSync3(dir)) {
711
+ mkdirSync3(dir, { recursive: true });
712
+ }
713
+ }
714
+ const config = loadConfig();
715
+ const env = buildEnv(config);
716
+ log.info("Starting CrawdBot backend...");
717
+ const logFd = openSync(LOG_FILES.crawdbot, "a");
718
+ const child = spawn("bun", ["run", backendEntry], {
719
+ cwd: join2(dirname3(backendEntry), "..", ".."),
720
+ env,
721
+ detached: true,
722
+ stdio: ["ignore", logFd, logFd]
723
+ });
724
+ child.unref();
725
+ if (child.pid) {
726
+ writePid("crawdbot", child.pid);
727
+ }
728
+ await new Promise((r) => setTimeout(r, 1500));
729
+ if (isRunning("crawdbot")) {
730
+ printHeader("CrawdBot started");
731
+ console.log();
732
+ log.success(`Backend running (PID ${child.pid})`);
733
+ printKv("Backend", fmt.url(`http://localhost:${config.ports.backend}`));
734
+ console.log();
735
+ log.dim("View logs: crawd logs");
736
+ log.dim("Stop: crawd stop");
737
+ } else {
738
+ log.error("Backend failed to start");
739
+ log.dim(`Check logs: tail ${LOG_FILES.crawdbot}`);
740
+ process.exit(1);
741
+ }
742
+ }
743
+
744
+ // src/commands/stop.ts
745
+ function stopCommand() {
746
+ if (!isRunning("crawdbot")) {
747
+ log.dim("CrawdBot backend is not running");
748
+ return;
749
+ }
750
+ const killed = killProcess("crawdbot");
751
+ if (killed) {
752
+ log.success("CrawdBot backend stopped");
753
+ } else {
754
+ log.error("Failed to stop CrawdBot backend");
755
+ }
756
+ }
757
+
758
+ // src/commands/update.ts
759
+ import { execSync } from "child_process";
760
+ import { realpathSync } from "fs";
761
+ var INSTALL_CMD = {
762
+ npm: "npm install -g @crawd/cli@latest",
763
+ pnpm: "pnpm add -g @crawd/cli@latest",
764
+ yarn: "yarn global add @crawd/cli@latest",
765
+ bun: "bun install -g @crawd/cli@latest"
766
+ };
767
+ function detectPackageManager() {
768
+ try {
769
+ const bin = execSync("which crawd", { encoding: "utf-8" }).trim();
770
+ const resolved = realpathSync(bin);
771
+ if (resolved.includes("/pnpm")) return "pnpm";
772
+ if (resolved.includes("/.bun/")) return "bun";
773
+ if (resolved.includes("/.yarn/") || resolved.includes("/yarn/")) return "yarn";
774
+ } catch {
775
+ }
776
+ return "npm";
777
+ }
778
+ function waitForExit(pid, timeoutMs = 5e3) {
779
+ const start = Date.now();
780
+ while (Date.now() - start < timeoutMs) {
781
+ if (!isProcessRunning(pid)) return true;
782
+ execSync("sleep 0.1");
783
+ }
784
+ return false;
785
+ }
786
+ async function updateCommand() {
787
+ const daemonWasRunning = isRunning("crawdbot");
788
+ const oldPid = readPid("crawdbot");
789
+ if (daemonWasRunning && oldPid) {
790
+ log.info("Stopping backend daemon...");
791
+ killProcess("crawdbot");
792
+ if (!waitForExit(oldPid)) {
793
+ log.error("Backend daemon did not stop in time");
794
+ process.exit(1);
795
+ }
796
+ log.success("Backend daemon stopped");
797
+ }
798
+ const pm = detectPackageManager();
799
+ const cmd = INSTALL_CMD[pm];
800
+ log.info(`Updating @crawd/cli via ${pm}...`);
801
+ try {
802
+ const output = execSync(`${cmd} 2>&1`, {
803
+ encoding: "utf-8",
804
+ timeout: 6e4
805
+ });
806
+ const versionMatch = output.match(/@crawd\/cli@([\d.]+)/);
807
+ if (versionMatch) {
808
+ log.success(`Updated to v${versionMatch[1]}`);
809
+ } else {
810
+ log.success("CLI updated");
811
+ }
812
+ } catch (e) {
813
+ const msg = e instanceof Error ? e.message : String(e);
814
+ log.error(`Failed to update: ${msg}`);
815
+ if (daemonWasRunning) {
816
+ log.info("Restarting backend daemon with current version...");
817
+ await startCommand();
818
+ }
819
+ process.exit(1);
820
+ }
821
+ if (daemonWasRunning) {
822
+ log.info("Restarting backend daemon...");
823
+ await startCommand();
824
+ } else {
825
+ log.dim("Backend daemon was not running, skipping restart");
826
+ }
827
+ }
828
+
829
+ // src/commands/talk.ts
830
+ async function talkCommand(message) {
831
+ const config = loadConfig();
832
+ const port = config.ports.backend;
833
+ const url = `http://localhost:${port}/crawd/talk`;
834
+ const body = { message };
835
+ try {
836
+ const res = await fetch(url, {
837
+ method: "POST",
838
+ headers: { "Content-Type": "application/json" },
839
+ body: JSON.stringify(body)
840
+ });
841
+ if (!res.ok) {
842
+ const data = await res.json().catch(() => ({}));
843
+ log.error(data.error ?? `Request failed (${res.status})`);
844
+ process.exit(1);
845
+ }
846
+ log.success(`Sent: "${message}"`);
847
+ } catch {
848
+ log.error("Could not reach the backend daemon. Is it running?");
849
+ log.dim("Start with: crawd start");
850
+ process.exit(1);
851
+ }
852
+ }
853
+
854
+ // src/commands/logs.ts
855
+ import { spawn as spawn2 } from "child_process";
856
+ import { existsSync as existsSync4 } from "fs";
857
+ function logsCommand(options) {
858
+ const lines = options.lines ?? 50;
859
+ const follow = options.follow ?? true;
860
+ const file = LOG_FILES.backend;
861
+ if (!existsSync4(file)) {
862
+ log.warn("No logs found. Is the daemon running?");
863
+ log.dim("Start with: crawd start");
864
+ return;
865
+ }
866
+ log.info(`Tailing ${fmt.path(file)}`);
867
+ console.log();
868
+ const args = follow ? ["-f", "-n", String(lines), file] : ["-n", String(lines), file];
869
+ const tail = spawn2("tail", args, { stdio: "inherit" });
870
+ tail.on("error", (err) => {
871
+ log.error(`Failed to tail logs: ${err.message}`);
872
+ });
873
+ process.on("SIGINT", () => {
874
+ tail.kill();
875
+ process.exit(0);
876
+ });
877
+ }
878
+
879
+ // src/commands/config.ts
880
+ function configShowCommand() {
881
+ const config = loadConfig();
882
+ console.log(JSON.stringify(config, null, 2));
883
+ }
884
+ function configGetCommand(path) {
885
+ const value = getConfigValue(path);
886
+ if (value === void 0) {
887
+ log.error(`Config key not found: ${path}`);
888
+ process.exit(1);
889
+ }
890
+ if (typeof value === "object") {
891
+ console.log(JSON.stringify(value, null, 2));
892
+ } else {
893
+ console.log(value);
894
+ }
895
+ }
896
+ function configSetCommand(path, value) {
897
+ let parsed = value;
898
+ try {
899
+ parsed = JSON.parse(value);
900
+ } catch {
901
+ }
902
+ if (value === "true") parsed = true;
903
+ if (value === "false") parsed = false;
904
+ try {
905
+ setConfigValue(path, parsed);
906
+ log.success(`Set ${fmt.bold(path)} = ${JSON.stringify(parsed)}`);
907
+ } catch (err) {
908
+ log.error(`Invalid config: ${err}`);
909
+ process.exit(1);
910
+ }
911
+ }
912
+
913
+ // src/commands/stream-key.ts
914
+ var PLATFORM_URL3 = "https://platform.crawd.bot";
915
+ async function streamKeyCommand() {
916
+ const apiKey = loadApiKey();
917
+ if (!apiKey) {
918
+ log.error("Not authenticated. Run: crawd auth");
919
+ process.exit(1);
920
+ }
921
+ try {
922
+ const response = await fetch(`${PLATFORM_URL3}/api/stream`, {
923
+ headers: { "Authorization": `Bearer ${apiKey}` }
924
+ });
925
+ if (response.status === 401) {
926
+ log.error("Authentication expired. Run: crawd auth");
927
+ process.exit(1);
928
+ }
929
+ const data = await response.json();
930
+ if (data.error) {
931
+ log.error(data.error);
932
+ process.exit(1);
933
+ }
934
+ if (!data.stream) {
935
+ log.error("No stream data returned");
936
+ process.exit(1);
937
+ }
938
+ printHeader("OBS Settings");
939
+ console.log();
940
+ printKv("Server", data.stream.rtmpUrl);
941
+ printKv("Stream Key", data.stream.streamKey);
942
+ console.log();
943
+ } catch (err) {
944
+ log.error(`Failed to fetch stream key: ${err instanceof Error ? err.message : err}`);
945
+ process.exit(1);
946
+ }
947
+ }
948
+
949
+ // src/cli.ts
950
+ var VERSION2 = "0.4.1";
951
+ var program = new Command();
952
+ program.name("crawd").description("CLI for crawd.bot - AI agent livestreaming platform").version(VERSION2, "-v, --version");
953
+ program.command("auth").description("Authenticate with crawd.bot").option("-f, --force", "Re-authenticate even if already logged in").action((opts) => opts.force ? authForceCommand() : authCommand());
954
+ var skillCmd = program.command("skill").description("Skill reference and management").action(skillInfoCommand);
955
+ skillCmd.command("show").description("Print the full skill reference").action(skillShowCommand);
956
+ skillCmd.command("install").description("Install the livestream skill").action(skillInstallCommand);
957
+ program.command("start").description("Start the backend daemon").action(startCommand);
958
+ program.command("stop").description("Stop the backend daemon").action(stopCommand);
959
+ program.command("update").description("Update CLI to latest version and restart daemon").action(updateCommand);
960
+ program.command("status").description("Show daemon status").action(statusCommand);
961
+ program.command("stream-key").description("Show RTMP URL and stream key for OBS").action(streamKeyCommand);
962
+ program.command("talk <message>").description("Send a message to the overlay with TTS").action((message) => talkCommand(message));
963
+ program.command("logs").description("Tail backend daemon logs").option("-n, --lines <n>", "Number of lines", "50").option("--no-follow", "Print logs and exit").action((opts) => {
964
+ logsCommand({ lines: parseInt(opts.lines, 10), follow: opts.follow });
965
+ });
966
+ var configCmd = program.command("config").description("Manage configuration");
967
+ configCmd.command("show").description("Show all configuration").action(configShowCommand);
968
+ configCmd.command("get <path>").description("Get a config value by dot-path").action(configGetCommand);
969
+ configCmd.command("set <path> <value>").description("Set a config value by dot-path").action(configSetCommand);
970
+ program.command("version").description("Show CLI version").action(() => console.log(VERSION2));
971
+ program.command("help").description("Show help").action(() => program.help());
972
+ program.action(() => {
973
+ program.help();
974
+ });
975
+ program.parse();