@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,153 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import {
4
+ normalizeLinkedMemoryIds,
5
+ normalizeMemoryLookupLimits,
6
+ resolveLookupRequest,
7
+ traverseLinkedMemoryGraph,
8
+ } from './memory-graph.ts'
9
+ import type { MemoryLookupLimits, LinkedMemoryNode } from './memory-graph.ts'
10
+
11
+ describe('normalizeLinkedMemoryIds', () => {
12
+ it('filters empty strings and self-references', () => {
13
+ assert.deepStrictEqual(
14
+ normalizeLinkedMemoryIds(['a', '', 'b', ' ', 'a', 'c'], 'self'),
15
+ ['a', 'b', 'c']
16
+ )
17
+ })
18
+
19
+ it('returns empty array for non-array input', () => {
20
+ assert.deepStrictEqual(normalizeLinkedMemoryIds(null), [])
21
+ assert.deepStrictEqual(normalizeLinkedMemoryIds(undefined), [])
22
+ assert.deepStrictEqual(normalizeLinkedMemoryIds('not-an-array'), [])
23
+ })
24
+
25
+ it('deduplicates ids', () => {
26
+ assert.deepStrictEqual(
27
+ normalizeLinkedMemoryIds(['a', 'b', 'a', 'a', 'c'], undefined),
28
+ ['a', 'b', 'c']
29
+ )
30
+ })
31
+ })
32
+
33
+ describe('normalizeMemoryLookupLimits', () => {
34
+ it('returns defaults for empty settings', () => {
35
+ const limits = normalizeMemoryLookupLimits({})
36
+ assert.strictEqual(limits.maxDepth, 3)
37
+ assert.strictEqual(limits.maxPerLookup, 20)
38
+ assert.strictEqual(limits.maxLinkedExpansion, 60)
39
+ })
40
+
41
+ it('clamps to valid ranges', () => {
42
+ const limits = normalizeMemoryLookupLimits({
43
+ memoryReferenceDepth: 100,
44
+ maxMemoriesPerLookup: 1000,
45
+ maxLinkedMemoriesExpanded: 5000,
46
+ })
47
+ assert.strictEqual(limits.maxDepth, 12) // max
48
+ assert.strictEqual(limits.maxPerLookup, 200) // max
49
+ assert.strictEqual(limits.maxLinkedExpansion, 1000) // max
50
+ })
51
+
52
+ it('allows zeros for depth and linked expansion', () => {
53
+ const limits = normalizeMemoryLookupLimits({
54
+ memoryReferenceDepth: 0,
55
+ maxMemoriesPerLookup: 5,
56
+ maxLinkedMemoriesExpanded: 0,
57
+ })
58
+ assert.strictEqual(limits.maxDepth, 0)
59
+ assert.strictEqual(limits.maxPerLookup, 5)
60
+ assert.strictEqual(limits.maxLinkedExpansion, 0)
61
+ })
62
+ })
63
+
64
+ describe('resolveLookupRequest', () => {
65
+ const defaults: MemoryLookupLimits = {
66
+ maxDepth: 3,
67
+ maxPerLookup: 20,
68
+ maxLinkedExpansion: 60,
69
+ }
70
+
71
+ it('uses defaults for empty request', () => {
72
+ assert.deepStrictEqual(resolveLookupRequest(defaults, {}), defaults)
73
+ })
74
+
75
+ it('overrides with request values', () => {
76
+ const result = resolveLookupRequest(defaults, { depth: 2, limit: 10, linkedLimit: 30 })
77
+ assert.strictEqual(result.maxDepth, 2)
78
+ assert.strictEqual(result.maxPerLookup, 10)
79
+ assert.strictEqual(result.maxLinkedExpansion, 30)
80
+ })
81
+
82
+ it('caps at defaults maxima', () => {
83
+ const result = resolveLookupRequest(defaults, { depth: 100, limit: 1000, linkedLimit: 5000 })
84
+ assert.strictEqual(result.maxDepth, 3)
85
+ assert.strictEqual(result.maxPerLookup, 20)
86
+ assert.strictEqual(result.maxLinkedExpansion, 60)
87
+ })
88
+ })
89
+
90
+ describe('traverseLinkedMemoryGraph', () => {
91
+ const fetchByIds = (ids: string[]): LinkedMemoryNode[] => {
92
+ return ids.map((id) => ({
93
+ id,
94
+ linkedMemoryIds: id === 'a' ? ['b', 'c'] : id === 'b' ? ['d'] : [],
95
+ }))
96
+ }
97
+
98
+ it('returns empty for empty seeds', () => {
99
+ const result = traverseLinkedMemoryGraph([], { maxDepth: 3, maxPerLookup: 20, maxLinkedExpansion: 60 }, fetchByIds)
100
+ assert.strictEqual(result.entries.length, 0)
101
+ assert.strictEqual(result.truncated, false)
102
+ assert.strictEqual(result.expandedLinkedCount, 0)
103
+ })
104
+
105
+ it('traverses linked nodes by depth', () => {
106
+ const seeds = [{ id: 'a', linkedMemoryIds: ['b', 'c'] }]
107
+ const result = traverseLinkedMemoryGraph(seeds, { maxDepth: 2, maxPerLookup: 20, maxLinkedExpansion: 60 }, fetchByIds)
108
+ const ids = result.entries.map((n) => n.id)
109
+ assert.ok(ids.includes('a'))
110
+ assert.ok(ids.includes('b'))
111
+ assert.ok(ids.includes('c'))
112
+ assert.ok(ids.includes('d')) // depth 2
113
+ assert.strictEqual(result.expandedLinkedCount, 3) // b, c, d
114
+ })
115
+
116
+ it('respects maxDepth', () => {
117
+ const limitedFetch = (ids: string[]): LinkedMemoryNode[] => {
118
+ const map: Record<string, string[]> = { a: ['b'], b: ['c'], c: ['d'], d: [] }
119
+ return ids.map((id) => ({ id, linkedMemoryIds: map[id] || [] }))
120
+ }
121
+ const seeds = [{ id: 'a', linkedMemoryIds: ['b'] }]
122
+ const result = traverseLinkedMemoryGraph(seeds, { maxDepth: 1, maxPerLookup: 20, maxLinkedExpansion: 60 }, limitedFetch)
123
+ const ids = result.entries.map((n) => n.id)
124
+ assert.ok(ids.includes('a'))
125
+ assert.ok(ids.includes('b'))
126
+ assert.ok(!ids.includes('c')) // depth 1 stops before c
127
+ })
128
+
129
+ it('respects maxPerLookup', () => {
130
+ const seeds = [{ id: 'a', linkedMemoryIds: ['b', 'c', 'd', 'e', 'f'] }]
131
+ const result = traverseLinkedMemoryGraph(seeds, { maxDepth: 3, maxPerLookup: 3, maxLinkedExpansion: 60 }, fetchByIds)
132
+ assert.strictEqual(result.entries.length, 3)
133
+ assert.strictEqual(result.truncated, true)
134
+ })
135
+
136
+ it('respects maxLinkedExpansion', () => {
137
+ const seeds = [{ id: 'a', linkedMemoryIds: ['b', 'c', 'd', 'e', 'f'] }]
138
+ const result = traverseLinkedMemoryGraph(seeds, { maxDepth: 3, maxPerLookup: 20, maxLinkedExpansion: 2 }, fetchByIds)
139
+ assert.strictEqual(result.expandedLinkedCount, 2)
140
+ assert.strictEqual(result.truncated, true)
141
+ })
142
+
143
+ it('handles circular links', () => {
144
+ const circularFetch = (ids: string[]): LinkedMemoryNode[] => {
145
+ const map: Record<string, string[]> = { a: ['b'], b: ['a'] }
146
+ return ids.map((id) => ({ id, linkedMemoryIds: map[id] || [] }))
147
+ }
148
+ const seeds = [{ id: 'a', linkedMemoryIds: ['b'] }]
149
+ const result = traverseLinkedMemoryGraph(seeds, { maxDepth: 10, maxPerLookup: 100, maxLinkedExpansion: 100 }, circularFetch)
150
+ assert.strictEqual(result.entries.length, 2) // just a and b
151
+ assert.strictEqual(result.truncated, false)
152
+ })
153
+ })
@@ -0,0 +1,138 @@
1
+ export const DEFAULT_MEMORY_REFERENCE_DEPTH = 3
2
+ export const DEFAULT_MAX_MEMORIES_PER_LOOKUP = 20
3
+ export const DEFAULT_MAX_LINKED_MEMORIES_EXPANDED = 60
4
+
5
+ const MAX_MEMORY_REFERENCE_DEPTH = 12
6
+ const MAX_MEMORIES_PER_LOOKUP = 200
7
+ const MAX_LINKED_MEMORIES_EXPANDED = 1000
8
+
9
+ export interface MemoryLookupLimits {
10
+ maxDepth: number
11
+ maxPerLookup: number
12
+ maxLinkedExpansion: number
13
+ }
14
+
15
+ export interface MemoryLookupRequest {
16
+ depth?: number | null
17
+ limit?: number | null
18
+ linkedLimit?: number | null
19
+ }
20
+
21
+ export interface LinkedMemoryNode {
22
+ id: string
23
+ linkedMemoryIds?: string[]
24
+ }
25
+
26
+ export interface TraversalResult<TNode extends LinkedMemoryNode> {
27
+ entries: TNode[]
28
+ truncated: boolean
29
+ expandedLinkedCount: number
30
+ }
31
+
32
+ function parseIntSetting(value: unknown): number | null {
33
+ const parsed = typeof value === 'number'
34
+ ? value
35
+ : typeof value === 'string'
36
+ ? Number.parseInt(value, 10)
37
+ : Number.NaN
38
+ if (!Number.isFinite(parsed)) return null
39
+ return Math.trunc(parsed)
40
+ }
41
+
42
+ function clamp(value: number, min: number, max: number): number {
43
+ return Math.max(min, Math.min(max, value))
44
+ }
45
+
46
+ export function normalizeMemoryLookupLimits(settings: Record<string, unknown>): MemoryLookupLimits {
47
+ const depthRaw = parseIntSetting(settings.memoryReferenceDepth ?? settings.memoryMaxDepth)
48
+ const perLookupRaw = parseIntSetting(settings.maxMemoriesPerLookup ?? settings.memoryMaxPerLookup)
49
+ const linkedRaw = parseIntSetting(settings.maxLinkedMemoriesExpanded)
50
+
51
+ const maxDepth = clamp(depthRaw ?? DEFAULT_MEMORY_REFERENCE_DEPTH, 0, MAX_MEMORY_REFERENCE_DEPTH)
52
+ const maxPerLookup = clamp(perLookupRaw ?? DEFAULT_MAX_MEMORIES_PER_LOOKUP, 1, MAX_MEMORIES_PER_LOOKUP)
53
+ const maxLinkedExpansion = clamp(linkedRaw ?? DEFAULT_MAX_LINKED_MEMORIES_EXPANDED, 0, MAX_LINKED_MEMORIES_EXPANDED)
54
+
55
+ return { maxDepth, maxPerLookup, maxLinkedExpansion }
56
+ }
57
+
58
+ export function resolveLookupRequest(
59
+ defaults: MemoryLookupLimits,
60
+ request: MemoryLookupRequest = {},
61
+ ): MemoryLookupLimits {
62
+ const depth = parseIntSetting(request.depth)
63
+ const limit = parseIntSetting(request.limit)
64
+ const linkedLimit = parseIntSetting(request.linkedLimit)
65
+
66
+ return {
67
+ maxDepth: clamp(depth ?? defaults.maxDepth, 0, defaults.maxDepth),
68
+ maxPerLookup: clamp(limit ?? defaults.maxPerLookup, 1, defaults.maxPerLookup),
69
+ maxLinkedExpansion: clamp(linkedLimit ?? defaults.maxLinkedExpansion, 0, defaults.maxLinkedExpansion),
70
+ }
71
+ }
72
+
73
+ export function normalizeLinkedMemoryIds(input: unknown, selfId?: string): string[] {
74
+ if (!Array.isArray(input)) return []
75
+ const out: string[] = []
76
+ const seen = new Set<string>()
77
+ for (const raw of input) {
78
+ const id = typeof raw === 'string' ? raw.trim() : ''
79
+ if (!id || id === selfId || seen.has(id)) continue
80
+ seen.add(id)
81
+ out.push(id)
82
+ }
83
+ return out
84
+ }
85
+
86
+ export function traverseLinkedMemoryGraph<TNode extends LinkedMemoryNode>(
87
+ seedNodes: TNode[],
88
+ opts: MemoryLookupLimits,
89
+ fetchByIds: (ids: string[]) => TNode[],
90
+ ): TraversalResult<TNode> {
91
+ if (!seedNodes.length || opts.maxPerLookup <= 0) {
92
+ return { entries: [], truncated: false, expandedLinkedCount: 0 }
93
+ }
94
+
95
+ const seen = new Set<string>()
96
+ const seedIds = new Set(seedNodes.map((n) => n.id))
97
+ const out: TNode[] = []
98
+ let queue: TNode[] = [...seedNodes]
99
+ let depth = 0
100
+ let truncated = false
101
+ let expandedLinkedCount = 0
102
+
103
+ while (queue.length > 0 && depth <= opts.maxDepth) {
104
+ const nextQueue: TNode[] = []
105
+ for (const entry of queue) {
106
+ if (seen.has(entry.id)) continue
107
+
108
+ const isLinkedExpansion = !seedIds.has(entry.id)
109
+ if (isLinkedExpansion) {
110
+ if (expandedLinkedCount >= opts.maxLinkedExpansion) {
111
+ truncated = true
112
+ return { entries: out, truncated, expandedLinkedCount }
113
+ }
114
+ expandedLinkedCount++
115
+ }
116
+
117
+ seen.add(entry.id)
118
+ out.push(entry)
119
+ if (out.length >= opts.maxPerLookup) {
120
+ truncated = true
121
+ return { entries: out, truncated, expandedLinkedCount }
122
+ }
123
+
124
+ if (depth >= opts.maxDepth) continue
125
+ const linkedIds = normalizeLinkedMemoryIds(entry.linkedMemoryIds, entry.id).filter((id) => !seen.has(id))
126
+ if (!linkedIds.length) continue
127
+ const linkedEntries = fetchByIds(linkedIds)
128
+ for (const linked of linkedEntries) {
129
+ if (!linked?.id || seen.has(linked.id)) continue
130
+ nextQueue.push(linked)
131
+ }
132
+ }
133
+ queue = nextQueue
134
+ depth++
135
+ }
136
+
137
+ return { entries: out, truncated, expandedLinkedCount }
138
+ }
@@ -0,0 +1,245 @@
1
+ import { deriveOpenClawWsUrl, normalizeOpenClawEndpoint } from '@/lib/openclaw-endpoint'
2
+ import { decryptKey, loadCredentials } from './storage'
3
+
4
+ export interface OpenClawHealthInput {
5
+ endpoint?: string | null
6
+ credentialId?: string | null
7
+ token?: string | null
8
+ model?: string | null
9
+ timeoutMs?: number
10
+ }
11
+
12
+ export interface OpenClawHealthResult {
13
+ ok: boolean
14
+ endpoint: string
15
+ wsUrl: string
16
+ authProvided: boolean
17
+ model: string | null
18
+ models: string[]
19
+ modelsStatus: number | null
20
+ chatStatus: number | null
21
+ completionSample?: string
22
+ error?: string
23
+ hint?: string
24
+ }
25
+
26
+ function normalizeToken(value: unknown): string | null {
27
+ return typeof value === 'string' && value.trim() ? value.trim() : null
28
+ }
29
+
30
+ function resolveCredentialToken(credentialId?: string | null): string | null {
31
+ const id = normalizeToken(credentialId)
32
+ if (!id) return null
33
+ const credentials = loadCredentials()
34
+ const credential = credentials[id]
35
+ if (!credential?.encryptedKey) return null
36
+ try {
37
+ return decryptKey(credential.encryptedKey)
38
+ } catch {
39
+ return null
40
+ }
41
+ }
42
+
43
+ function extractModels(payload: any): string[] {
44
+ const models = Array.isArray(payload?.data) ? payload.data : []
45
+ return models
46
+ .map((item: any) => (typeof item?.id === 'string' ? item.id.trim() : ''))
47
+ .filter(Boolean)
48
+ }
49
+
50
+ function extractChatText(payload: any): string {
51
+ const content = payload?.choices?.[0]?.message?.content
52
+ if (typeof content === 'string') return content.trim()
53
+ if (Array.isArray(content)) {
54
+ return content
55
+ .map((block: any) => {
56
+ if (typeof block?.text === 'string') return block.text
57
+ if (typeof block?.content === 'string') return block.content
58
+ return ''
59
+ })
60
+ .join(' ')
61
+ .trim()
62
+ }
63
+ return ''
64
+ }
65
+
66
+ function describeHttpError(status: number): { error: string; hint?: string } {
67
+ if (status === 401) {
68
+ return {
69
+ error: 'OpenClaw endpoint rejected auth (401 Unauthorized).',
70
+ hint: 'Set a valid OpenClaw token credential on the agent/session or pass credentialId/token to this health check.',
71
+ }
72
+ }
73
+ if (status === 404) {
74
+ return {
75
+ error: 'OpenClaw endpoint path is invalid (404).',
76
+ hint: 'Point to the gateway root/ws URL and let SwarmClaw normalize it, or use an explicit /v1 endpoint.',
77
+ }
78
+ }
79
+ if (status === 405) {
80
+ return {
81
+ error: 'OpenClaw endpoint method mismatch (405).',
82
+ hint: 'Ensure this is an OpenAI-compatible chat endpoint exposed by the OpenClaw gateway.',
83
+ }
84
+ }
85
+ return {
86
+ error: `OpenClaw endpoint returned HTTP ${status}.`,
87
+ }
88
+ }
89
+
90
+ function createTimeoutError(message: string): Error {
91
+ const timeoutErr = new Error(message)
92
+ ;(timeoutErr as any).name = 'TimeoutError'
93
+ return timeoutErr
94
+ }
95
+
96
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, onTimeout?: () => void, message?: string): Promise<T> {
97
+ let timer: ReturnType<typeof setTimeout> | null = null
98
+ try {
99
+ return await new Promise<T>((resolve, reject) => {
100
+ timer = setTimeout(() => {
101
+ try { onTimeout?.() } catch { /* noop */ }
102
+ reject(createTimeoutError(message || `Request timed out after ${timeoutMs}ms`))
103
+ }, timeoutMs)
104
+ promise
105
+ .then((value) => {
106
+ if (timer) clearTimeout(timer)
107
+ resolve(value)
108
+ })
109
+ .catch((err) => {
110
+ if (timer) clearTimeout(timer)
111
+ reject(err)
112
+ })
113
+ })
114
+ } finally {
115
+ if (timer) clearTimeout(timer)
116
+ }
117
+ }
118
+
119
+ async function fetchJsonWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<{ response: Response; body: any }> {
120
+ const controller = new AbortController()
121
+ try {
122
+ const response = await withTimeout(
123
+ fetch(url, { ...init, signal: controller.signal }),
124
+ timeoutMs,
125
+ () => controller.abort(),
126
+ `Request timed out after ${timeoutMs}ms`,
127
+ )
128
+ const text = await withTimeout(
129
+ response.text(),
130
+ timeoutMs,
131
+ () => controller.abort(),
132
+ `Response read timed out after ${timeoutMs}ms`,
133
+ )
134
+ let body: any = {}
135
+ if (text) {
136
+ try {
137
+ body = JSON.parse(text)
138
+ } catch {
139
+ body = {}
140
+ }
141
+ }
142
+ return { response, body }
143
+ } catch (err: any) {
144
+ if (err?.name === 'AbortError') throw createTimeoutError(`Request timed out after ${timeoutMs}ms`)
145
+ throw err
146
+ }
147
+ }
148
+
149
+ export async function probeOpenClawHealth(input: OpenClawHealthInput): Promise<OpenClawHealthResult> {
150
+ const endpoint = normalizeOpenClawEndpoint(input.endpoint || undefined)
151
+ const wsUrl = deriveOpenClawWsUrl(endpoint)
152
+ const timeoutMs = Math.max(1000, Math.min(30_000, Math.trunc(input.timeoutMs || 8000)))
153
+ const token = normalizeToken(input.token) || resolveCredentialToken(input.credentialId)
154
+ const authProvided = !!token
155
+ const headers: Record<string, string> = {
156
+ // Use text/plain to bypass Express body parsers in Hostinger/proxy setups.
157
+ // The OpenClaw gateway parses the body as JSON regardless of Content-Type.
158
+ 'content-type': 'text/plain',
159
+ }
160
+ if (token) headers.authorization = `Bearer ${token}`
161
+
162
+ let models: string[] = []
163
+ let modelsStatus: number | null = null
164
+ let chatStatus: number | null = null
165
+ let completionSample = ''
166
+ let lastError = ''
167
+ let lastHint: string | undefined
168
+
169
+ try {
170
+ const { response: modelsRes, body } = await fetchJsonWithTimeout(`${endpoint}/models`, {
171
+ headers,
172
+ cache: 'no-store',
173
+ }, timeoutMs)
174
+ modelsStatus = modelsRes.status
175
+ if (modelsRes.ok) {
176
+ models = extractModels(body)
177
+ } else {
178
+ const err = describeHttpError(modelsRes.status)
179
+ lastError = err.error
180
+ lastHint = err.hint
181
+ }
182
+ } catch (err: any) {
183
+ if (err?.name === 'TimeoutError') {
184
+ lastError = `OpenClaw models probe timed out after ${timeoutMs}ms.`
185
+ } else {
186
+ lastError = err?.message || 'Failed to connect to OpenClaw endpoint.'
187
+ }
188
+ return {
189
+ ok: false,
190
+ endpoint,
191
+ wsUrl,
192
+ authProvided,
193
+ model: null,
194
+ models: [],
195
+ modelsStatus: null,
196
+ chatStatus: null,
197
+ error: lastError,
198
+ hint: 'Verify the OpenClaw gateway is running and reachable at this host/port.',
199
+ }
200
+ }
201
+
202
+ const model = normalizeToken(input.model) || models[0] || 'default'
203
+
204
+ try {
205
+ const { response: chatRes, body } = await fetchJsonWithTimeout(`${endpoint}/chat/completions`, {
206
+ method: 'POST',
207
+ headers,
208
+ cache: 'no-store',
209
+ body: JSON.stringify({
210
+ model,
211
+ messages: [{ role: 'user', content: 'Reply with OPENCLAW_HEALTH_OK' }],
212
+ stream: false,
213
+ max_tokens: 12,
214
+ }),
215
+ }, timeoutMs)
216
+ chatStatus = chatRes.status
217
+ if (!chatRes.ok) {
218
+ const err = describeHttpError(chatRes.status)
219
+ lastError = err.error
220
+ lastHint = err.hint || lastHint
221
+ } else {
222
+ completionSample = extractChatText(body).slice(0, 240)
223
+ }
224
+ } catch (err: any) {
225
+ if (err?.name === 'TimeoutError') {
226
+ lastError = `OpenClaw chat probe timed out after ${timeoutMs}ms.`
227
+ } else {
228
+ lastError = err?.message || 'OpenClaw chat probe failed.'
229
+ }
230
+ }
231
+
232
+ return {
233
+ ok: !!chatStatus && chatStatus >= 200 && chatStatus < 300,
234
+ endpoint,
235
+ wsUrl,
236
+ authProvided,
237
+ model,
238
+ models,
239
+ modelsStatus,
240
+ chatStatus,
241
+ completionSample: completionSample || undefined,
242
+ error: lastError || undefined,
243
+ hint: lastHint,
244
+ }
245
+ }