@swarmclawai/swarmclaw 0.6.6 → 0.6.7

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 (80) hide show
  1. package/README.md +57 -27
  2. package/package.json +6 -1
  3. package/src/app/api/agents/[id]/clone/route.ts +40 -0
  4. package/src/app/api/agents/route.ts +39 -14
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +17 -1
  6. package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
  7. package/src/app/api/chatrooms/[id]/route.ts +19 -1
  8. package/src/app/api/chatrooms/route.ts +12 -2
  9. package/src/app/api/connectors/[id]/health/route.ts +64 -0
  10. package/src/app/api/connectors/route.ts +17 -2
  11. package/src/app/api/knowledge/route.ts +6 -1
  12. package/src/app/api/openclaw/doctor/route.ts +17 -0
  13. package/src/app/api/sessions/[id]/chat/route.ts +5 -1
  14. package/src/app/api/sessions/route.ts +11 -2
  15. package/src/app/api/tasks/[id]/route.ts +18 -13
  16. package/src/app/api/tasks/route.ts +20 -1
  17. package/src/app/api/usage/route.ts +16 -7
  18. package/src/cli/index.js +5 -0
  19. package/src/cli/index.ts +223 -39
  20. package/src/components/agents/agent-card.tsx +37 -6
  21. package/src/components/agents/agent-chat-list.tsx +78 -2
  22. package/src/components/agents/agent-sheet.tsx +79 -0
  23. package/src/components/auth/setup-wizard.tsx +268 -353
  24. package/src/components/chat/chat-area.tsx +22 -7
  25. package/src/components/chat/message-bubble.tsx +14 -14
  26. package/src/components/chat/message-list.tsx +1 -1
  27. package/src/components/chatrooms/chatroom-message.tsx +164 -22
  28. package/src/components/chatrooms/chatroom-sheet.tsx +288 -3
  29. package/src/components/chatrooms/chatroom-view.tsx +62 -17
  30. package/src/components/connectors/connector-health.tsx +120 -0
  31. package/src/components/connectors/connector-sheet.tsx +9 -0
  32. package/src/components/home/home-view.tsx +23 -2
  33. package/src/components/input/chat-input.tsx +8 -1
  34. package/src/components/layout/app-layout.tsx +17 -1
  35. package/src/components/schedules/schedule-list.tsx +55 -9
  36. package/src/components/schedules/schedule-sheet.tsx +134 -23
  37. package/src/components/shared/command-palette.tsx +237 -0
  38. package/src/components/shared/connector-platform-icon.tsx +1 -0
  39. package/src/components/tasks/task-card.tsx +22 -2
  40. package/src/components/tasks/task-sheet.tsx +91 -16
  41. package/src/components/usage/metrics-dashboard.tsx +13 -25
  42. package/src/hooks/use-swipe.ts +49 -0
  43. package/src/lib/providers/anthropic.ts +16 -2
  44. package/src/lib/providers/claude-cli.ts +7 -1
  45. package/src/lib/providers/index.ts +7 -0
  46. package/src/lib/providers/ollama.ts +16 -2
  47. package/src/lib/providers/openai.ts +7 -2
  48. package/src/lib/providers/openclaw.ts +6 -1
  49. package/src/lib/providers/provider-defaults.ts +7 -0
  50. package/src/lib/schedule-templates.ts +115 -0
  51. package/src/lib/server/alert-dispatch.ts +64 -0
  52. package/src/lib/server/chat-execution.ts +41 -1
  53. package/src/lib/server/chatroom-helpers.ts +22 -1
  54. package/src/lib/server/chatroom-routing.ts +65 -0
  55. package/src/lib/server/connectors/discord.ts +3 -0
  56. package/src/lib/server/connectors/email.ts +267 -0
  57. package/src/lib/server/connectors/manager.ts +159 -3
  58. package/src/lib/server/connectors/openclaw.ts +3 -0
  59. package/src/lib/server/connectors/slack.ts +6 -0
  60. package/src/lib/server/connectors/telegram.ts +18 -0
  61. package/src/lib/server/connectors/types.ts +2 -0
  62. package/src/lib/server/connectors/whatsapp.ts +9 -0
  63. package/src/lib/server/cost.ts +70 -0
  64. package/src/lib/server/create-notification.ts +2 -0
  65. package/src/lib/server/daemon-state.ts +124 -0
  66. package/src/lib/server/dag-validation.ts +115 -0
  67. package/src/lib/server/memory-db.ts +12 -7
  68. package/src/lib/server/openclaw-doctor.ts +48 -0
  69. package/src/lib/server/queue.ts +12 -0
  70. package/src/lib/server/session-run-manager.ts +22 -1
  71. package/src/lib/server/session-tools/index.ts +2 -0
  72. package/src/lib/server/session-tools/memory.ts +22 -3
  73. package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
  74. package/src/lib/server/storage.ts +120 -6
  75. package/src/lib/setup-defaults.ts +277 -0
  76. package/src/lib/validation/schemas.ts +69 -0
  77. package/src/stores/use-app-store.ts +7 -3
  78. package/src/stores/use-chatroom-store.ts +52 -2
  79. package/src/types/index.ts +38 -1
  80. package/tsconfig.json +2 -1
