@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,327 @@
1
+ import crypto from 'crypto'
2
+ import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
3
+
4
+ const MAX_LOG_CHARS = 200_000
5
+ const DEFAULT_BACKGROUND_YIELD_MS = 10_000
6
+ const DEFAULT_TIMEOUT_MS = 30 * 60_000
7
+ const DEFAULT_TTL_MS = 30 * 60_000
8
+
9
+ export type ProcessStatus = 'running' | 'exited' | 'killed' | 'failed' | 'timeout'
10
+
11
+ export interface ProcessRecord {
12
+ id: string
13
+ command: string
14
+ cwd: string
15
+ agentId?: string | null
16
+ sessionId?: string | null
17
+ status: ProcessStatus
18
+ pid: number | null
19
+ startedAt: number
20
+ endedAt: number | null
21
+ exitCode: number | null
22
+ signal: string | null
23
+ log: string
24
+ pollCursor: number
25
+ timeoutAt: number | null
26
+ }
27
+
28
+ export interface StartProcessOptions {
29
+ command: string
30
+ cwd: string
31
+ env?: Record<string, string>
32
+ agentId?: string | null
33
+ sessionId?: string | null
34
+ timeoutMs?: number
35
+ yieldMs?: number
36
+ background?: boolean
37
+ }
38
+
39
+ export interface StartProcessResult {
40
+ status: 'completed' | 'running'
41
+ processId: string
42
+ output?: string
43
+ tail?: string
44
+ exitCode?: number | null
45
+ signal?: string | null
46
+ }
47
+
48
+ interface RuntimeState {
49
+ records: Map<string, ProcessRecord>
50
+ children: Map<string, ChildProcessWithoutNullStreams>
51
+ exitWaiters: Map<string, Promise<ProcessRecord>>
52
+ }
53
+
54
+ const globalKey = '__swarmclaw_process_manager__' as const
55
+ const state: RuntimeState = (globalThis as any)[globalKey] ?? ((globalThis as any)[globalKey] = {
56
+ records: new Map<string, ProcessRecord>(),
57
+ children: new Map<string, ChildProcessWithoutNullStreams>(),
58
+ exitWaiters: new Map<string, Promise<ProcessRecord>>(),
59
+ })
60
+
61
+ function now() {
62
+ return Date.now()
63
+ }
64
+
65
+ function trimLog(text: string): string {
66
+ if (text.length <= MAX_LOG_CHARS) return text
67
+ return text.slice(text.length - MAX_LOG_CHARS)
68
+ }
69
+
70
+ function appendLog(id: string, chunk: string) {
71
+ const rec = state.records.get(id)
72
+ if (!rec) return
73
+ rec.log = trimLog(rec.log + chunk)
74
+ }
75
+
76
+ function getTail(text: string, n = 4000): string {
77
+ return text.length <= n ? text : text.slice(text.length - n)
78
+ }
79
+
80
+ function markEnded(id: string, patch: Partial<ProcessRecord>) {
81
+ const rec = state.records.get(id)
82
+ if (!rec) return
83
+ rec.status = (patch.status || rec.status) as ProcessStatus
84
+ rec.endedAt = patch.endedAt ?? now()
85
+ rec.exitCode = patch.exitCode ?? rec.exitCode
86
+ rec.signal = patch.signal ?? rec.signal
87
+ }
88
+
89
+ function normalizeLines(text: string): string[] {
90
+ return text.split('\n')
91
+ }
92
+
93
+ async function wait(ms: number): Promise<void> {
94
+ await new Promise((resolve) => setTimeout(resolve, ms))
95
+ }
96
+
97
+ function getShellCommand(command: string): { shell: string; args: string[] } {
98
+ return { shell: '/bin/zsh', args: ['-lc', command] }
99
+ }
100
+
101
+ export async function startManagedProcess(opts: StartProcessOptions): Promise<StartProcessResult> {
102
+ const id = crypto.randomBytes(8).toString('hex')
103
+ const timeoutMs = Math.max(1000, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
104
+ const yieldMs = Math.max(250, opts.yieldMs ?? DEFAULT_BACKGROUND_YIELD_MS)
105
+ const startedAt = now()
106
+ const timeoutAt = startedAt + timeoutMs
107
+
108
+ const record: ProcessRecord = {
109
+ id,
110
+ command: opts.command,
111
+ cwd: opts.cwd,
112
+ agentId: opts.agentId ?? null,
113
+ sessionId: opts.sessionId ?? null,
114
+ status: 'running',
115
+ pid: null,
116
+ startedAt,
117
+ endedAt: null,
118
+ exitCode: null,
119
+ signal: null,
120
+ log: '',
121
+ pollCursor: 0,
122
+ timeoutAt,
123
+ }
124
+ state.records.set(id, record)
125
+
126
+ const { shell, args } = getShellCommand(opts.command)
127
+ const child = spawn(shell, args, {
128
+ cwd: opts.cwd,
129
+ env: { ...process.env, ...(opts.env || {}) },
130
+ stdio: 'pipe',
131
+ })
132
+ state.children.set(id, child)
133
+ record.pid = child.pid ?? null
134
+
135
+ const timeoutTimer = setTimeout(() => {
136
+ const rec = state.records.get(id)
137
+ if (!rec || rec.status !== 'running') return
138
+ rec.status = 'timeout'
139
+ appendLog(id, '\n[process] Timeout reached. Terminating process.\n')
140
+ try { child.kill('SIGTERM') } catch { /* noop */ }
141
+ }, timeoutMs)
142
+
143
+ child.stdout.on('data', (buf: Buffer) => appendLog(id, buf.toString()))
144
+ child.stderr.on('data', (buf: Buffer) => appendLog(id, buf.toString()))
145
+
146
+ const exitPromise = new Promise<ProcessRecord>((resolve) => {
147
+ child.on('error', (err) => {
148
+ clearTimeout(timeoutTimer)
149
+ appendLog(id, `\n[process] Spawn error: ${err.message}\n`)
150
+ markEnded(id, { status: 'failed', exitCode: 1, signal: null, endedAt: now() })
151
+ state.children.delete(id)
152
+ resolve(state.records.get(id)!)
153
+ })
154
+ child.on('exit', (code, signal) => {
155
+ clearTimeout(timeoutTimer)
156
+ const rec = state.records.get(id)
157
+ if (!rec) return
158
+ const timedOut = rec.status === 'timeout'
159
+ const killed = rec.status === 'killed'
160
+ markEnded(id, {
161
+ status: timedOut ? 'timeout' : killed ? 'killed' : 'exited',
162
+ exitCode: typeof code === 'number' ? code : rec.exitCode,
163
+ signal: signal ? String(signal) : rec.signal,
164
+ endedAt: now(),
165
+ })
166
+ state.children.delete(id)
167
+ resolve(state.records.get(id)!)
168
+ })
169
+ })
170
+ state.exitWaiters.set(id, exitPromise)
171
+
172
+ if (opts.background) {
173
+ return {
174
+ status: 'running',
175
+ processId: id,
176
+ tail: getTail(record.log),
177
+ }
178
+ }
179
+
180
+ const completed = await Promise.race([
181
+ exitPromise.then((r) => ({ type: 'exit' as const, record: r })),
182
+ wait(yieldMs).then(() => ({ type: 'yield' as const })),
183
+ ])
184
+
185
+ if (completed.type === 'yield') {
186
+ return {
187
+ status: 'running',
188
+ processId: id,
189
+ tail: getTail(state.records.get(id)?.log || ''),
190
+ }
191
+ }
192
+
193
+ const rec = completed.record
194
+ return {
195
+ status: 'completed',
196
+ processId: id,
197
+ output: rec.log,
198
+ exitCode: rec.exitCode,
199
+ signal: rec.signal,
200
+ }
201
+ }
202
+
203
+ export function listManagedProcesses(agentId?: string | null): ProcessRecord[] {
204
+ sweepManagedProcesses()
205
+ const list = Array.from(state.records.values())
206
+ return list
207
+ .filter((r) => !agentId || r.agentId === agentId)
208
+ .sort((a, b) => b.startedAt - a.startedAt)
209
+ }
210
+
211
+ export function getManagedProcess(processId: string): ProcessRecord | null {
212
+ sweepManagedProcesses()
213
+ return state.records.get(processId) || null
214
+ }
215
+
216
+ export function pollManagedProcess(processId: string): { process: ProcessRecord; chunk: string } | null {
217
+ const rec = state.records.get(processId)
218
+ if (!rec) return null
219
+ const chunk = rec.log.slice(rec.pollCursor)
220
+ rec.pollCursor = rec.log.length
221
+ return { process: rec, chunk }
222
+ }
223
+
224
+ export function readManagedProcessLog(
225
+ processId: string,
226
+ offset?: number,
227
+ limit?: number,
228
+ ): { process: ProcessRecord; text: string; totalLines: number } | null {
229
+ const rec = state.records.get(processId)
230
+ if (!rec) return null
231
+ const lines = normalizeLines(rec.log)
232
+ const total = lines.length
233
+
234
+ const safeOffset = Math.max(0, Number.isFinite(offset) ? Math.trunc(offset as number) : Math.max(0, total - 200))
235
+ let safeLimit = Number.isFinite(limit) ? Math.max(1, Math.trunc(limit as number)) : 200
236
+ if (!Number.isFinite(limit) && Number.isFinite(offset)) {
237
+ safeLimit = Math.max(1, total - safeOffset)
238
+ }
239
+
240
+ const slice = lines.slice(safeOffset, safeOffset + safeLimit)
241
+ return {
242
+ process: rec,
243
+ text: slice.join('\n'),
244
+ totalLines: total,
245
+ }
246
+ }
247
+
248
+ export function writeManagedProcessStdin(processId: string, data: string, eof?: boolean): { ok: boolean; error?: string } {
249
+ const child = state.children.get(processId)
250
+ const rec = state.records.get(processId)
251
+ if (!child || !rec) return { ok: false, error: 'Process not running' }
252
+ if (rec.status !== 'running') return { ok: false, error: `Process is ${rec.status}` }
253
+ try {
254
+ if (data) child.stdin.write(data)
255
+ if (eof) child.stdin.end()
256
+ return { ok: true }
257
+ } catch (err: any) {
258
+ return { ok: false, error: err.message || String(err) }
259
+ }
260
+ }
261
+
262
+ export function killManagedProcess(processId: string, signal: NodeJS.Signals = 'SIGTERM'): { ok: boolean; error?: string } {
263
+ const child = state.children.get(processId)
264
+ const rec = state.records.get(processId)
265
+ if (!child || !rec) return { ok: false, error: 'Process not running' }
266
+ try {
267
+ rec.status = 'killed'
268
+ child.kill(signal)
269
+ return { ok: true }
270
+ } catch (err: any) {
271
+ return { ok: false, error: err.message || String(err) }
272
+ }
273
+ }
274
+
275
+ export function clearManagedProcess(processId: string): { ok: boolean; error?: string } {
276
+ const rec = state.records.get(processId)
277
+ if (!rec) return { ok: false, error: 'Process not found' }
278
+ if (rec.status === 'running') return { ok: false, error: 'Cannot clear a running process' }
279
+ state.records.delete(processId)
280
+ state.children.delete(processId)
281
+ state.exitWaiters.delete(processId)
282
+ return { ok: true }
283
+ }
284
+
285
+ export function removeManagedProcess(processId: string): { ok: boolean; error?: string } {
286
+ const rec = state.records.get(processId)
287
+ if (!rec) return { ok: false, error: 'Process not found' }
288
+ if (rec.status === 'running') {
289
+ const killed = killManagedProcess(processId, 'SIGTERM')
290
+ if (!killed.ok) return killed
291
+ }
292
+ state.records.delete(processId)
293
+ state.children.delete(processId)
294
+ state.exitWaiters.delete(processId)
295
+ return { ok: true }
296
+ }
297
+
298
+ export function sweepManagedProcesses(ttlMs = DEFAULT_TTL_MS): number {
299
+ const threshold = now() - Math.max(60_000, ttlMs)
300
+ let removed = 0
301
+ for (const [id, rec] of state.records) {
302
+ if (rec.status === 'running') continue
303
+ if (!rec.endedAt || rec.endedAt > threshold) continue
304
+ state.records.delete(id)
305
+ state.children.delete(id)
306
+ state.exitWaiters.delete(id)
307
+ removed++
308
+ }
309
+ return removed
310
+ }
311
+
312
+ /** Kill running processes and clear completed records that belong to a session. */
313
+ export function cleanupSessionProcesses(sessionId: string): number {
314
+ let cleaned = 0
315
+ for (const [id, rec] of state.records) {
316
+ if (rec.sessionId !== sessionId) continue
317
+ if (rec.status === 'running') {
318
+ const child = state.children.get(id)
319
+ try { child?.kill('SIGTERM') } catch { /* ignore */ }
320
+ }
321
+ state.records.delete(id)
322
+ state.children.delete(id)
323
+ state.exitWaiters.delete(id)
324
+ cleaned++
325
+ }
326
+ return cleaned
327
+ }
@@ -0,0 +1,113 @@
1
+ import { spawnSync } from 'child_process'
2
+
3
+ type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'
4
+
5
+ interface ProviderHealthState {
6
+ failures: number
7
+ lastError?: string
8
+ lastFailureAt?: number
9
+ lastSuccessAt?: number
10
+ cooldownUntil?: number
11
+ }
12
+
13
+ const gk = '__swarmclaw_provider_health__' as const
14
+ const states: Map<string, ProviderHealthState> =
15
+ (globalThis as any)[gk] ?? ((globalThis as any)[gk] = new Map<string, ProviderHealthState>())
16
+
17
+ const cliCheckCache = new Map<string, { at: number; ok: boolean }>()
18
+ const CLI_CHECK_TTL_MS = 30_000
19
+
20
+ function commandExists(binary: string): boolean {
21
+ const now = Date.now()
22
+ const cached = cliCheckCache.get(binary)
23
+ if (cached && now - cached.at < CLI_CHECK_TTL_MS) return cached.ok
24
+ const probe = spawnSync('/bin/zsh', ['-lc', `command -v ${binary} >/dev/null 2>&1`], { timeout: 2000 })
25
+ const ok = (probe.status ?? 1) === 0
26
+ cliCheckCache.set(binary, { at: now, ok })
27
+ return ok
28
+ }
29
+
30
+ function cooldownMsForFailures(failures: number): number {
31
+ const clamped = Math.max(1, Math.min(8, failures))
32
+ return Math.min(5 * 60_000, 10_000 * (2 ** (clamped - 1)))
33
+ }
34
+
35
+ export function markProviderFailure(providerId: string, error: string): void {
36
+ const now = Date.now()
37
+ const prev = states.get(providerId) || { failures: 0 }
38
+ const failures = Math.min(50, (prev.failures || 0) + 1)
39
+ states.set(providerId, {
40
+ failures,
41
+ lastError: error.slice(0, 500),
42
+ lastFailureAt: now,
43
+ lastSuccessAt: prev.lastSuccessAt,
44
+ cooldownUntil: now + cooldownMsForFailures(failures),
45
+ })
46
+ }
47
+
48
+ export function markProviderSuccess(providerId: string): void {
49
+ const now = Date.now()
50
+ const prev = states.get(providerId) || { failures: 0 }
51
+ states.set(providerId, {
52
+ failures: 0,
53
+ lastError: prev.lastError,
54
+ lastFailureAt: prev.lastFailureAt,
55
+ lastSuccessAt: now,
56
+ cooldownUntil: undefined,
57
+ })
58
+ }
59
+
60
+ export function isProviderCoolingDown(providerId: string): boolean {
61
+ const state = states.get(providerId)
62
+ if (!state?.cooldownUntil) return false
63
+ return Date.now() < state.cooldownUntil
64
+ }
65
+
66
+ function delegateBinary(delegateTool: DelegateTool): string {
67
+ if (delegateTool === 'delegate_to_claude_code') return 'claude'
68
+ if (delegateTool === 'delegate_to_codex_cli') return 'codex'
69
+ return 'opencode'
70
+ }
71
+
72
+ function delegateProviderId(delegateTool: DelegateTool): string {
73
+ if (delegateTool === 'delegate_to_claude_code') return 'claude-cli'
74
+ if (delegateTool === 'delegate_to_codex_cli') return 'codex-cli'
75
+ return 'opencode-cli'
76
+ }
77
+
78
+ export function rankDelegatesByHealth(order: DelegateTool[]): DelegateTool[] {
79
+ const seen = new Set<DelegateTool>()
80
+ const deduped = order.filter((tool) => {
81
+ if (seen.has(tool)) return false
82
+ seen.add(tool)
83
+ return true
84
+ })
85
+ return deduped.sort((a, b) => {
86
+ const aBinOk = commandExists(delegateBinary(a))
87
+ const bBinOk = commandExists(delegateBinary(b))
88
+ if (aBinOk !== bBinOk) return aBinOk ? -1 : 1
89
+
90
+ const aCool = isProviderCoolingDown(delegateProviderId(a))
91
+ const bCool = isProviderCoolingDown(delegateProviderId(b))
92
+ if (aCool !== bCool) return aCool ? 1 : -1
93
+
94
+ const aState = states.get(delegateProviderId(a))
95
+ const bState = states.get(delegateProviderId(b))
96
+ const aFails = aState?.failures || 0
97
+ const bFails = bState?.failures || 0
98
+ if (aFails !== bFails) return aFails - bFails
99
+ return 0
100
+ })
101
+ }
102
+
103
+ export function getProviderHealthSnapshot(): Record<string, ProviderHealthState & { coolingDown: boolean }> {
104
+ const out: Record<string, ProviderHealthState & { coolingDown: boolean }> = {}
105
+ for (const [providerId, state] of states.entries()) {
106
+ out[providerId] = {
107
+ ...state,
108
+ coolingDown: isProviderCoolingDown(providerId),
109
+ }
110
+ }
111
+ return out
112
+ }
113
+