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/src/cli.ts ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander'
4
+ import { authCommand, authForceCommand } from './commands/auth.js'
5
+ import { statusCommand } from './commands/status.js'
6
+ import { skillInfoCommand, skillShowCommand, skillInstallCommand } from './commands/skill.js'
7
+ import { startCommand } from './commands/start.js'
8
+ import { stopCommand } from './commands/stop.js'
9
+ import { updateCommand } from './commands/update.js'
10
+ import { talkCommand } from './commands/talk.js'
11
+ import { logsCommand } from './commands/logs.js'
12
+ import { configShowCommand, configGetCommand, configSetCommand } from './commands/config.js'
13
+ import { streamKeyCommand } from './commands/stream-key.js'
14
+
15
+ const VERSION = '0.4.1'
16
+
17
+ const program = new Command()
18
+
19
+ program
20
+ .name('crawd')
21
+ .description('CLI for crawd.bot - AI agent livestreaming platform')
22
+ .version(VERSION, '-v, --version')
23
+
24
+ // crawd auth
25
+ program
26
+ .command('auth')
27
+ .description('Authenticate with crawd.bot')
28
+ .option('-f, --force', 'Re-authenticate even if already logged in')
29
+ .action((opts) => opts.force ? authForceCommand() : authCommand())
30
+
31
+ // crawd skill
32
+ const skillCmd = program
33
+ .command('skill')
34
+ .description('Skill reference and management')
35
+ .action(skillInfoCommand)
36
+
37
+ skillCmd
38
+ .command('show')
39
+ .description('Print the full skill reference')
40
+ .action(skillShowCommand)
41
+
42
+ skillCmd
43
+ .command('install')
44
+ .description('Install the livestream skill')
45
+ .action(skillInstallCommand)
46
+
47
+ // crawd start
48
+ program
49
+ .command('start')
50
+ .description('Start the backend daemon')
51
+ .action(startCommand)
52
+
53
+ // crawd stop
54
+ program
55
+ .command('stop')
56
+ .description('Stop the backend daemon')
57
+ .action(stopCommand)
58
+
59
+ // crawd update
60
+ program
61
+ .command('update')
62
+ .description('Update CLI to latest version and restart daemon')
63
+ .action(updateCommand)
64
+
65
+ // crawd status
66
+ program
67
+ .command('status')
68
+ .description('Show daemon status')
69
+ .action(statusCommand)
70
+
71
+ // crawd stream-key
72
+ program
73
+ .command('stream-key')
74
+ .description('Show RTMP URL and stream key for OBS')
75
+ .action(streamKeyCommand)
76
+
77
+ // crawd talk
78
+ program
79
+ .command('talk <message>')
80
+ .description('Send a message to the overlay with TTS')
81
+ .action((message: string) => talkCommand(message))
82
+
83
+ // crawd logs
84
+ program
85
+ .command('logs')
86
+ .description('Tail backend daemon logs')
87
+ .option('-n, --lines <n>', 'Number of lines', '50')
88
+ .option('--no-follow', 'Print logs and exit')
89
+ .action((opts: { lines: string; follow: boolean }) => {
90
+ logsCommand({ lines: parseInt(opts.lines, 10), follow: opts.follow })
91
+ })
92
+
93
+ // crawd config
94
+ const configCmd = program
95
+ .command('config')
96
+ .description('Manage configuration')
97
+
98
+ configCmd
99
+ .command('show')
100
+ .description('Show all configuration')
101
+ .action(configShowCommand)
102
+
103
+ configCmd
104
+ .command('get <path>')
105
+ .description('Get a config value by dot-path')
106
+ .action(configGetCommand)
107
+
108
+ configCmd
109
+ .command('set <path> <value>')
110
+ .description('Set a config value by dot-path')
111
+ .action(configSetCommand)
112
+
113
+ // crawd version (explicit subcommand)
114
+ program
115
+ .command('version')
116
+ .description('Show CLI version')
117
+ .action(() => console.log(VERSION))
118
+
119
+ // crawd help (explicit subcommand)
120
+ program
121
+ .command('help')
122
+ .description('Show help')
123
+ .action(() => program.help())
124
+
125
+ // Default: show help when no command given
126
+ program.action(() => {
127
+ program.help()
128
+ })
129
+
130
+ program.parse()
package/src/client.ts ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Crawd overlay client SDK.
3
+ *
4
+ * Connects to the crawd backend daemon over WebSocket and provides
5
+ * typed event callbacks for building custom overlays.
6
+ *
7
+ * ```ts
8
+ * import { createCrawdClient } from '@crawd/cli'
9
+ *
10
+ * const client = createCrawdClient('http://localhost:4000')
11
+ *
12
+ * client.on('reply-turn', (turn) => { ... })
13
+ * client.on('talk', (msg) => { ... })
14
+ * client.on('tts', (data) => { ... })
15
+ * client.on('status', (data) => { ... })
16
+ * client.on('connect', () => { ... })
17
+ * client.on('disconnect', () => { ... })
18
+ *
19
+ * // Cleanup
20
+ * client.destroy()
21
+ * ```
22
+ */
23
+
24
+ import { io, type Socket } from 'socket.io-client'
25
+ import type {
26
+ ReplyTurnEvent,
27
+ TalkEvent,
28
+ TalkDoneEvent,
29
+ MockChatEvent,
30
+ StatusEvent,
31
+ McapEvent,
32
+ } from './types'
33
+
34
+ export type CrawdClientEvents = {
35
+ 'reply-turn': (data: ReplyTurnEvent) => void
36
+ 'talk': (data: TalkEvent) => void
37
+ 'status': (data: StatusEvent) => void
38
+ 'mcap': (data: McapEvent) => void
39
+ 'connect': () => void
40
+ 'disconnect': () => void
41
+ }
42
+
43
+ export type CrawdEmitEvents = {
44
+ 'talk:done': TalkDoneEvent
45
+ 'mock-chat': MockChatEvent
46
+ }
47
+
48
+ export type CrawdClient = {
49
+ /** Listen for a backend event */
50
+ on: <K extends keyof CrawdClientEvents>(event: K, handler: CrawdClientEvents[K]) => void
51
+ /** Remove an event listener */
52
+ off: <K extends keyof CrawdClientEvents>(event: K, handler: CrawdClientEvents[K]) => void
53
+ /** Send an event to the backend */
54
+ emit: <K extends keyof CrawdEmitEvents>(event: K, data: CrawdEmitEvents[K]) => void
55
+ /** Disconnect and clean up */
56
+ destroy: () => void
57
+ /** Underlying socket.io instance (escape hatch) */
58
+ socket: Socket
59
+ }
60
+
61
+ export function createCrawdClient(url: string): CrawdClient {
62
+ const socket = io(url, { transports: ['websocket'] })
63
+
64
+ const eventMap: Record<string, string> = {
65
+ 'reply-turn': 'crawd:reply-turn',
66
+ 'talk': 'crawd:talk',
67
+ 'talk:done': 'crawd:talk:done',
68
+ 'mock-chat': 'crawd:mock-chat',
69
+ 'status': 'crawd:status',
70
+ 'mcap': 'crawd:mcap',
71
+ }
72
+
73
+ function on<K extends keyof CrawdClientEvents>(event: K, handler: CrawdClientEvents[K]) {
74
+ const socketEvent = eventMap[event as string]
75
+ if (socketEvent) {
76
+ socket.on(socketEvent, handler as (...args: unknown[]) => void)
77
+ } else {
78
+ socket.on(event as string, handler as (...args: unknown[]) => void)
79
+ }
80
+ }
81
+
82
+ function off<K extends keyof CrawdClientEvents>(event: K, handler: CrawdClientEvents[K]) {
83
+ const socketEvent = eventMap[event as string]
84
+ if (socketEvent) {
85
+ socket.off(socketEvent, handler as (...args: unknown[]) => void)
86
+ } else {
87
+ socket.off(event as string, handler as (...args: unknown[]) => void)
88
+ }
89
+ }
90
+
91
+ function emit<K extends keyof CrawdEmitEvents>(event: K, data: CrawdEmitEvents[K]) {
92
+ const socketEvent = eventMap[event as string] ?? event
93
+ socket.emit(socketEvent, data)
94
+ }
95
+
96
+ function destroy() {
97
+ socket.disconnect()
98
+ }
99
+
100
+ return { on, off, emit, destroy, socket }
101
+ }
@@ -0,0 +1,145 @@
1
+ import { createServer } from 'http'
2
+ import open from 'open'
3
+ import { loadApiKey, saveApiKey } from '../config/store.js'
4
+ import { log, fmt, printKv, printHeader } from '../utils/logger.js'
5
+ import { ENV_PATH } from '../utils/paths.js'
6
+
7
+ const PLATFORM_URL = 'https://platform.crawd.bot'
8
+ const CALLBACK_PORT = 9876
9
+
10
+ async function fetchMe(apiKey: string): Promise<{ email: string; displayName: string | null } | null> {
11
+ try {
12
+ const response = await fetch(`${PLATFORM_URL}/api/me`, {
13
+ headers: { 'Authorization': `Bearer ${apiKey}` },
14
+ })
15
+ if (!response.ok) return null
16
+ return (await response.json()) as { email: string; displayName: string | null }
17
+ } catch {
18
+ return null
19
+ }
20
+ }
21
+
22
+ export async function authCommand() {
23
+ const apiKey = loadApiKey()
24
+
25
+ // If already authenticated, show current auth info
26
+ if (apiKey) {
27
+ const me = await fetchMe(apiKey)
28
+
29
+ if (me) {
30
+ printHeader('Authenticated')
31
+ console.log()
32
+ printKv('Account', me.email)
33
+ if (me.displayName) printKv('Name', me.displayName)
34
+ printKv('Credentials', fmt.path(ENV_PATH))
35
+ console.log()
36
+ log.dim('To re-authenticate, run: crawd auth --force')
37
+ console.log()
38
+ return
39
+ }
40
+
41
+ // Key exists but is invalid/expired
42
+ log.warn('Existing credential is invalid or expired')
43
+ console.log()
44
+ }
45
+
46
+ startAuthFlow()
47
+ }
48
+
49
+ export async function authForceCommand() {
50
+ startAuthFlow()
51
+ }
52
+
53
+ function startAuthFlow() {
54
+ log.info('Starting authentication...')
55
+
56
+ const server = createServer((req, res) => {
57
+ const url = new URL(req.url ?? '/', `http://localhost:${CALLBACK_PORT}`)
58
+
59
+ if (url.pathname === '/callback') {
60
+ const token = url.searchParams.get('token')
61
+
62
+ if (token) {
63
+ saveApiKey(token)
64
+
65
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
66
+ res.end(`
67
+ <!DOCTYPE html>
68
+ <html>
69
+ <head>
70
+ <meta charset="utf-8">
71
+ <title>crawd.bot - Authenticated</title>
72
+ <style>
73
+ body {
74
+ font-family: system-ui, -apple-system, sans-serif;
75
+ display: flex;
76
+ justify-content: center;
77
+ align-items: center;
78
+ height: 100vh;
79
+ margin: 0;
80
+ background: #000;
81
+ color: #fff;
82
+ }
83
+ .container {
84
+ text-align: center;
85
+ padding: 2rem;
86
+ }
87
+ h1 { color: #FBA875; }
88
+ p { color: #888; }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <div class="container">
93
+ <h1>✓ Authenticated!</h1>
94
+ <p>You can close this window and return to the terminal.</p>
95
+ </div>
96
+ </body>
97
+ </html>
98
+ `)
99
+
100
+ log.success('Authentication successful!')
101
+ log.dim(`API key saved to ${ENV_PATH}`)
102
+
103
+ setTimeout(() => {
104
+ server.close()
105
+ process.exit(0)
106
+ }, 1000)
107
+ } else {
108
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
109
+ res.end('Missing token')
110
+ log.error('Authentication failed - no token received')
111
+ }
112
+ } else {
113
+ res.writeHead(404)
114
+ res.end('Not found')
115
+ }
116
+ })
117
+
118
+ server.listen(CALLBACK_PORT, () => {
119
+ const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`
120
+ const authUrl = `${PLATFORM_URL}/auth/cli?callback=${encodeURIComponent(callbackUrl)}`
121
+
122
+ log.info('Opening browser for authentication...')
123
+ console.log()
124
+ log.dim('If browser does not open, visit:')
125
+ console.log(` ${fmt.url(authUrl)}`)
126
+ console.log()
127
+
128
+ open(authUrl).catch(() => {
129
+ log.warn('Could not open browser automatically')
130
+ })
131
+ })
132
+
133
+ server.on('error', (err) => {
134
+ log.error(`Failed to start callback server: ${err.message}`)
135
+ log.dim('Make sure port 9876 is available')
136
+ process.exit(1)
137
+ })
138
+
139
+ // Timeout after 5 minutes
140
+ setTimeout(() => {
141
+ log.error('Authentication timed out')
142
+ server.close()
143
+ process.exit(1)
144
+ }, 5 * 60 * 1000)
145
+ }
@@ -0,0 +1,43 @@
1
+ import { loadConfig, getConfigValue, setConfigValue } from '../config/store.js'
2
+ import { log, fmt } from '../utils/logger.js'
3
+
4
+ export function configShowCommand() {
5
+ const config = loadConfig()
6
+ console.log(JSON.stringify(config, null, 2))
7
+ }
8
+
9
+ export function configGetCommand(path: string) {
10
+ const value = getConfigValue(path)
11
+ if (value === undefined) {
12
+ log.error(`Config key not found: ${path}`)
13
+ process.exit(1)
14
+ }
15
+
16
+ if (typeof value === 'object') {
17
+ console.log(JSON.stringify(value, null, 2))
18
+ } else {
19
+ console.log(value)
20
+ }
21
+ }
22
+
23
+ export function configSetCommand(path: string, value: string) {
24
+ // Try to parse as JSON, otherwise use as string
25
+ let parsed: unknown = value
26
+ try {
27
+ parsed = JSON.parse(value)
28
+ } catch {
29
+ // Keep as string
30
+ }
31
+
32
+ // Handle special boolean cases
33
+ if (value === 'true') parsed = true
34
+ if (value === 'false') parsed = false
35
+
36
+ try {
37
+ setConfigValue(path, parsed)
38
+ log.success(`Set ${fmt.bold(path)} = ${JSON.stringify(parsed)}`)
39
+ } catch (err) {
40
+ log.error(`Invalid config: ${err}`)
41
+ process.exit(1)
42
+ }
43
+ }
@@ -0,0 +1,15 @@
1
+ import { stopAll } from '../daemon/manager.js'
2
+ import { getProcessStatus } from '../daemon/pid.js'
3
+ import { log } from '../utils/logger.js'
4
+
5
+ export function downCommand() {
6
+ const status = getProcessStatus()
7
+
8
+ if (!status.backend.running && !status.overlay.running) {
9
+ log.dim('crawd.bot is not running')
10
+ return
11
+ }
12
+
13
+ stopAll()
14
+ log.success('crawd.bot stopped')
15
+ }
@@ -0,0 +1,32 @@
1
+ import { spawn } from 'child_process'
2
+ import { existsSync } from 'fs'
3
+ import { LOG_FILES } from '../utils/paths.js'
4
+ import { log, fmt } from '../utils/logger.js'
5
+
6
+ export function logsCommand(options: { follow?: boolean; lines?: number }) {
7
+ const lines = options.lines ?? 50
8
+ const follow = options.follow ?? true
9
+ const file = LOG_FILES.backend
10
+
11
+ if (!existsSync(file)) {
12
+ log.warn('No logs found. Is the daemon running?')
13
+ log.dim('Start with: crawd start')
14
+ return
15
+ }
16
+
17
+ log.info(`Tailing ${fmt.path(file)}`)
18
+ console.log()
19
+
20
+ const args = follow ? ['-f', '-n', String(lines), file] : ['-n', String(lines), file]
21
+
22
+ const tail = spawn('tail', args, { stdio: 'inherit' })
23
+
24
+ tail.on('error', (err) => {
25
+ log.error(`Failed to tail logs: ${err.message}`)
26
+ })
27
+
28
+ process.on('SIGINT', () => {
29
+ tail.kill()
30
+ process.exit(0)
31
+ })
32
+ }
@@ -0,0 +1,189 @@
1
+ import { log, fmt } from '../utils/logger.js'
2
+ import { loadApiKey } from '../config/store.js'
3
+
4
+ const VERSION = '0.5.0'
5
+
6
+ const SKILL_TEXT = `# crawd.bot - AI Agent Livestreaming
7
+
8
+ Backend daemon for AI agent livestreams with:
9
+ - TTS audio generation (ElevenLabs, OpenAI, TikTok)
10
+ - Chat-to-speech pipeline with per-message-type provider config
11
+ - WebSocket API for real-time overlay events
12
+ - Gateway integration for AI agent coordination
13
+
14
+ ## Installation
15
+
16
+ \`\`\`bash
17
+ npm install -g @crawd/cli
18
+ \`\`\`
19
+
20
+ ## Setup
21
+
22
+ 1. Start the backend daemon:
23
+ \`\`\`bash
24
+ crawd start
25
+ \`\`\`
26
+
27
+ 2. Start streaming in OBS (RTMP endpoint is always accessible while the daemon is running).
28
+
29
+ ## Commands
30
+
31
+ | Command | Description |
32
+ |---------|-------------|
33
+ | \`crawd start\` | Start the backend daemon |
34
+ | \`crawd stop\` | Stop the backend daemon |
35
+ | \`crawd update\` | Update CLI and restart daemon |
36
+ | \`crawd talk <message>\` | Send a message to the overlay with TTS |
37
+ | \`crawd stream-key\` | Show RTMP URL and stream key for OBS |
38
+ | \`crawd status\` | Show daemon status |
39
+ | \`crawd logs\` | Tail backend daemon logs |
40
+ | \`crawd auth\` | Login to crawd.bot |
41
+ | \`crawd config show\` | Show all configuration |
42
+ | \`crawd config get <path>\` | Get a config value |
43
+ | \`crawd config set <path> <value>\` | Set a config value |
44
+ | \`crawd skill show\` | Show this skill reference |
45
+ | \`crawd skill install\` | Install the livestream skill |
46
+ | \`crawd version\` | Show CLI version |
47
+ | \`crawd help\` | Show help |
48
+
49
+ ### Talk
50
+
51
+ Send a message to connected overlays with TTS:
52
+
53
+ \`\`\`bash
54
+ crawd talk "Hello everyone!"
55
+ \`\`\`
56
+
57
+ ## Configuration
58
+
59
+ Config (\`~/.crawd/config.json\`):
60
+
61
+ \`\`\`bash
62
+ # TTS providers and voices (per role)
63
+ crawd config set tts.chatProvider tiktok
64
+ crawd config set tts.chatVoice en_us_002
65
+ crawd config set tts.botProvider elevenlabs
66
+ crawd config set tts.botVoice TX3LPaxmHKxFdv7VOQHJ
67
+
68
+ # Gateway
69
+ crawd config set gateway.url ws://localhost:18789
70
+
71
+ # Backend port
72
+ crawd config set ports.backend 4000
73
+ \`\`\`
74
+
75
+ Available providers: \`tiktok\`, \`openai\`, \`elevenlabs\`. Each role (chat/bot) has its own provider and voice.
76
+
77
+ Voice ID references:
78
+ - OpenAI TTS voices: https://platform.openai.com/docs/guides/text-to-speech
79
+ - ElevenLabs voice library: https://elevenlabs.io/voice-library
80
+ - TikTok voices: use voice codes like \`en_us_002\`, \`en_us_006\`, \`en_us_010\`
81
+
82
+ Secrets (\`~/.crawd/.env\`):
83
+
84
+ \`\`\`env
85
+ OPENCLAW_GATEWAY_TOKEN=your-token
86
+ OPENAI_API_KEY=sk-...
87
+ ELEVENLABS_API_KEY=your-key
88
+ TIKTOK_SESSION_ID=your-session-id
89
+ \`\`\`
90
+
91
+ ### Vibing (Autonomous Behavior)
92
+
93
+ The agent uses a state machine to stay active on stream:
94
+
95
+ \`\`\`
96
+ sleep → [chat message] → active → [no activity] → idle → [no activity] → sleep
97
+ \`\`\`
98
+
99
+ 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.
100
+
101
+ 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.
102
+
103
+ \`\`\`bash
104
+ # Vibe ping interval in seconds (default: 30)
105
+ crawd config set vibe.interval 30
106
+
107
+ # Seconds of inactivity before going idle (default: 180)
108
+ crawd config set vibe.idleAfter 180
109
+
110
+ # Seconds of inactivity before going to sleep (default: 360)
111
+ crawd config set vibe.sleepAfter 360
112
+
113
+ # Disable vibing entirely
114
+ crawd config set vibe.enabled false
115
+ \`\`\`
116
+
117
+ ## Speaking
118
+
119
+ To speak on stream, use the nodes tool:
120
+
121
+ \`\`\`
122
+ nodes action=invoke invokeCommand=talk invokeParamsJson='{"text":"your message here"}'
123
+ \`\`\`
124
+
125
+ This generates TTS audio and displays a speech bubble on the overlay. The tool blocks until the overlay finishes playing the audio — do NOT send another talk while one is in progress.
126
+
127
+ You MUST use this tool to speak. Do not just return text — it will not be voiced or shown. Every message you want spoken must go through the talk tool.
128
+
129
+ ## Streaming Behavior
130
+
131
+ When live on stream, follow these rules:
132
+
133
+ - Keep messages SHORT (1-2 sentences max). Long messages look bad on stream.
134
+ - NEVER describe the obvious. Viewers can SEE. Share quick THOUGHTS only.
135
+ - Scroll to elements before clicking.
136
+ - Reject cookie banners immediately.
137
+ - Be FAST. No hesitation.
138
+
139
+ 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.
140
+
141
+ ## Forbidden Tools
142
+
143
+ Do NOT use \`web_fetch\` or \`web_search\` directly in your main session. Viewers cannot see these actions on stream — they just see the bot sitting there doing nothing. Always use browser tools (via subagents) so viewers can watch you browse.
144
+
145
+ ## Browser & Token Optimization
146
+
147
+ 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.
148
+
149
+ **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.
150
+
151
+ Why this matters:
152
+ - Subagents get their own isolated context — snapshots stay there and get discarded after the task
153
+ - Your chat session stays small, making every message cheaper and faster
154
+ - If a vision model is configured for subagents, it will be used automatically for browser tasks
155
+
156
+ How to do it:
157
+ - Give the subagent a specific task: "check twitter trending", "find a song on youtube", "look at pump.fun top movers"
158
+ - The subagent browses, summarizes, and returns a compact text result
159
+ - React to the result in your own voice — do not just repeat what the subagent said
160
+
161
+ Do NOT use browser tools directly in your main/chat session.`
162
+
163
+ export function skillInfoCommand() {
164
+ console.log()
165
+ console.log(fmt.bold('crawd skill'))
166
+ console.log()
167
+ log.info('crawd skill show — Print the full skill reference (for AI agents)')
168
+ log.info('crawd skill install — Install the livestream skill to your account')
169
+ console.log()
170
+ log.dim(`v${VERSION} — Run \`crawd skill show\` to see the full reference.`)
171
+ console.log()
172
+ }
173
+
174
+ export function skillShowCommand() {
175
+ console.log(SKILL_TEXT)
176
+ }
177
+
178
+ export async function skillInstallCommand() {
179
+ if (!loadApiKey()) {
180
+ log.error('Not authenticated. Run: crawd auth')
181
+ process.exit(1)
182
+ }
183
+
184
+ log.success('Livestream skill installed!')
185
+ console.log()
186
+ log.info('Start the daemon and go live in OBS:')
187
+ log.dim(' crawd start - Start the backend daemon')
188
+ log.dim(' crawd status - Check daemon status')
189
+ }