@@ -6,7 +6,7 @@ import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
7
7
  import { useWs } from '@/hooks/use-ws'
8
8
  import { api } from '@/lib/api-client'
9
- import { createAgent, deleteAgent } from '@/lib/agents'
9
+ import { deleteAgent } from '@/lib/agents'
10
10
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
11
11
  import {
12
12
  DropdownMenu,
@@ -79,11 +79,18 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
79
79
  setRunning(false)
80
80
  }
81
81
 
82
+ const [cloning, setCloning] = useState(false)
82
83
  const handleDuplicate = async () => {
83
- const { id: _id, createdAt: _ca, updatedAt: _ua, ...rest } = agent
84
- await createAgent({ ...rest, name: agent.name + ' (Copy)' })
85
- await loadAgents()
86
- toast.success('Agent duplicated')
84
+ setCloning(true)
85
+ try {
86
+ await api('POST', `/agents/${agent.id}/clone`)
87
+ await loadAgents()
88
+ toast.success('Agent duplicated')
89
+ } catch (err: unknown) {
90
+ toast.error(err instanceof Error ? err.message : String(err))
91
+ } finally {
92
+ setCloning(false)
93
+ }
87
94
  }
88
95
 
89
96
  const handleDelete = async () => {
@@ -140,7 +147,9 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
140
147
  <DropdownMenuItem onClick={() => { togglePinAgent(agent.id); toast.success(agent.pinned ? 'Agent unpinned' : 'Agent pinned') }}>
141
148
  {agent.pinned ? 'Unpin' : 'Pin'}
142
149
  </DropdownMenuItem>
143
- <DropdownMenuItem onClick={handleDuplicate}>Duplicate</DropdownMenuItem>
150
+ <DropdownMenuItem onClick={handleDuplicate} disabled={cloning}>
151
+ {cloning ? 'Duplicating...' : 'Duplicate'}
152
+ </DropdownMenuItem>
144
153
  {!isDefault && onSetDefault && (
145
154
  <DropdownMenuItem onClick={() => { onSetDefault(agent.id); toast.success(`${agent.name} set as default`) }}>Set Default</DropdownMenuItem>
146
155
  )}
@@ -217,6 +226,28 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
217
226
  <span>Cost: ${agent.totalCost.toFixed(2)}</span>
218
227
  )}
219
228
  </div>
229
+ {typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0 && (
230
+ <div className="mt-2">
231
+ <div className="flex items-center justify-between text-[10px] text-text-3/60 mb-1">
232
+ <span>${(agent.monthlySpend ?? 0).toFixed(2)} / ${agent.monthlyBudget.toFixed(2)}</span>
233
+ <span className={`font-600 ${(agent.monthlySpend ?? 0) >= agent.monthlyBudget ? 'text-red-400' : 'text-text-3/50'}`}>
234
+ {agent.budgetAction === 'block' ? 'hard cap' : 'soft cap'}
235
+ </span>
236
+ </div>
237
+ <div className="h-1 rounded-full bg-white/[0.06] overflow-hidden">
238
+ <div
239
+ className={`h-full rounded-full transition-all duration-300 ${
240
+ (agent.monthlySpend ?? 0) >= agent.monthlyBudget
241
+ ? 'bg-red-400'
242
+ : (agent.monthlySpend ?? 0) >= agent.monthlyBudget * 0.8
243
+ ? 'bg-amber-400'
244
+ : 'bg-accent'
245
+ }`}
246
+ style={{ width: `${Math.min(100, ((agent.monthlySpend ?? 0) / agent.monthlyBudget) * 100)}%` }}
247
+ />
248
+ </div>
249
+ </div>
250
+ )}
220
251
  </div>
