@swarmclawai/swarmclaw 0.9.7 → 0.9.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -75,9 +75,9 @@
75
75
  "@huggingface/transformers": "^3.8.1",
76
76
  "@langchain/anthropic": "^1.3.18",
77
77
  "@langchain/core": "^1.1.31",
78
+ "@modelcontextprotocol/sdk": "^1.27.1",
78
79
  "@langchain/langgraph": "^1.2.2",
79
80
  "@langchain/openai": "^1.2.8",
80
- "@modelcontextprotocol/sdk": "^1.27.1",
81
81
  "@multiavatar/multiavatar": "^1.0.7",
82
82
  "@playwright/mcp": "^0.0.68",
83
83
  "@slack/bolt": "^4.6.0",
@@ -104,6 +104,8 @@ export function ScheduleSheet() {
104
104
  const [cron, setCron] = useState('0 * * * *')
105
105
  const [intervalMs, setIntervalMs] = useState(3600000)
106
106
  const [status, setStatus] = useState<ScheduleStatus>('active')
107
+ const [taskMode, setTaskMode] = useState<'task' | 'wake_only'>('task')
108
+ const [message, setMessage] = useState('')
107
109
  const [customCron, setCustomCron] = useState(false)
108
110
  const [confirmDelete, setConfirmDelete] = useState(false)
109
111
  const [deleting, setDeleting] = useState(false)
@@ -131,6 +133,8 @@ export function ScheduleSheet() {
131
133
  setCron(editing.cron || '0 * * * *')
132
134
  setIntervalMs(editing.intervalMs || 3600000)
133
135
  setStatus(editing.status)
136
+ setTaskMode(editing.taskMode === 'wake_only' ? 'wake_only' : 'task')
137
+ setMessage(editing.message || '')
134
138
  setCustomCron(!CRON_PRESETS.some((p) => p.cron === editing.cron))
135
139
  } else if (templatePrefill) {
136
140
  // Opened from a quick-start card with pre-filled values
@@ -155,6 +159,8 @@ export function ScheduleSheet() {
155
159
  setCron('0 * * * *')
156
160
  setIntervalMs(3600000)
157
161
  setStatus('active')
162
+ setTaskMode('task')
163
+ setMessage('')
158
164
  setCustomCron(false)
159
165
  }
160
166
  }
@@ -178,7 +184,9 @@ export function ScheduleSheet() {
178
184
  const data = {
179
185
  name: name.trim(),
180
186
  agentId,
181
- taskPrompt,
187
+ taskPrompt: taskMode === 'wake_only' ? message : taskPrompt,
188
+ taskMode,
189
+ message: taskMode === 'wake_only' ? message : undefined,
182
190
  scheduleType,
183
191
  cron: scheduleType === 'cron' ? cron : undefined,
184
192
  intervalMs: scheduleType === 'interval' ? intervalMs : undefined,
@@ -217,7 +225,7 @@ export function ScheduleSheet() {
217
225
  }
218
226
 
219
227
  // Step validation
220
- const step0Valid = name.trim().length > 0 && agentId.length > 0 && taskPrompt.trim().length > 0
228
+ const step0Valid = name.trim().length > 0 && agentId.length > 0 && (taskMode === 'wake_only' ? message.trim().length > 0 : taskPrompt.trim().length > 0)
221
229
  const step1Valid = scheduleType === 'cron' ? cron.trim().length > 0 : intervalMs > 0
222
230
 
223
231
  const selectedAgent = agentId ? agents[agentId] : null
@@ -333,16 +341,61 @@ export function ScheduleSheet() {
333
341
  </div>
334
342
 
335
343
  <div className="mb-8">
336
- <SectionLabel>Task Prompt</SectionLabel>
337
- <textarea
338
- value={taskPrompt}
339
- onChange={(e) => setTaskPrompt(e.target.value)}
340
- placeholder="What should the agent do when triggered?"
341
- rows={4}
342
- className={`${inputClass} resize-y min-h-[100px]`}
343
- style={{ fontFamily: 'inherit' }}
344
- />
344
+ <div className="flex items-center gap-2 mb-3">
345
+ <SectionLabel className="mb-0">Task Mode</SectionLabel>
346
+ <HintTip text="Create task: creates a board task for the agent. Wake agent only: sends a message to the agent without creating a task." />
347
+ </div>
348
+ <div className="grid grid-cols-2 gap-3">
349
+ <button
350
+ onClick={() => setTaskMode('task')}
351
+ className={`py-3 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
352
+ active:scale-[0.97] text-[14px] font-600 border
353
+ ${taskMode === 'task'
354
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
355
+ : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
356
+ style={{ fontFamily: 'inherit' }}
357
+ >
358
+ Create task
359
+ </button>
360
+ <button
361
+ onClick={() => setTaskMode('wake_only')}
362
+ className={`py-3 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
363
+ active:scale-[0.97] text-[14px] font-600 border
364
+ ${taskMode === 'wake_only'
365
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
366
+ : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
367
+ style={{ fontFamily: 'inherit' }}
368
+ >
369
+ Wake agent only
370
+ </button>
371
+ </div>
345
372
  </div>
373
+
374
+ {taskMode === 'wake_only' ? (
375
+ <div className="mb-8">
376
+ <SectionLabel>Wake Message</SectionLabel>
377
+ <textarea
378
+ value={message}
379
+ onChange={(e) => setMessage(e.target.value)}
380
+ placeholder="Message to send to the agent when woken"
381
+ rows={4}
382
+ className={`${inputClass} resize-y min-h-[100px]`}
383
+ style={{ fontFamily: 'inherit' }}
384
+ />
385
+ </div>
386
+ ) : (
387
+ <div className="mb-8">
388
+ <SectionLabel>Task Prompt</SectionLabel>
389
+ <textarea
390
+ value={taskPrompt}
391
+ onChange={(e) => setTaskPrompt(e.target.value)}
392
+ placeholder="What should the agent do when triggered?"
393
+ rows={4}
394
+ className={`${inputClass} resize-y min-h-[100px]`}
395
+ style={{ fontFamily: 'inherit' }}
396
+ />
397
+ </div>
398
+ )}
346
399
  </div>
347
400
  )}
348
401
 
@@ -498,8 +551,12 @@ export function ScheduleSheet() {
498
551
  </div>
499
552
  )}
500
553
  <div>
501
- <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Task</span>
502
- <div className="text-[13px] text-text-2 mt-0.5 whitespace-pre-wrap">{taskPrompt}</div>
554
+ <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Mode</span>
555
+ <div className="text-[14px] text-text font-600 mt-0.5">{taskMode === 'wake_only' ? 'Wake agent only' : 'Create task'}</div>
556
+ </div>
557
+ <div>
558
+ <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">{taskMode === 'wake_only' ? 'Wake Message' : 'Task'}</span>
559
+ <div className="text-[13px] text-text-2 mt-0.5 whitespace-pre-wrap">{taskMode === 'wake_only' ? message : taskPrompt}</div>
503
560
  </div>
504
561
  <div className="h-px bg-white/[0.06]" />
505
562
  <div>
@@ -6,7 +6,6 @@ import '@/lib/server/session-tools/memory'
6
6
  import '@/lib/server/session-tools/platform'
7
7
  import '@/lib/server/session-tools/monitor'
8
8
  import '@/lib/server/session-tools/discovery'
9
- import '@/lib/server/session-tools/sample-ui'
10
9
  import '@/lib/server/session-tools/git'
11
10
  import '@/lib/server/session-tools/wallet'
12
11
  import '@/lib/server/session-tools/connector'
@@ -40,6 +40,8 @@ interface SchedulerScheduleLike {
40
40
  followupThreadId?: string | null
41
41
  followupSenderId?: string | null
42
42
  followupSenderName?: string | null
43
+ taskMode?: 'task' | 'wake_only'
44
+ message?: string
43
45
  }
44
46
 
45
47
  export function startScheduler() {
@@ -67,7 +69,10 @@ function computeNextRuns() {
67
69
  if (schedule.status !== 'active') continue
68
70
  if (schedule.scheduleType === 'cron' && schedule.cron && !schedule.nextRunAt) {
69
71
  try {
70
- const interval = CronExpressionParser.parse(schedule.cron)
72
+ const interval = CronExpressionParser.parse(
73
+ schedule.cron,
74
+ schedule.timezone ? { tz: schedule.timezone } : undefined,
75
+ )
71
76
  schedule.nextRunAt = interval.next().getTime()
72
77
  changedEntries.push([schedule.id, schedule])
73
78
  } catch (err) {
@@ -157,34 +162,57 @@ async function tick() {
157
162
  // Compute next run
158
163
  advanceSchedule(schedule)
159
164
 
160
- const { taskId } = prepareScheduledTaskRun({
161
- schedule,
162
- tasks,
163
- now,
164
- scheduleSignature,
165
- })
166
-
167
- upsertTask(taskId, tasks[taskId])
168
- upsertSchedule(schedule.id, schedule)
169
-
170
- enqueueTask(taskId)
171
- if (scheduleSignature) inFlightScheduleKeys.add(scheduleSignature)
172
- pushMainLoopEventToMainSessions({
173
- type: 'schedule_fired',
174
- text: `Schedule fired: "${schedule.name}" (${schedule.id}) run #${schedule.runNumber} — task ${taskId}`,
175
- })
176
-
177
- // Enqueue system event + heartbeat wake for the schedule's agent
178
- if (schedule.createdInSessionId) {
179
- enqueueSystemEvent(schedule.createdInSessionId, `Schedule triggered: ${schedule.name}`)
165
+ if (schedule.taskMode === 'wake_only') {
166
+ // Wake-only: no board task, just heartbeat the agent
167
+ upsertSchedule(schedule.id, schedule)
168
+
169
+ const wakeMessage = schedule.message || `Schedule triggered: ${schedule.name}`
170
+ pushMainLoopEventToMainSessions({
171
+ type: 'schedule_fired',
172
+ text: `Schedule fired (wake-only): "${schedule.name}" (${schedule.id}) run #${schedule.runNumber}`,
173
+ })
174
+
175
+ if (schedule.createdInSessionId) {
176
+ enqueueSystemEvent(schedule.createdInSessionId, wakeMessage)
177
+ }
178
+ requestHeartbeatNow({
179
+ agentId: schedule.agentId,
180
+ eventId: `${schedule.id}:${schedule.runNumber}`,
181
+ reason: 'schedule',
182
+ source: `schedule:${schedule.id}`,
183
+ resumeMessage: wakeMessage,
184
+ detail: `Run #${schedule.runNumber} (wake-only).`,
185
+ })
186
+ } else {
187
+ // Default task mode: create a board task
188
+ const { taskId } = prepareScheduledTaskRun({
189
+ schedule,
190
+ tasks,
191
+ now,
192
+ scheduleSignature,
193
+ })
194
+
195
+ upsertTask(taskId, tasks[taskId])
196
+ upsertSchedule(schedule.id, schedule)
197
+
198
+ enqueueTask(taskId)
199
+ if (scheduleSignature) inFlightScheduleKeys.add(scheduleSignature)
200
+ pushMainLoopEventToMainSessions({
201
+ type: 'schedule_fired',
202
+ text: `Schedule fired: "${schedule.name}" (${schedule.id}) run #${schedule.runNumber} — task ${taskId}`,
203
+ })
204
+
205
+ if (schedule.createdInSessionId) {
206
+ enqueueSystemEvent(schedule.createdInSessionId, `Schedule triggered: ${schedule.name}`)
207
+ }
208
+ requestHeartbeatNow({
209
+ agentId: schedule.agentId,
210
+ eventId: `${schedule.id}:${schedule.runNumber}`,
211
+ reason: 'schedule',
212
+ source: `schedule:${schedule.id}`,
213
+ resumeMessage: `Schedule triggered: ${schedule.name}`,
214
+ detail: `Run #${schedule.runNumber} queued task ${taskId}.`,
215
+ })
180
216
  }
181
- requestHeartbeatNow({
182
- agentId: schedule.agentId,
183
- eventId: `${schedule.id}:${schedule.runNumber}`,
184
- reason: 'schedule',
185
- source: `schedule:${schedule.id}`,
186
- resumeMessage: `Schedule triggered: ${schedule.name}`,
187
- detail: `Run #${schedule.runNumber} queued task ${taskId}.`,
188
- })
189
217
  }
190
218
  }
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
+ import { CronExpressionParser } from 'cron-parser'
3
4
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
4
5
 
5
6
  type SchedulePayload = Record<string, unknown>
@@ -212,11 +213,24 @@ export function normalizeSchedulePayload(payload: SchedulePayload, opts: Normali
212
213
  }
213
214
  normalized.agentId = agentId
214
215
 
215
- const taskPrompt = deriveTaskPrompt(normalized)
216
- if (!taskPrompt) {
217
- return { ok: false, error: 'Error: schedules require a taskPrompt, command, or action/path payload.' }
216
+ // Preserve taskMode and message fields
217
+ const taskMode = normalized.taskMode === 'wake_only' ? 'wake_only' : 'task'
218
+ normalized.taskMode = taskMode
219
+ if (taskMode === 'wake_only') {
220
+ const message = trimString(normalized.message)
221
+ if (!message) {
222
+ return { ok: false, error: 'Error: wake_only schedules require a message.' }
223
+ }
224
+ normalized.message = message
225
+ // wake_only still needs a taskPrompt for display/logging — derive or use message
226
+ normalized.taskPrompt = normalized.taskPrompt ? trimString(normalized.taskPrompt) : message
227
+ } else {
228
+ const taskPrompt = deriveTaskPrompt(normalized)
229
+ if (!taskPrompt) {
230
+ return { ok: false, error: 'Error: schedules require a taskPrompt, command, or action/path payload.' }
231
+ }
232
+ normalized.taskPrompt = taskPrompt
218
233
  }
219
- normalized.taskPrompt = taskPrompt
220
234
 
221
235
  const validationError = validateScheduleArtifacts(normalized, baseDir)
222
236
  if (validationError) return { ok: false, error: `Error: ${validationError}` }
@@ -228,6 +242,17 @@ export function normalizeSchedulePayload(payload: SchedulePayload, opts: Normali
228
242
  } else if (normalized.scheduleType === 'interval') {
229
243
  const intervalMs = normalizePositiveInt(normalized.intervalMs)
230
244
  if (intervalMs != null) normalized.nextRunAt = applyStagger(now + intervalMs, normalized.staggerSec as number | null)
245
+ } else if (normalized.scheduleType === 'cron' && normalized.cron) {
246
+ try {
247
+ const cronTimezone = trimString(normalized.timezone)
248
+ const interval = CronExpressionParser.parse(
249
+ normalized.cron as string,
250
+ cronTimezone ? { tz: cronTimezone } : undefined,
251
+ )
252
+ normalized.nextRunAt = applyStagger(interval.next().getTime(), normalized.staggerSec as number | null)
253
+ } catch {
254
+ return { ok: false, error: 'Error: invalid cron expression.' }
255
+ }
231
256
  }
232
257
  }
233
258
 
@@ -32,7 +32,6 @@ import { buildOpenClawNodeTools } from './openclaw-nodes'
32
32
  import { buildContextTools } from './context-mgmt'
33
33
  import { buildDiscoveryTools } from './discovery'
34
34
  import { buildMonitorTools } from './monitor'
35
- import { buildSampleUITools } from './sample-ui'
36
35
  import { buildPluginCreatorTools } from './plugin-creator'
37
36
  import { buildImageGenTools } from './image-gen'
38
37
  import { buildEmailTools } from './email'
@@ -182,7 +181,6 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
182
181
  ['context_mgmt', buildContextTools],
183
182
  ['discovery', buildDiscoveryTools],
184
183
  ['monitor', buildMonitorTools],
185
- ['sample_ui', buildSampleUITools],
186
184
  ['plugin_creator', buildPluginCreatorTools],
187
185
  ['image_gen', buildImageGenTools],
188
186
  ['email', buildEmailTools],
@@ -24,7 +24,6 @@ const PLUGIN_ALIAS_GROUPS: string[][] = [
24
24
  ['sandbox', 'sandbox_exec', 'sandbox_list_runtimes'],
25
25
  ['wallet', 'wallet_tool'],
26
26
  ['monitor', 'monitor_tool'],
27
- ['sample_ui', 'show_plugin_card'],
28
27
  ['context_mgmt', 'context_status', 'context_summarize'],
29
28
  ['openclaw_workspace'],
30
29
  ['openclaw_nodes'],
@@ -83,7 +83,6 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
83
83
  spawn_subagent: { categories: ['delegation', 'platform'], concreteTools: ['spawn_subagent', 'delegate_to_agent'] },
84
84
  context_mgmt: { categories: ['memory'], concreteTools: ['context_mgmt', 'context_status', 'context_summarize'] },
85
85
  plugin_creator: { categories: ['filesystem', 'execution'], concreteTools: ['plugin_creator', 'plugin_creator_tool'] },
86
- sample_ui: { categories: ['platform'], concreteTools: ['sample_ui', 'show_plugin_card'] },
87
86
  mailbox: { categories: ['network', 'platform', 'outbound'], concreteTools: ['mailbox', 'inbox'] },
88
87
  ask_human: { categories: ['platform'], concreteTools: ['ask_human', 'human_loop'] },
89
88
  document: { categories: ['filesystem', 'platform'], concreteTools: ['document', 'ocr_document', 'parse_document'] },
@@ -34,7 +34,6 @@ const UNIVERSAL_CORE_PLUGIN_IDS = [
34
34
  'context_mgmt',
35
35
  'discovery',
36
36
  'plugin_creator',
37
- 'sample_ui',
38
37
  'image_gen',
39
38
  'email',
40
39
  'calendar',
@@ -24,7 +24,6 @@ export const AVAILABLE_TOOLS: ToolDefinition[] = [
24
24
  { id: 'wallet', label: 'Wallet', description: 'Manage agent crypto wallet — check balance, send SOL, view transactions' },
25
25
  { id: 'monitor', label: 'Monitor', description: 'System observability: check resource usage, watch logs, and ping endpoints' },
26
26
  { id: 'plugin_creator', label: 'Plugin Creator', description: 'Design focused plugins for durable capabilities and recurring automations' },
27
- { id: 'sample_ui', label: 'Sample UI', description: 'Demonstration of dynamic UI injection into Sidebar and Chat Header' },
28
27
  { id: 'image_gen', label: 'Image Generation', description: 'Generate images from text prompts using OpenAI, Stability AI, Replicate, fal.ai, and more' },
29
28
  { id: 'email', label: 'Email', description: 'Send emails via SMTP with plain text and HTML support' },
30
29
  { id: 'calendar', label: 'Calendar', description: 'Manage Google Calendar or Outlook events — list, create, update, delete' },
@@ -986,6 +986,10 @@ export interface Schedule {
986
986
  agentId: string
987
987
  projectId?: string
988
988
  taskPrompt: string
989
+ /** 'task' (default) creates a board task; 'wake_only' just wakes the agent with a message */
990
+ taskMode?: 'task' | 'wake_only'
991
+ /** Wake message sent to agent when taskMode is 'wake_only' */
992
+ message?: string
989
993
  scheduleType: ScheduleType
990
994
  action?: string
991
995
  path?: string
@@ -1,97 +0,0 @@
1
- import { z } from 'zod'
2
- import { tool } from '@langchain/core/tools'
3
- import { getPluginManager } from '../plugins'
4
- import type { Plugin, PluginHooks } from '@/types'
5
- import { normalizeToolInputArgs } from './normalize-tool-args'
6
-
7
- /**
8
- * Sample UI Extension Plugin
9
- * This demonstrates how a plugin can add a sidebar item,
10
- * a chat header widget, and a custom message type.
11
- */
12
- const SampleUIPlugin: Plugin = {
13
- name: 'Sample UI',
14
- description: 'Demonstration of plugin-driven UI: Sidebar, Header, and Chat.',
15
- ui: {
16
- sidebarItems: [
17
- {
18
- id: 'sample-dashboard',
19
- label: 'Plugin View',
20
- href: 'https://openclaw.ai',
21
- position: 'top'
22
- }
23
- ],
24
- headerWidgets: [
25
- {
26
- id: 'sample-status',
27
- label: '🔌 Plugin Active'
28
- }
29
- ],
30
- chatInputActions: [
31
- {
32
- id: 'sample-action',
33
- label: 'Quick Scan',
34
- tooltip: 'Run a sample system scan',
35
- action: 'message',
36
- value: 'Please perform a quick system scan and report the health.'
37
- }
38
- ]
39
- },
40
- hooks: {
41
- transformInboundMessage: async ({ text }) => {
42
- console.log('[plugin:sample_ui] Transforming inbound message')
43
- return text // No-op but demonstrates hook
44
- },
45
- transformOutboundMessage: async ({ text }) => {
46
- console.log('[plugin:sample_ui] Transforming outbound message')
47
- return text + '\n\n*-- Sent via Sample UI Plugin --*'
48
- }
49
- } as PluginHooks,
50
- tools: [
51
- {
52
- name: 'show_plugin_card',
53
- description: 'Trigger a rich UI card in the chat using the plugin-ui message kind.',
54
- parameters: {
55
- type: 'object',
56
- properties: {
57
- title: { type: 'string' },
58
- content: { type: 'string' }
59
- },
60
- required: ['title', 'content']
61
- },
62
- execute: async (args) => {
63
- const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
64
- const title = normalized.title as string
65
- const content = normalized.content as string
66
- // Return a structured payload that the frontend MessageBubble will interpret
67
- return JSON.stringify({
68
- kind: 'plugin-ui',
69
- text: `### ${title}\n\n${content}`,
70
- actions: [
71
- { id: 'view-more', label: 'View Details', href: 'https://openclaw.ai' }
72
- ]
73
- })
74
- }
75
- }
76
- ]
77
- }
78
-
79
- // Auto-register
80
- getPluginManager().registerBuiltin('sample_ui', SampleUIPlugin)
81
-
82
- export function buildSampleUITools(bctx: any) {
83
- if (!bctx.hasPlugin('sample_ui')) return []
84
- return [
85
- tool(
86
- async (args) => SampleUIPlugin.tools![0].execute(args as any, bctx),
87
- {
88
- name: 'show_plugin_card',
89
- description: SampleUIPlugin.tools![0].description,
90
- schema: z.object({
91
- title: z.string(),
92
- content: z.string()
93
- })
94
- }
95
- )
96
- ]
97
- }