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,120 @@
|
|
|
1
|
+
import { spawn } from 'child_process'
|
|
2
|
+
import { existsSync, openSync, mkdirSync } from 'fs'
|
|
3
|
+
import { join, dirname } from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { isRunning, writePid } from '../daemon/pid.js'
|
|
6
|
+
import { log, fmt, printHeader, printKv } from '../utils/logger.js'
|
|
7
|
+
import { LOGS_DIR, LOG_FILES, PIDS_DIR } from '../utils/paths.js'
|
|
8
|
+
import { loadConfig, loadEnv } from '../config/store.js'
|
|
9
|
+
import type { Config } from '../config/schema.js'
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
12
|
+
const __dirname = dirname(__filename)
|
|
13
|
+
|
|
14
|
+
/** Resolve the backend entry point (src/backend/index.ts relative to package root) */
|
|
15
|
+
function getBackendEntry(): string {
|
|
16
|
+
// From src/commands/ (dev with tsx)
|
|
17
|
+
const fromSrc = join(__dirname, '..', 'backend', 'index.ts')
|
|
18
|
+
if (existsSync(fromSrc)) return fromSrc
|
|
19
|
+
|
|
20
|
+
// From dist/ (built cli.js)
|
|
21
|
+
const fromDist = join(__dirname, '..', 'src', 'backend', 'index.ts')
|
|
22
|
+
if (existsSync(fromDist)) return fromDist
|
|
23
|
+
|
|
24
|
+
throw new Error('Backend entry point not found')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Build env vars from config + secrets for the backend process */
|
|
28
|
+
function buildEnv(config: Config): NodeJS.ProcessEnv {
|
|
29
|
+
const secrets = loadEnv()
|
|
30
|
+
const env: NodeJS.ProcessEnv = { ...process.env }
|
|
31
|
+
|
|
32
|
+
// Secrets from ~/.crawd/.env
|
|
33
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
34
|
+
env[key] = value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Config from ~/.crawd/config.json
|
|
38
|
+
env.PORT = String(config.ports.backend)
|
|
39
|
+
env.BACKEND_URL = `http://localhost:${config.ports.backend}`
|
|
40
|
+
env.OPENCLAW_GATEWAY_URL = config.gateway.url
|
|
41
|
+
env.CRAWD_CHANNEL_ID = config.gateway.channelId
|
|
42
|
+
env.TTS_CHAT_PROVIDER = config.tts.chatProvider
|
|
43
|
+
env.TTS_CHAT_VOICE = config.tts.chatVoice
|
|
44
|
+
env.TTS_BOT_PROVIDER = config.tts.botProvider
|
|
45
|
+
env.TTS_BOT_VOICE = config.tts.botVoice
|
|
46
|
+
if (config.chat.pumpfun) {
|
|
47
|
+
env.PUMPFUN_ENABLED = String(config.chat.pumpfun.enabled)
|
|
48
|
+
if (config.chat.pumpfun.tokenMint) {
|
|
49
|
+
env.NEXT_PUBLIC_TOKEN_MINT = config.chat.pumpfun.tokenMint
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Vibe state machine
|
|
53
|
+
env.VIBE_ENABLED = String(config.vibe.enabled)
|
|
54
|
+
env.VIBE_INTERVAL_MS = String(config.vibe.interval * 1000)
|
|
55
|
+
env.IDLE_AFTER_MS = String(config.vibe.idleAfter * 1000)
|
|
56
|
+
env.SLEEP_AFTER_IDLE_MS = String((config.vibe.sleepAfter - config.vibe.idleAfter) * 1000)
|
|
57
|
+
|
|
58
|
+
env.YOUTUBE_ENABLED = String(config.chat.youtube.enabled)
|
|
59
|
+
if (config.chat.youtube.videoId) {
|
|
60
|
+
env.YOUTUBE_VIDEO_ID = config.chat.youtube.videoId
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return env
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function startCommand() {
|
|
67
|
+
if (isRunning('crawdbot')) {
|
|
68
|
+
const config = loadConfig()
|
|
69
|
+
log.warn('Backend is already running')
|
|
70
|
+
printKv('Backend', fmt.url(`http://localhost:${config.ports.backend}`))
|
|
71
|
+
log.dim('Use `crawd stop` to stop it first')
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const backendEntry = getBackendEntry()
|
|
76
|
+
|
|
77
|
+
// Ensure dirs
|
|
78
|
+
for (const dir of [LOGS_DIR, PIDS_DIR]) {
|
|
79
|
+
if (!existsSync(dir)) {
|
|
80
|
+
mkdirSync(dir, { recursive: true })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const config = loadConfig()
|
|
85
|
+
const env = buildEnv(config)
|
|
86
|
+
|
|
87
|
+
log.info('Starting CrawdBot backend...')
|
|
88
|
+
|
|
89
|
+
const logFd = openSync(LOG_FILES.crawdbot, 'a')
|
|
90
|
+
|
|
91
|
+
const child = spawn('bun', ['run', backendEntry], {
|
|
92
|
+
cwd: join(dirname(backendEntry), '..', '..'),
|
|
93
|
+
env,
|
|
94
|
+
detached: true,
|
|
95
|
+
stdio: ['ignore', logFd, logFd],
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
child.unref()
|
|
99
|
+
|
|
100
|
+
if (child.pid) {
|
|
101
|
+
writePid('crawdbot', child.pid)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Wait for it to start
|
|
105
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
106
|
+
|
|
107
|
+
if (isRunning('crawdbot')) {
|
|
108
|
+
printHeader('CrawdBot started')
|
|
109
|
+
console.log()
|
|
110
|
+
log.success(`Backend running (PID ${child.pid})`)
|
|
111
|
+
printKv('Backend', fmt.url(`http://localhost:${config.ports.backend}`))
|
|
112
|
+
console.log()
|
|
113
|
+
log.dim('View logs: crawd logs')
|
|
114
|
+
log.dim('Stop: crawd stop')
|
|
115
|
+
} else {
|
|
116
|
+
log.error('Backend failed to start')
|
|
117
|
+
log.dim(`Check logs: tail ${LOG_FILES.crawdbot}`)
|
|
118
|
+
process.exit(1)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { loadApiKey } from '../config/store.js'
|
|
2
|
+
import { log, fmt } from '../utils/logger.js'
|
|
3
|
+
|
|
4
|
+
const PLATFORM_URL = 'https://platform.crawd.bot'
|
|
5
|
+
|
|
6
|
+
export async function statusCommand() {
|
|
7
|
+
const apiKey = loadApiKey()
|
|
8
|
+
|
|
9
|
+
console.log()
|
|
10
|
+
console.log(fmt.bold('crawd.bot CLI'))
|
|
11
|
+
console.log()
|
|
12
|
+
|
|
13
|
+
if (!apiKey) {
|
|
14
|
+
log.warn('Not authenticated')
|
|
15
|
+
log.dim('Run: crawd auth')
|
|
16
|
+
console.log()
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
log.info('Fetching stream status...')
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch(`${PLATFORM_URL}/api/stream`, {
|
|
24
|
+
headers: {
|
|
25
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
if (response.status === 401) {
|
|
30
|
+
log.error('Authentication expired')
|
|
31
|
+
log.dim('Run: crawd auth')
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const data = await response.json()
|
|
36
|
+
|
|
37
|
+
if (data.error) {
|
|
38
|
+
log.error(data.error)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { stream } = data
|
|
43
|
+
|
|
44
|
+
console.log()
|
|
45
|
+
console.log(fmt.bold('Stream Status'))
|
|
46
|
+
console.log()
|
|
47
|
+
|
|
48
|
+
if (stream.isLive) {
|
|
49
|
+
console.log(` Status: ${fmt.success('● LIVE')}`)
|
|
50
|
+
} else {
|
|
51
|
+
console.log(` Status: ${fmt.dim('○ Offline')}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(` Name: ${stream.name}`)
|
|
55
|
+
console.log(` Viewers: ${stream.viewerCount}`)
|
|
56
|
+
console.log()
|
|
57
|
+
console.log(fmt.bold('OBS Settings'))
|
|
58
|
+
console.log()
|
|
59
|
+
console.log(` Server: ${fmt.dim(stream.rtmpUrl)}`)
|
|
60
|
+
console.log(` Stream Key: ${fmt.dim(stream.streamKey.slice(0, 20) + '...')}`)
|
|
61
|
+
|
|
62
|
+
if (stream.playbackId) {
|
|
63
|
+
console.log()
|
|
64
|
+
console.log(fmt.bold('Preview'))
|
|
65
|
+
console.log()
|
|
66
|
+
console.log(` ${fmt.url(`${PLATFORM_URL}/preview/${stream.playbackId}`)}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log()
|
|
70
|
+
} catch (err) {
|
|
71
|
+
log.error(`Failed to fetch status: ${err instanceof Error ? err.message : err}`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isRunning, killProcess } from '../daemon/pid.js'
|
|
2
|
+
import { log } from '../utils/logger.js'
|
|
3
|
+
|
|
4
|
+
export function stopCommand() {
|
|
5
|
+
if (!isRunning('crawdbot')) {
|
|
6
|
+
log.dim('CrawdBot backend is not running')
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const killed = killProcess('crawdbot')
|
|
11
|
+
if (killed) {
|
|
12
|
+
log.success('CrawdBot backend stopped')
|
|
13
|
+
} else {
|
|
14
|
+
log.error('Failed to stop CrawdBot backend')
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { loadApiKey } from '../config/store.js'
|
|
2
|
+
import { log, printHeader, printKv } from '../utils/logger.js'
|
|
3
|
+
|
|
4
|
+
const PLATFORM_URL = 'https://platform.crawd.bot'
|
|
5
|
+
|
|
6
|
+
export async function streamKeyCommand() {
|
|
7
|
+
const apiKey = loadApiKey()
|
|
8
|
+
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
log.error('Not authenticated. Run: crawd auth')
|
|
11
|
+
process.exit(1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(`${PLATFORM_URL}/api/stream`, {
|
|
16
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
if (response.status === 401) {
|
|
20
|
+
log.error('Authentication expired. Run: crawd auth')
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const data = await response.json() as { error?: string; stream?: { rtmpUrl: string; streamKey: string } }
|
|
25
|
+
|
|
26
|
+
if (data.error) {
|
|
27
|
+
log.error(data.error)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!data.stream) {
|
|
32
|
+
log.error('No stream data returned')
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
printHeader('OBS Settings')
|
|
37
|
+
console.log()
|
|
38
|
+
printKv('Server', data.stream.rtmpUrl)
|
|
39
|
+
printKv('Stream Key', data.stream.streamKey)
|
|
40
|
+
console.log()
|
|
41
|
+
} catch (err) {
|
|
42
|
+
log.error(`Failed to fetch stream key: ${err instanceof Error ? err.message : err}`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { loadConfig } from '../config/store.js'
|
|
2
|
+
import { log } from '../utils/logger.js'
|
|
3
|
+
|
|
4
|
+
export async function talkCommand(message: string) {
|
|
5
|
+
const config = loadConfig()
|
|
6
|
+
const port = config.ports.backend
|
|
7
|
+
const url = `http://localhost:${port}/crawd/talk`
|
|
8
|
+
|
|
9
|
+
const body = { message }
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(url, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify(body),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const data = await res.json().catch(() => ({}))
|
|
20
|
+
log.error((data as { error?: string }).error ?? `Request failed (${res.status})`)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
log.success(`Sent: "${message}"`)
|
|
25
|
+
} catch {
|
|
26
|
+
log.error('Could not reach the backend daemon. Is it running?')
|
|
27
|
+
log.dim('Start with: crawd start')
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { loadConfig } from '../config/store.js'
|
|
2
|
+
import { startAll, ensureDirectories, ensureOverlay } from '../daemon/manager.js'
|
|
3
|
+
import { getProcessStatus } from '../daemon/pid.js'
|
|
4
|
+
import { log, fmt, printHeader, printKv } from '../utils/logger.js'
|
|
5
|
+
|
|
6
|
+
export async function upCommand(options: { force?: boolean }) {
|
|
7
|
+
const status = getProcessStatus()
|
|
8
|
+
|
|
9
|
+
if (status.backend.running && status.overlay.running) {
|
|
10
|
+
log.warn('crawd.bot is already running')
|
|
11
|
+
log.dim('Use `crawd restart` to restart, or `crawd down` to stop')
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ensureDirectories()
|
|
16
|
+
|
|
17
|
+
if (options.force) {
|
|
18
|
+
ensureOverlay(true)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await startAll()
|
|
23
|
+
|
|
24
|
+
const config = loadConfig()
|
|
25
|
+
|
|
26
|
+
// Wait a moment for processes to start
|
|
27
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
28
|
+
|
|
29
|
+
const newStatus = getProcessStatus()
|
|
30
|
+
|
|
31
|
+
printHeader('crawd.bot is starting...')
|
|
32
|
+
console.log()
|
|
33
|
+
|
|
34
|
+
if (newStatus.backend.running) {
|
|
35
|
+
log.success(`Backend running (PID ${newStatus.backend.pid})`)
|
|
36
|
+
} else {
|
|
37
|
+
log.error('Backend failed to start - check logs with `crawd logs`')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (newStatus.overlay.running) {
|
|
41
|
+
log.success(`Overlay running (PID ${newStatus.overlay.pid})`)
|
|
42
|
+
} else {
|
|
43
|
+
log.error('Overlay failed to start - check logs with `crawd logs`')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log()
|
|
47
|
+
printHeader('URLs')
|
|
48
|
+
printKv('Overlay', fmt.url(`http://localhost:${config.ports.overlay}`))
|
|
49
|
+
printKv('Backend', fmt.url(`http://localhost:${config.ports.backend}`))
|
|
50
|
+
printKv('OBS Source', fmt.url(`http://localhost:${config.ports.overlay}`))
|
|
51
|
+
|
|
52
|
+
console.log()
|
|
53
|
+
log.dim('View logs with: crawd logs')
|
|
54
|
+
log.dim('Stop with: crawd down')
|
|
55
|
+
} catch (err) {
|
|
56
|
+
log.error(`Failed to start: ${err}`)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
import { realpathSync } from 'fs'
|
|
3
|
+
import { isRunning, killProcess, readPid, isProcessRunning } from '../daemon/pid.js'
|
|
4
|
+
import { log } from '../utils/logger.js'
|
|
5
|
+
import { startCommand } from './start.js'
|
|
6
|
+
|
|
7
|
+
type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'
|
|
8
|
+
|
|
9
|
+
const INSTALL_CMD: Record<PackageManager, string> = {
|
|
10
|
+
npm: 'npm install -g @crawd/cli@latest',
|
|
11
|
+
pnpm: 'pnpm add -g @crawd/cli@latest',
|
|
12
|
+
yarn: 'yarn global add @crawd/cli@latest',
|
|
13
|
+
bun: 'bun install -g @crawd/cli@latest',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Detect which package manager installed the CLI by resolving the binary path */
|
|
17
|
+
function detectPackageManager(): PackageManager {
|
|
18
|
+
try {
|
|
19
|
+
const bin = execSync('which crawd', { encoding: 'utf-8' }).trim()
|
|
20
|
+
const resolved = realpathSync(bin)
|
|
21
|
+
|
|
22
|
+
if (resolved.includes('/pnpm')) return 'pnpm'
|
|
23
|
+
if (resolved.includes('/.bun/')) return 'bun'
|
|
24
|
+
if (resolved.includes('/.yarn/') || resolved.includes('/yarn/')) return 'yarn'
|
|
25
|
+
} catch {
|
|
26
|
+
// which failed or path unresolvable — fall through
|
|
27
|
+
}
|
|
28
|
+
return 'npm'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Wait for a process to exit (up to timeoutMs) */
|
|
32
|
+
function waitForExit(pid: number, timeoutMs = 5000): boolean {
|
|
33
|
+
const start = Date.now()
|
|
34
|
+
while (Date.now() - start < timeoutMs) {
|
|
35
|
+
if (!isProcessRunning(pid)) return true
|
|
36
|
+
execSync('sleep 0.1')
|
|
37
|
+
}
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function updateCommand() {
|
|
42
|
+
const daemonWasRunning = isRunning('crawdbot')
|
|
43
|
+
const oldPid = readPid('crawdbot')
|
|
44
|
+
|
|
45
|
+
// 1. Stop daemon if running
|
|
46
|
+
if (daemonWasRunning && oldPid) {
|
|
47
|
+
log.info('Stopping backend daemon...')
|
|
48
|
+
killProcess('crawdbot')
|
|
49
|
+
|
|
50
|
+
if (!waitForExit(oldPid)) {
|
|
51
|
+
log.error('Backend daemon did not stop in time')
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
log.success('Backend daemon stopped')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Detect package manager and update
|
|
58
|
+
const pm = detectPackageManager()
|
|
59
|
+
const cmd = INSTALL_CMD[pm]
|
|
60
|
+
log.info(`Updating @crawd/cli via ${pm}...`)
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const output = execSync(`${cmd} 2>&1`, {
|
|
64
|
+
encoding: 'utf-8',
|
|
65
|
+
timeout: 60_000,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const versionMatch = output.match(/@crawd\/cli@([\d.]+)/)
|
|
69
|
+
if (versionMatch) {
|
|
70
|
+
log.success(`Updated to v${versionMatch[1]}`)
|
|
71
|
+
} else {
|
|
72
|
+
log.success('CLI updated')
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
76
|
+
log.error(`Failed to update: ${msg}`)
|
|
77
|
+
// Still restart daemon if it was running
|
|
78
|
+
if (daemonWasRunning) {
|
|
79
|
+
log.info('Restarting backend daemon with current version...')
|
|
80
|
+
await startCommand()
|
|
81
|
+
}
|
|
82
|
+
process.exit(1)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Restart daemon if it was running
|
|
86
|
+
if (daemonWasRunning) {
|
|
87
|
+
log.info('Restarting backend daemon...')
|
|
88
|
+
await startCommand()
|
|
89
|
+
} else {
|
|
90
|
+
log.dim('Backend daemon was not running, skipping restart')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const ttsProviderEnum = z.enum(['openai', 'elevenlabs', 'tiktok'])
|
|
4
|
+
|
|
5
|
+
export const ConfigSchema = z.object({
|
|
6
|
+
/** Gateway configuration */
|
|
7
|
+
gateway: z.object({
|
|
8
|
+
url: z.string().default('ws://localhost:18789'),
|
|
9
|
+
/** Channel ID for the agent session */
|
|
10
|
+
channelId: z.string().default('agent:main:crawd:live'),
|
|
11
|
+
}).default({}),
|
|
12
|
+
|
|
13
|
+
/** Server ports */
|
|
14
|
+
ports: z.object({
|
|
15
|
+
backend: z.number().default(4000),
|
|
16
|
+
overlay: z.number().default(3000),
|
|
17
|
+
}).default({}),
|
|
18
|
+
|
|
19
|
+
/** TTS configuration */
|
|
20
|
+
tts: z.object({
|
|
21
|
+
/** Provider for reading chat messages aloud */
|
|
22
|
+
chatProvider: ttsProviderEnum.default('tiktok'),
|
|
23
|
+
/** Voice ID for chat TTS (must match chatProvider) */
|
|
24
|
+
chatVoice: z.string().default('en_us_002'),
|
|
25
|
+
/** Provider for bot speech */
|
|
26
|
+
botProvider: ttsProviderEnum.default('elevenlabs'),
|
|
27
|
+
/** Voice ID for bot TTS (must match botProvider) */
|
|
28
|
+
botVoice: z.string().default('TX3LPaxmHKxFdv7VOQHJ'),
|
|
29
|
+
}).default({}),
|
|
30
|
+
|
|
31
|
+
/** Chat platform configuration */
|
|
32
|
+
chat: z.object({
|
|
33
|
+
pumpfun: z.object({
|
|
34
|
+
enabled: z.boolean().default(false),
|
|
35
|
+
tokenMint: z.string().optional(),
|
|
36
|
+
}).optional(),
|
|
37
|
+
youtube: z.object({
|
|
38
|
+
enabled: z.boolean().default(false),
|
|
39
|
+
videoId: z.string().optional(),
|
|
40
|
+
}).default({}),
|
|
41
|
+
}).default({}),
|
|
42
|
+
|
|
43
|
+
/** Autonomous vibing state machine */
|
|
44
|
+
vibe: z.object({
|
|
45
|
+
/** Enable autonomous vibing (agent acts on its own between chat messages) */
|
|
46
|
+
enabled: z.boolean().default(true),
|
|
47
|
+
/** Seconds between vibe pings while active */
|
|
48
|
+
interval: z.number().default(30),
|
|
49
|
+
/** Seconds of inactivity before going idle */
|
|
50
|
+
idleAfter: z.number().default(180),
|
|
51
|
+
/** Seconds of inactivity before going to sleep (must be > idleAfter) */
|
|
52
|
+
sleepAfter: z.number().default(360),
|
|
53
|
+
}).default({}),
|
|
54
|
+
|
|
55
|
+
/** Stream configuration */
|
|
56
|
+
stream: z.object({
|
|
57
|
+
/** RTMP stream key for pump.fun */
|
|
58
|
+
key: z.string().optional(),
|
|
59
|
+
}).default({}),
|
|
60
|
+
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
export type Config = z.infer<typeof ConfigSchema>
|
|
64
|
+
|
|
65
|
+
/** Default configuration */
|
|
66
|
+
export const DEFAULT_CONFIG: Config = ConfigSchema.parse({})
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
2
|
+
import { dirname } from 'path'
|
|
3
|
+
import { CONFIG_PATH, CRAWD_HOME, ENV_PATH } from '../utils/paths.js'
|
|
4
|
+
import { Config, ConfigSchema, DEFAULT_CONFIG } from './schema.js'
|
|
5
|
+
|
|
6
|
+
/** Ensure the crawd home directory exists */
|
|
7
|
+
export function ensureHome() {
|
|
8
|
+
if (!existsSync(CRAWD_HOME)) {
|
|
9
|
+
mkdirSync(CRAWD_HOME, { recursive: true })
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Load configuration from disk */
|
|
14
|
+
export function loadConfig(): Config {
|
|
15
|
+
ensureHome()
|
|
16
|
+
|
|
17
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
18
|
+
return DEFAULT_CONFIG
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(CONFIG_PATH, 'utf-8')
|
|
23
|
+
const parsed = JSON.parse(raw)
|
|
24
|
+
return ConfigSchema.parse(parsed)
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.warn('Failed to parse config, using defaults:', e)
|
|
27
|
+
return DEFAULT_CONFIG
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Save configuration to disk */
|
|
32
|
+
export function saveConfig(config: Config) {
|
|
33
|
+
ensureHome()
|
|
34
|
+
const dir = dirname(CONFIG_PATH)
|
|
35
|
+
if (!existsSync(dir)) {
|
|
36
|
+
mkdirSync(dir, { recursive: true })
|
|
37
|
+
}
|
|
38
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Update specific config values (deep merge) */
|
|
42
|
+
export function updateConfig(updates: Partial<Config>) {
|
|
43
|
+
const current = loadConfig()
|
|
44
|
+
const merged = deepMerge(current, updates)
|
|
45
|
+
const validated = ConfigSchema.parse(merged)
|
|
46
|
+
saveConfig(validated)
|
|
47
|
+
return validated
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Get a specific config value by dot-notation path */
|
|
51
|
+
export function getConfigValue(path: string): unknown {
|
|
52
|
+
const config = loadConfig()
|
|
53
|
+
return getByPath(config, path)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Set a specific config value by dot-notation path */
|
|
57
|
+
export function setConfigValue(path: string, value: unknown) {
|
|
58
|
+
// Read raw JSON (without Zod defaults) so we only persist explicit values
|
|
59
|
+
let raw: Record<string, unknown> = {}
|
|
60
|
+
if (existsSync(CONFIG_PATH)) {
|
|
61
|
+
try {
|
|
62
|
+
raw = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'))
|
|
63
|
+
} catch { /* start fresh */ }
|
|
64
|
+
}
|
|
65
|
+
setByPath(raw, path, value)
|
|
66
|
+
// Validate through Zod — throws on invalid values
|
|
67
|
+
const validated = ConfigSchema.parse(raw)
|
|
68
|
+
// Save raw JSON (only user-set values, not all defaults)
|
|
69
|
+
ensureHome()
|
|
70
|
+
const dir = dirname(CONFIG_PATH)
|
|
71
|
+
if (!existsSync(dir)) {
|
|
72
|
+
mkdirSync(dir, { recursive: true })
|
|
73
|
+
}
|
|
74
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(raw, null, 2))
|
|
75
|
+
return validated
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Load secrets from ~/.crawd/.env */
|
|
79
|
+
export function loadEnv(): Record<string, string> {
|
|
80
|
+
if (!existsSync(ENV_PATH)) return {}
|
|
81
|
+
|
|
82
|
+
const content = readFileSync(ENV_PATH, 'utf-8')
|
|
83
|
+
const env: Record<string, string> = {}
|
|
84
|
+
|
|
85
|
+
for (const line of content.split('\n')) {
|
|
86
|
+
const trimmed = line.trim()
|
|
87
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
88
|
+
const eqIndex = trimmed.indexOf('=')
|
|
89
|
+
if (eqIndex === -1) continue
|
|
90
|
+
const key = trimmed.slice(0, eqIndex).trim()
|
|
91
|
+
let value = trimmed.slice(eqIndex + 1).trim()
|
|
92
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
93
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
94
|
+
value = value.slice(1, -1)
|
|
95
|
+
}
|
|
96
|
+
env[key] = value
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return env
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Save secrets to ~/.crawd/.env */
|
|
103
|
+
export function saveEnv(env: Record<string, string>) {
|
|
104
|
+
ensureHome()
|
|
105
|
+
const lines = Object.entries(env).map(([k, v]) => `${k}=${v}`)
|
|
106
|
+
writeFileSync(ENV_PATH, lines.join('\n') + '\n')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Load API key from ~/.crawd/.env */
|
|
110
|
+
export function loadApiKey(): string | null {
|
|
111
|
+
const env = loadEnv()
|
|
112
|
+
return env.CRAWD_API_KEY ?? null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Known .env keys — included with empty defaults so users can see what's available */
|
|
116
|
+
const ENV_TEMPLATE_KEYS = [
|
|
117
|
+
'CRAWD_API_KEY',
|
|
118
|
+
'OPENAI_API_KEY',
|
|
119
|
+
'ELEVENLABS_API_KEY',
|
|
120
|
+
'TIKTOK_SESSION_ID',
|
|
121
|
+
'OPENCLAW_GATEWAY_TOKEN',
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
/** Save API key to ~/.crawd/.env, seeding empty placeholders for known keys */
|
|
125
|
+
export function saveApiKey(apiKey: string) {
|
|
126
|
+
const env = loadEnv()
|
|
127
|
+
for (const key of ENV_TEMPLATE_KEYS) {
|
|
128
|
+
if (!(key in env)) env[key] = ''
|
|
129
|
+
}
|
|
130
|
+
env.CRAWD_API_KEY = apiKey
|
|
131
|
+
saveEnv(env)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Helper functions
|
|
135
|
+
|
|
136
|
+
function deepMerge<T extends Record<string, unknown>>(
|
|
137
|
+
target: T,
|
|
138
|
+
source: Partial<T>
|
|
139
|
+
): T {
|
|
140
|
+
const result = { ...target }
|
|
141
|
+
for (const key of Object.keys(source) as (keyof T)[]) {
|
|
142
|
+
const sourceVal = source[key]
|
|
143
|
+
const targetVal = target[key]
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
sourceVal !== undefined &&
|
|
147
|
+
typeof sourceVal === 'object' &&
|
|
148
|
+
sourceVal !== null &&
|
|
149
|
+
!Array.isArray(sourceVal) &&
|
|
150
|
+
typeof targetVal === 'object' &&
|
|
151
|
+
targetVal !== null
|
|
152
|
+
) {
|
|
153
|
+
result[key] = deepMerge(
|
|
154
|
+
targetVal as Record<string, unknown>,
|
|
155
|
+
sourceVal as Record<string, unknown>
|
|
156
|
+
) as T[keyof T]
|
|
157
|
+
} else if (sourceVal !== undefined) {
|
|
158
|
+
result[key] = sourceVal as T[keyof T]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return result
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getByPath(obj: unknown, path: string): unknown {
|
|
165
|
+
const parts = path.split('.')
|
|
166
|
+
let current = obj
|
|
167
|
+
for (const part of parts) {
|
|
168
|
+
if (current === null || current === undefined) return undefined
|
|
169
|
+
current = (current as Record<string, unknown>)[part]
|
|
170
|
+
}
|
|
171
|
+
return current
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function setByPath(obj: unknown, path: string, value: unknown) {
|
|
175
|
+
const parts = path.split('.')
|
|
176
|
+
let current = obj as Record<string, unknown>
|
|
177
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
178
|
+
const part = parts[i]
|
|
179
|
+
if (!(part in current) || typeof current[part] !== 'object') {
|
|
180
|
+
current[part] = {}
|
|
181
|
+
}
|
|
182
|
+
current = current[part] as Record<string, unknown>
|
|
183
|
+
}
|
|
184
|
+
current[parts[parts.length - 1]] = value
|
|
185
|
+
}
|