crawd 0.8.6 → 0.9.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/dist/cli.js +7 -1
- package/dist/types.d.ts +5 -23
- package/openclaw.plugin.json +8 -40
- package/package.json +13 -11
- package/skills/crawd/SKILL.md +37 -0
- package/src/backend/coordinator.test.ts +393 -0
- package/src/backend/coordinator.ts +274 -19
- package/src/backend/index.ts +29 -208
- package/src/backend/server.ts +75 -219
- package/src/commands/skill.ts +3 -0
- package/src/commands/start.ts +1 -0
- package/src/config/schema.ts +2 -0
- package/src/plugin.ts +124 -33
- package/src/types.ts +4 -23
- package/dist/backend/chunk-QITCQHSS.js +0 -2087
- package/dist/backend/fileFromPath-WZUZ37JN.js +0 -127
- package/dist/backend/index.js +0 -12418
- package/dist/chunk-QITCQHSS.js +0 -2087
- package/dist/fileFromPath-WZUZ37JN.js +0 -127
- package/dist/plugin.d.ts +0 -27
- package/dist/plugin.js +0 -12363
- package/src/lib/tts/tiktok.ts +0 -91
package/src/plugin.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - `crawd` service (Fastify + Socket.IO backend)
|
|
8
8
|
*/
|
|
9
9
|
import { Type } from '@sinclair/typebox'
|
|
10
|
-
import { CrawdBackend, type CrawdConfig
|
|
10
|
+
import { CrawdBackend, type CrawdConfig } from './backend/server.js'
|
|
11
11
|
|
|
12
12
|
// Minimal plugin types — the real types come from openclaw/plugin-sdk at runtime.
|
|
13
13
|
// Defined inline so this package builds without the openclaw peerDep installed.
|
|
@@ -36,18 +36,6 @@ type PluginDefinition = {
|
|
|
36
36
|
// Config parsing — transform pluginConfig → CrawdConfig
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
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
39
|
/** Resolve gateway WebSocket URL from env/defaults (same logic as OpenClaw's callGateway) */
|
|
52
40
|
function resolveGatewayUrl(port?: number): string {
|
|
53
41
|
if (port) return `ws://127.0.0.1:${port}`
|
|
@@ -74,7 +62,6 @@ function resolveGatewayFromHost(api: PluginApi): { token?: string; port?: number
|
|
|
74
62
|
|
|
75
63
|
function parsePluginConfig(raw: Record<string, unknown> | undefined): CrawdConfig {
|
|
76
64
|
const cfg = raw ?? {}
|
|
77
|
-
const tts = (cfg.tts ?? {}) as Record<string, unknown>
|
|
78
65
|
const vibe = (cfg.vibe ?? {}) as Record<string, unknown>
|
|
79
66
|
const chat = (cfg.chat ?? {}) as Record<string, unknown>
|
|
80
67
|
const youtube = (chat.youtube ?? {}) as Record<string, unknown>
|
|
@@ -86,19 +73,12 @@ function parsePluginConfig(raw: Record<string, unknown> | undefined): CrawdConfi
|
|
|
86
73
|
enabled: cfg.enabled !== false,
|
|
87
74
|
port,
|
|
88
75
|
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
76
|
vibe: {
|
|
98
77
|
enabled: vibe.enabled !== false,
|
|
99
78
|
intervalMs: typeof vibe.intervalMs === 'number' ? vibe.intervalMs : 10_000,
|
|
100
79
|
idleAfterMs: typeof vibe.idleAfterMs === 'number' ? vibe.idleAfterMs : 30_000,
|
|
101
80
|
sleepAfterIdleMs: typeof vibe.sleepAfterIdleMs === 'number' ? vibe.sleepAfterIdleMs : 60_000,
|
|
81
|
+
batchWindowMs: typeof vibe.batchWindowMs === 'number' ? vibe.batchWindowMs : 20_000,
|
|
102
82
|
prompt: typeof vibe.prompt === 'string' ? vibe.prompt : undefined,
|
|
103
83
|
},
|
|
104
84
|
chat: {
|
|
@@ -112,6 +92,10 @@ function parsePluginConfig(raw: Record<string, unknown> | undefined): CrawdConfi
|
|
|
112
92
|
authToken: typeof pumpfun.authToken === 'string' ? pumpfun.authToken : undefined,
|
|
113
93
|
},
|
|
114
94
|
},
|
|
95
|
+
// Autonomy mode: vibe (periodic prompts), plan (goal-driven), none (disabled)
|
|
96
|
+
autonomyMode: cfg.autonomyMode === 'vibe' || cfg.autonomyMode === 'plan' || cfg.autonomyMode === 'none'
|
|
97
|
+
? cfg.autonomyMode
|
|
98
|
+
: undefined,
|
|
115
99
|
// Gateway: plugin config overrides, then env vars, then OpenClaw defaults
|
|
116
100
|
gatewayUrl: typeof cfg.gatewayUrl === 'string' ? cfg.gatewayUrl
|
|
117
101
|
: process.env.OPENCLAW_GATEWAY_URL ?? resolveGatewayUrl(),
|
|
@@ -135,31 +119,27 @@ const crawdConfigSchema = {
|
|
|
135
119
|
enabled: { label: 'Enabled' },
|
|
136
120
|
port: { label: 'Backend Port', placeholder: '4000' },
|
|
137
121
|
bindHost: { label: 'Bind Host', placeholder: '0.0.0.0', advanced: true },
|
|
138
|
-
|
|
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 },
|
|
122
|
+
'autonomyMode': { label: 'Autonomy Mode', help: 'vibe = timed prompts, plan = goal-driven loop, none = disabled' },
|
|
144
123
|
'vibe.enabled': { label: 'Vibe Mode' },
|
|
145
124
|
'vibe.intervalMs': { label: 'Vibe Interval (ms)', advanced: true },
|
|
146
125
|
'vibe.idleAfterMs': { label: 'Idle After (ms)', advanced: true },
|
|
147
126
|
'vibe.sleepAfterIdleMs': { label: 'Sleep After Idle (ms)', advanced: true },
|
|
127
|
+
'vibe.batchWindowMs': { label: 'Chat Batch Window (ms)', advanced: true },
|
|
148
128
|
'vibe.prompt': { label: 'Vibe Prompt', advanced: true },
|
|
149
129
|
'chat.youtube.enabled': { label: 'YouTube Chat' },
|
|
150
130
|
'chat.youtube.videoId': { label: 'YouTube Video ID' },
|
|
151
131
|
'chat.pumpfun.enabled': { label: 'PumpFun Chat' },
|
|
152
132
|
'chat.pumpfun.tokenMint': { label: 'PumpFun Token Mint' },
|
|
153
133
|
'chat.pumpfun.authToken': { label: 'PumpFun Auth Token', sensitive: true },
|
|
154
|
-
gatewayUrl: { label: 'Gateway URL',
|
|
155
|
-
gatewayToken: { label: 'Gateway Token',
|
|
134
|
+
gatewayUrl: { label: 'Gateway URL', help: 'WebSocket URL for agent triggering', advanced: true },
|
|
135
|
+
gatewayToken: { label: 'Gateway Token', sensitive: true },
|
|
156
136
|
},
|
|
157
137
|
}
|
|
158
138
|
|
|
159
139
|
const plugin: PluginDefinition = {
|
|
160
140
|
id: 'crawd',
|
|
161
141
|
name: 'Crawd Livestream',
|
|
162
|
-
description: 'crawd.bot plugin — AI agent livestreaming with
|
|
142
|
+
description: 'crawd.bot plugin — AI agent livestreaming with chat integration and OBS overlay',
|
|
163
143
|
configSchema: crawdConfigSchema,
|
|
164
144
|
|
|
165
145
|
register(api: PluginApi) {
|
|
@@ -207,7 +187,7 @@ const plugin: PluginDefinition = {
|
|
|
207
187
|
name: 'livestream_talk',
|
|
208
188
|
label: 'Livestream Talk',
|
|
209
189
|
description:
|
|
210
|
-
'Speak on the livestream unprompted. Shows a speech bubble on the overlay
|
|
190
|
+
'Speak on the livestream unprompted. Shows a speech bubble on the overlay. Use for narration, vibes, and commentary — NOT for replying to chat (use livestream_reply for that).',
|
|
211
191
|
parameters: Type.Object({
|
|
212
192
|
text: Type.String({ description: 'Message to speak on stream' }),
|
|
213
193
|
}),
|
|
@@ -230,7 +210,7 @@ const plugin: PluginDefinition = {
|
|
|
230
210
|
name: 'livestream_reply',
|
|
231
211
|
label: 'Livestream Reply',
|
|
232
212
|
description:
|
|
233
|
-
'Reply to a chat message on the livestream.
|
|
213
|
+
'Reply to a chat message on the livestream. Shows the original message and your reply on the overlay. Use this ONLY when responding to a specific viewer message.',
|
|
234
214
|
parameters: Type.Object({
|
|
235
215
|
text: Type.String({ description: 'Your reply to the chat message' }),
|
|
236
216
|
username: Type.String({ description: 'Username of the person you are replying to' }),
|
|
@@ -249,6 +229,117 @@ const plugin: PluginDefinition = {
|
|
|
249
229
|
{ name: 'livestream_reply' },
|
|
250
230
|
)
|
|
251
231
|
|
|
232
|
+
// plan_set — create or replace current plan
|
|
233
|
+
api.registerTool(
|
|
234
|
+
{
|
|
235
|
+
name: 'plan_set',
|
|
236
|
+
label: 'Set Plan',
|
|
237
|
+
description:
|
|
238
|
+
'Create or replace the current plan. Provide a goal and ordered steps. Any existing active plan is abandoned.',
|
|
239
|
+
parameters: Type.Object({
|
|
240
|
+
goal: Type.String({ description: 'The overall goal of the plan' }),
|
|
241
|
+
steps: Type.Array(Type.String(), { description: 'Ordered list of steps to accomplish the goal', minItems: 1, maxItems: 20 }),
|
|
242
|
+
}),
|
|
243
|
+
async execute(_toolCallId: string, params: unknown) {
|
|
244
|
+
const b = await ensureBackend()
|
|
245
|
+
const { goal, steps } = params as { goal: string; steps: string[] }
|
|
246
|
+
const result = b.setPlan(goal, steps)
|
|
247
|
+
if ('error' in result) {
|
|
248
|
+
return { content: [{ type: 'text', text: `Failed: ${result.error}` }] }
|
|
249
|
+
}
|
|
250
|
+
const stepList = result.plan.steps.map((s, i) => ` ${i}. ${s.description}`).join('\n')
|
|
251
|
+
return {
|
|
252
|
+
content: [{ type: 'text', text: `Plan created: ${goal}\n${stepList}` }],
|
|
253
|
+
details: result,
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{ name: 'plan_set' },
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
// plan_step_done — mark a step as complete
|
|
261
|
+
api.registerTool(
|
|
262
|
+
{
|
|
263
|
+
name: 'plan_step_done',
|
|
264
|
+
label: 'Plan Step Done',
|
|
265
|
+
description:
|
|
266
|
+
'Mark a plan step as done by its 0-based index. The coordinator will nudge you for the next step.',
|
|
267
|
+
parameters: Type.Object({
|
|
268
|
+
step: Type.Number({ description: 'Zero-based index of the step to mark as done' }),
|
|
269
|
+
}),
|
|
270
|
+
async execute(_toolCallId: string, params: unknown) {
|
|
271
|
+
const b = await ensureBackend()
|
|
272
|
+
const { step } = params as { step: number }
|
|
273
|
+
const result = b.markPlanStepDone(step)
|
|
274
|
+
if ('error' in result) {
|
|
275
|
+
return { content: [{ type: 'text', text: `Failed: ${result.error}` }] }
|
|
276
|
+
}
|
|
277
|
+
const done = result.plan.steps.filter(s => s.status === 'done').length
|
|
278
|
+
const total = result.plan.steps.length
|
|
279
|
+
const isComplete = result.plan.status === 'completed'
|
|
280
|
+
return {
|
|
281
|
+
content: [{ type: 'text', text: isComplete
|
|
282
|
+
? `Plan completed! All ${total} steps done.`
|
|
283
|
+
: `Step ${step} done (${done}/${total} complete).`
|
|
284
|
+
}],
|
|
285
|
+
details: result,
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{ name: 'plan_step_done' },
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
// plan_abandon — abandon current plan
|
|
293
|
+
api.registerTool(
|
|
294
|
+
{
|
|
295
|
+
name: 'plan_abandon',
|
|
296
|
+
label: 'Abandon Plan',
|
|
297
|
+
description:
|
|
298
|
+
'Abandon the current plan. The coordinator will stop sending plan nudges.',
|
|
299
|
+
parameters: Type.Object({}),
|
|
300
|
+
async execute(_toolCallId: string, _params: unknown) {
|
|
301
|
+
const b = await ensureBackend()
|
|
302
|
+
const result = b.abandonPlan()
|
|
303
|
+
if ('error' in result) {
|
|
304
|
+
return { content: [{ type: 'text', text: `Failed: ${result.error}` }] }
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
content: [{ type: 'text', text: `Plan abandoned: ${result.plan.goal}` }],
|
|
308
|
+
details: result,
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
{ name: 'plan_abandon' },
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
// plan_get — view current plan state
|
|
316
|
+
api.registerTool(
|
|
317
|
+
{
|
|
318
|
+
name: 'plan_get',
|
|
319
|
+
label: 'Get Plan',
|
|
320
|
+
description:
|
|
321
|
+
'View the current plan state including goal, steps, and progress.',
|
|
322
|
+
parameters: Type.Object({}),
|
|
323
|
+
async execute(_toolCallId: string, _params: unknown) {
|
|
324
|
+
const b = await ensureBackend()
|
|
325
|
+
const result = b.getPlan()
|
|
326
|
+
if (!result.plan) {
|
|
327
|
+
return { content: [{ type: 'text', text: 'No active plan.' }] }
|
|
328
|
+
}
|
|
329
|
+
const p = result.plan
|
|
330
|
+
const stepList = p.steps.map((s, i) => {
|
|
331
|
+
const marker = s.status === 'done' ? '[x]' : '[ ]'
|
|
332
|
+
return ` ${marker} ${i}. ${s.description}`
|
|
333
|
+
}).join('\n')
|
|
334
|
+
return {
|
|
335
|
+
content: [{ type: 'text', text: `Plan (${p.status}): ${p.goal}\n${stepList}` }],
|
|
336
|
+
details: result,
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{ name: 'plan_get' },
|
|
341
|
+
)
|
|
342
|
+
|
|
252
343
|
// Service lifecycle
|
|
253
344
|
api.registerService({
|
|
254
345
|
id: 'crawd',
|
package/src/types.ts
CHANGED
|
@@ -13,41 +13,22 @@ export type {
|
|
|
13
13
|
SuperChatInfo,
|
|
14
14
|
} from './lib/chat/types'
|
|
15
15
|
|
|
16
|
-
/** TTS provider identifier */
|
|
17
|
-
export type TtsProvider = 'openai' | 'elevenlabs' | 'tiktok'
|
|
18
|
-
|
|
19
16
|
// --- Socket.IO event payloads ---
|
|
20
17
|
|
|
21
|
-
/** Turn-based reply: chat message + bot response
|
|
18
|
+
/** Turn-based reply: chat message + bot response (text only, TTS handled by overlay) */
|
|
22
19
|
export type ReplyTurnEvent = {
|
|
23
|
-
/** Correlation ID — overlay sends talk:done with this ID when
|
|
20
|
+
/** Correlation ID — overlay sends talk:done with this ID when finished */
|
|
24
21
|
id: string
|
|
25
22
|
chat: { username: string; message: string }
|
|
26
23
|
botMessage: string
|
|
27
|
-
chatTtsUrl: string
|
|
28
|
-
botTtsUrl: string
|
|
29
|
-
/** TTS provider used for the chat audio */
|
|
30
|
-
chatTtsProvider?: TtsProvider
|
|
31
|
-
/** TTS provider used for the bot audio */
|
|
32
|
-
botTtsProvider?: TtsProvider
|
|
33
24
|
}
|
|
34
25
|
|
|
35
|
-
/** Bot speech bubble
|
|
26
|
+
/** Bot speech bubble (text only, TTS handled by overlay) */
|
|
36
27
|
export type TalkEvent = {
|
|
37
|
-
/** Correlation ID — overlay sends talk:done with this ID when
|
|
28
|
+
/** Correlation ID — overlay sends talk:done with this ID when finished */
|
|
38
29
|
id: string
|
|
39
30
|
/** Bot reply text */
|
|
40
31
|
message: string
|
|
41
|
-
/** Bot TTS audio URL */
|
|
42
|
-
ttsUrl: string
|
|
43
|
-
/** TTS provider used for the bot audio */
|
|
44
|
-
ttsProvider?: TtsProvider
|
|
45
|
-
/** Optional: chat message being replied to (overlay plays this first) */
|
|
46
|
-
chat?: {
|
|
47
|
-
message: string
|
|
48
|
-
username: string
|
|
49
|
-
ttsUrl: string
|
|
50
|
-
}
|
|
51
32
|
}
|
|
52
33
|
|
|
53
34
|
/** Overlay → backend acknowledgement that a talk finished playing */
|