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/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, type TtsVoiceEntry } from './backend/server.js'
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
- 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 },
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', 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' },
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 TTS, chat integration, and OBS overlay',
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 and generates TTS audio. Use for narration, vibes, and commentary — NOT for replying to chat (use livestream_reply for that).',
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. 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.',
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, each with TTS audio */
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 both audios finish */
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 with pre-generated TTS (atomic event) */
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 audio finishes */
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 */