221
252
 
222
253
  <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
@@ -5,6 +5,8 @@ import { useAppStore } from '@/stores/use-app-store'
5
5
  import { useChatStore } from '@/stores/use-chat-store'
6
6
  import { useChatroomStore } from '@/stores/use-chatroom-store'
7
7
  import { fetchMessages } from '@/lib/sessions'
8
+ import { api } from '@/lib/api-client'
9
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
8
10
  import type { Agent, Session } from '@/types'
9
11
  import { AgentAvatar } from './agent-avatar'
10
12
  import { toast } from 'sonner'
@@ -32,6 +34,39 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
32
34
  const chatrooms = useChatroomStore((s) => s.chatrooms)
33
35
  const chatroomStreaming = useChatroomStore((s) => s.streamingAgents)
34
36
  const [search, setSearch] = useState('')
37
+ const [bulkMode, setBulkMode] = useState(false)
38
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
39
+ const [confirmBulkDelete, setConfirmBulkDelete] = useState(false)
40
+ const loadSessions = useAppStore((s) => s.loadSessions)
41
+
42
+ const toggleSelected = useCallback((id: string) => {
43
+ setSelectedIds((prev) => {
44
+ const next = new Set(prev)
45
+ if (next.has(id)) next.delete(id)
46
+ else next.add(id)
47
+ return next
48
+ })
49
+ }, [])
50
+
51
+ const handleBulkDelete = useCallback(async () => {
52
+ // Collect session IDs for selected agents
53
+ const sessionIds = [...selectedIds]
54
+ .map((agentId) => {
55
+ const agent = agents[agentId]
56
+ return agent?.threadSessionId
57
+ })
58
+ .filter(Boolean) as string[]
59
+ if (!sessionIds.length) { toast.error('No chats to delete'); return }
60
+ try {
61
+ await api('DELETE', '/sessions', { ids: sessionIds })
62
+ await loadSessions()
63
+ toast.success(`Deleted ${sessionIds.length} chat(s)`)
64
+ setBulkMode(false)
65
+ setSelectedIds(new Set())
66
+ } catch {
67
+ toast.error('Failed to delete chats')
68
+ }
69
+ }, [selectedIds, agents, loadSessions])
35
70
 
36
71
  // FLIP animation refs
37
72
  const rowRefs = useRef<Map<string, HTMLElement>>(new Map())
@@ -169,7 +204,7 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
169
204
 
