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