@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,330 @@
1
+ import { WebSocket } from 'ws'
2
+ import crypto, { randomUUID } from 'crypto'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import type { StreamChatOptions } from './index'
6
+
7
+ // --- Device Identity (Ed25519 keypair for gateway auth) ---
8
+
9
+ const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')
10
+
11
+ function base64UrlEncode(buf: Buffer): string {
12
+ return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '')
13
+ }
14
+
15
+ function derivePublicKeyRaw(publicKeyPem: string): Buffer {
16
+ const spki = crypto.createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' })
17
+ if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
18
+ return spki.subarray(ED25519_SPKI_PREFIX.length)
19
+ }
20
+ return spki
21
+ }
22
+
23
+ function fingerprintPublicKey(publicKeyPem: string): string {
24
+ return crypto.createHash('sha256').update(derivePublicKeyRaw(publicKeyPem)).digest('hex')
25
+ }
26
+
27
+ interface DeviceIdentity {
28
+ deviceId: string
29
+ publicKeyPem: string
30
+ privateKeyPem: string
31
+ }
32
+
33
+ function getIdentityPath(): string {
34
+ const dataDir = path.join(process.cwd(), 'data')
35
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
36
+ return path.join(dataDir, 'openclaw-device.json')
37
+ }
38
+
39
+ function loadOrCreateDeviceIdentity(): DeviceIdentity {
40
+ const filePath = getIdentityPath()
41
+ try {
42
+ if (fs.existsSync(filePath)) {
43
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'))
44
+ if (parsed?.deviceId && parsed?.publicKeyPem && parsed?.privateKeyPem) {
45
+ return parsed
46
+ }
47
+ }
48
+ } catch {}
49
+
50
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519')
51
+ const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }) as string
52
+ const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string
53
+ const identity: DeviceIdentity = {
54
+ deviceId: fingerprintPublicKey(publicKeyPem),
55
+ publicKeyPem,
56
+ privateKeyPem,
57
+ }
58
+ fs.writeFileSync(filePath, JSON.stringify({ version: 1, ...identity }, null, 2) + '\n', { mode: 0o600 })
59
+ return identity
60
+ }
61
+
62
+ /** Get the device ID that SwarmClaw would use for pairing. */
63
+ export function getDeviceId(): string {
64
+ return loadOrCreateDeviceIdentity().deviceId
65
+ }
66
+
67
+ // --- Protocol helpers ---
68
+
69
+ function normalizeWsUrl(raw: string): string {
70
+ let url = raw.replace(/\/+$/, '').replace(/\/v1$/i, '')
71
+ if (!/^(https?|wss?):\/\//i.test(url)) url = `http://${url}`
72
+ url = url.replace(/^ws:/i, 'http:').replace(/^wss:/i, 'https:')
73
+ return url.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')
74
+ }
75
+
76
+ /**
77
+ * Build connect params for the OpenClaw gateway protocol.
78
+ *
79
+ * The gateway allows operators with a valid token to skip device identity
80
+ * (roleCanSkipDeviceIdentity). When useDeviceAuth is true, includes an
81
+ * Ed25519-signed device identity for gateways that require device pairing.
82
+ */
83
+ export function buildOpenClawConnectParams(
84
+ token: string | undefined,
85
+ nonce: string | undefined,
86
+ opts?: { useDeviceAuth?: boolean },
87
+ ) {
88
+ const clientId = 'gateway-client'
89
+ const clientMode = 'backend'
90
+ const platform = process.platform
91
+ const role = 'operator'
92
+ const scopes = ['operator.admin']
93
+
94
+ const params: Record<string, unknown> = {
95
+ minProtocol: 1,
96
+ maxProtocol: 3,
97
+ auth: token ? { token } : undefined,
98
+ client: {
99
+ id: clientId,
100
+ version: '1.0.0',
101
+ platform,
102
+ mode: clientMode,
103
+ instanceId: randomUUID(),
104
+ },
105
+ caps: [],
106
+ role,
107
+ scopes,
108
+ }
109
+
110
+ if (opts?.useDeviceAuth) {
111
+ const identity = loadOrCreateDeviceIdentity()
112
+ const signedAtMs = Date.now()
113
+
114
+ const payload = [
115
+ 'v3', identity.deviceId, clientId, clientMode, role,
116
+ scopes.join(','), String(signedAtMs), token ?? '', nonce ?? '',
117
+ platform, '', // deviceFamily
118
+ ].join('|')
119
+ const signature = base64UrlEncode(
120
+ crypto.sign(null, Buffer.from(payload, 'utf8'), crypto.createPrivateKey(identity.privateKeyPem)),
121
+ )
122
+
123
+ params.device = {
124
+ id: identity.deviceId,
125
+ publicKey: base64UrlEncode(derivePublicKeyRaw(identity.publicKeyPem)),
126
+ signature,
127
+ signedAt: signedAtMs,
128
+ nonce: nonce ?? '',
129
+ }
130
+ }
131
+
132
+ return params
133
+ }
134
+
135
+ // --- Gateway connection ---
136
+
137
+ interface ConnectResult {
138
+ ok: boolean
139
+ message: string
140
+ errorCode?: string
141
+ ws?: InstanceType<typeof WebSocket>
142
+ }
143
+
144
+ /**
145
+ * Open a WebSocket and complete the connect handshake.
146
+ * Resolves with { ok, ws } on success or { ok: false, message, errorCode } on failure.
147
+ */
148
+ function wsConnect(
149
+ wsUrl: string,
150
+ token: string | undefined,
151
+ useDeviceAuth: boolean,
152
+ timeoutMs = 15_000,
153
+ ): Promise<ConnectResult> {
154
+ return new Promise((resolve) => {
155
+ let settled = false
156
+ const done = (result: ConnectResult) => {
157
+ if (settled) return
158
+ settled = true
159
+ clearTimeout(timer)
160
+ if (!result.ok) try { ws.close() } catch {}
161
+ resolve(result)
162
+ }
163
+
164
+ const timer = setTimeout(() => {
165
+ done({ ok: false, message: 'Connection timed out. Verify the gateway URL and network access.' })
166
+ }, timeoutMs)
167
+
168
+ const ws = new WebSocket(wsUrl)
169
+ let connectId: string | null = null
170
+
171
+ ws.on('message', (data) => {
172
+ try {
173
+ const msg = JSON.parse(data.toString())
174
+ if (msg.event === 'connect.challenge') {
175
+ connectId = randomUUID()
176
+ ws.send(JSON.stringify({
177
+ type: 'req',
178
+ id: connectId,
179
+ method: 'connect',
180
+ params: buildOpenClawConnectParams(token, msg.payload?.nonce, { useDeviceAuth }),
181
+ }))
182
+ return
183
+ }
184
+ if (msg.type === 'res' && msg.id === connectId) {
185
+ if (msg.ok) {
186
+ done({ ok: true, message: 'Connected.', ws })
187
+ } else {
188
+ done({
189
+ ok: false,
190
+ message: msg.error?.message || 'Gateway connect failed.',
191
+ errorCode: msg.error?.details?.code as string | undefined,
192
+ })
193
+ }
194
+ }
195
+ } catch {
196
+ done({ ok: false, message: 'Unexpected response from gateway.' })
197
+ }
198
+ })
199
+
200
+ ws.on('error', (err) => {
201
+ done({ ok: false, message: `Connection failed: ${err.message}` })
202
+ })
203
+
204
+ ws.on('close', (code, reason) => {
205
+ if (code === 1008) {
206
+ done({ ok: false, message: `Unauthorized: ${reason?.toString() || 'invalid token'}` })
207
+ } else {
208
+ done({ ok: false, message: `Connection closed unexpectedly (${code})` })
209
+ }
210
+ })
211
+ })
212
+ }
213
+
214
+ /**
215
+ * Connect to the gateway with device identity.
216
+ *
217
+ * Always includes Ed25519 device auth — the gateway may accept the initial
218
+ * connect handshake with token-only but still require device identity for
219
+ * agent operations. Sending device auth unconditionally avoids that mismatch.
220
+ */
221
+ async function connectToGateway(
222
+ wsUrl: string,
223
+ token: string | undefined,
224
+ timeoutMs = 15_000,
225
+ ): Promise<ConnectResult> {
226
+ return wsConnect(wsUrl, token, true, timeoutMs)
227
+ }
228
+
229
+ // --- Provider ---
230
+
231
+ export function streamOpenClawChat({ session, message, imagePath, write, active }: StreamChatOptions): Promise<string> {
232
+ let prompt = message
233
+ if (imagePath) {
234
+ prompt = `[The user has shared an image at: ${imagePath}]\n\n${message}`
235
+ }
236
+
237
+ const wsUrl = session.apiEndpoint ? normalizeWsUrl(session.apiEndpoint) : 'ws://127.0.0.1:18789'
238
+ const token = session.apiKey || undefined
239
+
240
+ return new Promise((resolve) => {
241
+ let fullResponse = ''
242
+ let settled = false
243
+
244
+ const finish = (errMsg?: string) => {
245
+ if (settled) return
246
+ settled = true
247
+ active.delete(session.id)
248
+ if (errMsg && !fullResponse.trim()) {
249
+ write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
250
+ }
251
+ resolve(fullResponse)
252
+ }
253
+
254
+ connectToGateway(wsUrl, token).then((result) => {
255
+ if (!result.ok || !result.ws) {
256
+ finish(result.message)
257
+ return
258
+ }
259
+
260
+ const ws = result.ws
261
+ const timeout = setTimeout(() => {
262
+ ws.close()
263
+ finish('OpenClaw gateway timed out after 120s.')
264
+ }, 120_000)
265
+
266
+ active.set(session.id, { kill: () => { ws.close(); clearTimeout(timeout); finish('Aborted.') } })
267
+
268
+ const agentReqId = randomUUID()
269
+ ws.send(JSON.stringify({
270
+ type: 'req',
271
+ id: agentReqId,
272
+ method: 'agent',
273
+ params: {
274
+ message: prompt,
275
+ agentId: 'main',
276
+ timeout: 120,
277
+ idempotencyKey: randomUUID(),
278
+ },
279
+ }))
280
+
281
+ ws.on('message', (data) => {
282
+ try {
283
+ const msg = JSON.parse(data.toString())
284
+ if (msg.type === 'res' && msg.id === agentReqId) {
285
+ if (!msg.ok) {
286
+ ws.close()
287
+ clearTimeout(timeout)
288
+ finish(msg.error?.message || 'Agent request failed.')
289
+ return
290
+ }
291
+ if (msg.payload?.status === 'accepted') return
292
+
293
+ const payloads = msg.payload?.result?.payloads ?? []
294
+ for (const p of payloads) {
295
+ const text = typeof p.text === 'string' ? p.text.trimEnd() : ''
296
+ if (text) {
297
+ fullResponse += text
298
+ write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
299
+ }
300
+ }
301
+ if (!fullResponse && msg.payload?.summary) {
302
+ const text = String(msg.payload.summary)
303
+ fullResponse = text
304
+ write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
305
+ }
306
+ ws.close()
307
+ clearTimeout(timeout)
308
+ finish()
309
+ }
310
+ } catch {}
311
+ })
312
+
313
+ ws.on('error', (err) => {
314
+ clearTimeout(timeout)
315
+ finish(`OpenClaw connection failed: ${err.message}`)
316
+ })
317
+
318
+ ws.on('close', (code, reason) => {
319
+ clearTimeout(timeout)
320
+ if (code === 1008) {
321
+ finish(`Unauthorized: ${reason?.toString() || 'invalid token'}`)
322
+ } else {
323
+ finish()
324
+ }
325
+ })
326
+ }).catch((err) => {
327
+ finish(`OpenClaw error: ${err?.message || 'unknown error'}`)
328
+ })
329
+ })
330
+ }
@@ -0,0 +1,164 @@
1
+ import fs from 'fs'
2
+ import os from 'os'
3
+ import path from 'path'
4
+ import { spawn } from 'child_process'
5
+ import type { StreamChatOptions } from './index'
6
+ import { log } from '../server/logger'
7
+ import { loadRuntimeSettings } from '../server/runtime-settings'
8
+
9
+ function findOpencode(): string {
10
+ const locations = [
11
+ path.join(os.homedir(), '.local/bin/opencode'),
12
+ '/usr/local/bin/opencode',
13
+ '/opt/homebrew/bin/opencode',
14
+ ]
15
+ // Check nvm paths
16
+ const nvmDir = process.env.NVM_DIR || path.join(os.homedir(), '.nvm')
17
+ try {
18
+ const versions = fs.readdirSync(path.join(nvmDir, 'versions/node'))
19
+ for (const v of versions) {
20
+ locations.push(path.join(nvmDir, 'versions/node', v, 'bin/opencode'))
21
+ }
22
+ } catch { /* nvm not installed */ }
23
+ for (const loc of locations) {
24
+ if (fs.existsSync(loc)) {
25
+ log.info('opencode-cli', `Found opencode at: ${loc}`)
26
+ return loc
27
+ }
28
+ }
29
+ log.warn('opencode-cli', 'opencode binary not found in known locations, falling back to PATH')
30
+ return 'opencode'
31
+ }
32
+
33
+ const OPENCODE = findOpencode()
34
+
35
+ function extractSessionId(raw: unknown): string | null {
36
+ if (!raw) return null
37
+ const text = String(raw).trim()
38
+ return text ? text : null
39
+ }
40
+
41
+ /**
42
+ * OpenCode CLI provider — spawns `opencode run <message> --format json` for non-interactive usage.
43
+ * Tracks `session.opencodeSessionId` from streamed JSON events to support multi-turn continuity.
44
+ */
45
+ export function streamOpenCodeCliChat({ session, message, imagePath, systemPrompt, write, active }: StreamChatOptions): Promise<string> {
46
+ const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
47
+ const cwd = session.cwd || process.cwd()
48
+ const promptParts: string[] = []
49
+ if (systemPrompt && !session.opencodeSessionId) {
50
+ promptParts.push(`[System instructions]\n${systemPrompt}`)
51
+ }
52
+ promptParts.push(message)
53
+ const prompt = promptParts.join('\n\n')
54
+
55
+ const env: NodeJS.ProcessEnv = {
56
+ ...process.env,
57
+ TERM: 'dumb',
58
+ NO_COLOR: '1',
59
+ }
60
+ // Set model via env if specified
61
+ if (session.model) {
62
+ env.OPENCODE_MODEL = session.model
63
+ }
64
+
65
+ const args = ['run', prompt, '--format', 'json']
66
+ if (session.opencodeSessionId) args.push('--session', session.opencodeSessionId)
67
+ if (session.model) args.push('--model', session.model)
68
+ if (imagePath) args.push('--file', imagePath)
69
+
70
+ log.info('opencode-cli', `Spawning: ${OPENCODE}`, {
71
+ args: args.map((a, i) => {
72
+ if (i === 1) return `(${prompt.length} chars)`
73
+ if (a.length > 120) return `${a.slice(0, 120)}...`
74
+ return a
75
+ }),
76
+ cwd,
77
+ hasSystemPrompt: !!systemPrompt,
78
+ hasImage: !!imagePath,
79
+ resumeSessionId: session.opencodeSessionId || null,
80
+ })
81
+
82
+ const proc = spawn(OPENCODE, args, {
83
+ cwd,
84
+ env,
85
+ stdio: ['pipe', 'pipe', 'pipe'],
86
+ timeout: processTimeoutMs,
87
+ })
88
+
89
+ log.info('opencode-cli', `Process spawned: pid=${proc.pid}`)
90
+ active.set(session.id, proc)
91
+
92
+ let fullResponse = ''
93
+ let stderrText = ''
94
+ let stdoutBuf = ''
95
+ let eventCount = 0
96
+ const eventErrors: string[] = []
97
+
98
+ proc.stdout!.on('data', (chunk: Buffer) => {
99
+ const text = chunk.toString()
100
+ stdoutBuf += text
101
+ const lines = stdoutBuf.split('\n')
102
+ stdoutBuf = lines.pop() || ''
103
+
104
+ for (const line of lines) {
105
+ const trimmed = line.trim()
106
+ if (!trimmed) continue
107
+ try {
108
+ const ev = JSON.parse(trimmed) as any
109
+ eventCount += 1
110
+ const discoveredSessionId = extractSessionId(ev?.sessionID ?? ev?.sessionId)
111
+ if (discoveredSessionId) session.opencodeSessionId = discoveredSessionId
112
+
113
+ if (ev?.type === 'text' && typeof ev?.part?.text === 'string') {
114
+ fullResponse += ev.part.text
115
+ write(`data: ${JSON.stringify({ t: 'd', text: ev.part.text })}\n\n`)
116
+ continue
117
+ }
118
+
119
+ if (ev?.type === 'error') {
120
+ const msg = typeof ev?.error === 'string'
121
+ ? ev.error
122
+ : typeof ev?.message === 'string'
123
+ ? ev.message
124
+ : 'Unknown OpenCode event error'
125
+ eventErrors.push(msg)
126
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
127
+ continue
128
+ }
129
+ } catch {
130
+ // Raw fallback line from the CLI.
131
+ fullResponse += `${line}\n`
132
+ write(`data: ${JSON.stringify({ t: 'd', text: `${line}\n` })}\n\n`)
133
+ }
134
+ }
135
+ })
136
+
137
+ proc.stderr!.on('data', (chunk: Buffer) => {
138
+ const text = chunk.toString()
139
+ stderrText += text
140
+ if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
141
+ log.warn('opencode-cli', `stderr [${session.id}]`, text.slice(0, 500))
142
+ })
143
+
144
+ return new Promise((resolve) => {
145
+ proc.on('close', (code, signal) => {
146
+ log.info('opencode-cli', `Process closed: code=${code} signal=${signal} events=${eventCount} response=${fullResponse.length}chars`)
147
+ active.delete(session.id)
148
+ if ((code ?? 0) !== 0 && !fullResponse.trim() && eventErrors.length === 0) {
149
+ const msg = stderrText.trim()
150
+ ? `OpenCode CLI exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}: ${stderrText.trim().slice(0, 1200)}`
151
+ : `OpenCode CLI exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''} and returned no output.`
152
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
153
+ }
154
+ resolve(fullResponse.trim())
155
+ })
156
+
157
+ proc.on('error', (e) => {
158
+ log.error('opencode-cli', `Process error: ${e.message}`)
159
+ active.delete(session.id)
160
+ write(`data: ${JSON.stringify({ t: 'err', text: e.message })}\n\n`)
161
+ resolve(fullResponse)
162
+ })
163
+ })
164
+ }
@@ -0,0 +1,15 @@
1
+ import type { LoopMode } from '@/types'
2
+
3
+ export const DEFAULT_LOOP_MODE: LoopMode = 'bounded'
4
+
5
+ // Loop limits
6
+ export const DEFAULT_AGENT_LOOP_RECURSION_LIMIT = 15
7
+ export const DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT = 25
8
+ export const DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS = 10
9
+ export const DEFAULT_ONGOING_LOOP_MAX_ITERATIONS = 250
10
+ export const DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES = 60
11
+
12
+ // Tool/process timeouts
13
+ export const DEFAULT_SHELL_COMMAND_TIMEOUT_SEC = 30
14
+ export const DEFAULT_CLAUDE_CODE_TIMEOUT_SEC = 120
15
+ export const DEFAULT_CLI_PROCESS_TIMEOUT_SEC = 300
@@ -0,0 +1,84 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import { findDuplicateSchedule, getScheduleSignatureKey, type ScheduleLike } from './schedule-dedupe.ts'
4
+
5
+ test('findDuplicateSchedule matches active interval schedules with normalized prompts', () => {
6
+ const schedules: Record<string, ScheduleLike> = {
7
+ a1: {
8
+ id: 'a1',
9
+ agentId: 'assistant',
10
+ taskPrompt: 'Take a screenshot of Wikipedia homepage',
11
+ scheduleType: 'interval',
12
+ intervalMs: 60_000,
13
+ status: 'active',
14
+ createdAt: 1,
15
+ },
16
+ }
17
+
18
+ const duplicate = findDuplicateSchedule(schedules, {
19
+ agentId: 'assistant',
20
+ taskPrompt: 'take a screenshot of wikipedia homepage',
21
+ scheduleType: 'interval',
22
+ intervalMs: 60_000,
23
+ })
24
+
25
+ assert.ok(duplicate)
26
+ assert.equal(duplicate?.id, 'a1')
27
+ })
28
+
29
+ test('findDuplicateSchedule ignores completed/failed schedules by default', () => {
30
+ const schedules: Record<string, ScheduleLike> = {
31
+ done1: {
32
+ id: 'done1',
33
+ agentId: 'assistant',
34
+ taskPrompt: 'Run report',
35
+ scheduleType: 'interval',
36
+ intervalMs: 300_000,
37
+ status: 'completed',
38
+ createdAt: 1,
39
+ },
40
+ fail1: {
41
+ id: 'fail1',
42
+ agentId: 'assistant',
43
+ taskPrompt: 'Run report',
44
+ scheduleType: 'interval',
45
+ intervalMs: 300_000,
46
+ status: 'failed',
47
+ createdAt: 2,
48
+ },
49
+ }
50
+
51
+ const duplicate = findDuplicateSchedule(schedules, {
52
+ agentId: 'assistant',
53
+ taskPrompt: 'run report',
54
+ scheduleType: 'interval',
55
+ intervalMs: 300_000,
56
+ })
57
+
58
+ assert.equal(duplicate, null)
59
+ })
60
+
61
+ test('getScheduleSignatureKey is stable for equivalent schedules', () => {
62
+ const keyA = getScheduleSignatureKey({
63
+ agentId: 'assistant',
64
+ taskPrompt: ' Check status ',
65
+ scheduleType: 'cron',
66
+ cron: '*/5 * * * *',
67
+ })
68
+ const keyB = getScheduleSignatureKey({
69
+ agentId: 'assistant',
70
+ taskPrompt: 'check status',
71
+ scheduleType: 'cron',
72
+ cron: '*/5 * * * *',
73
+ })
74
+ const keyC = getScheduleSignatureKey({
75
+ agentId: 'assistant',
76
+ taskPrompt: 'check status',
77
+ scheduleType: 'cron',
78
+ cron: '*/10 * * * *',
79
+ })
80
+
81
+ assert.ok(keyA)
82
+ assert.equal(keyA, keyB)
83
+ assert.notEqual(keyA, keyC)
84
+ })