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
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
|
+
}
|