170
205
  return (
171
206
  <div className="flex-1 overflow-y-auto">
172
- {/* Filter control */}
207
+ {/* Filter control + bulk mode toggle */}
173
208
  {sortedAgents.length > 2 && (
174
209
  <div className="flex items-center gap-1 px-4 pt-2.5 pb-1">
175
210
  {(['all', 'active', 'recent'] as const).map((f) => (
@@ -185,6 +220,28 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
185
220
  {f}
186
221
  </button>
187
222
  ))}
223
+ <button
224
+ type="button"
225
+ onClick={() => { setBulkMode(!bulkMode); setSelectedIds(new Set()) }}
226
+ aria-label={bulkMode ? 'Exit selection mode' : 'Select chats'}
227
+ className={`ml-auto label-mono px-2.5 py-1 rounded-[6px] border-none cursor-pointer transition-colors
228
+ ${bulkMode ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
229
+ >
230
+ {bulkMode ? 'Cancel' : 'Select'}
231
+ </button>
232
+ </div>
233
+ )}
234
+ {/* Bulk action bar */}
235
+ {bulkMode && selectedIds.size > 0 && (
236
+ <div className="flex items-center gap-2 px-4 py-2 bg-white/[0.02] border-b border-white/[0.04]">
237
+ <span className="text-[12px] text-text-2 font-500 flex-1">{selectedIds.size} selected</span>
238
+ <button
239
+ onClick={() => setConfirmBulkDelete(true)}
240
+ className="px-3 py-1.5 rounded-[8px] border-none bg-red-500/10 text-red-400 text-[12px] font-600 cursor-pointer hover:bg-red-500/20 transition-colors"
241
+ style={{ fontFamily: 'inherit' }}
242
+ >
243
+ Delete
244
+ </button>
188
245
  </div>
189
246
  )}
190
247
  {(sortedAgents.length > 5 || search) && (
@@ -219,9 +276,19 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
219
276
  ${isActive
220
277
  ? 'bg-accent-soft/80 border border-accent-bright/20'
221
278
  : 'bg-transparent hover:bg-white/[0.02]'}`}
222
- onClick={() => handleSelect(agent)}
279
+ onClick={() => bulkMode ? toggleSelected(agent.id) : handleSelect(agent)}
223
280
  >
224
281
  <div className="flex items-center gap-2.5">
282
+ {bulkMode && (
283
+ <div className={`w-5 h-5 rounded-[6px] border-2 flex items-center justify-center shrink-0 transition-colors
284
+ ${selectedIds.has(agent.id) ? 'bg-accent-bright border-accent-bright' : 'border-white/20 bg-transparent'}`}>
285
+ {selectedIds.has(agent.id) && (
286
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
287
+ <polyline points="20 6 9 17 4 12" />
288
+ </svg>
289
+ )}
290
+ </div>
291
+ )}
225
292
  <div className="relative shrink-0">
226
293
  <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={36} />
227
294
  <div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-bg ${
@@ -303,6 +370,15 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
303
370
  )
304
371
  })}
305
372
  </div>
373
+ <ConfirmDialog
374
+ open={confirmBulkDelete}
375
+ title="Delete Chats"
376
+ message={`Delete ${selectedIds.size} chat(s)? This cannot be undone.`}
377
+ confirmLabel="Delete"
378
+ danger
379
+ onConfirm={() => { setConfirmBulkDelete(false); handleBulkDelete() }}
380
+ onCancel={() => setConfirmBulkDelete(false)}
381
+ />
306
382
  </div>
307
383
  )
308
384
  }
@@ -114,6 +114,9 @@ export function AgentSheet() {
114
114
  const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
115
115
  const [heartbeatModel, setHeartbeatModel] = useState('')
116
116
  const [heartbeatPrompt, setHeartbeatPrompt] = useState('')
117
+ const [budgetEnabled, setBudgetEnabled] = useState(false)
118
+ const [monthlyBudget, setMonthlyBudget] = useState('')
119
+ const [budgetAction, setBudgetAction] = useState<'warn' | 'block'>('warn')
117
120
  const [agentWallet, setAgentWallet] = useState<(Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }) | null>(null)
118
121
  const [addingKey, setAddingKey] = useState(false)
119
122
  const [newKeyName, setNewKeyName] = useState('')
@@ -195,6 +198,9 @@ export function AgentSheet() {
195
198
  setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
196
199
  setHeartbeatModel(editing.heartbeatModel || '')
197
200
  setHeartbeatPrompt(editing.heartbeatPrompt || '')
201
+ setBudgetEnabled(typeof editing.monthlyBudget === 'number' && editing.monthlyBudget > 0)
202
+ setMonthlyBudget(typeof editing.monthlyBudget === 'number' && editing.monthlyBudget > 0 ? String(editing.monthlyBudget) : '')
203
+ setBudgetAction(editing.budgetAction || 'warn')
198
204
  // Load wallet if agent has one
199
205
  if (editing.walletId) {
200
206
  api<Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }>('GET', `/wallets/${editing.walletId}`)
@@ -234,6 +240,9 @@ export function AgentSheet() {
234
240
  setHeartbeatIntervalSec('')
235
241
  setHeartbeatModel('')
236
242
  setHeartbeatPrompt('')
243
+ setBudgetEnabled(false)
244
+ setMonthlyBudget('')
245
+ setBudgetAction('warn')
237
246
  }
238
247
  }
