@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -0,0 +1,532 @@
1
+ import crypto from 'crypto'
2
+ import fs from 'fs'
3
+ import * as cheerio from 'cheerio'
4
+ import { genId } from '@/lib/id'
5
+ import type { MailboxEnvelope, WatchJob } from '@/types'
6
+ import { requestHeartbeatNow } from './heartbeat-wake'
7
+ import { enqueueSystemEvent } from './system-events'
8
+ import { loadApprovals, loadTasks, loadWatchJobs, upsertWatchJob, upsertWatchJobs } from './storage'
9
+ import { notify } from './ws-hub'
10
+ import { fetchMailboxMessages, getMailboxHighwaterUid } from './mailbox-utils'
11
+
12
+ export interface CreateWatchJobInput {
13
+ type: WatchJob['type']
14
+ sessionId?: string | null
15
+ agentId?: string | null
16
+ createdByAgentId?: string | null
17
+ browserProfileId?: string | null
18
+ description?: string | null
19
+ resumeMessage: string
20
+ target: Record<string, unknown>
21
+ condition: Record<string, unknown>
22
+ runAt?: number | null
23
+ intervalMs?: number | null
24
+ timeoutAt?: number | null
25
+ }
26
+
27
+ function now() {
28
+ return Date.now()
29
+ }
30
+
31
+ function hashContent(value: string): string {
32
+ return crypto.createHash('sha1').update(value).digest('hex')
33
+ }
34
+
35
+ function cleanHtmlToText(html: string): string {
36
+ const $ = cheerio.load(html)
37
+ $('script, style, noscript').remove()
38
+ return $('body').text().replace(/\s+/g, ' ').trim()
39
+ }
40
+
41
+ function matchesRegex(body: string, pattern: unknown): boolean {
42
+ if (typeof pattern !== 'string' || !pattern.trim()) return false
43
+ try {
44
+ return new RegExp(pattern, 'i').test(body)
45
+ } catch {
46
+ return false
47
+ }
48
+ }
49
+
50
+ function scheduleNextCheck(job: WatchJob, at = now()): WatchJob {
51
+ const intervalMs = typeof job.intervalMs === 'number' && job.intervalMs > 0 ? job.intervalMs : 60_000
52
+ return {
53
+ ...job,
54
+ nextCheckAt: at + intervalMs,
55
+ updatedAt: at,
56
+ }
57
+ }
58
+
59
+ function notifyWatchJobsChanged() {
60
+ notify('watch_jobs')
61
+ }
62
+
63
+ function finalizeWatchJob(job: WatchJob, status: WatchJob['status'], result?: Record<string, unknown> | null, error?: string | null): WatchJob {
64
+ const updated: WatchJob = {
65
+ ...job,
66
+ status,
67
+ result: result ?? job.result ?? null,
68
+ lastError: error ?? null,
69
+ lastTriggeredAt: status === 'triggered' ? now() : job.lastTriggeredAt ?? null,
70
+ updatedAt: now(),
71
+ }
72
+ upsertWatchJob(updated.id, updated)
73
+ notifyWatchJobsChanged()
74
+ return updated
75
+ }
76
+
77
+ function wakeFromWatch(job: WatchJob, result?: Record<string, unknown> | null) {
78
+ const summary = job.description || `Watch ${job.id}`
79
+ const detail = result ? JSON.stringify(result).slice(0, 1200) : ''
80
+ if (job.sessionId) {
81
+ enqueueSystemEvent(
82
+ job.sessionId,
83
+ `[Watch Triggered] ${summary}\n${job.resumeMessage}${detail ? `\n\nObserved:\n${detail}` : ''}`,
84
+ )
85
+ requestHeartbeatNow({ sessionId: job.sessionId, reason: 'watch_job' })
86
+ } else if (job.agentId) {
87
+ requestHeartbeatNow({ agentId: job.agentId, reason: 'watch_job' })
88
+ }
89
+ }
90
+
91
+ export async function createWatchJob(input: CreateWatchJobInput): Promise<WatchJob> {
92
+ if (input.type === 'time' && typeof input.runAt !== 'number') {
93
+ throw new Error('Time watches require runAt or delayMinutes.')
94
+ }
95
+ if ((input.type === 'http' || input.type === 'page') && typeof input.target?.url !== 'string') {
96
+ throw new Error(`${input.type} watches require a url target.`)
97
+ }
98
+ if (input.type === 'file' && typeof input.target?.path !== 'string') {
99
+ throw new Error('File watches require a path target.')
100
+ }
101
+ if (input.type === 'task' && typeof input.target?.taskId !== 'string') {
102
+ throw new Error('Task watches require a taskId target.')
103
+ }
104
+ if (input.type === 'webhook' && typeof input.target?.webhookId !== 'string') {
105
+ throw new Error('Webhook watches require a webhookId target.')
106
+ }
107
+ if (input.type === 'email' && typeof input.target?.folder !== 'string' && typeof input.target?.folder !== 'undefined') {
108
+ throw new Error('Email watches expect a string folder when provided.')
109
+ }
110
+ if (input.type === 'mailbox' && typeof input.target?.sessionId !== 'string') {
111
+ throw new Error('Mailbox watches require a sessionId target.')
112
+ }
113
+ if (input.type === 'approval' && typeof input.target?.approvalId !== 'string') {
114
+ throw new Error('Approval watches require an approvalId target.')
115
+ }
116
+ const createdAt = now()
117
+ const job: WatchJob = {
118
+ id: genId(10),
119
+ type: input.type,
120
+ status: 'active',
121
+ description: input.description || null,
122
+ sessionId: input.sessionId || null,
123
+ agentId: input.agentId || null,
124
+ createdByAgentId: input.createdByAgentId || null,
125
+ browserProfileId: input.browserProfileId || null,
126
+ resumeMessage: input.resumeMessage,
127
+ target: { ...(input.target || {}) },
128
+ condition: { ...(input.condition || {}) },
129
+ runAt: input.runAt ?? null,
130
+ nextCheckAt: input.runAt ?? createdAt,
131
+ intervalMs: input.intervalMs ?? (input.type === 'time' ? null : 60_000),
132
+ timeoutAt: input.timeoutAt ?? null,
133
+ lastCheckedAt: null,
134
+ lastTriggeredAt: null,
135
+ lastError: null,
136
+ result: null,
137
+ createdAt,
138
+ updatedAt: createdAt,
139
+ }
140
+
141
+ // Capture initial baselines for change watches.
142
+ if ((job.type === 'http' || job.type === 'page') && typeof job.target.url === 'string' && job.condition.changed === true) {
143
+ try {
144
+ const res = await fetch(job.target.url, { signal: AbortSignal.timeout(15_000) })
145
+ if (res.ok) {
146
+ const text = job.type === 'page'
147
+ ? cleanHtmlToText(await res.text())
148
+ : await res.text()
149
+ job.target = { ...job.target, baselineHash: hashContent(text) }
150
+ }
151
+ } catch {
152
+ // Baseline creation is best-effort; the watch can still run later.
153
+ }
154
+ }
155
+
156
+ if (job.type === 'file' && typeof job.target.path === 'string' && job.condition.changed === true) {
157
+ try {
158
+ if (fs.existsSync(job.target.path)) {
159
+ const text = fs.readFileSync(job.target.path, 'utf8')
160
+ job.target = { ...job.target, baselineHash: hashContent(text) }
161
+ }
162
+ } catch {
163
+ // Best-effort baseline only.
164
+ }
165
+ }
166
+
167
+ if (job.type === 'email') {
168
+ try {
169
+ const baselineUid = await getMailboxHighwaterUid(undefined, typeof job.target.folder === 'string' ? job.target.folder : undefined)
170
+ job.target = {
171
+ ...job.target,
172
+ baselineUid,
173
+ }
174
+ } catch {
175
+ // best-effort baseline only
176
+ }
177
+ }
178
+
179
+ upsertWatchJob(job.id, job)
180
+ notifyWatchJobsChanged()
181
+ return job
182
+ }
183
+
184
+ export function cancelWatchJob(id: string): WatchJob | null {
185
+ const all = loadWatchJobs()
186
+ const current = all[id]
187
+ if (!current || typeof current !== 'object') return null
188
+ return finalizeWatchJob(current as WatchJob, 'cancelled', null, null)
189
+ }
190
+
191
+ export function getWatchJob(id: string): WatchJob | null {
192
+ const all = loadWatchJobs()
193
+ const current = all[id]
194
+ if (!current || typeof current !== 'object') return null
195
+ return current as WatchJob
196
+ }
197
+
198
+ export function listWatchJobs(filter?: { sessionId?: string | null; status?: WatchJob['status'] | null }): WatchJob[] {
199
+ return Object.values(loadWatchJobs())
200
+ .filter((job): job is WatchJob => !!job && typeof job === 'object')
201
+ .filter((job) => !filter?.sessionId || job.sessionId === filter.sessionId)
202
+ .filter((job) => !filter?.status || job.status === filter.status)
203
+ .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
204
+ }
205
+
206
+ async function evaluateHttpLikeJob(job: WatchJob, asPage: boolean): Promise<{ triggered: boolean; result: Record<string, unknown> }> {
207
+ const url = typeof job.target.url === 'string' ? job.target.url : ''
208
+ if (!url) return { triggered: false, result: { error: 'Missing url' } }
209
+ const startedAt = Date.now()
210
+ const res = await fetch(url, { signal: AbortSignal.timeout(15_000) })
211
+ const latencyMs = Date.now() - startedAt
212
+ const raw = await res.text()
213
+ const body = asPage ? cleanHtmlToText(raw) : raw
214
+ const bodyHash = hashContent(body)
215
+ const containsText = typeof job.condition.containsText === 'string' ? job.condition.containsText : ''
216
+ const textGone = typeof job.condition.textGone === 'string' ? job.condition.textGone : ''
217
+ const statusEquals = typeof job.condition.status === 'number' ? job.condition.status : null
218
+ const statusIn = Array.isArray(job.condition.statusIn)
219
+ ? job.condition.statusIn.filter((value): value is number => typeof value === 'number')
220
+ : []
221
+ const changed = job.condition.changed === true
222
+ const latencyThreshold = typeof job.condition.threshold === 'number' ? job.condition.threshold : null
223
+ const baselineHash = typeof job.target.baselineHash === 'string' ? job.target.baselineHash : ''
224
+ const regexMatched = matchesRegex(body, job.condition.regex)
225
+ const triggered =
226
+ (statusEquals !== null && res.status === statusEquals)
227
+ || (statusIn.length > 0 && statusIn.includes(res.status))
228
+ || (!!containsText && body.includes(containsText))
229
+ || (!!textGone && !body.includes(textGone))
230
+ || regexMatched
231
+ || (!!changed && !!baselineHash && baselineHash !== bodyHash)
232
+ || (latencyThreshold !== null && latencyMs >= latencyThreshold)
233
+ return {
234
+ triggered,
235
+ result: {
236
+ url,
237
+ status: res.status,
238
+ latencyMs,
239
+ containsText: containsText || undefined,
240
+ regex: typeof job.condition.regex === 'string' ? job.condition.regex : undefined,
241
+ changed: changed || undefined,
242
+ bodyHash,
243
+ preview: body.slice(0, 1200),
244
+ },
245
+ }
246
+ }
247
+
248
+ function evaluateFileJob(job: WatchJob): { triggered: boolean; result: Record<string, unknown> } {
249
+ const targetPath = typeof job.target.path === 'string' ? job.target.path : ''
250
+ if (!targetPath) return { triggered: false, result: { error: 'Missing path' } }
251
+ const exists = fs.existsSync(targetPath)
252
+ const expectExists = job.condition.exists !== false
253
+ const containsText = typeof job.condition.containsText === 'string' ? job.condition.containsText : ''
254
+ const changed = job.condition.changed === true
255
+ let text = ''
256
+ let bodyHash = ''
257
+ try {
258
+ if (exists) {
259
+ text = fs.readFileSync(targetPath, 'utf8')
260
+ bodyHash = hashContent(text)
261
+ }
262
+ } catch {
263
+ text = ''
264
+ }
265
+ const baselineHash = typeof job.target.baselineHash === 'string' ? job.target.baselineHash : ''
266
+ const triggered =
267
+ exists === expectExists
268
+ && (!containsText || text.includes(containsText))
269
+ && (!job.condition.regex || matchesRegex(text, job.condition.regex))
270
+ && (!changed || (!!baselineHash && baselineHash !== bodyHash))
271
+ return {
272
+ triggered,
273
+ result: {
274
+ path: targetPath,
275
+ exists,
276
+ regex: typeof job.condition.regex === 'string' ? job.condition.regex : undefined,
277
+ bodyHash: bodyHash || undefined,
278
+ preview: text.slice(0, 1200),
279
+ },
280
+ }
281
+ }
282
+
283
+ function evaluateTaskJob(job: WatchJob): { triggered: boolean; result: Record<string, unknown> } {
284
+ const taskId = typeof job.target.taskId === 'string' ? job.target.taskId : ''
285
+ if (!taskId) return { triggered: false, result: { error: 'Missing taskId' } }
286
+ const tasks = loadTasks()
287
+ const task = tasks[taskId] as Record<string, unknown> | undefined
288
+ const statuses = Array.isArray(job.condition.statusIn)
289
+ ? job.condition.statusIn.filter((value): value is string => typeof value === 'string')
290
+ : ['completed', 'failed']
291
+ const currentStatus = typeof task?.status === 'string' ? task.status : 'missing'
292
+ return {
293
+ triggered: statuses.includes(currentStatus),
294
+ result: {
295
+ taskId,
296
+ status: currentStatus,
297
+ title: typeof task?.title === 'string' ? task.title : null,
298
+ result: typeof task?.result === 'string' ? task.result.slice(0, 1000) : null,
299
+ error: typeof task?.error === 'string' ? task.error : null,
300
+ },
301
+ }
302
+ }
303
+
304
+ async function evaluateWatchJob(job: WatchJob): Promise<{ triggered: boolean; result: Record<string, unknown> }> {
305
+ if (job.type === 'time') {
306
+ const runAt = typeof job.runAt === 'number' ? job.runAt : 0
307
+ return { triggered: runAt > 0 && runAt <= now(), result: { runAt } }
308
+ }
309
+ if (job.type === 'http') return evaluateHttpLikeJob(job, false)
310
+ if (job.type === 'page') return evaluateHttpLikeJob(job, true)
311
+ if (job.type === 'file') return evaluateFileJob(job)
312
+ if (job.type === 'task') return evaluateTaskJob(job)
313
+ if (job.type === 'email') {
314
+ const folder = typeof job.target.folder === 'string' ? job.target.folder : undefined
315
+ const messages = await fetchMailboxMessages({
316
+ folder,
317
+ from: typeof job.condition.from === 'string' ? job.condition.from : undefined,
318
+ subjectContains: typeof job.condition.subjectContains === 'string' ? job.condition.subjectContains : undefined,
319
+ bodyContains: typeof job.condition.containsText === 'string' ? job.condition.containsText : undefined,
320
+ query: typeof job.condition.query === 'string' ? job.condition.query : undefined,
321
+ unreadOnly: job.condition.unreadOnly === true,
322
+ hasAttachments: job.condition.hasAttachments === true,
323
+ uidGreaterThan: typeof job.target.baselineUid === 'number' ? job.target.baselineUid : undefined,
324
+ limit: 20,
325
+ })
326
+ const match = messages[0]
327
+ return {
328
+ triggered: !!match,
329
+ result: match
330
+ ? {
331
+ uid: match.uid,
332
+ from: match.from,
333
+ subject: match.subject,
334
+ snippet: match.snippet,
335
+ attachmentCount: match.attachments.length,
336
+ messageId: match.messageId,
337
+ }
338
+ : {
339
+ folder: folder || 'INBOX',
340
+ baselineUid: typeof job.target.baselineUid === 'number' ? job.target.baselineUid : null,
341
+ },
342
+ }
343
+ }
344
+ return { triggered: false, result: { note: 'Webhook waits are triggered by inbound webhook delivery.' } }
345
+ }
346
+
347
+ export async function processDueWatchJobs(timestamp = now()): Promise<{ checked: number; triggered: number; failed: number }> {
348
+ const all = listWatchJobs({ status: 'active' })
349
+ let checked = 0
350
+ let triggered = 0
351
+ let failed = 0
352
+ const updates: Array<[string, WatchJob]> = []
353
+
354
+ for (const job of all) {
355
+ if (typeof job.timeoutAt === 'number' && job.timeoutAt > 0 && job.timeoutAt <= timestamp) {
356
+ failed += 1
357
+ updates.push([job.id, {
358
+ ...job,
359
+ status: 'failed',
360
+ lastError: 'Watch timed out before condition was met.',
361
+ updatedAt: timestamp,
362
+ }])
363
+ continue
364
+ }
365
+ if (typeof job.nextCheckAt === 'number' && job.nextCheckAt > timestamp) continue
366
+ if (job.type === 'webhook' || job.type === 'mailbox' || job.type === 'approval') continue
367
+
368
+ checked += 1
369
+ try {
370
+ const evaluation = await evaluateWatchJob(job)
371
+ if (evaluation.triggered) {
372
+ triggered += 1
373
+ const completed = {
374
+ ...job,
375
+ status: 'triggered' as const,
376
+ result: evaluation.result,
377
+ lastError: null,
378
+ lastCheckedAt: timestamp,
379
+ lastTriggeredAt: timestamp,
380
+ updatedAt: timestamp,
381
+ }
382
+ updates.push([job.id, completed])
383
+ wakeFromWatch(completed, evaluation.result)
384
+ } else {
385
+ updates.push([job.id, scheduleNextCheck({
386
+ ...job,
387
+ lastCheckedAt: timestamp,
388
+ result: evaluation.result,
389
+ }, timestamp)])
390
+ }
391
+ } catch (err: unknown) {
392
+ failed += 1
393
+ updates.push([job.id, scheduleNextCheck({
394
+ ...job,
395
+ lastCheckedAt: timestamp,
396
+ lastError: err instanceof Error ? err.message : String(err),
397
+ }, timestamp)])
398
+ }
399
+ }
400
+
401
+ if (updates.length > 0) {
402
+ upsertWatchJobs(updates)
403
+ notifyWatchJobsChanged()
404
+ }
405
+
406
+ return { checked, triggered, failed }
407
+ }
408
+
409
+ export function triggerWebhookWatchJobs(params: {
410
+ webhookId: string
411
+ event: string
412
+ payloadPreview?: string
413
+ }): WatchJob[] {
414
+ const matches = listWatchJobs({ status: 'active' }).filter((job) => {
415
+ if (job.type !== 'webhook') return false
416
+ const watchWebhookId = typeof job.target.webhookId === 'string' ? job.target.webhookId : ''
417
+ if (watchWebhookId !== params.webhookId) return false
418
+ const expectedEvent = typeof job.condition.event === 'string' ? job.condition.event.trim() : ''
419
+ return !expectedEvent || expectedEvent === params.event
420
+ })
421
+
422
+ const updated = matches.map((job) => {
423
+ const next: WatchJob = {
424
+ ...job,
425
+ status: 'triggered',
426
+ result: {
427
+ webhookId: params.webhookId,
428
+ event: params.event,
429
+ payloadPreview: params.payloadPreview?.slice(0, 1200) || '',
430
+ },
431
+ lastTriggeredAt: now(),
432
+ updatedAt: now(),
433
+ }
434
+ wakeFromWatch(next, next.result || null)
435
+ return [next.id, next] as [string, WatchJob]
436
+ })
437
+
438
+ if (updated.length > 0) {
439
+ upsertWatchJobs(updated)
440
+ notifyWatchJobsChanged()
441
+ }
442
+
443
+ return updated.map(([, job]) => job)
444
+ }
445
+
446
+ export function triggerMailboxWatchJobs(params: {
447
+ sessionId: string
448
+ envelope: MailboxEnvelope
449
+ }): WatchJob[] {
450
+ const matches = listWatchJobs({ status: 'active' }).filter((job) => {
451
+ if (job.type !== 'mailbox') return false
452
+ const targetSessionId = typeof job.target.sessionId === 'string' ? job.target.sessionId : ''
453
+ if (targetSessionId !== params.sessionId) return false
454
+ const expectedType = typeof job.condition.type === 'string' ? job.condition.type.trim() : ''
455
+ const correlationId = typeof job.condition.correlationId === 'string' ? job.condition.correlationId.trim() : ''
456
+ const fromSessionId = typeof job.condition.fromSessionId === 'string' ? job.condition.fromSessionId.trim() : ''
457
+ const payloadContains = typeof job.condition.containsText === 'string' ? job.condition.containsText.trim() : ''
458
+ if (expectedType && params.envelope.type !== expectedType) return false
459
+ if (correlationId && params.envelope.correlationId !== correlationId) return false
460
+ if (fromSessionId && params.envelope.fromSessionId !== fromSessionId) return false
461
+ if (payloadContains && !params.envelope.payload.includes(payloadContains)) return false
462
+ return true
463
+ })
464
+
465
+ const updated = matches.map((job) => {
466
+ const next: WatchJob = {
467
+ ...job,
468
+ status: 'triggered',
469
+ result: {
470
+ envelopeId: params.envelope.id,
471
+ type: params.envelope.type,
472
+ correlationId: params.envelope.correlationId || null,
473
+ payload: params.envelope.payload.slice(0, 1200),
474
+ fromSessionId: params.envelope.fromSessionId || null,
475
+ },
476
+ lastTriggeredAt: now(),
477
+ updatedAt: now(),
478
+ }
479
+ wakeFromWatch(next, next.result || null)
480
+ return [next.id, next] as [string, WatchJob]
481
+ })
482
+
483
+ if (updated.length > 0) {
484
+ upsertWatchJobs(updated)
485
+ notifyWatchJobsChanged()
486
+ }
487
+
488
+ return updated.map(([, job]) => job)
489
+ }
490
+
491
+ export function triggerApprovalWatchJobs(params: {
492
+ approvalId: string
493
+ status: 'approved' | 'rejected'
494
+ title?: string
495
+ description?: string
496
+ }): WatchJob[] {
497
+ const approvals = loadApprovals()
498
+ const approval = approvals[params.approvalId] as Record<string, unknown> | undefined
499
+ const matches = listWatchJobs({ status: 'active' }).filter((job) => {
500
+ if (job.type !== 'approval') return false
501
+ const targetApprovalId = typeof job.target.approvalId === 'string' ? job.target.approvalId : ''
502
+ if (targetApprovalId !== params.approvalId) return false
503
+ const statuses = Array.isArray(job.condition.statusIn)
504
+ ? job.condition.statusIn.filter((value): value is string => typeof value === 'string')
505
+ : ['approved']
506
+ return statuses.includes(params.status)
507
+ })
508
+
509
+ const updated = matches.map((job) => {
510
+ const next: WatchJob = {
511
+ ...job,
512
+ status: 'triggered',
513
+ result: {
514
+ approvalId: params.approvalId,
515
+ status: params.status,
516
+ title: params.title || (typeof approval?.title === 'string' ? approval.title : null),
517
+ description: params.description || (typeof approval?.description === 'string' ? approval.description : null),
518
+ },
519
+ lastTriggeredAt: now(),
520
+ updatedAt: now(),
521
+ }
522
+ wakeFromWatch(next, next.result || null)
523
+ return [next.id, next] as [string, WatchJob]
524
+ })
525
+
526
+ if (updated.length > 0) {
527
+ upsertWatchJobs(updated)
528
+ notifyWatchJobsChanged()
529
+ }
530
+
531
+ return updated.map(([, job]) => job)
532
+ }
@@ -1,6 +1,7 @@
1
1
  import { WebSocketServer, WebSocket } from 'ws'
