@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,232 @@
1
+ import type { Message, ProviderType } from '@/types'
2
+ import { getMemoryDb } from './memory-db'
3
+
4
+ // --- Context window sizes (tokens) per provider/model ---
5
+
6
+ const PROVIDER_CONTEXT_WINDOWS: Record<string, number> = {
7
+ // Anthropic
8
+ 'claude-opus-4-6': 200_000,
9
+ 'claude-sonnet-4-6': 200_000,
10
+ 'claude-haiku-4-5-20251001': 200_000,
11
+ 'claude-sonnet-4-5-20250514': 200_000,
12
+ // OpenAI
13
+ 'gpt-4o': 128_000,
14
+ 'gpt-4o-mini': 128_000,
15
+ 'gpt-4.1': 1_047_576,
16
+ 'gpt-4.1-mini': 1_047_576,
17
+ 'gpt-4.1-nano': 1_047_576,
18
+ 'o3': 200_000,
19
+ 'o3-mini': 128_000,
20
+ 'o4-mini': 200_000,
21
+ // Codex CLI
22
+ 'gpt-5.3-codex': 1_047_576,
23
+ 'gpt-5.2-codex': 1_047_576,
24
+ 'gpt-5.1-codex': 1_047_576,
25
+ 'gpt-5-codex': 1_047_576,
26
+ 'gpt-5-codex-mini': 1_047_576,
27
+ // Google Gemini
28
+ 'gemini-2.5-pro': 1_048_576,
29
+ 'gemini-2.5-flash': 1_048_576,
30
+ 'gemini-2.5-flash-lite': 1_048_576,
31
+ // DeepSeek
32
+ 'deepseek-chat': 64_000,
33
+ 'deepseek-reasoner': 64_000,
34
+ // Mistral
35
+ 'mistral-large-latest': 128_000,
36
+ 'mistral-small-latest': 128_000,
37
+ 'magistral-medium-2506': 128_000,
38
+ 'devstral-small-latest': 128_000,
39
+ // xAI
40
+ 'grok-3': 131_072,
41
+ 'grok-3-fast': 131_072,
42
+ 'grok-3-mini': 131_072,
43
+ 'grok-3-mini-fast': 131_072,
44
+ }
45
+
46
+ const PROVIDER_DEFAULT_WINDOWS: Record<string, number> = {
47
+ anthropic: 200_000,
48
+ 'claude-cli': 200_000,
49
+ openai: 128_000,
50
+ 'codex-cli': 1_047_576,
51
+ 'opencode-cli': 200_000,
52
+ google: 1_048_576,
53
+ deepseek: 64_000,
54
+ groq: 32_768,
55
+ together: 32_768,
56
+ mistral: 128_000,
57
+ xai: 131_072,
58
+ fireworks: 32_768,
59
+ ollama: 8_192,
60
+ openclaw: 128_000,
61
+ }
62
+
63
+ /** Get context window size for a model, falling back to provider default */
64
+ export function getContextWindowSize(provider: string, model: string): number {
65
+ return PROVIDER_CONTEXT_WINDOWS[model]
66
+ || PROVIDER_DEFAULT_WINDOWS[provider]
67
+ || 8_192
68
+ }
69
+
70
+ // --- Token estimation ---
71
+
72
+ /** Rough token estimate: ~4 chars per token for English text */
73
+ export function estimateTokens(text: string): number {
74
+ if (!text) return 0
75
+ return Math.ceil(text.length / 4)
76
+ }
77
+
78
+ /** Estimate total tokens for a message array */
79
+ export function estimateMessagesTokens(messages: Message[]): number {
80
+ let total = 0
81
+ for (const m of messages) {
82
+ // Role + overhead per message (~4 tokens)
83
+ total += 4
84
+ total += estimateTokens(m.text)
85
+ if (m.toolEvents) {
86
+ for (const te of m.toolEvents) {
87
+ total += estimateTokens(te.name) + estimateTokens(te.input)
88
+ if (te.output) total += estimateTokens(te.output)
89
+ }
90
+ }
91
+ }
92
+ return total
93
+ }
94
+
95
+ // --- Context status ---
96
+
97
+ export interface ContextStatus {
98
+ estimatedTokens: number
99
+ contextWindow: number
100
+ percentUsed: number
101
+ messageCount: number
102
+ strategy: 'ok' | 'warning' | 'critical'
103
+ }
104
+
105
+ export function getContextStatus(
106
+ messages: Message[],
107
+ systemPromptTokens: number,
108
+ provider: string,
109
+ model: string,
110
+ ): ContextStatus {
111
+ const contextWindow = getContextWindowSize(provider, model)
112
+ const messageTokens = estimateMessagesTokens(messages)
113
+ const estimatedTokens = messageTokens + systemPromptTokens
114
+ const percentUsed = Math.round((estimatedTokens / contextWindow) * 100)
115
+ return {
116
+ estimatedTokens,
117
+ contextWindow,
118
+ percentUsed,
119
+ messageCount: messages.length,
120
+ strategy: percentUsed >= 90 ? 'critical' : percentUsed >= 70 ? 'warning' : 'ok',
121
+ }
122
+ }
123
+
124
+ // --- Memory consolidation ---
125
+
126
+ /** Extract important facts from old messages before pruning */
127
+ export function consolidateToMemory(
128
+ messages: Message[],
129
+ agentId: string | null,
130
+ sessionId: string,
131
+ ): number {
132
+ if (!agentId) return 0
133
+ const db = getMemoryDb()
134
+ let stored = 0
135
+
136
+ for (const m of messages) {
137
+ if (m.role !== 'assistant' || !m.text) continue
138
+ // Look for decisions, commitments, key facts
139
+ const text = m.text
140
+ const hasDecision = /\b(decided|decision|agreed|committed|will do|plan is|approach is|chosen|selected)\b/i.test(text)
141
+ const hasKeyFact = /\b(important|critical|note|remember|key point|constraint|requirement|deadline)\b/i.test(text)
142
+ const hasResult = /\b(result|found|discovered|concluded|completed|built|created|deployed)\b/i.test(text)
143
+
144
+ if (hasDecision || hasKeyFact || hasResult) {
145
+ // Create a concise summary (first 500 chars)
146
+ const summary = text.length > 500 ? text.slice(0, 500) + '...' : text
147
+ const category = hasDecision ? 'decision' : hasResult ? 'result' : 'note'
148
+ const title = `[auto-consolidated] ${text.slice(0, 60).replace(/\n/g, ' ')}`
149
+
150
+ db.add({
151
+ agentId,
152
+ sessionId,
153
+ category,
154
+ title,
155
+ content: summary,
156
+ })
157
+ stored++
158
+ }
159
+ }
160
+ return stored
161
+ }
162
+
163
+ // --- Compaction strategies ---
164
+
165
+ export interface CompactionResult {
166
+ messages: Message[]
167
+ prunedCount: number
168
+ memoriesStored: number
169
+ summaryAdded: boolean
170
+ }
171
+
172
+ /** Sliding window: keep last N messages */
173
+ export function slidingWindowCompact(
174
+ messages: Message[],
175
+ keepLastN: number,
176
+ ): Message[] {
177
+ if (messages.length <= keepLastN) return messages
178
+ return messages.slice(-keepLastN)
179
+ }
180
+
181
+ /** Summarize old messages, keep recent ones */
182
+ export async function summarizeAndCompact(opts: {
183
+ messages: Message[]
184
+ keepLastN: number
185
+ agentId: string | null
186
+ sessionId: string
187
+ generateSummary: (text: string) => Promise<string>
188
+ }): Promise<CompactionResult> {
189
+ const { messages, keepLastN, agentId, sessionId, generateSummary } = opts
190
+ if (messages.length <= keepLastN) {
191
+ return { messages, prunedCount: 0, memoriesStored: 0, summaryAdded: false }
192
+ }
193
+
194
+ const oldMessages = messages.slice(0, -keepLastN)
195
+ const recentMessages = messages.slice(-keepLastN)
196
+
197
+ // Consolidate important info to memory before pruning
198
+ const memoriesStored = consolidateToMemory(oldMessages, agentId, sessionId)
199
+
200
+ // Build text for summarization
201
+ const conversationText = oldMessages
202
+ .map((m) => `${m.role}: ${m.text}`)
203
+ .join('\n\n')
204
+
205
+ const summary = await generateSummary(conversationText)
206
+
207
+ const summaryMessage: Message = {
208
+ role: 'assistant',
209
+ text: `[Context Summary]\n${summary}`,
210
+ time: Date.now(),
211
+ kind: 'system',
212
+ }
213
+
214
+ return {
215
+ messages: [summaryMessage, ...recentMessages],
216
+ prunedCount: oldMessages.length,
217
+ memoriesStored,
218
+ summaryAdded: true,
219
+ }
220
+ }
221
+
222
+ /** Auto-compact: triggers when estimated tokens exceed threshold */
223
+ export function shouldAutoCompact(
224
+ messages: Message[],
225
+ systemPromptTokens: number,
226
+ provider: string,
227
+ model: string,
228
+ triggerPercent = 80,
229
+ ): boolean {
230
+ const status = getContextStatus(messages, systemPromptTokens, provider, model)
231
+ return status.percentUsed >= triggerPercent
232
+ }
@@ -0,0 +1,31 @@
1
+ // Model cost table: [inputCostPer1M, outputCostPer1M] in USD
2
+ const MODEL_COSTS: Record<string, [number, number]> = {
3
+ // Anthropic
4
+ 'claude-opus-4-6': [15, 75],
5
+ 'claude-sonnet-4-6': [3, 15],
6
+ 'claude-haiku-4-5-20251001': [0.8, 4],
7
+ 'claude-sonnet-4-5-20250514': [3, 15],
8
+ // OpenAI
9
+ 'gpt-4o': [2.5, 10],
10
+ 'gpt-4o-mini': [0.15, 0.6],
11
+ 'gpt-4.1': [2, 8],
12
+ 'gpt-4.1-mini': [0.4, 1.6],
13
+ 'gpt-4.1-nano': [0.1, 0.4],
14
+ 'o3': [10, 40],
15
+ 'o3-mini': [1.1, 4.4],
16
+ 'o4-mini': [1.1, 4.4],
17
+ // OpenAI embeddings
18
+ 'text-embedding-3-small': [0.02, 0],
19
+ 'text-embedding-3-large': [0.13, 0],
20
+ }
21
+
22
+ export function estimateCost(model: string, inputTokens: number, outputTokens: number): number {
23
+ const costs = MODEL_COSTS[model]
24
+ if (!costs) return 0
25
+ const [inputRate, outputRate] = costs
26
+ return (inputTokens * inputRate + outputTokens * outputRate) / 1_000_000
27
+ }
28
+
29
+ export function getModelCosts(): Record<string, [number, number]> {
30
+ return { ...MODEL_COSTS }
31
+ }
@@ -0,0 +1,354 @@
1
+ import { loadQueue, loadSchedules, loadSessions, saveSessions, loadConnectors } from './storage'
2
+ import { processNext, cleanupFinishedTaskSessions, validateCompletedTasksQueue, recoverStalledRunningTasks } from './queue'
3
+ import { startScheduler, stopScheduler } from './scheduler'
4
+ import { sweepOrphanedBrowsers, getActiveBrowserCount } from './session-tools'
5
+ import {
6
+ autoStartConnectors,
7
+ stopAllConnectors,
8
+ listRunningConnectors,
9
+ sendConnectorMessage,
10
+ startConnector,
11
+ getConnectorStatus,
12
+ } from './connectors/manager'
13
+ import { startHeartbeatService, stopHeartbeatService, getHeartbeatServiceStatus } from './heartbeat-service'
14
+
15
+ const QUEUE_CHECK_INTERVAL = 30_000 // 30 seconds
16
+ const BROWSER_SWEEP_INTERVAL = 60_000 // 60 seconds
17
+ const BROWSER_MAX_AGE = 10 * 60 * 1000 // 10 minutes idle = orphaned
18
+ const HEALTH_CHECK_INTERVAL = 120_000 // 2 minutes
19
+ const STALE_MULTIPLIER = 4 // session is stale after N × heartbeat interval
20
+ const STALE_MIN_MS = 4 * 60 * 1000 // minimum 4 minutes regardless of interval
21
+ const STALE_AUTO_DISABLE_MULTIPLIER = 16 // auto-disable after much longer sustained staleness
22
+ const STALE_AUTO_DISABLE_MIN_MS = 45 * 60 * 1000 // never auto-disable before 45 minutes
23
+ const CONNECTOR_RESTART_BASE_MS = 30_000
24
+ const CONNECTOR_RESTART_MAX_MS = 15 * 60 * 1000
25
+
26
+ function parseBoolish(value: unknown, fallback: boolean): boolean {
27
+ if (typeof value === 'boolean') return value
28
+ if (typeof value !== 'string') return fallback
29
+ const normalized = value.trim().toLowerCase()
30
+ if (!normalized) return fallback
31
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
32
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false
33
+ return fallback
34
+ }
35
+
36
+ function daemonAutostartEnvEnabled(): boolean {
37
+ return parseBoolish(process.env.SWARMCLAW_DAEMON_AUTOSTART, true)
38
+ }
39
+
40
+ function parseHeartbeatIntervalSec(value: unknown, fallback = 120): number {
41
+ const parsed = typeof value === 'number'
42
+ ? value
43
+ : typeof value === 'string'
44
+ ? Number.parseInt(value, 10)
45
+ : Number.NaN
46
+ if (!Number.isFinite(parsed)) return fallback
47
+ return Math.max(0, Math.min(3600, Math.trunc(parsed)))
48
+ }
49
+
50
+ function normalizeWhatsappTarget(raw?: string | null): string | null {
51
+ const input = (raw || '').trim()
52
+ if (!input) return null
53
+ if (input.includes('@')) return input
54
+ let digits = input.replace(/[^\d+]/g, '')
55
+ if (digits.startsWith('+')) digits = digits.slice(1)
56
+ if (digits.startsWith('0') && digits.length >= 10) {
57
+ digits = `44${digits.slice(1)}`
58
+ }
59
+ digits = digits.replace(/[^\d]/g, '')
60
+ return digits ? `${digits}@s.whatsapp.net` : null
61
+ }
62
+
63
+ // Store daemon state on globalThis to survive HMR reloads
64
+ const gk = '__swarmclaw_daemon__' as const
65
+ const ds: {
66
+ queueIntervalId: ReturnType<typeof setInterval> | null
67
+ browserSweepId: ReturnType<typeof setInterval> | null
68
+ healthIntervalId: ReturnType<typeof setInterval> | null
69
+ /** Session IDs we've already alerted as stale (alert-once semantics). */
70
+ staleSessionIds: Set<string>
71
+ connectorRestartState: Map<string, { lastAttemptAt: number; failCount: number }>
72
+ manualStopRequested: boolean
73
+ running: boolean
74
+ lastProcessedAt: number | null
75
+ } = (globalThis as any)[gk] ?? ((globalThis as any)[gk] = {
76
+ queueIntervalId: null,
77
+ browserSweepId: null,
78
+ healthIntervalId: null,
79
+ staleSessionIds: new Set<string>(),
80
+ connectorRestartState: new Map<string, { lastAttemptAt: number; failCount: number }>(),
81
+ manualStopRequested: false,
82
+ running: false,
83
+ lastProcessedAt: null,
84
+ })
85
+
86
+ // Backfill fields for hot-reloaded daemon state objects from older code versions.
87
+ if (!ds.staleSessionIds) ds.staleSessionIds = new Set<string>()
88
+ if (!ds.connectorRestartState) ds.connectorRestartState = new Map<string, { lastAttemptAt: number; failCount: number }>()
89
+ // Migrate from old issueLastAlertAt map if present (HMR across code versions)
90
+ if ((ds as any).issueLastAlertAt) delete (ds as any).issueLastAlertAt
91
+ if (ds.healthIntervalId === undefined) ds.healthIntervalId = null
92
+ if (ds.manualStopRequested === undefined) ds.manualStopRequested = false
93
+
94
+ export function ensureDaemonStarted(source = 'unknown'): boolean {
95
+ if (ds.running) return false
96
+ if (!daemonAutostartEnvEnabled()) return false
97
+ if (ds.manualStopRequested) return false
98
+ startDaemon({ source, manualStart: false })
99
+ return true
100
+ }
101
+
102
+ export function startDaemon(options?: { source?: string; manualStart?: boolean }) {
103
+ const source = options?.source || 'unknown'
104
+ const manualStart = options?.manualStart === true
105
+ if (manualStart) ds.manualStopRequested = false
106
+
107
+ if (ds.running) {
108
+ // In dev/HMR, daemon can already be flagged running while new interval types
109
+ // (for example health monitor) were introduced in newer code.
110
+ startQueueProcessor()
111
+ startBrowserSweep()
112
+ startHealthMonitor()
113
+ startHeartbeatService()
114
+ return
115
+ }
116
+ ds.running = true
117
+ console.log(`[daemon] Starting daemon (source=${source}, scheduler + queue processor + heartbeat)`)
118
+
119
+ validateCompletedTasksQueue()
120
+ cleanupFinishedTaskSessions()
121
+ startScheduler()
122
+ startQueueProcessor()
123
+ startBrowserSweep()
124
+ startHealthMonitor()
125
+ startHeartbeatService()
126
+
127
+ // Auto-start enabled connectors
128
+ autoStartConnectors().catch((err) => {
129
+ console.error('[daemon] Error auto-starting connectors:', err.message)
130
+ })
131
+ }
132
+
133
+ export function stopDaemon(options?: { source?: string; manualStop?: boolean }) {
134
+ const source = options?.source || 'unknown'
135
+ if (options?.manualStop === true) ds.manualStopRequested = true
136
+ if (!ds.running) return
137
+ ds.running = false
138
+ console.log(`[daemon] Stopping daemon (source=${source})`)
139
+
140
+ stopScheduler()
141
+ stopQueueProcessor()
142
+ stopBrowserSweep()
143
+ stopHealthMonitor()
144
+ stopHeartbeatService()
145
+ stopAllConnectors().catch(() => {})
146
+ }
147
+
148
+ function startBrowserSweep() {
149
+ if (ds.browserSweepId) return
150
+ ds.browserSweepId = setInterval(() => {
151
+ const count = getActiveBrowserCount()
152
+ if (count > 0) {
153
+ const cleaned = sweepOrphanedBrowsers(BROWSER_MAX_AGE)
154
+ if (cleaned > 0) {
155
+ console.log(`[daemon] Cleaned ${cleaned} orphaned browser(s), ${getActiveBrowserCount()} still active`)
156
+ }
157
+ }
158
+ }, BROWSER_SWEEP_INTERVAL)
159
+ }
160
+
161
+ function stopBrowserSweep() {
162
+ if (ds.browserSweepId) {
163
+ clearInterval(ds.browserSweepId)
164
+ ds.browserSweepId = null
165
+ }
166
+ // Kill all remaining browsers on shutdown
167
+ sweepOrphanedBrowsers(0)
168
+ }
169
+
170
+ function startQueueProcessor() {
171
+ if (ds.queueIntervalId) return
172
+ ds.queueIntervalId = setInterval(async () => {
173
+ const queue = loadQueue()
174
+ if (queue.length > 0) {
175
+ console.log(`[daemon] Processing ${queue.length} queued task(s)`)
176
+ await processNext()
177
+ ds.lastProcessedAt = Date.now()
178
+ }
179
+ }, QUEUE_CHECK_INTERVAL)
180
+ }
181
+
182
+ function stopQueueProcessor() {
183
+ if (ds.queueIntervalId) {
184
+ clearInterval(ds.queueIntervalId)
185
+ ds.queueIntervalId = null
186
+ }
187
+ }
188
+
189
+ async function sendHealthAlert(text: string) {
190
+ console.warn(`[health] ${text}`)
191
+ try {
192
+ const running = listRunningConnectors('whatsapp')
193
+ if (!running.length) return
194
+ const candidate = running[0]
195
+ const target = candidate.recentChannelId
196
+ || normalizeWhatsappTarget(candidate.configuredTargets[0] || null)
197
+ if (!target) return
198
+ await sendConnectorMessage({
199
+ connectorId: candidate.id,
200
+ channelId: target,
201
+ text: `⚠️ SwarmClaw health alert: ${text}`,
202
+ })
203
+ } catch {
204
+ // alerts are best effort; log-only fallback is acceptable
205
+ }
206
+ }
207
+
208
+ async function runConnectorHealthChecks(now: number) {
209
+ const connectors = loadConnectors()
210
+ for (const connector of Object.values(connectors) as any[]) {
211
+ if (!connector?.id) continue
212
+ if (connector.isEnabled !== true) {
213
+ ds.connectorRestartState.delete(connector.id)
214
+ continue
215
+ }
216
+
217
+ const runtimeStatus = getConnectorStatus(connector.id)
218
+ if (runtimeStatus === 'running') {
219
+ ds.connectorRestartState.delete(connector.id)
220
+ continue
221
+ }
222
+
223
+ const current = ds.connectorRestartState.get(connector.id) || { lastAttemptAt: 0, failCount: 0 }
224
+ const backoffMs = Math.min(
225
+ CONNECTOR_RESTART_MAX_MS,
226
+ CONNECTOR_RESTART_BASE_MS * (2 ** Math.min(6, current.failCount)),
227
+ )
228
+ if ((now - current.lastAttemptAt) < backoffMs) continue
229
+
230
+ current.lastAttemptAt = now
231
+ ds.connectorRestartState.set(connector.id, current)
232
+ try {
233
+ await startConnector(connector.id)
234
+ ds.connectorRestartState.delete(connector.id)
235
+ await sendHealthAlert(`Connector "${connector.name}" (${connector.platform}) was down and has been auto-restarted.`)
236
+ } catch (err: any) {
237
+ current.failCount += 1
238
+ ds.connectorRestartState.set(connector.id, current)
239
+ console.warn(`[health] Connector auto-restart failed for ${connector.name}: ${err?.message || String(err)}`)
240
+ }
241
+ }
242
+ }
243
+
244
+ async function runHealthChecks() {
245
+ // Continuously keep the completed queue honest.
246
+ validateCompletedTasksQueue()
247
+ recoverStalledRunningTasks()
248
+
249
+ // Keep heartbeat state in sync with task terminal states even without daemon restarts.
250
+ cleanupFinishedTaskSessions()
251
+
252
+ const sessions = loadSessions()
253
+ const now = Date.now()
254
+ const currentlyStale = new Set<string>()
255
+ let sessionsDirty = false
256
+
257
+ for (const session of Object.values(sessions) as any[]) {
258
+ if (!session?.id) continue
259
+ if (session.heartbeatEnabled !== true) continue
260
+
261
+ const intervalSec = parseHeartbeatIntervalSec(session.heartbeatIntervalSec, 120)
262
+ if (intervalSec <= 0) continue
263
+ const staleAfter = Math.max(intervalSec * STALE_MULTIPLIER * 1000, STALE_MIN_MS)
264
+ const lastActive = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : 0
265
+ if (lastActive <= 0) continue
266
+
267
+ const staleForMs = now - lastActive
268
+ if (staleForMs > staleAfter) {
269
+ const autoDisableAfter = Math.max(intervalSec * STALE_AUTO_DISABLE_MULTIPLIER * 1000, STALE_AUTO_DISABLE_MIN_MS)
270
+ if (staleForMs > autoDisableAfter) {
271
+ session.heartbeatEnabled = false
272
+ session.lastActiveAt = now
273
+ sessionsDirty = true
274
+ ds.staleSessionIds.delete(session.id)
275
+ await sendHealthAlert(
276
+ `Auto-disabled heartbeat for stale session "${session.name || session.id}" after ${Math.round(staleForMs / 60_000)}m of inactivity.`,
277
+ )
278
+ continue
279
+ }
280
+
281
+ currentlyStale.add(session.id)
282
+ // Only alert on transition from healthy → stale (once per stale episode)
283
+ if (!ds.staleSessionIds.has(session.id)) {
284
+ ds.staleSessionIds.add(session.id)
285
+ await sendHealthAlert(
286
+ `Session "${session.name || session.id}" heartbeat appears stale (last active ${(Math.round(staleForMs / 1000))}s ago, interval ${intervalSec}s).`,
287
+ )
288
+ }
289
+ }
290
+ }
291
+
292
+ // Clear recovered sessions so they can re-alert if they go stale again later
293
+ for (const id of ds.staleSessionIds) {
294
+ if (!currentlyStale.has(id)) {
295
+ ds.staleSessionIds.delete(id)
296
+ }
297
+ }
298
+
299
+ if (sessionsDirty) saveSessions(sessions)
300
+
301
+ await runConnectorHealthChecks(now)
302
+ }
303
+
304
+ function startHealthMonitor() {
305
+ if (ds.healthIntervalId) return
306
+ ds.healthIntervalId = setInterval(() => {
307
+ runHealthChecks().catch((err) => {
308
+ console.error('[daemon] Health monitor tick failed:', err?.message || String(err))
309
+ })
310
+ }, HEALTH_CHECK_INTERVAL)
311
+ }
312
+
313
+ function stopHealthMonitor() {
314
+ if (ds.healthIntervalId) {
315
+ clearInterval(ds.healthIntervalId)
316
+ ds.healthIntervalId = null
317
+ }
318
+ }
319
+
320
+ export async function runDaemonHealthCheckNow() {
321
+ await runHealthChecks()
322
+ }
323
+
324
+ export function getDaemonStatus() {
325
+ const queue = loadQueue()
326
+ const schedules = loadSchedules()
327
+
328
+ // Find next scheduled task
329
+ let nextScheduled: number | null = null
330
+ for (const s of Object.values(schedules) as any[]) {
331
+ if (s.status === 'active' && s.nextRunAt) {
332
+ if (!nextScheduled || s.nextRunAt < nextScheduled) {
333
+ nextScheduled = s.nextRunAt
334
+ }
335
+ }
336
+ }
337
+
338
+ return {
339
+ running: ds.running,
340
+ schedulerActive: ds.running,
341
+ autostartEnabled: daemonAutostartEnvEnabled(),
342
+ manualStopRequested: ds.manualStopRequested,
343
+ queueLength: queue.length,
344
+ lastProcessed: ds.lastProcessedAt,
345
+ nextScheduled,
346
+ heartbeat: getHeartbeatServiceStatus(),
347
+ health: {
348
+ monitorActive: !!ds.healthIntervalId,
349
+ staleSessions: ds.staleSessionIds.size,
350
+ connectorsInBackoff: ds.connectorRestartState.size,
351
+ checkIntervalSec: Math.trunc(HEALTH_CHECK_INTERVAL / 1000),
352
+ },
353
+ }
354
+ }
@@ -0,0 +1,3 @@
1
+ import path from 'path'
2
+
3
+ export const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data')