239
248
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -331,6 +340,8 @@ export function AgentSheet() {
331
340
  heartbeatIntervalSec: heartbeatIntervalSec ? Number(heartbeatIntervalSec) : null,
332
341
  heartbeatModel: heartbeatModel.trim() || null,
333
342
  heartbeatPrompt: heartbeatPrompt.trim() || null,
343
+ monthlyBudget: budgetEnabled && monthlyBudget ? Number(monthlyBudget) : null,
344
+ budgetAction: budgetEnabled ? budgetAction : undefined,
334
345
  }
335
346
  if (editing) {
336
347
  await updateAgent(editing.id, data)
@@ -732,6 +743,74 @@ export function AgentSheet() {
732
743
  <p className="text-[11px] text-text-3/70 mt-1.5">Periodic check-in runs on idle sessions using this agent. Processes pending events and monitors status.</p>
733
744
  </div>
734
745
 
746
+ {/* Monthly Budget */}
747
+ <div className="mb-8">
748
+ <div className="flex items-center justify-between mb-3">
749
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Monthly Budget</label>
750
+ <button
751
+ type="button"
752
+ onClick={() => setBudgetEnabled(!budgetEnabled)}
753
+ className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${budgetEnabled ? 'bg-accent' : 'bg-white/[0.12]'}`}
754
+ >
755
+ <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${budgetEnabled ? 'translate-x-[18px]' : ''}`} />
756
+ </button>
757
+ </div>
758
+ {budgetEnabled && (
759
+ <div className="space-y-4 mt-3">
760
+ <div>
761
+ <label className="block text-[12px] text-text-3/70 mb-1.5">Budget cap (USD)</label>
762
+ <div className="relative">
763
+ <span className="absolute left-3.5 top-1/2 -translate-y-1/2 text-text-3/50 text-[14px]">$</span>
764
+ <input
765
+ type="number"
766
+ min="0.01"
767
+ step="0.01"
768
+ value={monthlyBudget}
769
+ onChange={(e) => setMonthlyBudget(e.target.value)}
770
+ placeholder="10.00"
771
+ className={`${inputClass} pl-7`}
772
+ style={{ fontFamily: 'inherit' }}
773
+ />
774
+ </div>
775
+ </div>
776
+ <div>
777
+ <label className="block text-[12px] text-text-3/70 mb-1.5">When exceeded</label>
778
+ <div className="flex gap-2">
779
+ <button
780
+ type="button"
781
+ onClick={() => setBudgetAction('warn')}
782
+ className={`flex-1 px-3 py-2.5 rounded-[10px] border text-[13px] font-600 cursor-pointer transition-all ${
783
+ budgetAction === 'warn'
784
+ ? 'border-amber-400/40 bg-amber-400/[0.08] text-amber-400'
785
+ : 'border-white/[0.08] bg-transparent text-text-3 hover:bg-white/[0.04]'
786
+ }`}
787
+ style={{ fontFamily: 'inherit' }}
788
+ >
789
+ Warn
790
+ </button>
791
+ <button
792
+ type="button"
793
+ onClick={() => setBudgetAction('block')}
794
+ className={`flex-1 px-3 py-2.5 rounded-[10px] border text-[13px] font-600 cursor-pointer transition-all ${
795
+ budgetAction === 'block'
796
+ ? 'border-red-400/40 bg-red-400/[0.08] text-red-400'
797
+ : 'border-white/[0.08] bg-transparent text-text-3 hover:bg-white/[0.04]'
798
+ }`}
799
+ style={{ fontFamily: 'inherit' }}
800
+ >
801
+ Block
802
+ </button>
803
+ </div>
804
+ </div>
805
+ </div>
806
+ )}
807
+ <p className="text-[11px] text-text-3/70 mt-1.5">
808
+ {budgetAction === 'block'
809
+ ? 'Cap monthly spend for this agent. When exceeded, chat runs are blocked until the next month.'
810
+ : 'Cap monthly spend for this agent. When exceeded, a warning is shown but runs continue.'}
811
+ </p>
812
+ </div>
813
+
735
814
  {/* Wallet Section */}
736
815
  {editingId && (
737
816
  <WalletSection