2
2
  import type { IncomingMessage } from 'http'
3
3
  import { validateAccessKey } from './storage'
4
+ import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
4
5
 
5
6
  interface WsClient {
6
7
  ws: WebSocket
@@ -29,9 +30,10 @@ export function initWsServer() {
29
30
  ;(globalThis as any)[GK] = hub
30
31
 
31
32
  wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
32
- // Auth: validate ?key= from upgrade URL
33
- const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
34
- const key = url.searchParams.get('key') || ''
33
+ const headerKey = req.headers['x-access-key']
34
+ const key = (Array.isArray(headerKey) ? headerKey[0] : headerKey)
35
+ || getCookieValue(req.headers.cookie, AUTH_COOKIE_NAME)
36
+ || ''
35
37
  if (!validateAccessKey(key)) {
36
38
  ws.close(4001, 'Unauthorized')
37
39
  return
@@ -25,6 +25,10 @@ export const AVAILABLE_TOOLS: ToolDefinition[] = [
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, write, and test custom SwarmClaw plugins dynamically' },
27
27
  { id: 'sample_ui', label: 'Sample UI', description: 'Demonstration of dynamic UI injection into Sidebar and Chat Header' },
28
+ { id: 'image_gen', label: 'Image Generation', description: 'Generate images from text prompts using OpenAI, Stability AI, Replicate, fal.ai, and more' },
29
+ { id: 'email', label: 'Email', description: 'Send emails via SMTP with plain text and HTML support' },
30
+ { id: 'calendar', label: 'Calendar', description: 'Manage Google Calendar or Outlook events — list, create, update, delete' },
31
+ { id: 'replicate', label: 'Replicate', description: 'Run any AI model on Replicate — image generation, LLMs, audio, video, and more' },
28
32
  ]
29
33
 
30
34
  /**
@@ -0,0 +1,26 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { AgentCreateSchema } from './schemas'
4
+
5
+ describe('AgentCreateSchema', () => {
6
+ it('defaults platformAssignScope to self', () => {
7
+ const parsed = AgentCreateSchema.parse({
8
+ name: 'Solo Agent',
9
+ provider: 'openai',
10
+ })
11
+
12
+ assert.equal(parsed.platformAssignScope, 'self')
13
+ })
14
+
15
+ it('accepts explicit all-scope delegation without relying on legacy orchestrator flags', () => {
16
+ const parsed = AgentCreateSchema.parse({
17
+ name: 'Coordinator',
18
+ provider: 'openai',
19
+ platformAssignScope: 'all',
20
+ isOrchestrator: false,
21
+ })
22
+
23
+ assert.equal(parsed.platformAssignScope, 'all')
24
+ assert.equal(parsed.isOrchestrator, false)
25
+ })
26
+ })
@@ -9,11 +9,20 @@ export const AgentCreateSchema = z.object({
9
9
  credentialId: z.string().nullable().optional().default(null),
10
10
  apiEndpoint: z.string().nullable().optional().default(null),
11
11
  isOrchestrator: z.boolean().optional().default(false),
12
+ platformAssignScope: z.enum(['self', 'all']).optional().default('self'),
12
13
  subAgentIds: z.array(z.string()).optional().default([]),
13
- tools: z.array(z.string()).optional().default([]),
14
+ plugins: z.array(z.string()).optional().default([]),
15
+ /** @deprecated Use plugins */
16
+ tools: z.array(z.string()).optional(),
14
17
  capabilities: z.array(z.string()).optional().default([]),
15
18
  thinkingLevel: z.string().optional(),
16
19
  soul: z.string().optional(),
20
+ identityState: z.record(z.string(), z.unknown()).nullable().optional().default(null),
21
+ sessionResetMode: z.enum(['idle', 'daily']).nullable().optional().default(null),
22
+ sessionIdleTimeoutSec: z.number().int().nonnegative().nullable().optional().default(null),
23
+ sessionMaxAgeSec: z.number().int().nonnegative().nullable().optional().default(null),
24
+ sessionDailyResetAt: z.string().nullable().optional().default(null),
25
+ sessionResetTimezone: z.string().nullable().optional().default(null),
17
26
  autoRecovery: z.boolean().optional().default(false),
18
27
  monthlyBudget: z.number().positive().nullable().optional().default(null),
19
28
  dailyBudget: z.number().positive().nullable().optional().default(null),
@@ -1,15 +1,15 @@
1
1
  type WsCallback = () => void
2
2
 
3
3
  let ws: WebSocket | null = null
4
- let accessKey = ''
4
+ let wsEnabled = false
5
5
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null
6
6
  let reconnectDelay = 1000
7
7
  const MAX_RECONNECT_DELAY = 30_000
8
8
  const listeners = new Map<string, Set<WsCallback>>()
9
9
  let connected = false
10
10
 
11
- function getWsUrl(key: string): string {
12
- if (typeof window === 'undefined') return `ws://localhost:3457/ws?key=${encodeURIComponent(key)}`
11
+ function getWsUrl(): string {
12
+ if (typeof window === 'undefined') return 'ws://localhost:3457/ws'
13
13
 
14
14
  const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
15
15
  const pagePort = window.location.port
@@ -22,7 +22,7 @@ function getWsUrl(key: string): string {
22
22
  const behindProxy = !pagePort || pagePort === '80' || pagePort === '443' || pagePort !== appPort
23
23
  const wsHost = behindProxy ? window.location.host : `${window.location.hostname}:${buildPort}`
24
24
 
25
- return `${protocol}://${wsHost}/ws?key=${encodeURIComponent(key)}`
25
+ return `${protocol}://${wsHost}/ws`
26
26
  }
27
27
 
28
28
  function handleMessage(event: MessageEvent) {
@@ -44,17 +44,18 @@ function scheduleReconnect() {
44
44
  const jitter = Math.random() * 2000
45
45
  reconnectTimer = setTimeout(() => {
46
46
  reconnectTimer = null
47
- if (!accessKey) return
48
- connect(accessKey)
47
+ if (!wsEnabled) return
48
+ connect()
49
49
  }, reconnectDelay + jitter)
50
50
  reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY)
51
51
  }
52
52
 
53
- function connect(key: string) {
53
+ function connect() {
54
+ if (!wsEnabled) return
54
55
  if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return
55
56
 
56
57
  try {
57
- ws = new WebSocket(getWsUrl(key))
58
+ ws = new WebSocket(getWsUrl())
58
59
  } catch {
59
60
  scheduleReconnect()
60
61
  return
@@ -75,7 +76,7 @@ function connect(key: string) {
75
76
  ws.onclose = () => {
76
77
  connected = false
77
78
  ws = null
78
- if (accessKey) scheduleReconnect()
79
+ if (wsEnabled) scheduleReconnect()
79
80
  }
80
81
 
81
82
  ws.onerror = () => {
@@ -84,13 +85,14 @@ function connect(key: string) {
84
85
  }
85
86
 
86
87
  export function connectWs(key: string) {
87
- accessKey = key
88
+ void key
89
+ wsEnabled = true
88
90
  reconnectDelay = 1000
89
- connect(key)
91
+ connect()
90
92
  }
91
93
 
92
94
  export function disconnectWs() {
93
- accessKey = ''
95
+ wsEnabled = false
94
96
  if (reconnectTimer) {
95
97
  clearTimeout(reconnectTimer)
96
98
  reconnectTimer = null