@swarmclawai/swarmclaw 0.2.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.
Files changed (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,83 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useRef, useState } from 'react'
4
+
5
+ interface SpeechRecognitionErrorEvent {
6
+ error?: string
7
+ }
8
+
9
+ interface SpeechRecognitionEvent {
10
+ results: { [index: number]: { [index: number]: { transcript: string } } }
11
+ }
12
+
13
+ interface UseSpeechRecognitionOptions {
14
+ lang?: string
15
+ }
16
+
17
+ export function useSpeechRecognition(onResult: (text: string) => void, options?: UseSpeechRecognitionOptions) {
18
+ const [recording, setRecording] = useState(false)
19
+ const [error, setError] = useState<string | null>(null)
20
+ const recogRef = useRef<{
21
+ stop: () => void
22
+ start: () => void
23
+ continuous: boolean
24
+ interimResults: boolean
25
+ lang: string
26
+ maxAlternatives?: number
27
+ onresult?: (e: SpeechRecognitionEvent) => void
28
+ onerror?: (e: SpeechRecognitionErrorEvent) => void
29
+ onend?: () => void
30
+ } | null>(null)
31
+
32
+ const toggle = useCallback(() => {
33
+ setError(null)
34
+ if (recording) {
35
+ recogRef.current?.stop()
36
+ setRecording(false)
37
+ return
38
+ }
39
+
40
+ const SR = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
41
+ if (!SR) {
42
+ setError('Speech recognition is not supported in this browser.')
43
+ return
44
+ }
45
+
46
+ const recog = new SR()
47
+ recog.continuous = false
48
+ recog.interimResults = false
49
+ recog.maxAlternatives = 1
50
+ recog.lang = options?.lang || (typeof navigator !== 'undefined' ? navigator.language : 'en-US')
51
+
52
+ recog.onresult = (e: SpeechRecognitionEvent) => {
53
+ setRecording(false)
54
+ const transcript = e.results?.[0]?.[0]?.transcript?.trim() || ''
55
+ if (transcript) onResult(transcript)
56
+ }
57
+ recog.onerror = (e: SpeechRecognitionErrorEvent) => {
58
+ setRecording(false)
59
+ const code = e?.error || 'unknown'
60
+ const message = code === 'not-allowed'
61
+ ? 'Microphone access denied. Allow mic permission and try again.'
62
+ : code === 'no-speech'
63
+ ? 'No speech detected. Try again.'
64
+ : `Speech recognition error: ${code}`
65
+ setError(message)
66
+ }
67
+ recog.onend = () => setRecording(false)
68
+
69
+ recogRef.current = recog
70
+ setRecording(true)
71
+ try {
72
+ recog.start()
73
+ } catch {
74
+ setRecording(false)
75
+ setError('Could not start speech recognition.')
76
+ }
77
+ }, [recording, onResult, options?.lang])
78
+
79
+ const supported = typeof window !== 'undefined' &&
80
+ !!((window as any).SpeechRecognition || (window as any).webkitSpeechRecognition)
81
+
82
+ return { recording, toggle, supported, error }
83
+ }
@@ -0,0 +1,8 @@
1
+ export async function register() {
2
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
3
+ const { startScheduler } = await import('./lib/server/scheduler')
4
+ const { resumeQueue } = await import('./lib/server/queue')
5
+ startScheduler()
6
+ resumeQueue()
7
+ }
8
+ }
@@ -0,0 +1,13 @@
1
+ import { api } from './api-client'
2
+ import type { Agent } from '../types'
3
+
4
+ export const fetchAgents = () => api<Record<string, Agent>>('GET', '/agents')
5
+
6
+ export const createAgent = (data: Omit<Agent, 'id' | 'createdAt' | 'updatedAt'>) =>
7
+ api<Agent>('POST', '/agents', data)
8
+
9
+ export const updateAgent = (id: string, data: Partial<Agent>) =>
10
+ api<Agent>('PUT', `/agents/${id}`, data)
11
+
12
+ export const deleteAgent = (id: string) =>
13
+ api<string>('DELETE', `/agents/${id}`)
@@ -0,0 +1,100 @@
1
+ const ACCESS_KEY_STORAGE = 'sc_access_key'
2
+ const DEFAULT_API_TIMEOUT_MS = 12_000
3
+ const DEFAULT_GET_RETRIES = 2
4
+ const RETRY_DELAY_BASE_MS = 300
5
+
6
+ export function getStoredAccessKey(): string {
7
+ if (typeof window === 'undefined') return ''
8
+ return localStorage.getItem(ACCESS_KEY_STORAGE) || ''
9
+ }
10
+
11
+ export function setStoredAccessKey(key: string) {
12
+ localStorage.setItem(ACCESS_KEY_STORAGE, key)
13
+ }
14
+
15
+ export function clearStoredAccessKey() {
16
+ localStorage.removeItem(ACCESS_KEY_STORAGE)
17
+ }
18
+
19
+ function sleep(ms: number): Promise<void> {
20
+ return new Promise((resolve) => setTimeout(resolve, ms))
21
+ }
22
+
23
+ async function fetchWithTimeout(
24
+ input: RequestInfo | URL,
25
+ init: RequestInit,
26
+ timeoutMs: number,
27
+ ): Promise<Response> {
28
+ const controller = new AbortController()
29
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
30
+ try {
31
+ return await fetch(input, { ...init, signal: controller.signal })
32
+ } finally {
33
+ clearTimeout(timer)
34
+ }
35
+ }
36
+
37
+ function isAbortError(err: unknown): boolean {
38
+ if (!err || typeof err !== 'object') return false
39
+ return (err as { name?: string }).name === 'AbortError'
40
+ }
41
+
42
+ export async function api<T = unknown>(
43
+ method: string,
44
+ path: string,
45
+ body?: unknown,
46
+ options?: { timeoutMs?: number; retries?: number },
47
+ ): Promise<T> {
48
+ const key = getStoredAccessKey()
49
+ const timeoutMs = Math.max(1_000, Math.trunc(options?.timeoutMs ?? DEFAULT_API_TIMEOUT_MS))
50
+ const upperMethod = method.toUpperCase()
51
+ const retries = Math.max(0, Math.trunc(options?.retries ?? (upperMethod === 'GET' ? DEFAULT_GET_RETRIES : 0)))
52
+
53
+ const requestInit: RequestInit = {
54
+ method: upperMethod,
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ ...(key ? { 'X-Access-Key': key } : {}),
58
+ },
59
+ }
60
+ if (body) requestInit.body = JSON.stringify(body)
61
+
62
+ for (let attempt = 0; attempt <= retries; attempt++) {
63
+ try {
64
+ const r = await fetchWithTimeout('/api' + path, requestInit, timeoutMs)
65
+
66
+ if (r.status === 401) {
67
+ // Clear stored key on auth failure, redirect to login
68
+ clearStoredAccessKey()
69
+ if (typeof window !== 'undefined') {
70
+ window.dispatchEvent(new Event('sc_auth_required'))
71
+ }
72
+ throw new Error('Unauthorized — invalid access key')
73
+ }
74
+
75
+ const ct = r.headers.get('content-type') || ''
76
+
77
+ if (!r.ok) {
78
+ if (ct.includes('json')) {
79
+ const payload = await r.json().catch(() => null) as { error?: unknown; message?: unknown } | null
80
+ const msg =
81
+ (typeof payload?.error === 'string' && payload.error.trim())
82
+ || (typeof payload?.message === 'string' && payload.message.trim())
83
+ || `Request failed (${r.status})`
84
+ throw new Error(msg)
85
+ }
86
+ const text = (await r.text().catch(() => '')).trim()
87
+ throw new Error(text || `Request failed (${r.status})`)
88
+ }
89
+
90
+ if (ct.includes('json')) return r.json() as Promise<T>
91
+ return r.text() as unknown as T
92
+ } catch (err) {
93
+ const isLastAttempt = attempt >= retries
94
+ const retryable = isAbortError(err) || (err instanceof TypeError && !String(err.message || '').includes('Unauthorized'))
95
+ if (isLastAttempt || !retryable) throw err
96
+ await sleep(RETRY_DELAY_BASE_MS * (attempt + 1))
97
+ }
98
+ }
99
+ throw new Error('Request failed')
100
+ }
@@ -0,0 +1,60 @@
1
+ import type { SSEEvent } from '../types'
2
+ import { getStoredAccessKey } from './api-client'
3
+
4
+ interface StreamChatOptions {
5
+ internal?: boolean
6
+ queueMode?: 'followup' | 'steer' | 'collect'
7
+ }
8
+
9
+ export async function streamChat(
10
+ sessionId: string,
11
+ message: string,
12
+ imagePath?: string,
13
+ imageUrl?: string,
14
+ onEvent?: (event: SSEEvent) => void,
15
+ options?: StreamChatOptions,
16
+ ): Promise<void> {
17
+ const key = getStoredAccessKey()
18
+ const res = await fetch(`/api/sessions/${sessionId}/chat`, {
19
+ method: 'POST',
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ ...(key ? { 'X-Access-Key': key } : {}),
23
+ },
24
+ body: JSON.stringify({
25
+ message,
26
+ imagePath,
27
+ imageUrl,
28
+ internal: !!options?.internal,
29
+ queueMode: options?.queueMode,
30
+ }),
31
+ })
32
+
33
+ if (!res.ok || !res.body) {
34
+ onEvent?.({ t: 'err', text: `Request failed (${res.status})` })
35
+ onEvent?.({ t: 'done' })
36
+ return
37
+ }
38
+
39
+ const reader = res.body.getReader()
40
+ const decoder = new TextDecoder()
41
+ let buf = ''
42
+
43
+ while (true) {
44
+ const { done, value } = await reader.read()
45
+ if (done) break
46
+ buf += decoder.decode(value, { stream: true })
47
+ const lines = buf.split('\n')
48
+ buf = lines.pop() || ''
49
+ for (const line of lines) {
50
+ if (!line.startsWith('data: ')) continue
51
+ try {
52
+ const event = JSON.parse(line.slice(6)) as SSEEvent
53
+ // Forward all event types including tool_call and tool_result
54
+ onEvent?.(event)
55
+ } catch {
56
+ // skip malformed
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,42 @@
1
+ import { api } from './api-client'
2
+ import type { MemoryEntry } from '../types'
3
+
4
+ interface MemoryQueryOptions {
5
+ q?: string
6
+ agentId?: string
7
+ depth?: number
8
+ limit?: number
9
+ linkedLimit?: number
10
+ envelope?: boolean
11
+ }
12
+
13
+ export const searchMemory = (opts: MemoryQueryOptions = {}) => {
14
+ const params = new URLSearchParams()
15
+ if (opts.q) params.set('q', opts.q)
16
+ if (opts.agentId) params.set('agentId', opts.agentId)
17
+ if (typeof opts.depth === 'number') params.set('depth', String(opts.depth))
18
+ if (typeof opts.limit === 'number') params.set('limit', String(opts.limit))
19
+ if (typeof opts.linkedLimit === 'number') params.set('linkedLimit', String(opts.linkedLimit))
20
+ if (opts.envelope) params.set('envelope', 'true')
21
+ const qs = params.toString()
22
+ return api<MemoryEntry[]>('GET', `/memory${qs ? '?' + qs : ''}`)
23
+ }
24
+
25
+ export const getMemory = (id: string, opts: Omit<MemoryQueryOptions, 'q' | 'agentId'> = {}) => {
26
+ const params = new URLSearchParams()
27
+ if (typeof opts.depth === 'number') params.set('depth', String(opts.depth))
28
+ if (typeof opts.limit === 'number') params.set('limit', String(opts.limit))
29
+ if (typeof opts.linkedLimit === 'number') params.set('linkedLimit', String(opts.linkedLimit))
30
+ if (opts.envelope) params.set('envelope', 'true')
31
+ const qs = params.toString()
32
+ return api<MemoryEntry | MemoryEntry[]>('GET', `/memory/${id}${qs ? '?' + qs : ''}`)
33
+ }
34
+
35
+ export const createMemory = (data: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>) =>
36
+ api<MemoryEntry>('POST', '/memory', data)
37
+
38
+ export const updateMemory = (id: string, data: Partial<MemoryEntry>) =>
39
+ api<MemoryEntry>('PUT', `/memory/${id}`, data)
40
+
41
+ export const deleteMemory = (id: string) =>
42
+ api<string>('DELETE', `/memory/${id}`)
@@ -0,0 +1,48 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import {
4
+ deriveOpenClawWsUrl,
5
+ normalizeOpenClawEndpoint,
6
+ normalizeProviderEndpoint,
7
+ } from './openclaw-endpoint.ts'
8
+
9
+ test('normalizeOpenClawEndpoint handles ws/http/path variants', () => {
10
+ assert.equal(
11
+ normalizeOpenClawEndpoint('ws://127.0.0.1:18789'),
12
+ 'http://127.0.0.1:18789/v1',
13
+ )
14
+ assert.equal(
15
+ normalizeOpenClawEndpoint('http://localhost:18789'),
16
+ 'http://localhost:18789/v1',
17
+ )
18
+ assert.equal(
19
+ normalizeOpenClawEndpoint('http://localhost:18789/v1/chat/completions'),
20
+ 'http://localhost:18789/v1',
21
+ )
22
+ })
23
+
24
+ test('deriveOpenClawWsUrl strips v1 suffix and preserves host', () => {
25
+ assert.equal(
26
+ deriveOpenClawWsUrl('http://localhost:18789/v1'),
27
+ 'ws://localhost:18789',
28
+ )
29
+ assert.equal(
30
+ deriveOpenClawWsUrl('https://openclaw.example.com/v1'),
31
+ 'wss://openclaw.example.com',
32
+ )
33
+ })
34
+
35
+ test('normalizeProviderEndpoint only rewrites openclaw provider', () => {
36
+ assert.equal(
37
+ normalizeProviderEndpoint('openclaw', 'ws://localhost:18789'),
38
+ 'http://localhost:18789/v1',
39
+ )
40
+ assert.equal(
41
+ normalizeProviderEndpoint('openai', 'https://api.openai.com/v1/'),
42
+ 'https://api.openai.com/v1',
43
+ )
44
+ assert.equal(
45
+ normalizeProviderEndpoint('openai', null),
46
+ null,
47
+ )
48
+ })
@@ -0,0 +1,67 @@
1
+ const DEFAULT_OPENCLAW_ENDPOINT = 'http://localhost:18789/v1'
2
+
3
+ function hasScheme(value: string): boolean {
4
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(value)
5
+ }
6
+
7
+ function parseUrl(raw: string): URL | null {
8
+ const value = raw.trim()
9
+ if (!value) return null
10
+ try {
11
+ return new URL(hasScheme(value) ? value : `http://${value}`)
12
+ } catch {
13
+ return null
14
+ }
15
+ }
16
+
17
+ function toHttpProtocol(protocol: string): string {
18
+ if (protocol === 'ws:') return 'http:'
19
+ if (protocol === 'wss:') return 'https:'
20
+ return protocol
21
+ }
22
+
23
+ function cleanPath(pathname: string): string {
24
+ let path = pathname.replace(/\/+$/, '')
25
+ const lower = path.toLowerCase()
26
+
27
+ if (lower.endsWith('/chat/completions')) {
28
+ path = path.slice(0, -'/chat/completions'.length)
29
+ }
30
+ if (path.toLowerCase().endsWith('/models')) {
31
+ path = path.slice(0, -'/models'.length)
32
+ }
33
+
34
+ path = path.replace(/\/+$/, '')
35
+ if (!path || path === '/') return '/v1'
36
+ if (!/\/v1$/i.test(path)) return `${path}/v1`
37
+ return path
38
+ }
39
+
40
+ export function normalizeOpenClawEndpoint(input?: string | null): string {
41
+ const parsed = parseUrl(input || '') || parseUrl(DEFAULT_OPENCLAW_ENDPOINT)!
42
+ parsed.protocol = toHttpProtocol(parsed.protocol)
43
+ parsed.pathname = cleanPath(parsed.pathname || '/')
44
+ parsed.search = ''
45
+ parsed.hash = ''
46
+ return parsed.toString().replace(/\/+$/, '')
47
+ }
48
+
49
+ export function deriveOpenClawWsUrl(input?: string | null): string {
50
+ const api = normalizeOpenClawEndpoint(input)
51
+ const parsed = parseUrl(api) || parseUrl(DEFAULT_OPENCLAW_ENDPOINT)!
52
+ parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:'
53
+ parsed.pathname = parsed.pathname.replace(/\/v1$/i, '') || '/'
54
+ parsed.search = ''
55
+ parsed.hash = ''
56
+ const value = parsed.toString()
57
+ return value.endsWith('/') ? value.slice(0, -1) : value
58
+ }
59
+
60
+ export function normalizeProviderEndpoint(provider: string | null | undefined, endpoint: string | null | undefined): string | null {
61
+ if (typeof endpoint !== 'string') return null
62
+ const trimmed = endpoint.trim()
63
+ if (!trimmed) return null
64
+ if (provider === 'openclaw') return normalizeOpenClawEndpoint(trimmed)
65
+ return trimmed.replace(/\/+$/, '')
66
+ }
67
+
@@ -0,0 +1,13 @@
1
+ import { api } from './api-client'
2
+ import type { ProviderConfig } from '../types'
3
+
4
+ export const fetchProviderConfigs = () => api<ProviderConfig[]>('GET', '/providers/configs')
5
+
6
+ export const createProviderConfig = (data: Partial<ProviderConfig>) =>
7
+ api<ProviderConfig>('POST', '/providers', data)
8
+
9
+ export const updateProviderConfig = (id: string, data: Partial<ProviderConfig>) =>
10
+ api<ProviderConfig>('PUT', `/providers/${id}`, data)
11
+
12
+ export const deleteProviderConfig = (id: string) =>
13
+ api<{ ok: boolean }>('DELETE', `/providers/${id}`)
@@ -0,0 +1,135 @@
1
+ import fs from 'fs'
2
+ import https from 'https'
3
+ import type { StreamChatOptions } from './index'
4
+
5
+ const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
6
+ const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
7
+
8
+ function fileToContentBlocks(filePath: string): any[] {
9
+ if (!filePath || !fs.existsSync(filePath)) return []
10
+ if (IMAGE_EXTS.test(filePath)) {
11
+ const data = fs.readFileSync(filePath).toString('base64')
12
+ const ext = filePath.split('.').pop()?.toLowerCase() || 'png'
13
+ const mediaType = ext === 'jpg' ? 'image/jpeg' : `image/${ext}`
14
+ return [{ type: 'image', source: { type: 'base64', media_type: mediaType, data } }]
15
+ }
16
+ if (TEXT_EXTS.test(filePath) || filePath.endsWith('.pdf')) {
17
+ try {
18
+ const text = fs.readFileSync(filePath, 'utf-8')
19
+ const name = filePath.split('/').pop() || 'file'
20
+ return [{ type: 'text', text: `[Attached file: ${name}]\n\n${text}` }]
21
+ } catch { return [] }
22
+ }
23
+ return [{ type: 'text', text: `[Attached file: ${filePath.split('/').pop()}]` }]
24
+ }
25
+
26
+ export function streamAnthropicChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory }: StreamChatOptions): Promise<string> {
27
+ return new Promise((resolve) => {
28
+ const messages = buildMessages(session, message, imagePath, loadHistory)
29
+ const model = session.model || 'claude-sonnet-4-6'
30
+
31
+ const body: Record<string, unknown> = {
32
+ model,
33
+ max_tokens: 8192,
34
+ messages,
35
+ stream: true,
36
+ }
37
+ if (systemPrompt) {
38
+ body.system = systemPrompt
39
+ }
40
+
41
+ const payload = JSON.stringify(body)
42
+ const abortController = { aborted: false }
43
+ let fullResponse = ''
44
+
45
+ const apiReq = https.request({
46
+ hostname: 'api.anthropic.com',
47
+ path: '/v1/messages',
48
+ method: 'POST',
49
+ headers: {
50
+ 'x-api-key': apiKey || '',
51
+ 'anthropic-version': '2023-06-01',
52
+ 'Content-Type': 'application/json',
53
+ },
54
+ }, (apiRes) => {
55
+ if (apiRes.statusCode !== 200) {
56
+ let errBody = ''
57
+ apiRes.on('data', (c: Buffer) => errBody += c)
58
+ apiRes.on('end', () => {
59
+ console.error(`[${session.id}] anthropic error ${apiRes.statusCode}:`, errBody.slice(0, 200))
60
+ let errMsg = `Anthropic API error (${apiRes.statusCode})`
61
+ try {
62
+ const parsed = JSON.parse(errBody)
63
+ if (parsed.error?.message) errMsg = parsed.error.message
64
+ } catch {}
65
+ write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
66
+ active.delete(session.id)
67
+ resolve(fullResponse)
68
+ })
69
+ return
70
+ }
71
+
72
+ let buf = ''
73
+ apiRes.on('data', (chunk: Buffer) => {
74
+ if (abortController.aborted) return
75
+ buf += chunk.toString()
76
+ const lines = buf.split('\n')
77
+ buf = lines.pop()!
78
+
79
+ for (const line of lines) {
80
+ if (!line.startsWith('data: ')) continue
81
+ const data = line.slice(6).trim()
82
+ if (!data) continue
83
+ try {
84
+ const parsed = JSON.parse(data)
85
+ if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
86
+ fullResponse += parsed.delta.text
87
+ write(`data: ${JSON.stringify({ t: 'd', text: parsed.delta.text })}\n\n`)
88
+ }
89
+ } catch {}
90
+ }
91
+ })
92
+
93
+ apiRes.on('end', () => {
94
+ active.delete(session.id)
95
+ resolve(fullResponse)
96
+ })
97
+ })
98
+
99
+ active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
100
+
101
+ apiReq.on('error', (e) => {
102
+ console.error(`[${session.id}] anthropic request error:`, e.message)
103
+ write(`data: ${JSON.stringify({ t: 'err', text: e.message })}\n\n`)
104
+ active.delete(session.id)
105
+ resolve(fullResponse)
106
+ })
107
+
108
+ apiReq.end(payload)
109
+ })
110
+ }
111
+
112
+ function buildMessages(session: any, message: string, imagePath: string | undefined, loadHistory: (id: string) => any[]) {
113
+ const msgs: Array<{ role: string; content: any }> = []
114
+
115
+ if (loadHistory) {
116
+ const history = loadHistory(session.id)
117
+ for (const m of history) {
118
+ if (m.role === 'user' && m.imagePath) {
119
+ const blocks = fileToContentBlocks(m.imagePath)
120
+ msgs.push({ role: 'user', content: [...blocks, { type: 'text', text: m.text }] })
121
+ } else {
122
+ msgs.push({ role: m.role, content: m.text })
123
+ }
124
+ }
125
+ }
126
+
127
+ // Current message with optional attachment
128
+ if (imagePath) {
129
+ const blocks = fileToContentBlocks(imagePath)
130
+ msgs.push({ role: 'user', content: [...blocks, { type: 'text', text: message }] })
131
+ } else {
132
+ msgs.push({ role: 'user', content: message })
133
+ }
134
+ return msgs
135
+ }